Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions .github/scripts/bump-homebrew-formula.py
Original file line number Diff line number Diff line change
@@ -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=<sha> DARWIN_X86=<sha> LINUX_ARM=<sha> LINUX_X86=<sha> \\
.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]} <formula-path>", 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
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.


with open(path, "w") as f:
f.write(src)
return 0


if __name__ == "__main__":
sys.exit(main(sys.argv))
120 changes: 120 additions & 0 deletions .github/workflows/cli.bump-homebrew-tap.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
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
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


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@')
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}' || true
}
{
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 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
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; }

.github/scripts/bump-homebrew-formula.py "$formula"
Comment thread
cursor[bot] marked this conversation as resolved.

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
Loading