Skip to content

ci: let a trusted release dispatch update the tap, and harden the guard - #16187

Closed
teamleaderleo wants to merge 1 commit into
mainfrom
ci/homebrew-gate-dispatch-recovery
Closed

teamleaderleo wants to merge 1 commit into
mainfrom
ci/homebrew-gate-dispatch-recovery

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Follow-up to #16140. A review subagent went over that diff after it merged, so the four findings land here instead of on it.

The substantive one: the gate locks out release recovery

#16140 added a workflow_run gate to update-homebrew.yml that admits only github.event.workflow_run.event == 'push'. release.yml runs on a v* tag push and on workflow_dispatch, and the dispatch is how a release gets rescued when the tag-push run fails. That is not hypothetical: the tag-push runs for v0.64.23, v0.64.24 and v0.64.25 all failed, and 20 of the last 30 Release macOS app runs were dispatches. Under the merged gate every one of those recoveries leaves the tap behind, which is the stuck-tap failure the file's own comment says it was recently fixed for.

The gate now accepts either event:

contains(fromJSON('["push","workflow_dispatch"]'), github.event.workflow_run.event)

This admits no new authors. A dispatch of release.yml needs write access, and the run still has to satisfy the existing path and head_repository.full_name checks, so a fork pull request's copy of a workflow with the same display name is still rejected. pull_request, the trigger a fork can actually reach, stays out.

Three hardening items

  • Download DMG and get SHA256 was the last step still interpolating ${{ steps.version.outputs.version }} straight into its script. It goes through env: now, so the step no longer depends on the semver regex one step above staying strict.
  • The comment on Get version had lost a sentence boundary and read as the opposite of what it means. Rewritten.
  • The guard's sweep only rejected expressions starting with github.event.workflow_run and github.event.inputs. inputs. is the other spelling of a workflow_dispatch input and reaches the same attacker-influenced value, so it is in the prefix tuple now.

Guard changes

tests/test_ci_homebrew_untrusted_input.py grew a second hostile payload (v1.2.3$(touch ...), whose leading digits make it look like a release ref), an assertion pinning the version regex to a bare three-part semver rather than trusting the payload list to cover every future widening, and the new gate condition.

Verification

Every new assertion was mutation-tested, each mutant restored immediately after:

mutation result
widen the regex to ^[0-9]+\.[0-9]+\.[0-9]+.*$ 2 failures
interpolate ${{ inputs.version }} into a script 1 failure
revert the gate to push-only 1 failure
unmutated 8 ok, exit 0

The widened-regex mutant did not create the marker file, only tripped the "is skipped" assertion. That is direct evidence the env: change removed the script's dependence on the regex, so it is a hardening rather than a bug fix.

actionlint is clean on the workflow.

This is a CI fix, so it skips team review and merges on green.

— Raindrop g2 🫧


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Fixes the Homebrew tap gate so releases recovered via workflow_dispatch re-runs of release.yml can update the tap; the push-only gate was silently skipping those recoveries, leaving the cask stuck.

The dispatch path still requires write access and the existing repository checks, so no new authors are admitted — pull_request, the only trigger a fork can reach, stays rejected.

Hardening

  • The DMG/SHA step now reads the version through env: instead of interpolating it into the script.
  • The guard's sweep also rejects the inputs. spelling of a dispatch input.
  • Tests add a digit-prefixed hostile payload and pin the version regex to a bare three-part semver.

Written for commit da7eb13. Summary will update on new commits.

Review in cubic

Follow-up to #16140, which closed the injection. Four things it left.

The gate now requires `workflow_run.event == 'push'`, but release.yml runs
on both a `v*` tag push and workflow_dispatch, and the dispatch is how a
release is recovered when the tag-push run fails. That happened three
times in September: v0.64.23, v0.64.24 and v0.64.25 all concluded
failure. Recovering by dispatching release.yml on the tag publishes the
DMG but leaves workflow_run.event as workflow_dispatch, so the gate
skips silently and the tap stays behind. That is the same failure this
file's comment says it was recently fixed for. Accept a dispatch as well,
using the contains(fromJSON(...)) idiom already in
ci-compile-attribution.yml. A dispatch needs write access and still has
to pass the head_repository check, so no new author is admitted, and
pull_request stays out.

`steps.version.outputs.version` was the last value still substituted
into a script, and its safety rested entirely on the semver regex above
it. Take it through env: too, so widening that regex later cannot become
code execution.

The guard's sweep knew only `github.event.inputs`, not the bare `inputs.`
spelling of the same workflow_dispatch value. It also probed the regex
with one payload containing slashes, so a regex widened to accept
prerelease suffixes passed. Add the `inputs.` prefix, pin the regex, and
add a digit-prefixed hostile payload.

The comment explaining the security property had lost a sentence
boundary, which inverted what it said.

Verified. Each new assertion mutation-tested: widening the regex to
`^[0-9]+\.[0-9]+\.[0-9]+.*$` gives 2 failures, interpolating
`${{ inputs.version }}` into a script gives 1, reverting the gate to
push-only gives 1, and restoring gives exit 0 with 8 ok. The widened
regex no longer creates the marker file, which is the env: change doing
its job. actionlint clean at CI severity (SHELLCHECK_OPTS="-S warning"),
YAML parses, and test_release_homebrew_gate.py,
test_ci_workflow_run_sources.py and
test_ci_actionlint_covers_every_workflow.py all pass. Registry valid at
366 tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@cursor

cursor Bot commented Sep 30, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@coderabbitai

coderabbitai Bot commented Sep 30, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 18 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: manaflow-ai/cmux/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 1e1675d5-0836-418c-a564-8099bbe1464c

📥 Commits

Reviewing files that changed from the base of the PR and between 152174b and da7eb13.

📒 Files selected for processing (2)
  • .github/workflows/update-homebrew.yml
  • tests/test_ci_homebrew_untrusted_input.py
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Closing this in favour of #16197, which is the same change with a stronger guard. My own duplicate, not anyone else's: I opened this one earlier today, then reviewed #16140 and rebuilt the fix from the review's findings without first checking my own open PRs. Entirely my error, and the lesson is mine to keep.

#16197 carries everything here plus three things this one lacks:

  • steps.version.outputs is in the interpolation sweep's untrusted prefix list, so the env: move is guarded rather than just made
  • the gate's trigger allow-list is parsed and compared to exactly {push, workflow_dispatch}, so a later widening to pull_request fails the guard; this PR asserted the literal string, which a widening would leave in place
  • the payload cases also assert that no version= output is written at all

It also lands as two commits, guard first (failing 3 checks against the current workflow) then fix, which is the shape this repo wants for a regression test.

This branch's CI was also sitting on a stale base: its Fast static checks failure was the actions.discovery.* French parity error, fixed on main by #16175, and its CLA failure was the app-wide API rate limit rather than anything in the diff. #16197 branches off current main and has neither. :)

— Raindrop g2 🫧

auto-merge was automatically disabled September 30, 2026 20:09

Pull request was closed

@teamleaderleo
teamleaderleo deleted the ci/homebrew-gate-dispatch-recovery branch September 30, 2026 20:09
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.

1 participant