From e85630bf2fdd45ca7edfdb8cfd28be2b3e2c6483 Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Fri, 2 Oct 2026 13:35:14 -0700 Subject: [PATCH 1/8] ci: pin actions to commit hashes A version tag (even a "full" one like v7.0.1) is mutable: whoever controls the action's repository can move it to point to any other commit. Pinning to a full commit hash is the only way to get the exact same action code on every run. Found by zizmor's unpinned-uses audit. Dependabot is already enabled for the github-actions ecosystem and knows how to update hash pins together with their version comments, so this should not add maintenance burden. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Kir Kolyshkin --- .github/workflows/test.yml | 10 +++++----- .github/workflows/validate.yml | 16 ++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 77de70c..4472e14 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,8 +28,8 @@ jobs: runs-on: ubuntu-26.04 steps: - - uses: actions/checkout@v7 - - uses: actions/setup-go@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: ${{ matrix.go-version }} go-version-file: go.mod # Take version from go.mod when go-version is empty. @@ -40,12 +40,12 @@ jobs: timeout-minutes: 20 runs-on: ubuntu-26.04 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: lima-vm/lima-actions/setup@v1 + - uses: lima-vm/lima-actions/setup@55627e31b78637bf254a8b2a14da8ea7d12564e5 # v1.1.0 id: lima-actions-setup - - uses: actions/cache@v6 + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.cache/lima key: lima-${{ steps.lima-actions-setup.outputs.version }} diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 71accb3..d07e241 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -22,13 +22,13 @@ jobs: checks: write # to allow the action to annotate code in the PR. runs-on: ubuntu-26.04 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 2 - - uses: actions/setup-go@v7 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: stable - - uses: golangci/golangci-lint-action@v9 + - uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0 # Extra linters, only checking new code from a pull request. - name: lint-extra if: github.event_name == 'pull_request' @@ -38,7 +38,7 @@ jobs: codespell: runs-on: ubuntu-26.04 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: install deps # Use a known version of codespell. run: pip install --break-system-packages codespell==v2.4.3 @@ -48,14 +48,14 @@ jobs: space-at-eol: runs-on: ubuntu-26.04 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - run: if git -P grep -I -n '\s$'; then echo "^^^ extra whitespace at EOL, please fix"; exit 1; fi deps: runs-on: ubuntu-26.04 steps: - - uses: actions/checkout@v7 - - uses: actions/setup-go@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: stable - run: go mod tidy --diff @@ -63,7 +63,7 @@ jobs: govulncheck: runs-on: ubuntu-26.04 steps: - - uses: golang/govulncheck-action@v1 + - uses: golang/govulncheck-action@032d45514ae346b1db93c04b0c90b841c370344f # v1.1.0 all-done: needs: From 741d76f78e2927fdce42774bcf3dc676fd585877 Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Fri, 2 Oct 2026 13:35:30 -0700 Subject: [PATCH 2/8] ci: harden workflows as suggested by zizmor Set persist-credentials: false for actions/checkout, as applied by "zizmor --fix". By default checkout leaves the credentials it used in .git/config, where any later step (or anything that archives the workspace) can pick them up. Nothing here pushes back to the repository, so they are not needed. The remaining findings are all cache-poisoning, and are ignored via .github/zizmor.yml, which explains why they do not apply to us. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Kir Kolyshkin --- .github/workflows/test.yml | 4 ++++ .github/workflows/validate.yml | 7 +++++++ .github/zizmor.yml | 24 ++++++++++++++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 .github/zizmor.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4472e14..ee371c5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,6 +29,8 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: ${{ matrix.go-version }} @@ -41,6 +43,8 @@ jobs: runs-on: ubuntu-26.04 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - uses: lima-vm/lima-actions/setup@55627e31b78637bf254a8b2a14da8ea7d12564e5 # v1.1.0 id: lima-actions-setup diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index d07e241..6d5fae2 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -25,6 +25,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 2 + persist-credentials: false - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: stable @@ -39,6 +40,8 @@ jobs: runs-on: ubuntu-26.04 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: install deps # Use a known version of codespell. run: pip install --break-system-packages codespell==v2.4.3 @@ -49,12 +52,16 @@ jobs: runs-on: ubuntu-26.04 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - run: if git -P grep -I -n '\s$'; then echo "^^^ extra whitespace at EOL, please fix"; exit 1; fi deps: runs-on: ubuntu-26.04 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: stable diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 0000000..43bd811 --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,24 @@ +# Configuration for zizmor (https://docs.zizmor.sh/), run by the zizmor job +# in .github/workflows/validate.yml. + +rules: + cache-poisoning: + # The audit fires because both workflows are also triggered by pushing a + # v* tag, so anything they cache is, in zizmor's view, a possible input to + # a release build. That does not hold here: + # + # - This is a Go library, and no job in any workflow builds or publishes + # release artifacts. A tag push merely runs the same tests and linters. + # + # - GitHub scopes caches per branch: a pull request can read the default + # branch's cache, but cannot write to it. So a pull request cannot plant + # an entry that a later push or tag build would restore. + # + # The alternative (cache: false everywhere, lookup-only for the lima cache) + # makes every Go job re-download modules and start from a cold build cache, + # and every cgroup v1 job re-download the VM image, for no gain. + # + # Revisit this if a job ever starts publishing artifacts. + ignore: + - test.yml + - validate.yml From 93082f8c8df77225a792d5c5adea979dc17c2482 Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Fri, 2 Oct 2026 13:35:30 -0700 Subject: [PATCH 3/8] dependabot: add cooldown Wait for 7 days before proposing an update, so that a compromised or broken release has a chance to be noticed and pulled before it lands here. As recommended by zizmor's dependabot-cooldown audit. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Kir Kolyshkin --- .github/dependabot.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 807baca..d770afb 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,3 +8,5 @@ updates: directory: "/" schedule: interval: "daily" + cooldown: + default-days: 7 From 2e8eb472fe240eef82af91dd8101e3c125230672 Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Fri, 2 Oct 2026 13:35:46 -0700 Subject: [PATCH 4/8] ci: add zizmor job zizmor is a static analysis tool for GitHub Actions workflows, focused on security issues. Use the official action, which runs zizmor from a digest-pinned container image. A few non-default settings: - version: pin it, so a new zizmor release adding new audits does not suddenly fail CI on an unrelated pull request. Bumping it is then a deliberate change. - advanced-security: false + annotations: true. The default uploads SARIF to the repository's security tab, which needs the job to have security-events: write. Inline annotations are enough for us and keep the workflow at contents: read. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Kir Kolyshkin --- .github/workflows/validate.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 6d5fae2..87f7af6 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -72,6 +72,18 @@ jobs: steps: - uses: golang/govulncheck-action@032d45514ae346b1db93c04b0c90b841c370344f # v1.1.0 + zizmor: + runs-on: ubuntu-26.04 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: zizmorcore/zizmor-action@53290badfa369cb0bf7ad16342cb62c2fa47ac7b # v0.8.0 + with: + version: 1.31.1 + advanced-security: false + annotations: true + all-done: needs: - codespell @@ -79,6 +91,7 @@ jobs: - govulncheck - lint - space-at-eol + - zizmor runs-on: ubuntu-26.04 steps: - run: echo "All jobs completed" From 9cc384d91eb63667d60f965ed3464b84b7e69ea5 Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Fri, 2 Oct 2026 13:35:46 -0700 Subject: [PATCH 5/8] ci: add actionlint job Debugging GHA yaml is not fun -- usually when there is an issue with a workflow file, it just doesn't run. Add a separate actionlint workflow to catch workflow issues. It is deliberately a separate workflow, so that it still runs when another workflow file is broken. Use the actively maintained kjanat/actionlint fork of rhysd/actionlint, which knows about the newer GitHub-hosted runner images (such as ubuntu-26.04 used here), and ships its own action. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Kir Kolyshkin --- .github/workflows/actionlint.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/actionlint.yml diff --git a/.github/workflows/actionlint.yml b/.github/workflows/actionlint.yml new file mode 100644 index 0000000..43a9ae9 --- /dev/null +++ b/.github/workflows/actionlint.yml @@ -0,0 +1,30 @@ +# This is deliberately a separate workflow -- so that it still runs when +# another workflow file is broken. +name: actionlint +on: + push: + tags: + - v* + branches: + - main + - release-* + pull_request: + +permissions: + contents: read + +jobs: + actionlint: + runs-on: ubuntu-26.04 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: kjanat/actionlint@722799fa4b8cc6734debb97bae24aca9e0dbaf59 # v1.17 + + all-done-actionlint: + needs: + - actionlint + runs-on: ubuntu-26.04 + steps: + - run: echo "All jobs completed" From ac37840bc744bbf86622315cc1809f4d2f697c8c Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Fri, 2 Oct 2026 13:35:54 -0700 Subject: [PATCH 6/8] ci: drop unneeded pull-requests permission from lint job The lint job does not need pull-requests: read: golangci-lint-action only uses it for its only-new-issues option, which we do not set. The lint-extra step does its own "new code only" filtering with --new-from-rev=HEAD~1, which is plain git against the fetch-depth: 2 checkout, and the PR annotations come from checks: write. Found by zizmor's undocumented-permissions audit (--persona=pedantic). Co-Authored-By: Claude Opus 5.5 Signed-off-by: Kir Kolyshkin --- .github/workflows/validate.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 87f7af6..9e5618c 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -18,7 +18,6 @@ jobs: timeout-minutes: 30 permissions: contents: read - pull-requests: read checks: write # to allow the action to annotate code in the PR. runs-on: ubuntu-26.04 steps: From a3bd0b805bcee42609461adbcbe1070bae0552d4 Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Fri, 2 Oct 2026 13:36:00 -0700 Subject: [PATCH 7/8] ci: add concurrency limits Cancel CI runs that are already superseded, rather than letting them run to completion and waste runner time. The concurrency group is keyed by PR number, so only runs of the same pull request cancel each other. For anything else -- in particular pushes to main, to release-*, to a v* tag, and scheduled runs -- there is no PR number, so the group falls back to the unique run_id and no run is ever cancelled. Keying by github.head_ref instead would be wrong, as two pull requests from different forks can use the same branch name. Found by zizmor's concurrency-limits audit (--persona=pedantic). Co-Authored-By: Claude Opus 5.5 Signed-off-by: Kir Kolyshkin --- .github/workflows/actionlint.yml | 7 +++++++ .github/workflows/test.yml | 7 +++++++ .github/workflows/validate.yml | 7 +++++++ 3 files changed, 21 insertions(+) diff --git a/.github/workflows/actionlint.yml b/.github/workflows/actionlint.yml index 43a9ae9..4fa35c2 100644 --- a/.github/workflows/actionlint.yml +++ b/.github/workflows/actionlint.yml @@ -10,6 +10,13 @@ on: - release-* pull_request: +# Cancel superseded runs of the same pull request. For everything else +# (in particular, pushes to a branch or a tag) there is no PR number, so +# the group falls back to the unique run_id and nothing is cancelled. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + permissions: contents: read diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ee371c5..e0399a2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,6 +10,13 @@ on: schedule: # Runs at 00:00 UTC every Monday - cron: '0 0 * * 1' +# Cancel superseded runs of the same pull request. For everything else +# (in particular, pushes to a branch or a tag) there is no PR number, so +# the group falls back to the unique run_id and nothing is cancelled. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + permissions: contents: read diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 9e5618c..bd005c7 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -10,6 +10,13 @@ on: schedule: # Runs at 00:00 UTC every Monday - cron: '0 0 * * 1' +# Cancel superseded runs of the same pull request. For everything else +# (in particular, pushes to a branch or a tag) there is no PR number, so +# the group falls back to the unique run_id and nothing is cancelled. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + permissions: contents: read From 9585c2290296cd6f85b3ff64da0d3a950c829f2b Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Fri, 2 Oct 2026 13:38:18 -0700 Subject: [PATCH 8/8] ci: make validate job names unique Rename all-done job in validate.yml to all-done-validate, so it does not collide with the all-done job in test.yml in CI summaries and UI output when both workflows are triggered together. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Kir Kolyshkin --- .github/workflows/validate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index bd005c7..9360f14 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -90,7 +90,7 @@ jobs: advanced-security: false annotations: true - all-done: + all-done-validate: needs: - codespell - deps