docs(jenkins-mcp): record operator traps and client recovery (#374) #61
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: build-jenkins-mcp-image | |
| on: | |
| pull_request: | |
| paths: | |
| - 'images/jenkins-mcp/**' | |
| - '.github/workflows/build-jenkins-mcp-image.yml' | |
| push: | |
| branches: [main] | |
| paths: | |
| - 'images/jenkins-mcp/**' | |
| - '.github/workflows/build-jenkins-mcp-image.yml' | |
| # Default: read-only. The privileged push job re-declares its own permissions. | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| IMAGE_REPO: percona-cd/jenkins-mcp | |
| AWS_REGION: us-east-1 | |
| jobs: | |
| # ---- actionlint/zizmor gate (shared composite, mirrors ci.yml) ---- | |
| lint: | |
| name: actions lint | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| - uses: ./.github/actions/actions-lint | |
| # ---- unit tests + ruff on the vendored source ---- | |
| test: | |
| name: pytest + ruff | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 | |
| with: | |
| enable-cache: true | |
| - name: sync (frozen) | |
| working-directory: images/jenkins-mcp | |
| run: uv sync --all-extras --dev --frozen | |
| - name: pytest | |
| working-directory: images/jenkins-mcp | |
| run: uv run --frozen pytest -q | |
| - name: ruff check + format | |
| working-directory: images/jenkins-mcp | |
| run: | | |
| uv run --frozen ruff check . | |
| uv run --frozen ruff format --check . | |
| # ---- PR validation: build + smoke, NO AWS token, NO push ---- | |
| validate: | |
| name: build + smoke (no push) | |
| if: github.event_name == 'pull_request' | |
| needs: [lint, test] | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 20 | |
| permissions: | |
| contents: read # explicitly NO id-token | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| # docker/* is SHA-pinned (not in the repo zizmor ref-pin allowlist). Refresh with: | |
| # gh api repos/docker/<repo>/git/refs/tags/<tag> --jq .object.sha | |
| - uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 | |
| - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 | |
| # A multi-arch manifest cannot be loaded into the daemon, so build + smoke each | |
| # arch single-platform with --load. arm64 runs under the QEMU binfmt registered | |
| # by setup-qemu above. | |
| - name: build amd64 (load, no push) | |
| uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 | |
| with: | |
| context: images/jenkins-mcp | |
| platforms: linux/amd64 | |
| load: true | |
| push: false | |
| tags: jenkins-mcp:pr-amd64-${{ github.sha }} | |
| - name: smoke amd64 (/healthz) | |
| run: images/jenkins-mcp/smoke.sh jenkins-mcp:pr-amd64-${{ github.sha }} | |
| - name: build arm64 (load, no push) | |
| uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 | |
| with: | |
| context: images/jenkins-mcp | |
| platforms: linux/arm64 | |
| load: true | |
| push: false | |
| tags: jenkins-mcp:pr-arm64-${{ github.sha }} | |
| - name: smoke arm64 (/healthz, via QEMU) | |
| run: images/jenkins-mcp/smoke.sh jenkins-mcp:pr-arm64-${{ github.sha }} | |
| # ---- main: build + smoke + push to ECR via OIDC ---- | |
| publish: | |
| name: build + smoke + push (OIDC) | |
| if: github.event_name == 'push' | |
| needs: [lint, test] | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 30 | |
| outputs: | |
| tag: ${{ steps.meta.outputs.tag }} | |
| permissions: | |
| contents: read | |
| id-token: write # ONLY this job mints the OIDC JWT | |
| steps: | |
| # Third-party actions pinned by commit SHA below this line (the job holds | |
| # AWS creds after configure-aws-credentials). Resolve/refresh SHAs with: | |
| # gh api repos/<owner>/<repo>/git/refs/tags/<tag> --jq .object.sha | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| persist-credentials: false | |
| - uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 | |
| - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 | |
| - name: compute image tag (<version>-<short-sha>) | |
| id: meta | |
| run: | | |
| set -euo pipefail | |
| ver="$(grep -m1 '^version = ' images/jenkins-mcp/pyproject.toml | cut -d'"' -f2)" | |
| [ -n "$ver" ] || { echo "could not parse version from pyproject.toml"; exit 1; } | |
| # Constrain the version to a known-safe charset before it flows into the | |
| # image tag, the bump job's sed program, and $GITHUB_OUTPUT. Excludes the | |
| # sed delimiter and every shell/markdown metacharacter, so a malformed | |
| # version fails here rather than corrupting values.yaml downstream. | |
| case "$ver" in | |
| *[!0-9A-Za-z.+_-]*) echo "version has unexpected characters: '$ver'"; exit 1 ;; | |
| esac | |
| echo "tag=${ver}-${GITHUB_SHA::12}" >> "$GITHUB_OUTPUT" | |
| - name: build amd64 (load, NO push yet) | |
| uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 | |
| with: | |
| context: images/jenkins-mcp | |
| platforms: linux/amd64 | |
| load: true | |
| push: false | |
| tags: jenkins-mcp:amd64-${{ github.sha }} | |
| - name: smoke-boot amd64 BEFORE push (/healthz) | |
| run: images/jenkins-mcp/smoke.sh jenkins-mcp:amd64-${{ github.sha }} | |
| - name: build arm64 (load, NO push yet) | |
| uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 | |
| with: | |
| context: images/jenkins-mcp | |
| platforms: linux/arm64 | |
| load: true | |
| push: false | |
| tags: jenkins-mcp:arm64-${{ github.sha }} | |
| - name: smoke-boot arm64 BEFORE push (/healthz, via QEMU) | |
| run: images/jenkins-mcp/smoke.sh jenkins-mcp:arm64-${{ github.sha }} | |
| # --- AWS creds appear here; everything after is privileged --- | |
| - name: configure AWS credentials (OIDC) | |
| uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 | |
| with: | |
| role-to-assume: ${{ secrets.JENKINS_MCP_IMAGE_PUSH_ROLE_ARN }} | |
| aws-region: ${{ env.AWS_REGION }} | |
| - name: ECR login | |
| id: ecr | |
| uses: aws-actions/amazon-ecr-login@fa648b43de3d4d023bcb3f89ed6940096949c419 # v2 | |
| - name: push (multi-arch manifest list) with SBOM + provenance | |
| id: push | |
| uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 | |
| with: | |
| context: images/jenkins-mcp | |
| platforms: linux/amd64,linux/arm64 | |
| push: true | |
| provenance: true | |
| sbom: true | |
| tags: | | |
| ${{ steps.ecr.outputs.registry }}/${{ env.IMAGE_REPO }}:${{ steps.meta.outputs.tag }} | |
| - name: SECOND smoke against the PUSHED digest | |
| env: | |
| REGISTRY: ${{ steps.ecr.outputs.registry }} | |
| DIGEST: ${{ steps.push.outputs.digest }} | |
| run: images/jenkins-mcp/smoke.sh "${REGISTRY}/${IMAGE_REPO}@${DIGEST}" | |
| - name: summary (image ref + deploy hint) | |
| env: | |
| REGISTRY: ${{ steps.ecr.outputs.registry }} | |
| TAG: ${{ steps.meta.outputs.tag }} | |
| DIGEST: ${{ steps.push.outputs.digest }} | |
| run: | | |
| { | |
| echo "Pushed \`${REGISTRY}/${IMAGE_REPO}:${TAG}\`" | |
| echo "Digest \`${DIGEST}\`" | |
| echo "A deploy bump PR for \`${TAG}\` is opened automatically by the bump job." | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| # ---- open a deploy bump PR for the just-pushed tag (verified commit, no GitHub App) ---- | |
| # createCommitOnBranch signs the commit with GitHub's key, so the bump PR meets main's | |
| # required-signed-commits rule with no GPG key or GitHub App (mirrors refresh-fork-locks.yml). | |
| # A human squash-merges the one-line PR to deploy; ArgoCD then syncs HEAD. The image is | |
| # already built, smoke-booted, and pushed by publish, so the bump is pre-validated. | |
| bump: | |
| name: open deploy bump PR | |
| if: github.event_name == 'push' | |
| needs: [publish] | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: write # createCommitOnBranch on the bump branch | |
| pull-requests: write # gh pr create | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| persist-credentials: false | |
| - name: open bump PR | |
| env: | |
| # An optional fine-grained PAT makes the PR re-trigger ci-gate; without it the PR | |
| # still opens and the commit is still GitHub-verified, but ci-gate does not run on | |
| # a GITHUB_TOKEN-opened PR (the image is already validated by publish). | |
| GH_TOKEN: ${{ secrets.JENKINS_MCP_BUMP_TOKEN || secrets.GITHUB_TOKEN }} | |
| NEW_TAG: ${{ needs.publish.outputs.tag }} | |
| BASE_SHA: ${{ github.sha }} | |
| RUN_ID: ${{ github.run_id }} | |
| VALUES: resources/addons/jenkins-mcp/values.yaml | |
| run: | | |
| set -euo pipefail | |
| [ -n "$NEW_TAG" ] || { echo "no tag from publish job"; exit 1; } | |
| n="$(grep -cE '^ tag: ' "$VALUES")" | |
| [ "$n" = "1" ] || { echo "expected exactly one ' tag:' line in $VALUES, got $n"; exit 1; } | |
| cur="$(grep -E '^ tag: ' "$VALUES" | head -1 | sed -E 's|^ tag: "?([^"]*)"?.*|\1|')" | |
| if [ "$cur" = "$NEW_TAG" ]; then | |
| echo "values.yaml already on ${NEW_TAG}; nothing to bump." | |
| exit 0 | |
| fi | |
| BRANCH="chore/deploy-jenkins-mcp-${RUN_ID}" | |
| gh api -X POST "repos/${GITHUB_REPOSITORY}/git/refs" \ | |
| -f ref="refs/heads/${BRANCH}" -f sha="${BASE_SHA}" | |
| sed -E "s|^ tag: .*| tag: \"${NEW_TAG}\"|" "$VALUES" > /tmp/values.yaml | |
| CONTENT_B64="$(base64 -w0 /tmp/values.yaml)" | |
| # shellcheck disable=SC2016 # $vars are GraphQL variables substituted by gh -f, not the shell. | |
| gh api graphql -f query=' | |
| mutation($nwo:String!,$branch:String!,$path:String!,$contents:Base64String!,$head:GitObjectID!,$msg:String!){ | |
| createCommitOnBranch(input:{ | |
| branch:{repositoryNameWithOwner:$nwo, branchName:$branch}, | |
| message:{headline:$msg}, | |
| expectedHeadOid:$head, | |
| fileChanges:{additions:[{path:$path, contents:$contents}]} | |
| }){ commit { oid } } | |
| }' \ | |
| -f nwo="$GITHUB_REPOSITORY" -f branch="$BRANCH" \ | |
| -f path="$VALUES" -f contents="$CONTENT_B64" -f head="$BASE_SHA" \ | |
| -f msg="chore(jenkins-mcp): deploy image ${NEW_TAG}" | |
| BODY="$(mktemp)" | |
| { | |
| printf '%s\n' '## Deploy' | |
| printf '%s\n' '' | |
| printf '%s\n' "- Bump ${VALUES} image.tag to ${NEW_TAG} (built, smoke-booted, and pushed to ECR by this run)." | |
| printf '%s\n' '- The commit is GitHub-verified (createCommitOnBranch), so it satisfies the signed-commits rule on main.' | |
| printf '%s\n' '- Squash-merge to deploy. ArgoCD syncs HEAD and the pod rolls. No auto-merge.' | |
| } > "$BODY" | |
| gh pr create --base main --head "$BRANCH" \ | |
| --title "chore(jenkins-mcp): deploy image ${NEW_TAG}" \ | |
| --body-file "$BODY" |