ci: add bump-homebrew-tap workflow#3355
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
jkomyno
left a comment
There was a problem hiding this comment.
Hi @sarahsimionescu — workflow looks solid overall. Strong points: top-level permissions: contents: read, every event/input value routed through env: blocks (correct script-injection defense), set -euo pipefail everywhere (stricter than the rest of the repo's workflows), exact-length lowercase-hex SHA validation, hardcoded --repo "${{ github.repository }}" so there's no attacker-controlled-repo path. Trigger-side trust is also fine — release: published and workflow_dispatch both require write/release permission on the repo, so no fork path.
Four inline suggestions below — one real bug, three nice-to-haves. Two file-level notes that don't fit as inline comments:
Filename convention. The repo splits between prefixed (ts.*, py.*, docs.*, cli.* — 17 files) and unprefixed (build-cli-binaries.yml, stale.yml, etc.). Since this listens for @composio/cli@* releases, cli.bump-homebrew-tap.yml would slot it next to cli.install-health-check.yml and cli.test-installation.yml. Your call.
Inline Python heredoc. No other workflow in the repo embeds Python at this scale (build-cli-binaries.yml only uses python3 -c one-liners; non-trivial logic gets a real script file). Extracting the 35-line heredoc to ts/packages/cli/scripts/bump-homebrew-formula.py (or similar) would make the regex bumping locally testable and easier to review on changes. The bash-side VERSION / TAG / DARWIN_* / LINUX_* env vars already form a clean script interface. Nit, not blocking.
|
|
||
| src = open(path).read() | ||
| # Bump version line. | ||
| src = re.sub(r'^(\s*version\s+)"[^"]+"', rf'\g<1>"{version}"', src, count=1, flags=re.M) |
There was a problem hiding this comment.
Real finding (low severity, insider threat). version is embedded in a re.sub replacement string, where Python interprets \1, \g<...>, \\. re.escape only escapes patterns, not replacements. A maliciously-crafted release tag (e.g. @composio/cli@1.0.0\g<1>) would slip past the existing ^@composio/cli@ and prerelease guards and mangle the formula here. Only org members with release-publish rights can craft a tag, so the threat is insider/compromised-maintainer — but the fix is one-liner-cheap. Use a lambda to bypass replacement-string parsing entirely:
| src = re.sub(r'^(\s*version\s+)"[^"]+"', rf'\g<1>"{version}"', src, count=1, flags=re.M) | |
| src = re.sub(r'^(\s*version\s+)"[^"]+"', lambda m: f'{m.group(1)}"{version}"', src, count=1, flags=re.M) |
| src = re.sub( | ||
| r'(releases/download/)@composio/cli@[^/]+(/composio-)', | ||
| rf'\g<1>{tag}\g<2>', | ||
| src, | ||
| ) |
There was a problem hiding this comment.
Same replacement-string issue as line 109 — tag flows into a re.sub replacement and is subject to backreference interpretation. Lambda fix:
| src = re.sub( | |
| r'(releases/download/)@composio/cli@[^/]+(/composio-)', | |
| rf'\g<1>{tag}\g<2>', | |
| src, | |
| ) | |
| src = re.sub( | |
| r'(releases/download/)@composio/cli@[^/]+(/composio-)', | |
| lambda m: f'{m.group(1)}{tag}{m.group(2)}', | |
| src, | |
| ) |
The third re.sub at lines 123–127 already uses a function (repl) and is safe — no change needed there.
| --pattern checksums.txt \ | ||
| --output checksums.txt | ||
| extract() { | ||
| grep " $1\$" checksums.txt | awk '{print $1}' |
There was a problem hiding this comment.
Minor UX (not security). Under set -euo pipefail, if a checksum line is missing, grep exits 1 and pipefail kills the step here — before the clearer missing/invalid sha for ... error at line 105 has a chance to fire. Tacking on || true lets the SHA validator produce the better error message:
| grep " $1\$" checksums.txt | awk '{print $1}' | |
| grep " $1\$" checksums.txt | awk '{print $1}' || true |
| tag: | ||
| description: 'Release tag to sync (e.g. @composio/cli@0.2.28)' | ||
| required: true | ||
| type: string |
There was a problem hiding this comment.
Worth adding a concurrency: block. None of the repo's release-style workflows currently set one (so this is a deviation), but this workflow is unique in that it pushes to a separate repo — if two stable CLI releases publish in quick succession, both runs race to push to homebrew-tap. The git diff --quiet check at line 143 only saves you if the first push has already landed; otherwise both commit and the second push fails with a non-fast-forward error. Cheap to serialize:
| type: string | |
| type: string | |
| concurrency: | |
| group: bump-homebrew-tap | |
| cancel-in-progress: false |
…ep fallback Address PR review: - Rename to cli.bump-homebrew-tap.yml to match cli.* convention. - Extract inline Python heredoc to .github/scripts/bump-homebrew-formula.py so the bumper is locally testable. - Switch re.sub replacements to lambdas so a hostile tag containing \g<...> backreference syntax can't mangle the formula. - Add concurrency group so back-to-back releases serialize their pushes to the tap repo instead of racing on non-fast-forward. - 'grep | awk || true' so a missing checksum line surfaces the script's clearer 'invalid sha' error instead of pipefail killing the step early.
|
(from sarah's claude) Thanks for the review @jkomyno — accepted all six in f427a94. Summary:
Verified the extracted script locally before pushing:
Ready for another look when you have a moment. |
After extracting the bumper to .github/scripts/bump-homebrew-formula.py (prior commit), the workflow needed an actions/checkout of the source repo. Only the tap repo was being checked out, so the script invocation failed with 'No such file or directory'. Caught by Cursor Bugbot.
|
(from sarah's claude) Good catch @cursor — confirmed real bug introduced by the heredoc extraction. When the Python was inline, no source-repo checkout was needed; after extracting to `.github/scripts/bump-homebrew-formula.py` I forgot to add one. Fixed in f921eb9 with an `actions/checkout@v4` step before the tap checkout (tap uses `path: tap` so no collision). Still waiting on @jkomyno for re-review of the original six. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f921eb9. Configure here.
| f"bump produced unexpected edit counts (version={n_ver}, url={n_url}, sha={n_sha})", | ||
| file=sys.stderr, | ||
| ) | ||
| return 1 |
There was a problem hiding this comment.
SHA validation count includes no-op replacements
Low Severity
re.subn counts every pattern match as a substitution even when repl_sha returns match.group(0) unchanged (i.e., when no platform name in the URL matches the shas dict). This means n_sha reflects the number of url+sha blocks found, not the number of SHAs actually updated. The guard n_sha == 0 only catches a completely missing formula structure, not the case where platform names diverge and all SHA replacements silently no-op — resulting in a committed formula with fresh version/URLs but stale checksums.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit f921eb9. Configure here.


Summary
release: publishedfor tags matching@composio/cli@*. Skips beta/rc/alpha tags. Also exposesworkflow_dispatchfor manual replay.checksums.txt, editsFormula/composio.rbin the tap repo, and commits/pushes (no-op if already current).Setup required before this can run
ComposioHQ/homebrew-taponly, withContents: read/write.HOMEBREW_TAP_TOKENin this repo.Without that secret the checkout step will fail; the workflow won't break anything else.
Test plan
HOMEBREW_TAP_TOKENsecretworkflow_dispatchwith tag@composio/cli@0.2.28— should be a no-op (formula already at 0.2.28)