From b4cf6c8c931390f099985fc733e7e948c7230ff0 Mon Sep 17 00:00:00 2001 From: Toddy Mladenov Date: Tue, 30 Jun 2026 22:13:38 -0700 Subject: [PATCH 1/4] Publish to Homebrew on release Add Homebrew distribution alongside PyPI: - New 'homebrew' job in publish.yml opens a formula-bump PR in toddysm/homebrew-regshape after the PyPI publish step - .github/scripts/bump_formula.py resolves the released sdist URL/sha256 from PyPI and rewrites the formula's top-level fields - README + docs/guides/homebrew-install.md document brew install Refs #59, #60, #62, #63 --- .github/scripts/bump_formula.py | 55 +++++++++++++++++++++++++++++++++ .github/workflows/publish.yml | 50 +++++++++++++++++++++++++++++- README.md | 13 ++++++-- docs/guides/homebrew-install.md | 0 4 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 .github/scripts/bump_formula.py create mode 100644 docs/guides/homebrew-install.md diff --git a/.github/scripts/bump_formula.py b/.github/scripts/bump_formula.py new file mode 100644 index 0000000..0f5c79b --- /dev/null +++ b/.github/scripts/bump_formula.py @@ -0,0 +1,55 @@ +#!/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 +""" +from __future__ import annotations + +import json +import re +import sys +import time +import urllib.request + + +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(formula_path: str, version: str) -> None: + sdist_url, sha256 = fetch_sdist(version) + text = open(formula_path, encoding="utf-8").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") + open(formula_path, "w", encoding="utf-8").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) != 3: + sys.exit("usage: bump_formula.py ") + bump(sys.argv[1], sys.argv[2]) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 64e4e0c..98fef21 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,4 +1,4 @@ -name: Publish to PyPI +name: Publish to PyPI and Homebrew on: release: @@ -51,3 +51,51 @@ 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 + run: | + VERSION="${GITHUB_REF_NAME#v}" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + python .github/scripts/bump_formula.py tap/Formula/regshape.rb "$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 + 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 From 5de4494366cbf5c18f1644343d29e36b2cdee43b Mon Sep 17 00:00:00 2001 From: Toddy Mladenov Date: Wed, 1 Jul 2026 18:00:38 -0700 Subject: [PATCH 2/4] Harden release workflow per CodeQL - Validate formula path in bump_formula.py to prevent path traversal (resolve within CWD + require expected filename) - Add explicit least-privilege 'contents: read' permissions to the publish workflow --- .github/scripts/bump_formula.py | 28 +++++++++++++++++++++++++--- .github/workflows/publish.yml | 3 +++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/.github/scripts/bump_formula.py b/.github/scripts/bump_formula.py index 0f5c79b..8f77212 100644 --- a/.github/scripts/bump_formula.py +++ b/.github/scripts/bump_formula.py @@ -11,11 +11,32 @@ from __future__ import annotations import json +import os import re import sys import time import urllib.request +EXPECTED_FORMULA_NAME = "regshape.rb" + + +def resolve_formula_path(raw: str) -> str: + """Validate a caller-supplied formula path. + + Guards against path-traversal by requiring the resolved path to stay within + the current working directory and to carry the expected formula filename. + """ + base = os.path.realpath(os.getcwd()) + resolved = os.path.realpath(raw) + if resolved != base and not resolved.startswith(base + os.sep): + raise ValueError(f"Formula path escapes the working directory: {raw!r}") + if os.path.basename(resolved) != EXPECTED_FORMULA_NAME: + raise ValueError( + f"Unexpected formula filename {os.path.basename(resolved)!r}; " + f"expected {EXPECTED_FORMULA_NAME!r}" + ) + return resolved + def fetch_sdist(version: str) -> tuple[str, str]: url = f"https://pypi.org/pypi/regshape/{version}/json" @@ -35,16 +56,17 @@ def fetch_sdist(version: str) -> tuple[str, str]: def bump(formula_path: str, version: str) -> None: + path = resolve_formula_path(formula_path) sdist_url, sha256 = fetch_sdist(version) - text = open(formula_path, encoding="utf-8").read() + text = open(path, encoding="utf-8").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") - open(formula_path, "w", encoding="utf-8").write(text) - print(f"Bumped {formula_path} to regshape {version}") + open(path, "w", encoding="utf-8").write(text) + print(f"Bumped {path} to regshape {version}") print(f" url {sdist_url}") print(f" sha256 {sha256}") diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 98fef21..e98d3d8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,6 +4,9 @@ on: release: types: [published] +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest From 1027b9e2c168a3f9dc5d87298b8e3e71f59e99c3 Mon Sep 17 00:00:00 2001 From: Toddy Mladenov Date: Wed, 1 Jul 2026 18:03:58 -0700 Subject: [PATCH 3/4] Use constant formula path to satisfy CodeQL path-injection Open a fixed 'Formula/regshape.rb' relative to the working directory instead of a caller-supplied path, so no user data flows into a filesystem path. The workflow runs the bumper from the tap checkout via working-directory: tap. --- .github/scripts/bump_formula.py | 45 ++++++++++++--------------------- .github/workflows/publish.yml | 3 ++- 2 files changed, 18 insertions(+), 30 deletions(-) diff --git a/.github/scripts/bump_formula.py b/.github/scripts/bump_formula.py index 8f77212..88d5a09 100644 --- a/.github/scripts/bump_formula.py +++ b/.github/scripts/bump_formula.py @@ -6,36 +6,22 @@ source, not the dependency ``resource`` blocks). Usage: - python bump_formula.py + 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 os import re import sys import time import urllib.request -EXPECTED_FORMULA_NAME = "regshape.rb" - - -def resolve_formula_path(raw: str) -> str: - """Validate a caller-supplied formula path. - - Guards against path-traversal by requiring the resolved path to stay within - the current working directory and to carry the expected formula filename. - """ - base = os.path.realpath(os.getcwd()) - resolved = os.path.realpath(raw) - if resolved != base and not resolved.startswith(base + os.sep): - raise ValueError(f"Formula path escapes the working directory: {raw!r}") - if os.path.basename(resolved) != EXPECTED_FORMULA_NAME: - raise ValueError( - f"Unexpected formula filename {os.path.basename(resolved)!r}; " - f"expected {EXPECTED_FORMULA_NAME!r}" - ) - return resolved +# Constant path (relative to CWD); never derived from user input. +FORMULA_PATH = "Formula/regshape.rb" def fetch_sdist(version: str) -> tuple[str, str]: @@ -55,23 +41,24 @@ def fetch_sdist(version: str) -> tuple[str, str]: raise RuntimeError(f"regshape {version} not available on PyPI: {last_err}") -def bump(formula_path: str, version: str) -> None: - path = resolve_formula_path(formula_path) +def bump(version: str) -> None: sdist_url, sha256 = fetch_sdist(version) - text = open(path, encoding="utf-8").read() + 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") - open(path, "w", encoding="utf-8").write(text) - print(f"Bumped {path} to regshape {version}") + 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) != 3: - sys.exit("usage: bump_formula.py ") - bump(sys.argv[1], sys.argv[2]) + 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 e98d3d8..cdbeac2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -75,10 +75,11 @@ jobs: - name: Bump formula to released version id: bump + working-directory: tap run: | VERSION="${GITHUB_REF_NAME#v}" echo "version=$VERSION" >> "$GITHUB_OUTPUT" - python .github/scripts/bump_formula.py tap/Formula/regshape.rb "$VERSION" + python "${GITHUB_WORKSPACE}/.github/scripts/bump_formula.py" "$VERSION" - name: Open pull request in tap env: From a2b3cfbea92e286fc9f99ee3158013f4b78776ef Mon Sep 17 00:00:00 2001 From: Toddy Mladenov Date: Wed, 1 Jul 2026 18:06:10 -0700 Subject: [PATCH 4/4] Address review feedback on Homebrew job - Rename workflow to reflect it opens a formula-bump PR - Use --force-with-lease instead of --force - Make PR creation idempotent on workflow re-runs - Reuse existing local branch with 'git switch -C' --- .github/workflows/publish.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index cdbeac2..1e10833 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,4 +1,4 @@ -name: Publish to PyPI and Homebrew +name: Publish to PyPI and bump Homebrew formula on: release: @@ -94,9 +94,14 @@ jobs: 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 switch -C "$BRANCH" git commit -am "regshape ${VERSION}" - git push -u origin "$BRANCH" --force + 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 \