Skip to content

Refresh the Nix vendorHash on Dependabot Go bumps - #19

Merged
jeremy merged 5 commits into
mainfrom
dependabot-sync-nix-vendor-hash
Sep 10, 2026
Merged

Refresh the Nix vendorHash on Dependabot Go bumps#19
jeremy merged 5 commits into
mainfrom
dependabot-sync-nix-vendor-hash

Conversation

@jeremy

@jeremy jeremy commented Sep 10, 2026

Copy link
Copy Markdown
Member

Dependabot's Go bumps change go.sum and leave the flake's vendorHash in nix/package.nix stale, so the caller's "Nix flake builds" check fails with a fixed-output hash mismatch. Where that check is required the PR wedges until a human runs make update-nix-hash with Docker (hey-cli #427 today); where it is not, auto-merge lands the PR red and main's next flake build is broken until someone notices (basecamp-cli #697 → #701 this morning). This adds a reusable workflow that refreshes the hash on the Dependabot PR itself, so the PR's own Nix check goes green and main is never broken.

How it works

Two jobs on separate runners:

  • computecontents: read, no secrets. Builds the flake at the PR's merge commit (what the Nix check builds) and, only when Nix reports a fixed-output mismatch for the *-go-modules.drv derivation, takes the SRI hash from that diagnostic's got: line. A build that fails for any other reason fails the job with Nix's own output and nothing downstream runs. The classifier is scripts/extract-nix-vendor-hash.sh in this repo (derived from hey-cli's), embedded by heredoc the way the sync-actions-comments updater is: check-embedded-sync.sh keeps the copy identical, and extract-nix-vendor-hash.test.mjs holds the fixtures (the real go-modules mismatch, another fixed-output derivation's mismatch before and after it, a bare got:, malformed SRI values). Before any of that, compute lists the PR's changed files and refuses anything outside go.mod, go.sum and the package file — the "Go bumps only" boundary is enforced on the diff itself, dispatch included — and is a no-op for a PR touching neither go.mod nor go.sum.
  • push — runs only when compute produced a hash. Mints a one-hour GitHub App installation token scoped to the calling repository with contents: write, fetches the package file at the captured head, rewrites exactly the vendorHash = "..." line, proves the diff is that one line (one removed, one added, both vendorHash lines — a rewrite that touched anything else refuses), and commits through GraphQL createCommitOnBranch with expectedHeadOid set to the captured head. That is a compare-and-swap: a Dependabot rebase mid-run makes the commit fail cleanly and the rebase's own run recomputes. No clone and no git credential ever touch a runner; the token exists only in this job, which runs no build.

An App push is a normal push, so the caller's full pull_request CI re-runs on the new head — the Nix check there is the verification that the refreshed hash builds. (A GITHUB_TOKEN push would not re-trigger anything, and the required checks would never report on the new head.) The re-triggered run is actored by the App bot, not dependabot[bot], so the actor guard ends the loop. workflow_dispatch with a PR number re-runs the refresh by hand; the PR still has to be an open, same-repo, Dependabot-authored one.

Why this is sound for Go bumps when it was abandoned for actions bumps (#11)

After any non-Dependabot push, the re-triggered runs are actored by the pusher and see the caller's Actions secrets instead of the Dependabot sandbox. For an actions bump the PR's freshly bumped, unreviewed workflow code would run with those secrets — the reason #11 moved to post-merge repair. For a Go bump the workflow code that runs is the default branch's (Dependabot does not touch workflows; the commit pushed here is proven to touch one line of nix/package.nix). The unreviewed content is Go dependency code inside reviewed jobs, which the Test workflow already executes on the same head. So the lift exposes exactly the secrets those reviewed pull_request jobs reference. I audited both callers: hey-cli references none outside the actor-gated auto-merge job; basecamp-cli's only one (ANTHROPIC_API_KEY) sits in a job path-gated to skills/ changes a Go bump never makes. That invariant is written into the header for the next reader. The App token grants the power every write-access member already has: the default branch is ruleset-protected and the App is not a bypass actor. No pull_request_target.

Ordering against auto-merge

Auto-merge waits for required checks only, and this build takes minutes, so a caller has to make the refresh land first; this workflow cannot do that on its own. The contract gives the two ways: keep "Nix flake builds" a required check (hey-cli — the stale head stays red until the push, and the App push, a write-access actor's, leaves auto-merge enabled for the new head), or sequence auto-merge after this workflow in one file with needs: (basecamp-cli, basecamp/basecamp-cli#702 — the changed-files boundary makes non-Go bumps a no-op, so the caller runs it on every Dependabot PR with no path filter). A base branch whose go.sum moves after the push is the exposure every PR's checks have, and only the required check holds against it; the head-only expectedHeadOid lease claims no more than a clean failure on a Dependabot rebase mid-run.

Alternatives weighed

  • Per-repo copies: duplicates the credentialed push logic against the fleet's thin-caller pattern.
  • Removing the hash (vendorHash = null with a committed vendor/ tree that Dependabot keeps in sync): the only option that removes the class outright, at the cost of vendoring third-party code into each repo and changing every developer's build. A product call, recorded rather than taken.
  • Making the check required everywhere: turns basecamp-cli's "merges red" into hey-cli's "wedges"; a human with Docker still refreshes the hash weekly. Worth doing for basecamp-cli regardless, once this lands.
  • The exact house post-merge repair: leaves the Dependabot PR red, and hey-cli's required check may not be weakened.

Rollout

Callers follow, pinned to this branch's SHA and to be re-pinned after this merges: basecamp/hey-cli#432 and basecamp/basecamp-cli — the only fleet repos with a flake. Each caller needs the cli-release-bot App installed on the repository with contents: write, and its private key stored as a repository Dependabot secret RELEASE_APP_PRIVATE_KEY (basecamp-cli has it; hey-cli does not) plus, for the dispatch path, a repository Actions secret of the same name. The copies release.yml uses are scoped to the release environment and unreachable elsewhere (a probe on the hey-cli branch confirmed vars.RELEASE_CLIENT_ID is empty there), which is also why the client id is a literal input rather than a vars lookup.

actionlint 1.7.12 and zizmor 1.30.0 (regular persona, online) are clean; the one ignore is the same bot-conditions dual-check reasoning the auto-merge workflows carry.

A Dependabot Go bump changes go.sum and leaves the flake's vendorHash in
nix/package.nix stale, so the caller's "Nix flake builds" check fails with a
fixed-output hash mismatch: the PR wedges where the check is required
(hey-cli #427) and merges red where it is not, breaking main's next flake
build (basecamp-cli #697, repaired by #701). This reusable workflow refreshes
the hash on the Dependabot PR itself.

A compute job with no secrets builds the flake at the PR's merge commit and
takes the hash only from a go-modules fixed-output mismatch; a separate push
job mints a one-hour App token scoped to the calling repository, rewrites
exactly the vendorHash line, proves the diff is that one line, and commits
through createCommitOnBranch with the captured head as expectedHeadOid. The
App push re-triggers the PR's pull_request CI, whose Nix check verifies the
new hash; the re-triggered run is actored by the App bot, so the Dependabot
actor guard ends the loop. Every other path fails closed with nothing pushed.

The header records why pushing into Dependabot PRs is sound for Go bumps
even though it was abandoned for actions bumps (#11): the workflow code that
re-runs is the default branch's, and the callers' pull_request jobs reference
no secret a Go bump can reach.
Copilot AI balanced review requested due to automatic review settings September 10, 2026 19:35
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-10T21:52:54.893522Z 49eaa21 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Comment thread .github/workflows/dependabot-sync-nix-vendor-hash.yml Fixed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Auto-merge can precede refresh, and the head-only lease does not protect against merge-base changes.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds a reusable workflow to refresh stale Nix vendorHash values on Dependabot Go PRs.

Changes:

  • Computes hashes from Nix merge builds.
  • Commits guarded one-line updates using a GitHub App token.
  • Verifies PR CI was retriggered.

[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

File summaries
File Description
.github/workflows/dependabot-sync-nix-vendor-hash.yml Implements hash computation, validation, and guarded commit workflow.
Review details
  • Files reviewed: 1/1 changed files
  • Comments generated: 3
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread .github/workflows/dependabot-sync-nix-vendor-hash.yml
Comment thread .github/workflows/dependabot-sync-nix-vendor-hash.yml Outdated
Comment thread .github/workflows/dependabot-sync-nix-vendor-hash.yml
The caller's path filter is an inclusion filter and workflow_dispatch
bypasses it, so the reusable workflow now decides "Go bump" from the PR's
changed files: anything outside go.mod, go.sum and the package file is
refused before Nix is installed, and a PR touching neither go.mod nor
go.sum is a no-op. That lets a caller run it on every Dependabot PR and
sequence auto-merge after it, which is the second of the two ways a caller
keeps a stale head from merging first; the header now states both.

The classifier moves out of the run step into scripts/, embedded by
heredoc like the sync-actions-comments updater, with fixtures for the
go-modules mismatch, another fixed-output derivation before and after
it, a bare `got:`, and malformed SRI values. Writing the fixtures found
that a go-modules block without a `got:` line would take the next
derivation's, so a mismatch for any other derivation now closes the
block. check-embedded-sync.sh covers both embeddings.
Comment thread .github/workflows/dependabot-sync-nix-vendor-hash.yml Dismissed
At 37d260a the changed-files allowlist ran before the "is this a Go bump"
decision, so an actions- or docker-only Dependabot PR was refused with an
error rather than being the no-op the header promised. A caller that
sequences auto-merge after this workflow with `needs:` would then lose
auto-merge for docker bumps and show a failing job on every non-Go PR. The
no-op decision now comes first; the allowlist applies to Go bumps, where it
still refuses a mixed PR whoever dispatched it.

The lease note stops claiming a required check holds the hash against a base
whose go.sum moves after the build: required checks are not re-run when the
base moves, so two Go bumps merging on the same day leave the second with a
stale hash whatever the ruleset. That is the exposure every PR has to a
moving base; the structural closure is a post-merge repair on main.
@jeremy
jeremy merged commit 293dc74 into main Sep 10, 2026
5 checks passed
@jeremy
jeremy deleted the dependabot-sync-nix-vendor-hash branch September 10, 2026 22:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants