diff --git a/.github/scripts/bump_formula.py b/.github/scripts/bump_formula.py new file mode 100644 index 0000000..88d5a09 --- /dev/null +++ b/.github/scripts/bump_formula.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +"""Update the RegShape Homebrew formula to a given version. + +Fetches the sdist URL and sha256 for ``regshape==`` from PyPI and +rewrites the top-level ``url``/``sha256`` fields of the formula (the package +source, not the dependency ``resource`` blocks). + +Usage: + python bump_formula.py + +The formula is always read from and written to a fixed, constant path relative to +the current working directory, so no caller-supplied data is ever used to build a +filesystem path. Run this script from the tap checkout root. +""" +from __future__ import annotations + +import json +import re +import sys +import time +import urllib.request + +# Constant path (relative to CWD); never derived from user input. +FORMULA_PATH = "Formula/regshape.rb" + + +def fetch_sdist(version: str) -> tuple[str, str]: + url = f"https://pypi.org/pypi/regshape/{version}/json" + last_err: Exception | None = None + for _ in range(30): + try: + with urllib.request.urlopen(url, timeout=30) as resp: + data = json.load(resp) + for entry in data["urls"]: + if entry["packagetype"] == "sdist": + return entry["url"], entry["digests"]["sha256"] + raise RuntimeError(f"No sdist found for regshape {version}") + except Exception as err: # noqa: BLE001 - retry on propagation delay + last_err = err + time.sleep(10) + raise RuntimeError(f"regshape {version} not available on PyPI: {last_err}") + + +def bump(version: str) -> None: + sdist_url, sha256 = fetch_sdist(version) + with open(FORMULA_PATH, encoding="utf-8") as fh: + text = fh.read() + # Only the top-level fields are indented with exactly two spaces; resource + # blocks use four spaces, so anchored two-space matches target the package. + text, n_url = re.subn(r'^ url ".*"$', f' url "{sdist_url}"', text, count=1, flags=re.M) + text, n_sha = re.subn(r'^ sha256 ".*"$', f' sha256 "{sha256}"', text, count=1, flags=re.M) + if n_url != 1 or n_sha != 1: + raise RuntimeError("Failed to locate top-level url/sha256 in formula") + with open(FORMULA_PATH, "w", encoding="utf-8") as fh: + fh.write(text) + print(f"Bumped {FORMULA_PATH} to regshape {version}") + print(f" url {sdist_url}") + print(f" sha256 {sha256}") + + +if __name__ == "__main__": + if len(sys.argv) != 2: + sys.exit("usage: bump_formula.py ") + bump(sys.argv[1]) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 64e4e0c..1e10833 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,9 +1,12 @@ -name: Publish to PyPI +name: Publish to PyPI and bump Homebrew formula on: release: types: [published] +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest @@ -51,3 +54,57 @@ jobs: path: dist/ - uses: pypa/gh-action-pypi-publish@release/v1 + + homebrew: + needs: publish + runs-on: ubuntu-latest + steps: + - name: Checkout regshape + uses: actions/checkout@v4 + + - name: Checkout Homebrew tap + uses: actions/checkout@v4 + with: + repository: toddysm/homebrew-regshape + token: ${{ secrets.HOMEBREW_TAP_TOKEN }} + path: tap + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Bump formula to released version + id: bump + working-directory: tap + run: | + VERSION="${GITHUB_REF_NAME#v}" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + python "${GITHUB_WORKSPACE}/.github/scripts/bump_formula.py" "$VERSION" + + - name: Open pull request in tap + env: + GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} + VERSION: ${{ steps.bump.outputs.version }} + run: | + cd tap + if git diff --quiet; then + echo "Formula already up to date; nothing to do." + exit 0 + fi + BRANCH="bump-regshape-${VERSION}" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git switch -C "$BRANCH" + git commit -am "regshape ${VERSION}" + git push -u origin "$BRANCH" --force-with-lease + if gh pr view "$BRANCH" --repo toddysm/homebrew-regshape --json state \ + --jq '.state' 2>/dev/null | grep -qx OPEN; then + echo "An open PR already exists for ${BRANCH}; the force-push updated it." + exit 0 + fi + gh pr create \ + --repo toddysm/homebrew-regshape \ + --base main \ + --head "$BRANCH" \ + --title "regshape ${VERSION}" \ + --body "Automated formula bump for regshape ${VERSION}, triggered by the release workflow in toddysm/regshape." diff --git a/README.md b/README.md index 88d54b7..b61e9a9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # RegShape ![GitHub issues](https://img.shields.io/github/issues-raw/toddysm/regshape?link=https%3A%2F%2Fgithub.com%2Ftoddysm%2Fregshape%2Fissues) - ![GitHub pull requests](https://img.shields.io/github/issues-pr-raw/toddysm/regshape?link=https%3A%2F%2Fgithub.com%2Ftoddysm%2Fregshape%2Fpulls) +![GitHub pull requests](https://img.shields.io/github/issues-pr-raw/toddysm/regshape?link=https%3A%2F%2Fgithub.com%2Ftoddysm%2Fregshape%2Fpulls)

RegShape @@ -26,9 +26,9 @@ an intention to break the consistency of the artifacts. You can use RegShape in two modes: - **Standard mode** — interact with registries as you would with any other tool: - pull and push manifests, blobs, tags, and more. + pull and push manifests, blobs, tags, and more. - **Expert / break mode** — manually craft requests to test registry - implementations and probe their security boundaries. + implementations and probe their security boundaries. RegShape is written in Python and offers Python libraries that can be leveraged to build your own tools. The CLI is built on top of the libraries and uses the @@ -39,6 +39,13 @@ to build your own tools. The CLI is built on top of the libraries and uses the ## Installation ```bash +# Homebrew (macOS / Linux) — recommended +brew install toddysm/regshape/regshape + +# pip (from PyPI) +pip install regshape + +# From source git clone https://github.com/toddysm/regshape.git cd regshape pip install -e . diff --git a/docs/guides/homebrew-install.md b/docs/guides/homebrew-install.md new file mode 100644 index 0000000..e69de29