fix(ci): restore the required-check producers — allow-list, actions.lock gate, K9 pedigree #13
Workflow file for this run
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
| # This workflow is managed by gh actions-lock. | |
| # SPDX-License-Identifier: MPL-2.0 | |
| # | |
| # actions-lock.yml — keep .github/workflows/actions.lock in step with the | |
| # workflows themselves. | |
| # | |
| # Why this exists: an out-of-date lockfile is not a soft failure. Every | |
| # workflow listed in actions.lock dies with `startup_failure` before a single | |
| # step runs, so it produces no check runs at all — which is how | |
| # game-server-admin#103 took out both required-check producers (`ABI Contract` | |
| # and `Cross-Platform Build & Test`) in one commit. Dependabot bumps action | |
| # tags weekly and cannot run `gh actions-lock`, so without a gate here the next | |
| # group bump re-breaks the whole CI estate, silently. | |
| # | |
| # The lockfile is not decoration for another reason: this repo's Actions policy | |
| # requires every action to be pinned to a full-length commit SHA, and a tag ref | |
| # such as actions/checkout@v7.0.1 is accepted *only because the lockfile pins | |
| # it*. Delete the lockfile and even actions/checkout is refused. | |
| # | |
| # The gate has two halves: | |
| # verify — read-only (`gh actions-lock --verify`). Fails when the lockfile | |
| # and the workflows disagree, so a bump cannot merge unrelocked. | |
| # relock — runs when verify asked it to. Regenerates the lockfile and | |
| # delivers it: pushed straight back to the branch on a pull | |
| # request, or opened as a PR when the target is the default branch | |
| # (main requires signed commits, so a bot cannot push there). | |
| # | |
| # A push made with GITHUB_TOKEN does not start a new workflow run, so the | |
| # relock commit cannot loop. | |
| name: Actions Lockfile | |
| on: | |
| pull_request: | |
| push: | |
| branches: [main, master] | |
| workflow_dispatch: | |
| inputs: | |
| mode: | |
| description: 'verify = report only; relock = regenerate and deliver' | |
| required: false | |
| default: 'verify' | |
| type: choice | |
| options: | |
| - verify | |
| - relock | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: actions-lockfile-${{ github.ref }} | |
| cancel-in-progress: false | |
| env: | |
| # Bump deliberately: the lockfile format is pre-1.0 and its shape can | |
| # change between releases. | |
| ACTIONS_LOCK_EXTENSION_VERSION: v0.1.7-rc.1 | |
| jobs: | |
| verify: | |
| name: Verify actions.lock | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: read | |
| outputs: | |
| # `relock` is computed in the shell, not in an `if:` expression: | |
| # `inputs.mode` is not a recognised named value outside | |
| # workflow_dispatch, and referencing it there is a start-up error. | |
| in_sync: ${{ steps.check.outputs.in_sync }} | |
| relock: ${{ steps.check.outputs.relock }} | |
| steps: | |
| # Egress audit: Hypatia's RE001 asks for it on any job that reaches for | |
| # secrets.*, and both jobs here pass GITHUB_TOKEN to gh. | |
| - name: Harden runner (egress audit) | |
| uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout | |
| id: checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Install gh actions-lock | |
| id: install | |
| run: | | |
| set -euo pipefail | |
| gh extension install github/gh-actions-lock --pin "$ACTIONS_LOCK_EXTENSION_VERSION" | |
| gh actions-lock --help | |
| - name: Check the lockfile matches the workflows | |
| id: check | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| DISPATCH_MODE: ${{ github.event.inputs.mode }} | |
| run: | | |
| # The runner's default shell is `bash -e {0}`, and `set -uo pipefail` | |
| # does NOT clear errexit — so every step that inspects an exit code | |
| # has to switch it off explicitly, or a failing command aborts the | |
| # script before the handler below it can report anything. | |
| set -uo pipefail | |
| set +e | |
| code=0 | |
| gh actions-lock --verify --no-interactive \ | |
| --json=valid,findings >actions-lock-report.json 2>actions-lock-report.err \ | |
| || code=$? | |
| echo "gh actions-lock --verify exited ${code}" | tee -a "$GITHUB_STEP_SUMMARY" | |
| if [ "$code" -eq 0 ]; then | |
| echo "in_sync=true" >> "$GITHUB_OUTPUT" | |
| # An explicit relock dispatch still wins even when the file looks | |
| # fine: it is how a maintainer refreshes pins that have moved. | |
| if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ "$DISPATCH_MODE" = "relock" ]; then | |
| echo "relock=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "relock=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| echo "actions.lock agrees with every workflow." >> "$GITHUB_STEP_SUMMARY" | |
| exit 0 | |
| fi | |
| if [ "$code" -ne 1 ]; then | |
| # 1 = blocking findings (out of sync). Anything else is the tool | |
| # failing, not the repo: do not let the relock job paper over it. | |
| # Re-emit the tool's stderr as annotations — a bare exit code on a | |
| # runner nobody can read logs from is worse than a verbose diff. | |
| echo "in_sync=unknown" >> "$GITHUB_OUTPUT" | |
| echo "relock=false" >> "$GITHUB_OUTPUT" | |
| echo "::error::gh actions-lock --verify exited ${code} (tool failure, not a lockfile mismatch)" | |
| while IFS= read -r line; do | |
| if [ -n "$line" ]; then echo "::error::[gh actions-lock] ${line}"; fi | |
| done < <(tail -n 30 actions-lock-report.err | tr -d '\r' | cut -c1-200) | |
| exit "$code" | |
| fi | |
| echo "in_sync=false" >> "$GITHUB_OUTPUT" | |
| echo "relock=true" >> "$GITHUB_OUTPUT" | |
| echo "::error::actions.lock is out of step with .github/workflows — every onboarded workflow will fail with startup_failure until it is regenerated" | |
| { | |
| echo '## actions.lock is out of sync' | |
| echo | |
| echo '```json' | |
| jq . actions-lock-report.json 2>/dev/null || cat actions-lock-report.json | |
| echo '```' | |
| echo | |
| echo 'Fix locally with `gh actions-lock`, or run this workflow in `relock` mode.' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| relock: | |
| name: Regenerate actions.lock | |
| needs: verify | |
| if: always() && needs.verify.outputs.relock == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| # The write lives here, on the only job that writes. `contents: write` is | |
| # what lets the PAT-backed push land, and `pull-requests: write` is what | |
| # lets the fallback publish the regenerated file as a comment. | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| steps: | |
| - name: Decide where the fix goes | |
| id: target | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| HEAD_REF: ${{ github.event.pull_request.head.ref }} | |
| HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} | |
| REPO: ${{ github.repository }} | |
| REF: ${{ github.ref }} | |
| DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} | |
| RELOCK_TOKEN: ${{ secrets.ACTIONS_LOCK_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| if [ -n "${RELOCK_TOKEN:-}" ]; then | |
| echo "has_token=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "has_token=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| if [ "$EVENT_NAME" = "pull_request" ]; then | |
| fork=no | |
| if [ "$HEAD_REPO" != "$REPO" ]; then fork=yes; fi | |
| echo "skipped=false" >> "$GITHUB_OUTPUT" | |
| echo "fork=${fork}" >> "$GITHUB_OUTPUT" | |
| echo "branch=${HEAD_REF}" >> "$GITHUB_OUTPUT" | |
| echo "on_default=false" >> "$GITHUB_OUTPUT" | |
| else | |
| branch="${REF#refs/heads/}" | |
| echo "skipped=false" >> "$GITHUB_OUTPUT" | |
| echo "fork=no" >> "$GITHUB_OUTPUT" | |
| echo "branch=${branch}" >> "$GITHUB_OUTPUT" | |
| if [ "$branch" = "$DEFAULT_BRANCH" ]; then | |
| # main requires signed commits, so a bot commit cannot land | |
| # there even with a token: deliver the fix as a pull request. | |
| echo "on_default=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "on_default=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| fi | |
| # Egress audit: Hypatia's RE001 asks for it on any job that reaches for | |
| # secrets.*, and both jobs here pass GITHUB_TOKEN to gh. | |
| - name: Harden runner (egress audit) | |
| uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout the branch that needs the lockfile | |
| id: checkout-lock | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ steps.target.outputs.branch }} | |
| persist-credentials: false | |
| - name: Install gh actions-lock | |
| id: install | |
| run: | | |
| set -euo pipefail | |
| gh extension install github/gh-actions-lock --pin "$ACTIONS_LOCK_EXTENSION_VERSION" | |
| # NOT --relock. --relock means "bump moved branch/version refs to their | |
| # current upstream SHA", so it upgrades every ref to latest and writes a | |
| # lockfile keyed by TAG — which is the wrong shape for a repo whose | |
| # workflows are SHA-pinned. The estate's validator keys on | |
| # `owner/repo@<40-hex>` exactly as written in the workflow, so a | |
| # tag-keyed lockfile contradicts every workflow in the repo and GitHub | |
| # rejects the lot at startup with "The lockfile could not be validated." | |
| # | |
| # Plain fix mode with --no-migrate-local-actions --no-narrow is the | |
| # canonical regeneration: it keys each entry by the ref the workflow | |
| # actually names. See hyperpolymath/standards | |
| # .githooks/validate-actions-lock.sh. | |
| - name: Regenerate the lockfile | |
| id: regenerate | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -uo pipefail | |
| set +e | |
| code=0 | |
| gh actions-lock --no-migrate-local-actions --no-narrow --no-interactive \ | |
| >relock.out 2>relock.err || code=$? | |
| cat relock.out || true | |
| cat relock.err || true | |
| # A non-zero exit here does NOT mean nothing was written. The tool | |
| # writes the lockfile and then reports whatever it still objects to | |
| # (a bare SHA with no symbolic ref, say) — findings that do not | |
| # invalidate the file. "The file did not change" is the real | |
| # failure, and that is checked below. | |
| if [ "$code" -ne 0 ]; then | |
| echo "::warning::gh actions-lock exited ${code}; the regenerated lockfile is still delivered if it changed" | |
| while IFS= read -r line; do | |
| if [ -n "$line" ]; then echo "::warning::[gh actions-lock] ${line}"; fi | |
| done < <(tail -n 30 relock.err | tr -d '\r' | cut -c1-200) | |
| fi | |
| # Regenerating the lockfile must not rewrite the workflows. If it | |
| # did, the file it just wrote describes a tree that is not on disk — | |
| # name the files, then put them back and keep only the lockfile. | |
| touched="$(git status --porcelain -- '.github/workflows/*.yml' '.github/workflows/*.yaml' || true)" | |
| if [ -n "$touched" ]; then | |
| echo "::warning::gh actions-lock modified workflow files while regenerating the lockfile; they have been reverted and only actions.lock is kept" | |
| while IFS= read -r line; do | |
| if [ -n "$line" ]; then echo "::warning::[workflow touched] ${line}"; fi | |
| done <<< "$touched" | |
| git checkout -- .github/workflows || true | |
| fi | |
| if git diff --quiet -- .github/workflows/actions.lock; then | |
| echo "changed=false" >> "$GITHUB_OUTPUT" | |
| echo "::error::gh actions-lock produced no change to actions.lock — the branch stays unstartable and the file must be regenerated by hand" | |
| else | |
| echo "changed=true" >> "$GITHUB_OUTPUT" | |
| git --no-pager diff --stat -- .github/workflows/actions.lock || true | |
| fi | |
| - name: Upload the regenerated lockfile | |
| id: upload | |
| if: steps.regenerate.outputs.changed == 'true' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: actions-lock-regenerated | |
| path: .github/workflows/actions.lock | |
| if-no-files-found: error | |
| retention-days: 14 | |
| # A GitHub App cannot write under .github/workflows/ at all: `permissions:` | |
| # has no `workflows` key, and a GITHUB_TOKEN push is rejected outright with | |
| # "refusing to allow a GitHub App to create or update workflow | |
| # `.github/workflows/actions.lock` without `workflows` permission". So the | |
| # push path needs a PAT (fine-grained, Workflows + Contents write) supplied | |
| # as ACTIONS_LOCK_TOKEN. Without it the file is still delivered — as an | |
| # artefact and, on a pull request, as a comment anyone can apply. | |
| - name: Deliver the fix with a PAT | |
| id: deliver | |
| if: steps.regenerate.outputs.changed == 'true' && steps.target.outputs.has_token == 'true' | |
| env: | |
| RELOCK_TOKEN: ${{ secrets.ACTIONS_LOCK_TOKEN }} | |
| GH_TOKEN: ${{ secrets.ACTIONS_LOCK_TOKEN }} | |
| BRANCH: ${{ steps.target.outputs.branch }} | |
| run: | | |
| set -uo pipefail | |
| set +e | |
| git config user.name 'github-actions[bot]' | |
| git config user.email '41898282+github-actions[bot]@users.noreply.github.com' | |
| git remote set-url origin "https://x-access-token:${RELOCK_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" | |
| code=0 | |
| git commit --only -m "chore(ci): regenerate actions.lock (gh actions-lock --relock) | |
| The lockfile and the workflows had drifted apart. While they do, | |
| every onboarded workflow ends in startup_failure and produces no | |
| check runs at all. See game-server-admin#103." -- .github/workflows/actions.lock >commit.out 2>&1 \ | |
| || code=$? | |
| if [ "$code" -ne 0 ]; then | |
| cat commit.out || true | |
| while IFS= read -r line; do | |
| if [ -n "$line" ]; then echo "::error::[git commit] ${line}"; fi | |
| done < <(tail -n 20 commit.out | tr -d '\r' | cut -c1-200) | |
| exit 1 | |
| fi | |
| if [ "${{ steps.target.outputs.on_default }}" = "true" ]; then | |
| branch="ci/actions-lock-relock-${GITHUB_RUN_ID}" | |
| git switch --create "$branch" >/dev/null 2>&1 | |
| code=0 | |
| git push --set-upstream origin "$branch" >push.out 2>&1 || code=$? | |
| if [ "$code" -ne 0 ]; then | |
| cat push.out || true | |
| while IFS= read -r line; do | |
| if [ -n "$line" ]; then echo "::error::[git push] ${line}"; fi | |
| done < <(tail -n 20 push.out | tr -d '\r' | cut -c1-200) | |
| exit 1 | |
| fi | |
| gh pr create --base "$BRANCH" --head "$branch" \ | |
| --title 'chore(ci): regenerate actions.lock' \ | |
| --body '`gh actions-lock --verify` failed because `actions.lock` no longer matches the workflows. While they disagree, every onboarded workflow ends in `startup_failure` and produces no check runs at all. This is the output of `gh actions-lock --relock`. See game-server-admin#103.' | |
| else | |
| code=0 | |
| git push origin "HEAD:refs/heads/${BRANCH}" >push.out 2>&1 || code=$? | |
| if [ "$code" -ne 0 ]; then | |
| cat push.out || true | |
| while IFS= read -r line; do | |
| if [ -n "$line" ]; then echo "::error::[git push] ${line}"; fi | |
| done < <(tail -n 20 push.out | tr -d '\r' | cut -c1-200) | |
| exit 1 | |
| fi | |
| echo "::notice::Regenerated actions.lock pushed to ${BRANCH}; this pull request will re-verify on the next run." | |
| fi | |
| - name: Publish the regenerated lockfile on the pull request | |
| id: publish | |
| if: always() && steps.regenerate.outputs.changed == 'true' && github.event_name == 'pull_request' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| RELOCK_TOKEN: ${{ secrets.ACTIONS_LOCK_TOKEN }} | |
| run: | | |
| set -uo pipefail | |
| set +e | |
| body="$(mktemp)" | |
| { | |
| echo '## regenerated `.github/workflows/actions.lock`' | |
| echo | |
| echo '`gh actions-lock --verify` failed on this pull request: the lockfile and the workflows disagree, so every onboarded workflow ends in `startup_failure` and produces no check runs at all.' | |
| echo | |
| if [ -n "${RELOCK_TOKEN:-}" ]; then | |
| echo 'The workflow tried to deliver this automatically; see the `deliver` step for whether it landed.' | |
| else | |
| echo 'Automatic delivery is not configured: a GitHub App cannot write under `.github/workflows/` (`permissions:` has no `workflows` key), so pushing the fix needs `ACTIONS_LOCK_TOKEN` — a fine-grained PAT with Contents and Workflows write. Until that secret exists, apply the file below by hand:' | |
| echo | |
| echo '```sh' | |
| echo 'gh run download ${{ github.run_id }} -n actions-lock-regenerated' | |
| echo 'mv actions.lock .github/workflows/actions.lock && gh actions-lock --verify' | |
| echo '```' | |
| fi | |
| echo | |
| echo '<details><summary>actions.lock</summary>' | |
| echo | |
| echo '```yaml' | |
| cat .github/workflows/actions.lock | |
| echo '```' | |
| echo | |
| echo '</details>' | |
| } > "$body" | |
| gh pr comment "$PR_NUMBER" --body-file "$body" | |
| - name: Diagnostics | |
| if: always() | |
| run: | | |
| set -uo pipefail | |
| set +e | |
| echo "--- git state ---" | |
| git --no-pager log --oneline -1 || true | |
| git status --short || true | |
| outcomes="target=${{ steps.target.outcome }} checkout=${{ steps.checkout-lock.outcome }} install=${{ steps.install.outcome }} regenerate=${{ steps.regenerate.outcome }} upload=${{ steps.upload.outcome }} deliver=${{ steps.deliver.outcome }} publish=${{ steps.publish.outcome }}" | |
| echo "::notice::relock outcomes — ${outcomes}" | |
| { | |
| echo '## Regenerate actions.lock — step outcomes' | |
| echo | |
| echo '```' | |
| echo "${outcomes}" | |
| echo '```' | |
| } >> "$GITHUB_STEP_SUMMARY" |