From 9f16ea303fe53e72abfa644c7f4dc463f8c26da5 Mon Sep 17 00:00:00 2001 From: Sarah Simionescu Date: Wed, 6 May 2026 19:51:34 -0700 Subject: [PATCH 1/3] ci: add bump-homebrew-tap workflow --- .github/workflows/bump-homebrew-tap.yml | 149 ++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 .github/workflows/bump-homebrew-tap.yml diff --git a/.github/workflows/bump-homebrew-tap.yml b/.github/workflows/bump-homebrew-tap.yml new file mode 100644 index 0000000000..c8944d15c4 --- /dev/null +++ b/.github/workflows/bump-homebrew-tap.yml @@ -0,0 +1,149 @@ +name: Bump Homebrew Tap + +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: 'Release tag to sync (e.g. @composio/cli@0.2.28)' + required: true + type: string + +permissions: + contents: read + +jobs: + bump: + if: github.event_name == 'workflow_dispatch' || startsWith(github.event.release.tag_name, '@composio/cli@') + runs-on: ubuntu-latest + steps: + - name: Resolve release tag and version + id: meta + env: + TAG_INPUT: ${{ inputs.tag }} + TAG_RELEASE: ${{ github.event.release.tag_name }} + run: | + set -euo pipefail + tag="${TAG_INPUT:-$TAG_RELEASE}" + if [[ -z "$tag" ]]; then + echo "::error::No tag resolved"; exit 1 + fi + if [[ ! "$tag" =~ ^@composio/cli@ ]]; then + echo "::notice::Tag $tag is not a CLI release, skipping." + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + # Skip beta/prerelease tags — only stable bumps the formula. + if [[ "$tag" == *"-beta."* || "$tag" == *"-rc."* || "$tag" == *"-alpha."* ]]; then + echo "::notice::Tag $tag is a prerelease, skipping." + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + version="${tag#@composio/cli@}" + echo "tag=$tag" >> "$GITHUB_OUTPUT" + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "skip=false" >> "$GITHUB_OUTPUT" + + - name: Download checksums + if: steps.meta.outputs.skip != 'true' + id: sha + env: + TAG: ${{ steps.meta.outputs.tag }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + gh release download "$TAG" \ + --repo "${{ github.repository }}" \ + --pattern checksums.txt \ + --output checksums.txt + extract() { + grep " $1\$" checksums.txt | awk '{print $1}' + } + { + echo "darwin_arm=$(extract composio-darwin-aarch64.zip)" + echo "darwin_x86=$(extract composio-darwin-x64.zip)" + echo "linux_arm=$(extract composio-linux-aarch64.zip)" + echo "linux_x86=$(extract composio-linux-x64.zip)" + } >> "$GITHUB_OUTPUT" + + - name: Checkout homebrew tap + if: steps.meta.outputs.skip != 'true' + uses: actions/checkout@v4 + with: + repository: ComposioHQ/homebrew-tap + token: ${{ secrets.HOMEBREW_TAP_TOKEN }} + path: tap + + - name: Update formula + if: steps.meta.outputs.skip != 'true' + env: + VERSION: ${{ steps.meta.outputs.version }} + TAG: ${{ steps.meta.outputs.tag }} + DARWIN_ARM: ${{ steps.sha.outputs.darwin_arm }} + DARWIN_X86: ${{ steps.sha.outputs.darwin_x86 }} + LINUX_ARM: ${{ steps.sha.outputs.linux_arm }} + LINUX_X86: ${{ steps.sha.outputs.linux_x86 }} + run: | + set -euo pipefail + formula="tap/Formula/composio.rb" + [[ -f "$formula" ]] || { echo "::error::$formula not found"; exit 1; } + + python3 - "$formula" <<'PY' + import os, re, sys + path = sys.argv[1] + tag = os.environ["TAG"] + version = os.environ["VERSION"] + shas = { + "darwin-aarch64": os.environ["DARWIN_ARM"], + "darwin-x64": os.environ["DARWIN_X86"], + "linux-aarch64": os.environ["LINUX_ARM"], + "linux-x64": os.environ["LINUX_X86"], + } + for k, v in shas.items(): + if not re.fullmatch(r"[0-9a-f]{64}", v or ""): + sys.exit(f"missing/invalid sha for {k}: {v!r}") + + src = open(path).read() + # Bump version line. + src = re.sub(r'^(\s*version\s+)"[^"]+"', rf'\g<1>"{version}"', src, count=1, flags=re.M) + # Bump each url's tag segment. + src = re.sub( + r'(releases/download/)@composio/cli@[^/]+(/composio-)', + rf'\g<1>{tag}\g<2>', + src, + ) + # Bump each sha256 by matching the platform from the preceding url line. + def repl(match): + url_line, sha_line = match.group(1), match.group(2) + for plat, sha in shas.items(): + if f"composio-{plat}.zip" in url_line: + return url_line + re.sub(r'"[0-9a-f]{64}"', f'"{sha}"', sha_line) + return match.group(0) + src = re.sub( + r'(url\s+"[^"]+"\s*\n)(\s*sha256\s+"[0-9a-f]{64}")', + repl, + src, + ) + open(path, "w").write(src) + PY + + echo "--- updated formula ---" + cat "$formula" + + - name: Commit and push + if: steps.meta.outputs.skip != 'true' + working-directory: tap + env: + VERSION: ${{ steps.meta.outputs.version }} + run: | + set -euo pipefail + git config user.name "composio-release-bot" + git config user.email "composio-release-bot@users.noreply.github.com" + if git diff --quiet; then + echo "Formula already up to date for $VERSION; nothing to commit." + exit 0 + fi + git add Formula/composio.rb + git commit -m "composio $VERSION" + git push origin HEAD From f427a940c72678e404972636d6e4b3af5f36af4d Mon Sep 17 00:00:00 2001 From: Sarah Simionescu Date: Tue, 12 May 2026 23:24:12 -0400 Subject: [PATCH 2/3] review: extract bump script, add concurrency, lambda replacements, grep 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. --- .github/scripts/bump-homebrew-formula.py | 100 ++++++++++++++++++ ...brew-tap.yml => cli.bump-homebrew-tap.yml} | 45 ++------ 2 files changed, 106 insertions(+), 39 deletions(-) create mode 100755 .github/scripts/bump-homebrew-formula.py rename .github/workflows/{bump-homebrew-tap.yml => cli.bump-homebrew-tap.yml} (69%) diff --git a/.github/scripts/bump-homebrew-formula.py b/.github/scripts/bump-homebrew-formula.py new file mode 100755 index 0000000000..ee10820408 --- /dev/null +++ b/.github/scripts/bump-homebrew-formula.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +"""Bump version, release-tag URLs, and per-platform sha256 in a Composio Homebrew formula. + +Used by .github/workflows/cli.bump-homebrew-tap.yml. Reads inputs from env vars +so the bash side has a clean interface; can be run locally for testing: + + TAG=@composio/cli@0.2.29 VERSION=0.2.29 \\ + DARWIN_ARM= DARWIN_X86= LINUX_ARM= LINUX_X86= \\ + .github/scripts/bump-homebrew-formula.py path/to/Formula/composio.rb + +Exits non-zero if any required env var is missing/invalid or if no edits land. +""" + +from __future__ import annotations + +import os +import re +import sys + + +REQUIRED_ENV = ("TAG", "VERSION", "DARWIN_ARM", "DARWIN_X86", "LINUX_ARM", "LINUX_X86") +SHA256_RE = re.compile(r"[0-9a-f]{64}") + + +def main(argv: list[str]) -> int: + if len(argv) != 2: + print(f"usage: {argv[0]} ", file=sys.stderr) + return 2 + + path = argv[1] + missing = [k for k in REQUIRED_ENV if not os.environ.get(k)] + if missing: + print(f"missing env vars: {', '.join(missing)}", file=sys.stderr) + return 2 + + tag = os.environ["TAG"] + version = os.environ["VERSION"] + shas = { + "darwin-aarch64": os.environ["DARWIN_ARM"], + "darwin-x64": os.environ["DARWIN_X86"], + "linux-aarch64": os.environ["LINUX_ARM"], + "linux-x64": os.environ["LINUX_X86"], + } + for plat, sha in shas.items(): + if not SHA256_RE.fullmatch(sha): + print(f"invalid sha for {plat}: {sha!r}", file=sys.stderr) + return 2 + + with open(path) as f: + src = f.read() + + # Bump version line. Lambda avoids replacement-string backreference parsing + # (e.g. a hostile tag containing \g<1>); same defense on tag/sha below. + src, n_ver = re.subn( + r'^(\s*version\s+)"[^"]+"', + lambda m: f'{m.group(1)}"{version}"', + src, + count=1, + flags=re.M, + ) + + # Bump the release-tag segment of each download URL. + src, n_url = re.subn( + r"(releases/download/)@composio/cli@[^/]+(/composio-)", + lambda m: f"{m.group(1)}{tag}{m.group(2)}", + src, + ) + + # Bump each sha256 by pairing it with the URL line directly above. + def repl_sha(match: re.Match[str]) -> str: + url_line, sha_line = match.group(1), match.group(2) + for plat, sha in shas.items(): + if f"composio-{plat}.zip" in url_line: + return url_line + re.sub( + r'"[0-9a-f]{64}"', + lambda _m: f'"{sha}"', + sha_line, + ) + return match.group(0) + + src, n_sha = re.subn( + r'(url\s+"[^"]+"\s*\n)(\s*sha256\s+"[0-9a-f]{64}")', + repl_sha, + src, + ) + + if n_ver != 1 or n_url == 0 or n_sha == 0: + print( + f"bump produced unexpected edit counts (version={n_ver}, url={n_url}, sha={n_sha})", + file=sys.stderr, + ) + return 1 + + with open(path, "w") as f: + f.write(src) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/.github/workflows/bump-homebrew-tap.yml b/.github/workflows/cli.bump-homebrew-tap.yml similarity index 69% rename from .github/workflows/bump-homebrew-tap.yml rename to .github/workflows/cli.bump-homebrew-tap.yml index c8944d15c4..9498670075 100644 --- a/.github/workflows/bump-homebrew-tap.yml +++ b/.github/workflows/cli.bump-homebrew-tap.yml @@ -13,6 +13,10 @@ on: permissions: contents: read +concurrency: + group: bump-homebrew-tap + cancel-in-progress: false + jobs: bump: if: github.event_name == 'workflow_dispatch' || startsWith(github.event.release.tag_name, '@composio/cli@') @@ -58,7 +62,7 @@ jobs: --pattern checksums.txt \ --output checksums.txt extract() { - grep " $1\$" checksums.txt | awk '{print $1}' + grep " $1\$" checksums.txt | awk '{print $1}' || true } { echo "darwin_arm=$(extract composio-darwin-aarch64.zip)" @@ -89,44 +93,7 @@ jobs: formula="tap/Formula/composio.rb" [[ -f "$formula" ]] || { echo "::error::$formula not found"; exit 1; } - python3 - "$formula" <<'PY' - import os, re, sys - path = sys.argv[1] - tag = os.environ["TAG"] - version = os.environ["VERSION"] - shas = { - "darwin-aarch64": os.environ["DARWIN_ARM"], - "darwin-x64": os.environ["DARWIN_X86"], - "linux-aarch64": os.environ["LINUX_ARM"], - "linux-x64": os.environ["LINUX_X86"], - } - for k, v in shas.items(): - if not re.fullmatch(r"[0-9a-f]{64}", v or ""): - sys.exit(f"missing/invalid sha for {k}: {v!r}") - - src = open(path).read() - # Bump version line. - src = re.sub(r'^(\s*version\s+)"[^"]+"', rf'\g<1>"{version}"', src, count=1, flags=re.M) - # Bump each url's tag segment. - src = re.sub( - r'(releases/download/)@composio/cli@[^/]+(/composio-)', - rf'\g<1>{tag}\g<2>', - src, - ) - # Bump each sha256 by matching the platform from the preceding url line. - def repl(match): - url_line, sha_line = match.group(1), match.group(2) - for plat, sha in shas.items(): - if f"composio-{plat}.zip" in url_line: - return url_line + re.sub(r'"[0-9a-f]{64}"', f'"{sha}"', sha_line) - return match.group(0) - src = re.sub( - r'(url\s+"[^"]+"\s*\n)(\s*sha256\s+"[0-9a-f]{64}")', - repl, - src, - ) - open(path, "w").write(src) - PY + .github/scripts/bump-homebrew-formula.py "$formula" echo "--- updated formula ---" cat "$formula" From f921eb959b06be9eaeaf8f4589c3f552556b2c78 Mon Sep 17 00:00:00 2001 From: Sarah Simionescu Date: Wed, 13 May 2026 20:53:22 -0400 Subject: [PATCH 3/3] ci: checkout source repo so bump script is available 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. --- .github/workflows/cli.bump-homebrew-tap.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/cli.bump-homebrew-tap.yml b/.github/workflows/cli.bump-homebrew-tap.yml index 9498670075..aa8c2e26a4 100644 --- a/.github/workflows/cli.bump-homebrew-tap.yml +++ b/.github/workflows/cli.bump-homebrew-tap.yml @@ -71,6 +71,10 @@ jobs: echo "linux_x86=$(extract composio-linux-x64.zip)" } >> "$GITHUB_OUTPUT" + - name: Checkout source repo (for bump script) + if: steps.meta.outputs.skip != 'true' + uses: actions/checkout@v4 + - name: Checkout homebrew tap if: steps.meta.outputs.skip != 'true' uses: actions/checkout@v4