Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions .github/workflows/pmm-groovy-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Groovy lint gate for the PMM pipelines. PMM-scoped on purpose, twice over:
# the path filter keeps this workflow off every other product's pull requests,
# and the file list is filtered to pmm/ again so a PR that happens to touch both
# pmm/ and, say, ppg/ is still only ever judged on its pmm/ files. Nothing here
# gates, annotates or reports on a directory PMM does not own -- root vars/
# included, since that shared library is loaded by every product's builds.
#
# Scoped to the pmm/ .groovy files CHANGED in the pull request, never all of
# them. The 62 files under pmm/ predate any linting and carry a backlog; a gate
# over all of them would be red on every PR and would be ignored within a week.
# New and edited files are held to pmm/.groovylintrc.json, and the backlog is
# paid down as files get touched.
#
# Severity decides the outcome: `error` fails the check and is annotated on the
# diff; `warning`/`info` are advisory and go to the step log and job summary
# only, so they never bury the finding that actually blocks. A file that does
# not parse always fails -- Jenkins could not load it either.
#
# `workflow_dispatch` with scope=all re-measures the pmm/ backlog. That mode
# never gates anything; it only writes the job summary.
#
# Actions are pinned to commit SHAs; the trailing "# vX.Y.Z" records the tag.

name: pmm-groovy-lint
run-name: >-
pmm groovy lint (${{ github.event_name == 'pull_request'
&& format('PR #{0}', github.event.pull_request.number)
|| format('{0} scope', inputs.scope) }})

on:
pull_request:
paths:
- 'pmm/**.groovy'
- 'pmm/.groovylintrc.json'
- 'pmm/scripts/groovy-lint-report.py'
- '.github/workflows/pmm-groovy-lint.yml'
workflow_dispatch:
inputs:
scope:
description: 'all = lint every .groovy file under pmm/ (backlog report, gates nothing)'
required: false
default: 'all'
type: choice
options: [all]

permissions:
contents: read

concurrency:
group: pmm-groovy-lint-${{ github.ref }}
cancel-in-progress: true

env:
# Exact version, never a range: a linter that silently gains rules turns a
# green PR red on re-run. Bump deliberately, with the backlog re-measured.
NGL_VERSION: '18.0.0'

jobs:
lint:
name: npm-groovy-lint (pmm/)
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
# Full history: the changed-file list is a diff against the PR base.
fetch-depth: 0

- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
# npm-groovy-lint 18 declares engines.node >= 22; do not inherit
# whatever the runner image happens to ship.
node-version: '22'

- name: Resolve the file list
id: files
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
set -euo pipefail
if [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then
# Two-dot diff against base.sha: HEAD is the PR merge commit, which
# already contains base, so this is exactly the PR's own changes.
# ACMR drops deletions -- a removed file has nothing left to lint.
git diff --name-only --diff-filter=ACMR "${BASE_SHA}" HEAD -- 'pmm/**.groovy' > files.txt
else
git ls-files -- 'pmm/**.groovy' > files.txt
fi
count=$(wc -l < files.txt)
echo "count=${count}" >> "$GITHUB_OUTPUT"
echo "linting ${count} file(s)"
cat files.txt

- name: Lint
if: steps.files.outputs.count != '0'
run: |
set -uo pipefail
mapfile -t FILES < files.txt
# --failon none: the gate decision is the report step's, so that
# warnings still get annotated instead of aborting the run here.
# --noserver: one-shot run, no lingering CodeNarc daemon (and no
# 120s client timeout on large file lists).
npx --yes "npm-groovy-lint@${NGL_VERSION}" \
--noserver \
--no-insight \
--failon none \
--config "${GITHUB_WORKSPACE}/pmm" \
--output json \
"${FILES[@]}" > report.json
test -s report.json

- name: Report
if: steps.files.outputs.count != '0'
run: |
set -euo pipefail
# The backlog sweep reports without failing; only a PR gates.
advisory=''
[ "${GITHUB_EVENT_NAME}" = "pull_request" ] || advisory='--advisory'
python3 pmm/scripts/groovy-lint-report.py report.json ${advisory}
98 changes: 98 additions & 0 deletions pmm/.groovylintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// npm-groovy-lint config for the PMM pipelines. Applies to pmm/ only -- it is
// read by .github/workflows/pmm-groovy-lint.yml, which lints the pmm/ .groovy
// files a pull request changed. Nothing outside pmm/ is linted or gated; see
// pmm/AGENTS.md.
//
// Only `error` severity fails a PR. Everything else is advisory, so the rules
// below are split by that line: a rule is `error` when a violation is a defect,
// off when Jenkins pipeline DSL structurally violates it and always will, and
// left at its inherited severity otherwise.
//
// Counts in the comments are the measured pmm/ backlog (62 .groovy files) under
// the stock `recommended` ruleset, which is why each rule is where it is.
{
// Same base as CodeNarc's own Jenkinsfile profile: no CompileStatic, no
// static typing rules, no VariableName, NestedBlockDepth raised to 10.
"extends": "recommended-jenkinsfile",

"rules": {
// --- Defects. These fail the check. ---
// Inherited as errors from `recommended`: unused.UnusedPrivateField/Method/
// MethodParameter, unused.UnusedArray, unused.UnusedObject, basic.DeadCode.
// Plus NglParseError, which is always an error and cannot be configured --
// no pmm/ file trips it today.
//
// Restated in full because `recommended-jenkinsfile` overrides this rule to
// add ignoreVariableNames, which drops `recommended`'s error severity back
// to CodeNarc's default warning. The 7 hits it had under pmm/ are removed in
// the same change that adds this config, so the gate starts clean.
"unused.UnusedVariable": {
"severity": "error",
"ignoreVariableNames": "_"
},
"basic.EmptyCatchBlock": "error",
"basic.EmptyIfStatement": "error",
"basic.EmptyElseBlock": "error",
"basic.EmptyWhileStatement": "error",
"basic.ConstantIfExpression": "error",
"basic.ComparisonOfTwoConstants": "error",
"basic.DuplicateCaseStatement": "error",
"basic.BrokenOddnessCheck": "error",
"imports.DuplicateImport": "error",
"imports.UnusedImport": "error",

// --- Off: the DSL works this way. ---
// Pipelines repeat branch names, agent labels, credential ids and repo URLs
// by nature; deduplicating them into constants is not how Jenkinsfiles read.
"dry.DuplicateStringLiteral": "off", // 1002
"dry.DuplicateMapLiteral": "off", // 133
"dry.DuplicateListLiteral": "off", // 20
"dry.DuplicateNumberLiteral": "off", // 11
// Double-quoted strings are the Jenkins norm -- interpolation gets added and
// removed constantly, and churning quotes would touch every file.
"unnecessary.UnnecessaryGString": "off", // 486
// Both 2- and 4-space styles are established across pmm/.
"formatting.Indentation": "off", // 210
// `KEY=credentials('id')` inside an environment block is the idiom.
"formatting.SpaceAroundOperator": "off", // 667
// Jenkins job parameters are UPPER_CASE by convention.
"naming.ParameterName": "off", // 268
// Same reason CompileStatic/NoDef/VariableTypeRequired are off upstream:
// steps take untyped closures and maps.
"convention.MethodParameterTypeRequired": "off", // 191
"convention.FieldTypeRequired": "off",
// Fires on every `createX()`-style helper in a pipeline.
"design.BuilderMethodWithSideEffects": "off",
// A pipeline that catches broadly to keep a stage non-fatal is deliberate.
"exceptions.CatchException": "off",
// `env.X = env.X` is not a mistake: environment{} vars are applied via
// withEnv and are only visible inside the pipeline, so re-assigning through
// env.X= is what persists them on the build for `build job:` callers to read
// out of buildVariables. Used deliberately in pmm3-ha-eks and pmm3-ha-rosa.
"unnecessary.UnnecessarySelfAssignment": "off", // 2
// Declarative pipelines are long and deeply nested by construction; the
// inherited NestedBlockDepth of 10 already covers the real outliers
// (570 hits at the default depth of 5, 2 at 10).
"size.MethodSize": "off",
"size.MethodCount": "off",
"size.ClassSize": "off",
"size.ParameterCount": "off", // 19

// --- Off: subjective refactoring advice, not defects. ---
"convention.InvertedIfElse": "off",
"convention.ConfusingTernary": "off",
"convention.CouldBeElvis": "off",
"convention.CouldBeSwitchStatement": "off",
"convention.IfStatementCouldBeTernary": "off",
"convention.PublicMethodsBeforeNonPublicMethods": "off",
"unnecessary.UnnecessaryObjectReferences": "off",
"design.Instanceof": "off",

// --- Advisory, kept on. ---
// 120 is unworkable for embedded shell one-liners and long image URLs:
// 661 hits at 120, 82 at 200.
"formatting.LineLength": {
"length": 200
}
}
}
90 changes: 90 additions & 0 deletions pmm/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# PMM pipelines — guide for AI agents

Tool-neutral entry point (the `AGENTS.md` convention) for `pmm/` in
Percona-Lab/jenkins-pipelines. Read this before editing anything here.

## Scope: `pmm/` and nothing else

This repository holds the Jenkins job definitions for **every** Percona product — `ppg`, `ps`,
`pxc`, `pxb`, `psmdb`, `pbm`, the distributions, `cloud` (k8s operators), release tooling. Each
directory belongs to a different team (`.github/CODEOWNERS`), and PMM owns `pmm/`.

**Stay inside `pmm/`.** Do not edit, lint, report on, or "fix in passing" another product's
directory, and treat the repo-root `vars/` shared library the same way: it is loaded as `lib@master`
by every product's builds, so a change there is a change to everyone's CI. If PMM genuinely needs
something from `vars/`, raise it with the owners rather than editing it — `pmm/v3/vars/` is PMM's
own shared library and is the right home for PMM-only steps.

The same rule applies to findings: problems noticed elsewhere in the repo are not ours to file,
fix, or put in a report.

## What these files are

- `*.groovy` — declarative Jenkins pipelines, one file per job, plus shared-library steps.
- `*.yml` — [Jenkins Job Builder](https://jenkins-job-builder.readthedocs.io/) definitions that
register a pipeline as a job, with its parameters, triggers and retention.

**Nothing here runs its own product code.** These files are executed by Percona's Jenkins masters
against real cloud resources, so a mistake surfaces as a failed (or expensive) build, not as a
failing test. There is no way to run a job from a checkout — verify by reading carefully and by
running the Groovy linter.

## Layout

| Path | What |
|------|------|
| `pmm/v3/` | All PMM 3 pipelines (`pmm3-*.groovy`): server and client autobuilds, AMI/OVF images, UI / API / upgrade / migration / package tests, HA on EKS and ROSA, release and release-candidate |
| `pmm/v3/vars/` | PMM's own shared library, loaded as `v3lib@master` via `libraryPath: 'pmm/v3/'` |
| `pmm/` (root) | Older PMM jobs (`aws-staging-stop*.groovy`) |
| `pmm/openshift/`, `pmm/infrastructure/` | Adjacent PMM jobs: OpenShift cluster lifecycle, RPM builds |
| `pmm/scripts/` | Helpers for PMM's own tooling (not called from pipelines) |
| `pmm/README.md` | Agent labels (`agent-amd64`, `agent-arm64`, `cli`) and the "Zen of Jenkinsfile" conventions every new PMM pipeline follows: `buildDiscarder`, `deleteDir()` in `post`, Python over long bash |

Pipelines also load the repo-root `vars/` library as `lib@master`. Call its steps freely; do not
change them.

## Repos on the other side of these pipelines

| Repo | Relationship |
|------|--------------|
| [percona/pmm](https://github.com/percona/pmm) | The product. Built by `pmm3-server-autobuild.groovy` and `pmm3-client-autobuild*.groovy` |
| [percona/pmm-qa](https://github.com/percona/pmm-qa) | The e2e / CLI / package test suites. Test pipelines clone it at `PMM_QA_GIT_BRANCH`, rsync it to `/srv/pmm-qa`, then run out of `qa-integration/pmm_qa` and `e2e_tests`. **Test logic lives there, not here** — this repo only provisions and invokes it |
| [Percona-Lab/pmm-submodules](https://github.com/Percona-Lab/pmm-submodules) | Feature builds (FB). `pmm3-submodules.groovy` builds server and client images from a submodules PR, comments the tags back on the PR, triggers `pmm3-api-tests`, and dispatches pmm-qa's `pmm-qa-fb-checks.yml` workflow with those image tags |
| [percona/grafana](https://github.com/percona/grafana) | Percona's Grafana fork, pulled into the PMM server image by the server build |

## Groovy lint

`pmm/.groovylintrc.json` (rules) + `.github/workflows/pmm-groovy-lint.yml` (the gate).

The gate lints **only the `pmm/` `.groovy` files changed in the pull request** — not the rest of
`pmm/`, and never another product's directory. The 62 files here predate any linting and carry a
backlog; a gate over all of them would be red on every PR and would be ignored within a week. New
and edited files are held to the config, and the backlog is paid down as files get touched.

Run the same check locally before pushing (pin the version CI pins, in the workflow):

```bash
npx npm-groovy-lint@18.0.0 --noserver --failon error --config pmm pmm/v3/pmm3-ui-tests.groovy
```

- Only `error` severity fails the gate; `warning` and `info` are advisory and printed for context.
`pmm/` is clean of errors today, so a red check means the PR introduced one.
- A file that does not parse always fails — Jenkins could not load it either. Note that Groovy
reports the line where the parser gave up, which is often far from the actual mistake.
- Many stylistic rules are deliberately off because Jenkins DSL structurally violates them; the
reasoning and the measured counts are in `pmm/.groovylintrc.json`.
- If a rule genuinely does not fit Jenkins pipeline DSL, change `pmm/.groovylintrc.json` in its own
PR with the reasoning, and re-measure with the workflow's manual `scope=all` run. Do not
`/* groovylint-disable */` a rule to get one file through, and do not widen the gate beyond `pmm/`.

## Working rules

- **Do not mix pipeline logic with tooling/config changes in one PR.** They have different
reviewers and very different blast radius.
- Match the surrounding file. These are Jenkins DSL scripts, not general-purpose Groovy — copy the
idioms already used by the neighbouring `pmm3-*` pipelines.
- Keep comments minimal and only where the intent is non-obvious (a credential quirk, a retry
reason, an agent-label constraint). Do not narrate what the DSL already says.
- Never hardcode credentials. Use `credentials(...)` / `withCredentials` as the existing pipelines do.
- Commit and PR titles carry the Jira key: `PMM-1234 Short summary`. Check `git log -- pmm/` and
match what is already there.
7 changes: 7 additions & 0 deletions pmm/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Claude Code — PMM pipelines

@AGENTS.md

These files are executed by Percona's Jenkins masters against real cloud resources. Verify against
`AGENTS.md`, the neighbouring `pmm3-*` pipelines, and the Groovy linter — never from general Groovy
knowledge. Stay inside `pmm/`: every other directory in this repo belongs to another team.
4 changes: 1 addition & 3 deletions pmm/openshift/openshift_cluster_destroy.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -216,9 +216,7 @@ pipeline {
if (!params.DRY_RUN) {
echo "Cluster ${params.CLUSTER_NAME} destroyed successfully"

// Get final resource count and OCP version for description
def resourceCount = env.DESTROY_RESULT ?
new JsonSlurper().parseText(env.DESTROY_RESULT).resourcesDeleted : 'Unknown'
// Get OCP version for description
def ocpVersion = env.CLUSTER_METADATA ?
new JsonSlurper().parseText(env.CLUSTER_METADATA).openshift_version : 'Unknown'

Expand Down
Loading
Loading