From 1d990965b25d08e40d82f96ea533bf068d250175 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Thu, 10 Sep 2026 12:41:17 -0700 Subject: [PATCH 1/3] Refresh the Nix vendorHash on Dependabot Go bumps #697 changed go.sum, nix/package.nix's vendorHash stayed put, and because "Nix flake builds" is not a required check here the PR auto-merged red and main's flake was broken until #701 refreshed the hash by hand. This thin caller runs basecamp/.github's reusable workflow on Dependabot PRs that touch go.mod or go.sum: it builds the flake at the PR's merge commit, takes the corrected hash from Nix's go-modules fixed-output mismatch, and commits exactly that one line back to the PR with a one-hour App token, so the PR's own Nix check verifies it before auto-merge lands it. Pinned to the reusable workflow's branch SHA; re-pin once it merges. --- .../dependabot-sync-nix-vendor-hash.yml | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .github/workflows/dependabot-sync-nix-vendor-hash.yml diff --git a/.github/workflows/dependabot-sync-nix-vendor-hash.yml b/.github/workflows/dependabot-sync-nix-vendor-hash.yml new file mode 100644 index 00000000..7fb54f76 --- /dev/null +++ b/.github/workflows/dependabot-sync-nix-vendor-hash.yml @@ -0,0 +1,54 @@ +name: Refresh Nix vendorHash on Dependabot PRs + +# A Dependabot Go bump changes go.sum and leaves nix/package.nix's vendorHash +# stale, so "Nix flake builds" fails with a fixed-output hash mismatch. It is +# not a required check here, so #697 auto-merged red and main's flake was +# broken until #701 refreshed the hash by hand. This refreshes it on the PR +# itself, before the merge. +# +# The SHA-pinned reusable workflow builds the flake at the PR's merge commit, +# takes the corrected hash from Nix's go-modules fixed-output mismatch, and +# commits exactly that one line of nix/package.nix back to the PR with a +# one-hour cli-release-bot token scoped to this repository, so the PR's own +# "Nix flake builds" check re-runs on the new head and verifies it. Go bumps +# only: the pull_request jobs that then re-run under the App bot's actor are +# main's own, and none of them references a secret a Go bump can reach — keep +# it that way. The full actor analysis is in the reusable workflow's header. +# +# Needs the cli-release-bot App installed here with contents: write, and its +# private key stored as a repository *Dependabot* secret named +# RELEASE_APP_PRIVATE_KEY (Dependabot-triggered runs see only Dependabot +# secrets) and, for the workflow_dispatch path, as a repository Actions secret +# of the same name. release.yml's copies live in the `release` environment +# and are not reachable from here, by design. + +on: + pull_request: + paths: + - go.mod + - go.sum + workflow_dispatch: + inputs: + pr: + description: Dependabot pull request number to refresh + required: true + type: string + +permissions: {} + +jobs: + refresh: + if: github.event_name == 'workflow_dispatch' || (github.actor == 'dependabot[bot]' && github.event.pull_request.user.login == 'dependabot[bot]') # zizmor: ignore[bot-conditions] -- dual check: actor validates the current trigger (and stops the App bot's own push from looping back), user.login validates PR origin; on:pull_request, not pull_request_target, so GitHub sets the actor from who pushed + uses: basecamp/.github/.github/workflows/dependabot-sync-nix-vendor-hash.yml@12f8950fc925285f3dd5bce2e5cc9ea8b7f72da6 + with: + pr: ${{ inputs.pr }} + # cli-release-bot's client id — the `release` environment's + # RELEASE_CLIENT_ID, which is a public identifier (every release run + # prints it) and, being environment-scoped, is not readable here. + app-client-id: Iv23liAbERu6z8d7t5A8 + permissions: + contents: read + pull-requests: read + actions: read + secrets: + app-private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} From 101fad7f8c6732783b9dd52a551971a25e3ced2a Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Thu, 10 Sep 2026 14:42:07 -0700 Subject: [PATCH 2/3] Sequence Dependabot auto-merge after the vendorHash refresh Auto-merge waits for required checks only, and "Nix flake builds" is not one here, so a stand-alone refresh workflow raced the merge: the required checks could finish and land the stale head minutes before the push, which would then reach a closed PR. The refresh now lives in the auto-merge workflow and the auto-merge job `needs:` it, so `gh pr merge --auto` runs only once the hash is current or the push has landed, and not at all when the refresh failed. The reusable workflow decides "Go bump" from the PR's changed files and is a no-op otherwise, so the path filter goes away and an actions bump can never receive the push; re-pinned to the basecamp/.github head that enforces that. --- .github/workflows/dependabot-auto-merge.yml | 63 ++++++++++++++++++- .../dependabot-sync-nix-vendor-hash.yml | 54 ---------------- 2 files changed, 61 insertions(+), 56 deletions(-) delete mode 100644 .github/workflows/dependabot-sync-nix-vendor-hash.yml diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index a7a90403..eac3fa72 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -1,16 +1,75 @@ name: Dependabot Auto-Merge -on: pull_request +# Approves and enables auto-merge for Dependabot's patch and minor bumps, after +# refreshing the flake's vendorHash when the bump moved go.sum. +# +# A Go bump changes go.sum and leaves nix/package.nix's vendorHash stale, so +# "Nix flake builds" fails with a fixed-output hash mismatch. It is not a +# required check here, so #697 auto-merged red and main's flake was broken +# until #701 refreshed the hash by hand. The refresh job runs the SHA-pinned +# reusable workflow on the PR before auto-merge is enabled: it builds the +# flake at the PR's merge commit, takes the corrected hash from Nix's +# go-modules fixed-output mismatch, and commits exactly that one line of +# nix/package.nix back to the PR with a one-hour cli-release-bot token scoped +# to this repository, so the PR's own "Nix flake builds" check re-runs on the +# new head and verifies it. The auto-merge job `needs:` it — the ordering is +# structural, not a race against the required checks — so `gh pr merge --auto` +# runs only once the hash is current or the push has landed, and not at all +# when the refresh failed; that PR waits for a human. The App push is a +# write-access actor's, so auto-merge stays enabled for the new head. +# +# The reusable workflow decides "Go bump" from the PR's changed files +# (go.mod, go.sum and nix/package.nix, nothing else) and is a no-op for any +# other Dependabot PR, which is why this workflow needs no path filter and +# why an actions bump can never receive the push. Go bumps only: the +# pull_request jobs that re-run under the App bot's actor are main's own, and +# none of them references a secret a Go bump can reach — keep it that way. +# The full actor analysis is in the reusable workflow's header. +# +# Needs the cli-release-bot App installed here with contents: write, and its +# private key stored as a repository *Dependabot* secret named +# RELEASE_APP_PRIVATE_KEY (Dependabot-triggered runs see only Dependabot +# secrets) and, for the workflow_dispatch path, as a repository Actions secret +# of the same name. release.yml's copies live in the `release` environment +# and are not reachable from here, by design. + +on: + pull_request: + workflow_dispatch: + inputs: + pr: + description: Dependabot pull request number whose vendorHash to refresh + required: true + type: string permissions: {} jobs: + refresh-nix-vendor-hash: + name: Refresh the Nix vendorHash + if: github.event_name == 'workflow_dispatch' || (github.actor == 'dependabot[bot]' && github.event.pull_request.user.login == 'dependabot[bot]') # zizmor: ignore[bot-conditions] -- dual check: actor validates the current trigger (and stops the App bot's own push from looping back), user.login validates PR origin; on:pull_request, not pull_request_target, so GitHub sets the actor from who pushed + uses: basecamp/.github/.github/workflows/dependabot-sync-nix-vendor-hash.yml@37d260ae0bb28f0710e6281b87a3422ae5687494 + with: + pr: ${{ inputs.pr }} + # cli-release-bot's client id — the `release` environment's + # RELEASE_CLIENT_ID, which is a public identifier (every release run + # prints it) and, being environment-scoped, is not readable here. + app-client-id: Iv23liAbERu6z8d7t5A8 + permissions: + contents: read + pull-requests: read + actions: read + secrets: + app-private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + auto-merge: + name: Auto-merge runs-on: ubuntu-latest + needs: refresh-nix-vendor-hash permissions: contents: write pull-requests: write - if: github.actor == 'dependabot[bot]' && github.event.pull_request.user.login == 'dependabot[bot]' # zizmor: ignore[bot-conditions] -- dual check is intentional: actor validates current trigger, user.login validates PR origin; on:pull_request (not pull_request_target) so actor is set by GitHub based on who pushed + if: github.event_name == 'pull_request' && github.actor == 'dependabot[bot]' && github.event.pull_request.user.login == 'dependabot[bot]' # zizmor: ignore[bot-conditions] -- dual check is intentional: actor validates current trigger, user.login validates PR origin; on:pull_request (not pull_request_target) so actor is set by GitHub based on who pushed steps: - name: Fetch Dependabot metadata id: metadata diff --git a/.github/workflows/dependabot-sync-nix-vendor-hash.yml b/.github/workflows/dependabot-sync-nix-vendor-hash.yml deleted file mode 100644 index 7fb54f76..00000000 --- a/.github/workflows/dependabot-sync-nix-vendor-hash.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Refresh Nix vendorHash on Dependabot PRs - -# A Dependabot Go bump changes go.sum and leaves nix/package.nix's vendorHash -# stale, so "Nix flake builds" fails with a fixed-output hash mismatch. It is -# not a required check here, so #697 auto-merged red and main's flake was -# broken until #701 refreshed the hash by hand. This refreshes it on the PR -# itself, before the merge. -# -# The SHA-pinned reusable workflow builds the flake at the PR's merge commit, -# takes the corrected hash from Nix's go-modules fixed-output mismatch, and -# commits exactly that one line of nix/package.nix back to the PR with a -# one-hour cli-release-bot token scoped to this repository, so the PR's own -# "Nix flake builds" check re-runs on the new head and verifies it. Go bumps -# only: the pull_request jobs that then re-run under the App bot's actor are -# main's own, and none of them references a secret a Go bump can reach — keep -# it that way. The full actor analysis is in the reusable workflow's header. -# -# Needs the cli-release-bot App installed here with contents: write, and its -# private key stored as a repository *Dependabot* secret named -# RELEASE_APP_PRIVATE_KEY (Dependabot-triggered runs see only Dependabot -# secrets) and, for the workflow_dispatch path, as a repository Actions secret -# of the same name. release.yml's copies live in the `release` environment -# and are not reachable from here, by design. - -on: - pull_request: - paths: - - go.mod - - go.sum - workflow_dispatch: - inputs: - pr: - description: Dependabot pull request number to refresh - required: true - type: string - -permissions: {} - -jobs: - refresh: - if: github.event_name == 'workflow_dispatch' || (github.actor == 'dependabot[bot]' && github.event.pull_request.user.login == 'dependabot[bot]') # zizmor: ignore[bot-conditions] -- dual check: actor validates the current trigger (and stops the App bot's own push from looping back), user.login validates PR origin; on:pull_request, not pull_request_target, so GitHub sets the actor from who pushed - uses: basecamp/.github/.github/workflows/dependabot-sync-nix-vendor-hash.yml@12f8950fc925285f3dd5bce2e5cc9ea8b7f72da6 - with: - pr: ${{ inputs.pr }} - # cli-release-bot's client id — the `release` environment's - # RELEASE_CLIENT_ID, which is a public identifier (every release run - # prints it) and, being environment-scoped, is not readable here. - app-client-id: Iv23liAbERu6z8d7t5A8 - permissions: - contents: read - pull-requests: read - actions: read - secrets: - app-private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} From d968679dded3075333f27a44d01132686595bd17 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Thu, 10 Sep 2026 14:48:45 -0700 Subject: [PATCH 3/3] Pin the vendorHash refresh at the revision that makes non-Go bumps a no-op --- .github/workflows/dependabot-auto-merge.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index eac3fa72..1d6180d2 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -48,7 +48,7 @@ jobs: refresh-nix-vendor-hash: name: Refresh the Nix vendorHash if: github.event_name == 'workflow_dispatch' || (github.actor == 'dependabot[bot]' && github.event.pull_request.user.login == 'dependabot[bot]') # zizmor: ignore[bot-conditions] -- dual check: actor validates the current trigger (and stops the App bot's own push from looping back), user.login validates PR origin; on:pull_request, not pull_request_target, so GitHub sets the actor from who pushed - uses: basecamp/.github/.github/workflows/dependabot-sync-nix-vendor-hash.yml@37d260ae0bb28f0710e6281b87a3422ae5687494 + uses: basecamp/.github/.github/workflows/dependabot-sync-nix-vendor-hash.yml@49eaa2156461cee5ac15c5236d3ff3f5fe900d3f with: pr: ${{ inputs.pr }} # cli-release-bot's client id — the `release` environment's