From 618932e49447bb16dfedb6609fbfeb84ec8df8b1 Mon Sep 17 00:00:00 2001 From: Adam Tagscherer Date: Wed, 2 Sep 2026 13:36:49 +0200 Subject: [PATCH 1/3] fix: add homebrew Signed-off-by: Adam Tagscherer --- .github/workflows/homebrew-formula.yaml | 90 ++++++++++++++ .github/workflows/release-binaries.yaml | 10 ++ README.md | 10 ++ scripts/render_homebrew_formula.py | 150 ++++++++++++++++++++++++ 4 files changed, 260 insertions(+) create mode 100644 .github/workflows/homebrew-formula.yaml create mode 100644 scripts/render_homebrew_formula.py diff --git a/.github/workflows/homebrew-formula.yaml b/.github/workflows/homebrew-formula.yaml new file mode 100644 index 0000000..316b545 --- /dev/null +++ b/.github/workflows/homebrew-formula.yaml @@ -0,0 +1,90 @@ +# Copyright AI-Catalog Contributors (https://github.com/Agent-Card/ai-catalog-cli) +# Copyright AGNTCY Contributors (https://github.com/agntcy) +# SPDX-License-Identifier: Apache-2.0 + +--- +name: homebrew-formula + +on: + workflow_call: + inputs: + tag: + description: Release tag to render into Formula/ai-catalog.rb + required: true + type: string + workflow_dispatch: + inputs: + tag: + description: Release tag to render into Formula/ai-catalog.rb + required: true + type: string + +concurrency: + group: ${{ github.workflow }}-${{ inputs.tag }} + cancel-in-progress: false + +jobs: + refresh-formula: + name: Refresh Homebrew formula + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: main + + - name: Setup Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: "3.12" + + - name: Render Homebrew formula + env: + TAG: ${{ inputs.tag }} + run: python3 scripts/render_homebrew_formula.py --tag "$TAG" + + - name: Open formula refresh PR + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ inputs.tag }} + FORMULA: Formula/ai-catalog.rb + run: | + if [ -z "$(git status --porcelain -- "$FORMULA")" ]; then + echo "Formula already up to date for $TAG" + exit 0 + fi + + branch="automation/homebrew-${TAG#v}" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git checkout -B "$branch" + git add "$FORMULA" + git commit -s -m "release: update Homebrew formula for $TAG" + git push --force --set-upstream origin "$branch" + + open_pr=$(gh pr list --repo "$GITHUB_REPOSITORY" --head "$branch" \ + --base main --state open --json number --jq 'first(.[].number) // ""') + if [ -n "$open_pr" ]; then + echo "Updated existing PR #$open_pr" >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + + if gh pr create --repo "$GITHUB_REPOSITORY" --base main --head "$branch" \ + --title "release: update Homebrew formula for $TAG" \ + --body "Repoints \`$FORMULA\` at the $TAG archives and their published SHA256 digests." + then + exit 0 + fi + + # GITHUB_TOKEN cannot open pull requests unless the organization + # allows it, so leave the branch and a compare link behind instead. + { + echo "### Homebrew formula for $TAG" + echo + echo "Pushed \`$branch\`, but could not open the PR. Open it here:" + echo + echo "https://github.com/$GITHUB_REPOSITORY/compare/main...$branch?expand=1" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/release-binaries.yaml b/.github/workflows/release-binaries.yaml index 6a1fdea..e2bf273 100644 --- a/.github/workflows/release-binaries.yaml +++ b/.github/workflows/release-binaries.yaml @@ -77,3 +77,13 @@ jobs: checksum: sha256 include: LICENSE,README.md token: ${{ secrets.GITHUB_TOKEN }} + + homebrew-formula: + name: Homebrew formula + needs: build-binaries + permissions: + contents: write + pull-requests: write + uses: ./.github/workflows/homebrew-formula.yaml + with: + tag: ${{ github.event.release.tag_name }} diff --git a/README.md b/README.md index 527dcf5..011f920 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,16 @@ The crate is named `ai-catalog-cli`; the installed binary is `ai-catalog`. ## Install +On macOS and Linux, install from the tap in this repository: + +```sh +brew tap Agent-Card/ai-catalog-cli https://github.com/Agent-Card/ai-catalog-cli +brew install ai-catalog +``` + +The URL is required because Homebrew only infers it for repositories named +`homebrew-`. + Prebuilt binaries for Linux, macOS, and Windows are attached to each [release](https://github.com/Agent-Card/ai-catalog-cli/releases). Download the archive for your platform, verify it against the accompanying `.sha256` file, diff --git a/scripts/render_homebrew_formula.py b/scripts/render_homebrew_formula.py new file mode 100644 index 0000000..0fbb225 --- /dev/null +++ b/scripts/render_homebrew_formula.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +# Copyright AI-Catalog Contributors (https://github.com/Agent-Card/ai-catalog-cli) +# Copyright AGNTCY Contributors (https://github.com/agntcy) +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import argparse +import hashlib +import re +import textwrap +import tomllib +import urllib.request +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +MANIFEST = ROOT / "Cargo.toml" +DEFAULT_OUTPUT = ROOT / "Formula" / "ai-catalog.rb" + +TAG_PATTERN = re.compile(r"^v(?P[0-9A-Za-z.+-]+)$") +BINARY_NAME = "ai-catalog" +CLASS_NAME = "AiCatalog" +USER_AGENT = "ai-catalog-cli-release-automation" + +# Homebrew on_/on_ block to release archive name. The gnu Linux +# archives are used rather than musl: Homebrew targets glibc distributions. +ARCHIVES = { + "macos": {"arm": "darwin-arm64", "intel": "darwin-amd64"}, + "linux": {"arm": "linux-arm64-gnu", "intel": "linux-amd64-gnu"}, +} + +HEADER = textwrap.dedent( + """\ + # Copyright AI-Catalog Contributors (https://github.com/Agent-Card/ai-catalog-cli) + # Copyright AGNTCY Contributors (https://github.com/agntcy) + # SPDX-License-Identifier: Apache-2.0 + + """ +) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Render the Homebrew formula for a released CLI tag." + ) + parser.add_argument("--tag", required=True, help="Release tag, e.g. v0.2.2") + parser.add_argument( + "--output", + type=Path, + default=DEFAULT_OUTPUT, + help=f"Formula output path (default: {DEFAULT_OUTPUT}).", + ) + return parser.parse_args() + + +def fetch(url: str) -> urllib.request.addinfourl: + request = urllib.request.Request(url, headers={"User-Agent": USER_AGENT}) + return urllib.request.urlopen(request, timeout=60) + + +def hash_archive(url: str) -> str: + digest = hashlib.sha256() + with fetch(url) as response: + while chunk := response.read(1024 * 1024): + digest.update(chunk) + return digest.hexdigest() + + +def archive_sha256(base_url: str, archive: str) -> str: + """Prefer the digest the release published over re-hashing the archive.""" + try: + with fetch(f"{base_url}/{BINARY_NAME}-{archive}.sha256") as response: + fields = response.read().decode("utf-8").split() + if fields: + return fields[0] + except OSError: + pass + return hash_archive(f"{base_url}/{BINARY_NAME}-{archive}.tar.gz") + + +def ruby_string(value: str) -> str: + return value.replace("\\", "\\\\").replace('"', '\\"') + + +def arch_blocks(base_url: str, targets: dict[str, str]) -> str: + blocks = [] + for cpu, archive in targets.items(): + url = f"{base_url}/{BINARY_NAME}-{archive}.tar.gz" + blocks.append( + f" on_{cpu} do\n" + f' url "{ruby_string(url)}"\n' + f' sha256 "{archive_sha256(base_url, archive)}"\n' + f" end" + ) + return "\n\n".join(blocks) + + +def render_formula(tag: str) -> str: + match = TAG_PATTERN.match(tag) + if not match: + raise SystemExit(f"expected {TAG_PATTERN.pattern} tag, got: {tag}") + version = match.group("version") + + with MANIFEST.open("rb") as handle: + package = tomllib.load(handle)["package"] + + homepage = package["repository"].rstrip("/") + base_url = f"{homepage}/releases/download/{tag}" + git_url = homepage if homepage.endswith(".git") else f"{homepage}.git" + + body = textwrap.dedent( + f"""\ + class {CLASS_NAME} < Formula + desc "{ruby_string(package["description"])}" + homepage "{ruby_string(homepage)}" + version "{version}" + license "{ruby_string(package["license"])}" + head "{ruby_string(git_url)}", branch: "main" + + __MACOS__ + + __LINUX__ + + def install + bin.install "{BINARY_NAME}" + end + + test do + assert_match "{BINARY_NAME}", shell_output("#{{bin}}/{BINARY_NAME} --help") + end + end + """ + ) + + for placeholder, os_name in (("__MACOS__", "macos"), ("__LINUX__", "linux")): + blocks = arch_blocks(base_url, ARCHIVES[os_name]) + body = body.replace(placeholder, f" on_{os_name} do\n{blocks}\n end") + + return f"{HEADER}{body}" + + +def main() -> int: + args = parse_args() + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(render_formula(args.tag), encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From b24306524550995284833836e2d1a6d4df751d01 Mon Sep 17 00:00:00 2001 From: Adam Tagscherer Date: Wed, 2 Sep 2026 14:36:57 +0200 Subject: [PATCH 2/3] fix: add homebrew Signed-off-by: Adam Tagscherer --- .github/workflows/homebrew-formula.yaml | 2 +- README.md | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/homebrew-formula.yaml b/.github/workflows/homebrew-formula.yaml index 316b545..d3afebe 100644 --- a/.github/workflows/homebrew-formula.yaml +++ b/.github/workflows/homebrew-formula.yaml @@ -37,7 +37,7 @@ jobs: ref: main - name: Setup Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" diff --git a/README.md b/README.md index 011f920..c6fbc06 100644 --- a/README.md +++ b/README.md @@ -17,12 +17,9 @@ On macOS and Linux, install from the tap in this repository: ```sh brew tap Agent-Card/ai-catalog-cli https://github.com/Agent-Card/ai-catalog-cli -brew install ai-catalog +brew install Agent-Card/ai-catalog-cli/ai-catalog ``` -The URL is required because Homebrew only infers it for repositories named -`homebrew-`. - Prebuilt binaries for Linux, macOS, and Windows are attached to each [release](https://github.com/Agent-Card/ai-catalog-cli/releases). Download the archive for your platform, verify it against the accompanying `.sha256` file, From 5c37a86d665e00291b1a86e7ecaf611057799013 Mon Sep 17 00:00:00 2001 From: Adam Tagscherer Date: Wed, 2 Sep 2026 14:47:37 +0200 Subject: [PATCH 3/3] fix: add homebrew Signed-off-by: Adam Tagscherer --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c6fbc06..ef93825 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,8 @@ On macOS and Linux, install from the tap in this repository: ```sh brew tap Agent-Card/ai-catalog-cli https://github.com/Agent-Card/ai-catalog-cli -brew install Agent-Card/ai-catalog-cli/ai-catalog +brew trust Agent-Card/ai-catalog-cli +brew install ai-catalog ``` Prebuilt binaries for Linux, macOS, and Windows are attached to each