Skip to content

ci: add bump-homebrew-tap workflow#3355

Open
sarahsimionescu wants to merge 3 commits into
nextfrom
sarah/bump-homebrew-tap
Open

ci: add bump-homebrew-tap workflow#3355
sarahsimionescu wants to merge 3 commits into
nextfrom
sarah/bump-homebrew-tap

Conversation

@sarahsimionescu
Copy link
Copy Markdown
Contributor

Summary

  • New workflow that auto-bumps the Homebrew formula in ComposioHQ/homebrew-tap whenever a stable CLI release is published.
  • Triggers on release: published for tags matching @composio/cli@*. Skips beta/rc/alpha tags. Also exposes workflow_dispatch for manual replay.
  • Pulls SHA256s from the release's checksums.txt, edits Formula/composio.rb in the tap repo, and commits/pushes (no-op if already current).

Setup required before this can run

  • Create a fine-grained PAT scoped to ComposioHQ/homebrew-tap only, with Contents: read/write.
  • Add it as a repo secret named HOMEBREW_TAP_TOKEN in this repo.

Without that secret the checkout step will fail; the workflow won't break anything else.

Test plan

  • Land this PR
  • Add HOMEBREW_TAP_TOKEN secret
  • Manually run via workflow_dispatch with tag @composio/cli@0.2.28 — should be a no-op (formula already at 0.2.28)
  • Verify next CLI release auto-bumps the formula

@vercel
Copy link
Copy Markdown

vercel Bot commented May 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment May 14, 2026 0:54am

Request Review

Copy link
Copy Markdown
Contributor

@jkomyno jkomyno left a comment

Choose a reason for hiding this comment

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

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.

Comment thread .github/workflows/bump-homebrew-tap.yml Outdated

src = open(path).read()
# Bump version line.
src = re.sub(r'^(\s*version\s+)"[^"]+"', rf'\g<1>"{version}"', src, count=1, flags=re.M)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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:

Suggested change
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)

Comment thread .github/workflows/bump-homebrew-tap.yml Outdated
Comment on lines +111 to +115
src = re.sub(
r'(releases/download/)@composio/cli@[^/]+(/composio-)',
rf'\g<1>{tag}\g<2>',
src,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same replacement-string issue as line 109 — tag flows into a re.sub replacement and is subject to backreference interpretation. Lambda fix:

Suggested change
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.

Comment thread .github/workflows/bump-homebrew-tap.yml Outdated
--pattern checksums.txt \
--output checksums.txt
extract() {
grep " $1\$" checksums.txt | awk '{print $1}'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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:

Suggested change
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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:

Suggested change
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.
@sarahsimionescu
Copy link
Copy Markdown
Contributor Author

(from sarah's claude)

Thanks for the review @jkomyno — accepted all six in f427a94. Summary:

# Finding Resolution
1 version re.sub replacement-string injection Lambda replacement in extracted script
2 tag re.sub replacement-string injection Lambda replacement in extracted script
3 Concurrent releases racing on tap push concurrency: { group: bump-homebrew-tap, cancel-in-progress: false }
4 `grep pipefail` swallowing the clearer error
5 Filename convention Renamed to cli.bump-homebrew-tap.yml
6 Extract Python heredoc New .github/scripts/bump-homebrew-formula.py, executable, locally testable

Verified the extracted script locally before pushing:

  • Happy path: produces an identical diff to the inlined version (version + 4 URLs + 4 SHAs all bump correctly).
  • Injection defense: ran the script with TAG='@composio/cli@1.0.0\g<1>'\g<1> is written literally to the formula instead of being interpreted as capture-group 1. Defense confirmed.
  • Also added an unexpected-edit-count guard (n_ver != 1 or n_url == 0 or n_sha == 0) so a malformed formula fails loudly instead of silently no-op'ing.

Ready for another look when you have a moment.

Comment thread .github/workflows/cli.bump-homebrew-tap.yml
@sarahsimionescu sarahsimionescu requested a review from jkomyno May 13, 2026 03:36
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.
@sarahsimionescu
Copy link
Copy Markdown
Contributor Author

(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.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f921eb9. Configure here.

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