Skip to content

[COVAL-7631] Open a Homebrew tap pull request instead of pushing to main - #136

Merged
callumreid merged 4 commits into
mainfrom
fix/coval-7631-homebrew-tap-pr
Oct 15, 2026
Merged

callumreid merged 4 commits into
mainfrom
fix/coval-7631-homebrew-tap-pr

Conversation

@callumreid

@callumreid callumreid commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Problem

update-homebrew pushed Formula/coval.rb straight to coval-ai/homebrew-tap main. Three active rulesets on the tap now require a pull request for every change to main, so the push is rejected with GH013: Repository rule violations ... Changes must be made through a pull request. The three rebase-and-retry attempts cannot help.

v0.9.0 failed this way and was recovered by hand in coval-ai/homebrew-tap#7. v0.9.1 failed the same way on 2026-10-14 (release run 38583417559) and was recovered in coval-ai/homebrew-tap#8. Each failure reopens #133.

Change

  • The tap update opens a pull request. The job renders the formula, pushes it to release/coval-vX.Y.Z with HOMEBREW_TAP_TOKEN, then opens a PR against main, or updates the open one. The PR body links the release, the SHA256SUMS asset and each tarball, with its SHA-256, plus the run that opened it. The PR body comes from a new render_pull_request_body in scripts/render_homebrew_formula.py, which shares the formula's version and checksum validation.
  • Reruns are idempotent. "Formula already matches; nothing to push" is unchanged and is still checked against main. If the release branch already carries the rendered formula, it is reused without a push. If the release assets were rebuilt and the checksums changed, the branch is replaced with a fresh commit on main. That push uses --force-with-lease against the SHA the job observed. An open PR for the branch is updated, not duplicated. The direct-push retry loop is gone, because nothing races on a bot-owned branch, and the release concurrency group already serializes runs for one tag.
  • Failure reporting. The job succeeds once the PR is open or updated, so report-failure fires only when the push or PR call fails. close-recovered-failure still closes cli#133, and its comment now names the tap PR that still has to merge. The PR URL is also exposed as the tap_pr_url job output and written to the step summary.
  • validate checks Pull requests access. permissions.push alone does not show that the token can open a PR. The step now POSTs to /pulls with nonexistent head and base branches, so it can never create a PR. GitHub returns 422 only after it authorizes the write. Any other status fails the release before the build starts, with an error that names the missing access. This is the same probe api-parity-audit.yml uses for REGEN_PR_TOKEN.
  • The tap token never shares a job with release-ref code. update-homebrew now checks out the workflow's own ref (main, or the pushed tag) instead of RELEASE_REF, because it holds HOMEBREW_TAP_TOKEN and runs the formula renderer. The token check moves out of validate into a new verify-homebrew-token job with no checkout and no GITHUB_TOKEN permissions; build waits for both. validate itself now runs the workflow's own release_version.py --root against Cargo.toml and Cargo.lock fetched from RELEASE_REF through the contents API, so no release-ref code runs there either (CodeQL otherwise moved it to a cache-poisoning alert). I checked the fetch-and-validate path locally for both refs/tags/v0.9.1 and the bare SHA: it accepts v0.9.1 and rejects v0.9.2. CodeQL flagged both jobs as untrusted code in a privileged workflow_run context (open on main as alerts docs: remove development section from readme #3 and docs: remove homebrew install #1 since 2026-10-01). Alert docs: update homebrew tap name #2 in release-on-version-bump.yml is handled separately under COVAL-7697.
  • README. Updates the release checklist, the recovery commands, and the HOMEBREW_TAP_TOKEN prerequisite (Contents and Pull requests read/write).

Before the next release

  • Token scope. HOMEBREW_TAP_TOKEN appears to be Callum's token: callumreid was the push actor for the last automated tap update (0.8.3). I could not read its granted permissions from here. If it was created with Contents access only, add Pull requests: Read and write for coval-ai/homebrew-tap. Otherwise the next release fails in validate with that message, before anything is built.
  • Approval. The tap requires approval from someone other than the last pusher. Because the token owner pushes the branch, someone else has to approve the release PR, as with fix: use ~/.config for config path #7 and chore: bump version to 0.1.1 #8.
  • Old branches. The tap has delete_branch_on_merge: false, so merged release/coval-* branches stay until someone deletes them. They do no harm.

Validation

  • I ran the new tap step's script locally against a bare-repo stand-in for the tap, with a stubbed gh, in four cases:
    1. A fresh version creates the branch and calls POST /pulls.
    2. A rerun with the same checksums reuses the branch without a push and PATCHes the open PR.
    3. A rerun with rebuilt checksums force-updates the branch through the lease and PATCHes the PR.
    4. A version already on main prints "already matches; nothing to push" and makes no gh calls.
  • An empty-expect --force-with-lease is rejected (stale info) when the branch already exists.
  • Side-effect-free real gh checks:
    • pulls?head=coval-ai:<branch>&base=main finds the tap's chore: bump version to 0.1.1 #8 when queried with state=closed, and returns empty with exit 0 when no PR matches.
    • -F body=@file sends the file verbatim, checked through POST /markdown.
    • gh pr view <branch> --repo coval-ai/homebrew-tap resolves.
  • I ran the validate probe with my own credentials against the tap. It returned HTTP 422 Validation Failed (base and head invalid), and no PR was created.
  • actionlint (with shellcheck) is clean. Script unit tests: 63 pass, including two new PR-body tests. ruff check / ruff format --check scripts and python3 scripts/release_version.py pass.
  • Not covered: a real release run against the tap. That needs a version bump, so the next release is the end-to-end check.

No database queries change.

🤖 Generated with Claude Code

The tap's rulesets reject direct pushes to main, so v0.9.0 and v0.9.1 both
failed at update-homebrew and needed hand-made tap PRs. The release now pushes
release/coval-vX.Y.Z and opens or updates a PR with the release URLs and
checksums. Reruns reuse the branch and PR, and validate checks that the token
can open pull requests on the tap before building.
update-homebrew holds HOMEBREW_TAP_TOKEN and runs the formula renderer, so
check out the workflow's own ref instead of RELEASE_REF. CodeQL flagged the
release-ref checkout as untrusted code in a privileged workflow_run context.
validate checked out RELEASE_REF and ran release_version.py in the same job
that received HOMEBREW_TAP_TOKEN (CodeQL alert #1). The token check now runs
in its own job with no checkout and no GITHUB_TOKEN permissions; build waits
for both.
With the token check moved out, CodeQL flagged validate for cache poisoning:
it still ran release_version.py from the RELEASE_REF checkout in a
default-branch context. validate now runs the workflow's own copy of the
script against Cargo.toml and Cargo.lock fetched from RELEASE_REF as data.
@callumreid
callumreid marked this pull request as ready for review October 15, 2026 13:43
@callumreid
callumreid merged commit 5b8ac3a into main Oct 15, 2026
10 checks passed
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.

2 participants