From 52b085525f5edd15ee6eced46b45ff5e0bcd267a Mon Sep 17 00:00:00 2001 From: Scott Gerlach Date: Tue, 21 Apr 2026 14:29:22 -0600 Subject: [PATCH 01/15] feat: add homebrew formula template --- scripts/formula-template.rb | 39 +++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 scripts/formula-template.rb diff --git a/scripts/formula-template.rb b/scripts/formula-template.rb new file mode 100644 index 0000000..8984b39 --- /dev/null +++ b/scripts/formula-template.rb @@ -0,0 +1,39 @@ +# This file is rendered by scripts/update-formula.sh to produce Formula/hawkop.rb. +# Do not edit Formula/hawkop.rb by hand. +class Hawkop < Formula + desc "CLI companion for the StackHawk AppSec Intelligence Platform" + homepage "https://www.stackhawk.com/" + version "${version}" + license "MIT" + + on_macos do + on_intel do + url "https://download.stackhawk.com/hawkop/cli/hawkop-v${version}-x86_64-apple-darwin.tar.gz" + sha256 "${mac_x64_sha256}" + end + on_arm do + url "https://download.stackhawk.com/hawkop/cli/hawkop-v${version}-aarch64-apple-darwin.tar.gz" + sha256 "${mac_arm64_sha256}" + end + end + + on_linux do + on_intel do + url "https://download.stackhawk.com/hawkop/cli/hawkop-v${version}-x86_64-unknown-linux-gnu.tar.gz" + sha256 "${linux_x64_sha256}" + end + on_arm do + url "https://download.stackhawk.com/hawkop/cli/hawkop-v${version}-aarch64-unknown-linux-gnu.tar.gz" + sha256 "${linux_arm64_sha256}" + end + end + + def install + bin.install "hawkop" + end + + test do + assert_match version.to_s, shell_output("#{bin}/hawkop --version") + system bin/"hawkop", "--help" + end +end From b23964eb3dfafafcf6ec2cdc5d01dc1634a610e2 Mon Sep 17 00:00:00 2001 From: Scott Gerlach Date: Tue, 21 Apr 2026 14:33:46 -0600 Subject: [PATCH 02/15] feat: add formula renderer skeleton with arg validation --- scripts/test-update-formula.bats | 37 +++++++++++++++++++++++ scripts/update-formula.sh | 52 ++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100755 scripts/test-update-formula.bats create mode 100755 scripts/update-formula.sh diff --git a/scripts/test-update-formula.bats b/scripts/test-update-formula.bats new file mode 100755 index 0000000..0feecad --- /dev/null +++ b/scripts/test-update-formula.bats @@ -0,0 +1,37 @@ +#!/usr/bin/env bats + +setup() { + export SCRIPT="$BATS_TEST_DIRNAME/update-formula.sh" + export TMPDIR_TEST="$(mktemp -d)" + cd "$TMPDIR_TEST" + cp "$BATS_TEST_DIRNAME/formula-template.rb" . + mkdir -p scripts + cp "$BATS_TEST_DIRNAME/formula-template.rb" scripts/ +} + +teardown() { + rm -rf "$TMPDIR_TEST" +} + +@test "rejects missing --version" { + run "$SCRIPT" + [ "$status" -ne 0 ] + [[ "$output" == *"--version is required"* ]] +} + +@test "rejects invalid version format" { + run "$SCRIPT" --version "not-a-version" + [ "$status" -ne 0 ] + [[ "$output" == *"invalid version"* ]] +} + +@test "accepts semver" { + # Dry-run and offline mode so we don't hit the network + run "$SCRIPT" --version "1.2.3" --dry-run --offline + [ "$status" -eq 0 ] +} + +@test "accepts semver with prerelease suffix" { + run "$SCRIPT" --version "1.2.3-rc.1" --dry-run --offline + [ "$status" -eq 0 ] +} diff --git a/scripts/update-formula.sh b/scripts/update-formula.sh new file mode 100755 index 0000000..cc4a617 --- /dev/null +++ b/scripts/update-formula.sh @@ -0,0 +1,52 @@ +#!/bin/sh +# Render scripts/formula-template.rb to Formula/hawkop.rb for a given hawkop version. +# Fetches SHA256 sidecars from download.stackhawk.com. +# +# Usage: +# scripts/update-formula.sh --version 0.6.2 +# scripts/update-formula.sh --version 0.6.2 --dry-run +# scripts/update-formula.sh --version 0.6.2 --offline # skip network, use zeros +set -eu + +VERSION="" +DRY_RUN=0 +OFFLINE=0 + +while [ $# -gt 0 ]; do + case "$1" in + --version) + VERSION="$2" + shift 2 + ;; + --dry-run) + DRY_RUN=1 + shift + ;; + --offline) + OFFLINE=1 + shift + ;; + -h|--help) + sed -n '2,10p' "$0" + exit 0 + ;; + *) + echo "unknown arg: $1" >&2 + exit 2 + ;; + esac +done + +if [ -z "$VERSION" ]; then + echo "error: --version is required" >&2 + exit 2 +fi + +# Strict semver with optional prerelease suffix +if ! printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?$'; then + echo "error: invalid version '$VERSION' — expected semver like 1.2.3 or 1.2.3-rc.1" >&2 + exit 2 +fi + +# Placeholder — rest of implementation lands in later tasks +echo "version=$VERSION dry_run=$DRY_RUN offline=$OFFLINE" From 7b44dbf5c0c5cae7648a14fb5d93e40a8fe53c75 Mon Sep 17 00:00:00 2001 From: Scott Gerlach Date: Tue, 21 Apr 2026 14:37:59 -0600 Subject: [PATCH 03/15] fix: trim set -eu from renderer help output --- scripts/update-formula.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/update-formula.sh b/scripts/update-formula.sh index cc4a617..e2bb360 100755 --- a/scripts/update-formula.sh +++ b/scripts/update-formula.sh @@ -27,7 +27,7 @@ while [ $# -gt 0 ]; do shift ;; -h|--help) - sed -n '2,10p' "$0" + sed -n '2,8p' "$0" exit 0 ;; *) From ffaffe99c51c9eada659c257a559d971079d40e6 Mon Sep 17 00:00:00 2001 From: Scott Gerlach Date: Tue, 21 Apr 2026 14:40:16 -0600 Subject: [PATCH 04/15] feat: fetch and parse SHA256 sidecars in renderer --- scripts/test-update-formula.bats | 13 ++++++ scripts/update-formula.sh | 69 +++++++++++++++++++++----------- 2 files changed, 59 insertions(+), 23 deletions(-) diff --git a/scripts/test-update-formula.bats b/scripts/test-update-formula.bats index 0feecad..8b057dc 100755 --- a/scripts/test-update-formula.bats +++ b/scripts/test-update-formula.bats @@ -35,3 +35,16 @@ teardown() { run "$SCRIPT" --version "1.2.3-rc.1" --dry-run --offline [ "$status" -eq 0 ] } + +@test "extracts 64-char hex from sha256 sidecar" { + run bash -c "echo 'abc123$(printf "%.0s0" {1..58}) hawkop-v0.0.0-x86_64-apple-darwin.tar.gz' | '$SCRIPT' --parse-sha-stdin" + [ "$status" -eq 0 ] + [[ "$output" == abc123* ]] + [ ${#output} -eq 64 ] +} + +@test "rejects sidecar without 64-char hex" { + run bash -c "echo 'not-a-hash' | '$SCRIPT' --parse-sha-stdin" + [ "$status" -ne 0 ] + [[ "$output" == *"expected 64-char hex"* ]] +} diff --git a/scripts/update-formula.sh b/scripts/update-formula.sh index e2bb360..d1d56e0 100755 --- a/scripts/update-formula.sh +++ b/scripts/update-formula.sh @@ -8,32 +8,31 @@ # scripts/update-formula.sh --version 0.6.2 --offline # skip network, use zeros set -eu +parse_sha_from_stdin() { + sha=$(tr -d '\r' | grep -Eo '[0-9a-fA-F]{64}' | head -n1) + if [ -z "$sha" ]; then + echo "error: expected 64-char hex in sha256 sidecar" >&2 + return 1 + fi + printf '%s' "$sha" +} + +if [ "${1:-}" = "--parse-sha-stdin" ]; then + parse_sha_from_stdin + exit $? +fi + VERSION="" DRY_RUN=0 OFFLINE=0 while [ $# -gt 0 ]; do case "$1" in - --version) - VERSION="$2" - shift 2 - ;; - --dry-run) - DRY_RUN=1 - shift - ;; - --offline) - OFFLINE=1 - shift - ;; - -h|--help) - sed -n '2,8p' "$0" - exit 0 - ;; - *) - echo "unknown arg: $1" >&2 - exit 2 - ;; + --version) VERSION="$2"; shift 2 ;; + --dry-run) DRY_RUN=1; shift ;; + --offline) OFFLINE=1; shift ;; + -h|--help) sed -n '2,8p' "$0"; exit 0 ;; + *) echo "unknown arg: $1" >&2; exit 2 ;; esac done @@ -42,11 +41,35 @@ if [ -z "$VERSION" ]; then exit 2 fi -# Strict semver with optional prerelease suffix if ! printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?$'; then echo "error: invalid version '$VERSION' — expected semver like 1.2.3 or 1.2.3-rc.1" >&2 exit 2 fi -# Placeholder — rest of implementation lands in later tasks -echo "version=$VERSION dry_run=$DRY_RUN offline=$OFFLINE" +BASE_URL="https://download.stackhawk.com/hawkop/cli" + +fetch_sha() { + target="$1" + if [ "$OFFLINE" -eq 1 ]; then + printf '%064d' 0 + return 0 + fi + archive="hawkop-v${VERSION}-${target}.tar.gz" + archive_status=$(curl -sSIo /dev/null -w '%{http_code}' "${BASE_URL}/${archive}") + if [ "$archive_status" != "200" ]; then + echo "error: archive ${archive} returned HTTP ${archive_status}" >&2 + exit 1 + fi + curl -sSf "${BASE_URL}/${archive}.sha256" | parse_sha_from_stdin +} + +MAC_X64_SHA=$(fetch_sha "x86_64-apple-darwin") +MAC_ARM64_SHA=$(fetch_sha "aarch64-apple-darwin") +LINUX_X64_SHA=$(fetch_sha "x86_64-unknown-linux-gnu") +LINUX_ARM64_SHA=$(fetch_sha "aarch64-unknown-linux-gnu") + +echo "version=$VERSION" +echo "mac_x64=$MAC_X64_SHA" +echo "mac_arm64=$MAC_ARM64_SHA" +echo "linux_x64=$LINUX_X64_SHA" +echo "linux_arm64=$LINUX_ARM64_SHA" From de278bf97c4fa021e04866a8b136932e185f0873 Mon Sep 17 00:00:00 2001 From: Scott Gerlach Date: Tue, 21 Apr 2026 14:45:12 -0600 Subject: [PATCH 05/15] feat: render formula template to Formula/hawkop.rb --- scripts/test-update-formula.bats | 27 +++++++++++++++++++++++++++ scripts/update-formula.sh | 31 ++++++++++++++++++++++++++----- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/scripts/test-update-formula.bats b/scripts/test-update-formula.bats index 8b057dc..b036778 100755 --- a/scripts/test-update-formula.bats +++ b/scripts/test-update-formula.bats @@ -48,3 +48,30 @@ teardown() { [ "$status" -ne 0 ] [[ "$output" == *"expected 64-char hex"* ]] } + +@test "dry-run offline prints rendered formula to stdout" { + run "$SCRIPT" --version 1.2.3 --dry-run --offline + [ "$status" -eq 0 ] + [[ "$output" == *'class Hawkop < Formula'* ]] + [[ "$output" == *'version "1.2.3"'* ]] + [[ "$output" == *'hawkop-v1.2.3-x86_64-apple-darwin.tar.gz'* ]] + # No unsubstituted placeholders remain + [[ "$output" != *'${version}'* ]] + [[ "$output" != *'${mac_x64_sha256}'* ]] +} + +@test "writes Formula/hawkop.rb when not dry-run" { + run "$SCRIPT" --version 1.2.3 --offline + [ "$status" -eq 0 ] + [ -f "Formula/hawkop.rb" ] + run cat Formula/hawkop.rb + [[ "$output" == *'class Hawkop < Formula'* ]] +} + +@test "is idempotent — second run produces the same file" { + "$SCRIPT" --version 1.2.3 --offline + sum1=$(shasum -a 256 Formula/hawkop.rb | awk '{print $1}') + "$SCRIPT" --version 1.2.3 --offline + sum2=$(shasum -a 256 Formula/hawkop.rb | awk '{print $1}') + [ "$sum1" = "$sum2" ] +} diff --git a/scripts/update-formula.sh b/scripts/update-formula.sh index d1d56e0..8fec195 100755 --- a/scripts/update-formula.sh +++ b/scripts/update-formula.sh @@ -68,8 +68,29 @@ MAC_ARM64_SHA=$(fetch_sha "aarch64-apple-darwin") LINUX_X64_SHA=$(fetch_sha "x86_64-unknown-linux-gnu") LINUX_ARM64_SHA=$(fetch_sha "aarch64-unknown-linux-gnu") -echo "version=$VERSION" -echo "mac_x64=$MAC_X64_SHA" -echo "mac_arm64=$MAC_ARM64_SHA" -echo "linux_x64=$LINUX_X64_SHA" -echo "linux_arm64=$LINUX_ARM64_SHA" +# --- Render template --- +# Resolve template path relative to this script. +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +TEMPLATE="${SCRIPT_DIR}/formula-template.rb" +if [ ! -f "$TEMPLATE" ]; then + echo "error: template not found at $TEMPLATE" >&2 + exit 1 +fi + +render() { + sed \ + -e "s|\${version}|${VERSION}|g" \ + -e "s|\${mac_x64_sha256}|${MAC_X64_SHA}|g" \ + -e "s|\${mac_arm64_sha256}|${MAC_ARM64_SHA}|g" \ + -e "s|\${linux_x64_sha256}|${LINUX_X64_SHA}|g" \ + -e "s|\${linux_arm64_sha256}|${LINUX_ARM64_SHA}|g" \ + "$TEMPLATE" +} + +if [ "$DRY_RUN" -eq 1 ]; then + render +else + mkdir -p Formula + render > Formula/hawkop.rb + echo "wrote Formula/hawkop.rb (version $VERSION)" +fi From 821768043a4cff2080a46ad64c7abbe093c29836 Mon Sep 17 00:00:00 2001 From: Scott Gerlach Date: Tue, 21 Apr 2026 14:48:54 -0600 Subject: [PATCH 06/15] fix: route renderer success message to stderr --- scripts/update-formula.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/update-formula.sh b/scripts/update-formula.sh index 8fec195..272838f 100755 --- a/scripts/update-formula.sh +++ b/scripts/update-formula.sh @@ -92,5 +92,5 @@ if [ "$DRY_RUN" -eq 1 ]; then else mkdir -p Formula render > Formula/hawkop.rb - echo "wrote Formula/hawkop.rb (version $VERSION)" + echo "wrote Formula/hawkop.rb (version $VERSION)" >&2 fi From 6c45ba588c5acd3319aa38a17a263035905a85bc Mon Sep 17 00:00:00 2001 From: Scott Gerlach Date: Tue, 21 Apr 2026 14:49:48 -0600 Subject: [PATCH 07/15] feat: run brew audit locally when available --- scripts/update-formula.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/update-formula.sh b/scripts/update-formula.sh index 272838f..2a1c892 100755 --- a/scripts/update-formula.sh +++ b/scripts/update-formula.sh @@ -94,3 +94,11 @@ else render > Formula/hawkop.rb echo "wrote Formula/hawkop.rb (version $VERSION)" >&2 fi + +# --- Optional local audit --- +# If brew is on PATH and we actually wrote a file, run audit. Skip in dry-run +# or offline mode (offline produces zeroed SHAs that would fail audit). +if [ "$DRY_RUN" -eq 0 ] && [ "$OFFLINE" -eq 0 ] && command -v brew >/dev/null 2>&1; then + echo "running: brew audit --strict --online Formula/hawkop.rb" >&2 + brew audit --strict --online Formula/hawkop.rb +fi From 713de2aa5a976ff76b4783237e9a86aec4ffcc92 Mon Sep 17 00:00:00 2001 From: Scott Gerlach Date: Tue, 21 Apr 2026 14:55:13 -0600 Subject: [PATCH 08/15] ci: add release workflow for formula updates --- .github/workflows/update-formula.yml | 52 ++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .github/workflows/update-formula.yml diff --git a/.github/workflows/update-formula.yml b/.github/workflows/update-formula.yml new file mode 100644 index 0000000..861874d --- /dev/null +++ b/.github/workflows/update-formula.yml @@ -0,0 +1,52 @@ +name: Update formula + +on: + workflow_dispatch: + inputs: + version: + description: 'hawkop version to release (e.g. 0.6.2)' + required: true + type: string + repository_dispatch: + types: [hawkop-release] + +permissions: + contents: write + +jobs: + update: + name: Render and commit Formula/hawkop.rb + runs-on: ubuntu-latest + steps: + - name: Resolve version + id: resolve + run: | + if [ -n "${{ github.event.inputs.version }}" ]; then + v="${{ github.event.inputs.version }}" + else + v="${{ github.event.client_payload.version }}" + fi + if [ -z "$v" ]; then + echo "error: no version supplied (inputs.version or client_payload.version)" >&2 + exit 2 + fi + echo "version=$v" >> "$GITHUB_OUTPUT" + + - uses: actions/checkout@v4 + + - uses: Homebrew/actions/setup-homebrew@master + + - name: Render formula + run: scripts/update-formula.sh --version "${{ steps.resolve.outputs.version }}" + + - name: Commit and push + run: | + git config user.name "hawkop-bot" + git config user.email "noreply@stackhawk.com" + if git diff --quiet Formula/hawkop.rb 2>/dev/null && [ ! -n "$(git status --porcelain Formula/hawkop.rb 2>/dev/null)" ]; then + echo "no changes to commit" + exit 0 + fi + git add Formula/hawkop.rb + git commit -m "hawkop: update to ${{ steps.resolve.outputs.version }}" + git push origin main From 261e3c5b39f0e994fc7172f8e1a1689cb9b95396 Mon Sep 17 00:00:00 2001 From: Scott Gerlach Date: Tue, 21 Apr 2026 14:58:20 -0600 Subject: [PATCH 09/15] ci: add concurrency guard and simplify formula-update workflow --- .github/workflows/update-formula.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/update-formula.yml b/.github/workflows/update-formula.yml index 861874d..815e8f7 100644 --- a/.github/workflows/update-formula.yml +++ b/.github/workflows/update-formula.yml @@ -13,6 +13,10 @@ on: permissions: contents: write +concurrency: + group: update-formula + cancel-in-progress: false + jobs: update: name: Render and commit Formula/hawkop.rb @@ -43,7 +47,7 @@ jobs: run: | git config user.name "hawkop-bot" git config user.email "noreply@stackhawk.com" - if git diff --quiet Formula/hawkop.rb 2>/dev/null && [ ! -n "$(git status --porcelain Formula/hawkop.rb 2>/dev/null)" ]; then + if [ -z "$(git status --porcelain Formula/hawkop.rb 2>/dev/null)" ]; then echo "no changes to commit" exit 0 fi From 34b4c5abe0036556248555295fdf929f82b1a131 Mon Sep 17 00:00:00 2001 From: Scott Gerlach Date: Tue, 21 Apr 2026 14:59:33 -0600 Subject: [PATCH 10/15] ci: add PR audit + conditional install workflow --- .github/workflows/test.yml | 78 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..a7548a3 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,78 @@ +name: Test formula + +on: + pull_request: + push: + branches: [main] + +jobs: + audit: + name: Audit and install (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [macos-latest, ubuntu-latest] + steps: + - uses: actions/checkout@v4 + + - uses: Homebrew/actions/setup-homebrew@master + + - name: Skip if formula not generated yet + id: guard + run: | + if [ ! -f Formula/hawkop.rb ]; then + echo "formula not generated yet — skipping" + echo "skip=1" >> "$GITHUB_OUTPUT" + else + echo "skip=0" >> "$GITHUB_OUTPUT" + fi + + - name: Audit + if: steps.guard.outputs.skip == '0' + run: brew audit --strict --online Formula/hawkop.rb + + - name: Probe tarball availability + if: steps.guard.outputs.skip == '0' + id: probe + run: | + # Pick the URL matching this runner's platform. + case "${{ runner.os }}-${{ runner.arch }}" in + macOS-ARM64) target="aarch64-apple-darwin" ;; + macOS-X64) target="x86_64-apple-darwin" ;; + Linux-X64) target="x86_64-unknown-linux-gnu" ;; + Linux-ARM64) target="aarch64-unknown-linux-gnu" ;; + *) echo "unsupported runner: ${{ runner.os }}-${{ runner.arch }}"; exit 1 ;; + esac + version=$(grep -E '^\s*version ' Formula/hawkop.rb | head -n1 | sed -E 's/.*"([^"]+)".*/\1/') + url="https://download.stackhawk.com/hawkop/cli/hawkop-v${version}-${target}.tar.gz" + code=$(curl -sSIo /dev/null -w '%{http_code}' "$url") + echo "url=$url" + echo "code=$code" + echo "installable=$([ "$code" = "200" ] && echo 1 || echo 0)" >> "$GITHUB_OUTPUT" + if [ "$code" != "200" ] && [ "$code" != "404" ]; then + echo "unexpected HTTP $code from $url" >&2 + exit 1 + fi + + - name: Install + if: steps.guard.outputs.skip == '0' && steps.probe.outputs.installable == '1' + run: brew install --verbose Formula/hawkop.rb + + - name: Test + if: steps.guard.outputs.skip == '0' && steps.probe.outputs.installable == '1' + run: brew test hawkop + + lint-scripts: + name: Shellcheck + bats + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install tools + run: | + sudo apt-get update + sudo apt-get install -y shellcheck bats + - name: Shellcheck + run: shellcheck scripts/update-formula.sh + - name: Bats + run: bats scripts/test-update-formula.bats From 05234879919ddb6298b1c168ad01fadea5d5e633 Mon Sep 17 00:00:00 2001 From: Scott Gerlach Date: Tue, 21 Apr 2026 15:02:45 -0600 Subject: [PATCH 11/15] ci: restrict test workflow to contents:read --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a7548a3..81a5cb5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,6 +5,9 @@ on: push: branches: [main] +permissions: + contents: read + jobs: audit: name: Audit and install (${{ matrix.os }}) From 32d1bdf1f3d7f9f37e9cb5841b595f56e94d6316 Mon Sep 17 00:00:00 2001 From: Scott Gerlach Date: Tue, 21 Apr 2026 15:03:28 -0600 Subject: [PATCH 12/15] docs: add release runbook and troubleshooting --- README.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/README.md b/README.md index 410ac70..18ea7b9 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,41 @@ brew upgrade hawkop brew uninstall hawkop brew untap stackhawk/hawkop ``` + +## Releasing a new version + +The formula is auto-generated. Do **not** edit `Formula/hawkop.rb` by hand. + +### Manual release + +```sh +gh workflow run update-formula.yml \ + -R stackhawk/homebrew-hawkop \ + -f version=X.Y.Z +``` + +This triggers the `update-formula` workflow, which renders +`scripts/formula-template.rb` using SHA256 sidecars from +`download.stackhawk.com/hawkop/cli/` and commits the result to `main`. + +### Automated release + +Upstream `stackhawk/hawkop` can send a `repository_dispatch` event of type +`hawkop-release` with payload `{"version": "X.Y.Z"}` to this repo to trigger +the same workflow. + +## Troubleshooting + +**`brew install` fails with a checksum mismatch.** +The tarball was re-uploaded with different content than the formula recorded. +Re-run the update workflow for the affected version. + +**`brew install` fails with HTTP 404.** +The formula is pinned to a version whose tarballs have not been published, +or the download bucket was rolled back. Check +`https://download.stackhawk.com/hawkop/cli/hawkop-v-.tar.gz` +with `curl -I` and coordinate with the `stackhawk/hawkop` release owner. + +**Shell renderer fails locally.** +Run `bats scripts/test-update-formula.bats` and +`shellcheck scripts/update-formula.sh` from the repo root. From 893facd1e1f1573791f0b1164c532a04b815f03b Mon Sep 17 00:00:00 2001 From: Scott Gerlach Date: Tue, 21 Apr 2026 15:04:37 -0600 Subject: [PATCH 13/15] docs: clarify install instructions for pre-release state --- Formula/.gitkeep | 1 - README.md | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) delete mode 100644 Formula/.gitkeep diff --git a/Formula/.gitkeep b/Formula/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/Formula/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/README.md b/README.md index 18ea7b9..ff9335b 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,9 @@ brew tap stackhawk/hawkop brew install hawkop ``` +> Requires a published release. If you see `Error: No available formula`, the +> first release has not yet been cut — see [Releasing a new version](#releasing-a-new-version). + ## Upgrade ```sh From 1ceb00dc9cb47fb1bbb39107a6d9345dbedd98bf Mon Sep 17 00:00:00 2001 From: Scott Gerlach Date: Tue, 21 Apr 2026 15:08:28 -0600 Subject: [PATCH 14/15] fix: harden release workflow and renderer against injection and pipe pitfalls --- .github/workflows/update-formula.yml | 22 +++++++++++++++++----- scripts/update-formula.sh | 10 ++++++++-- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/.github/workflows/update-formula.yml b/.github/workflows/update-formula.yml index 815e8f7..6bd9de3 100644 --- a/.github/workflows/update-formula.yml +++ b/.github/workflows/update-formula.yml @@ -24,16 +24,24 @@ jobs: steps: - name: Resolve version id: resolve + env: + INPUT_VERSION: ${{ github.event.inputs.version }} + PAYLOAD_VERSION: ${{ github.event.client_payload.version }} run: | - if [ -n "${{ github.event.inputs.version }}" ]; then - v="${{ github.event.inputs.version }}" + if [ -n "$INPUT_VERSION" ]; then + v="$INPUT_VERSION" else - v="${{ github.event.client_payload.version }}" + v="$PAYLOAD_VERSION" fi if [ -z "$v" ]; then echo "error: no version supplied (inputs.version or client_payload.version)" >&2 exit 2 fi + # Defense in depth — reject anything that isn't a semver before we hand it to any shell. + if ! printf '%s' "$v" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?$'; then + echo "error: invalid version format '$v'" >&2 + exit 2 + fi echo "version=$v" >> "$GITHUB_OUTPUT" - uses: actions/checkout@v4 @@ -41,9 +49,13 @@ jobs: - uses: Homebrew/actions/setup-homebrew@master - name: Render formula - run: scripts/update-formula.sh --version "${{ steps.resolve.outputs.version }}" + env: + HAWKOP_VERSION: ${{ steps.resolve.outputs.version }} + run: scripts/update-formula.sh --version "$HAWKOP_VERSION" - name: Commit and push + env: + HAWKOP_VERSION: ${{ steps.resolve.outputs.version }} run: | git config user.name "hawkop-bot" git config user.email "noreply@stackhawk.com" @@ -52,5 +64,5 @@ jobs: exit 0 fi git add Formula/hawkop.rb - git commit -m "hawkop: update to ${{ steps.resolve.outputs.version }}" + git commit -m "hawkop: update to $HAWKOP_VERSION" git push origin main diff --git a/scripts/update-formula.sh b/scripts/update-formula.sh index 2a1c892..bca8dc5 100755 --- a/scripts/update-formula.sh +++ b/scripts/update-formula.sh @@ -28,7 +28,13 @@ OFFLINE=0 while [ $# -gt 0 ]; do case "$1" in - --version) VERSION="$2"; shift 2 ;; + --version) + if [ -z "${2:-}" ]; then + echo "error: --version requires an argument" >&2 + exit 2 + fi + VERSION="$2"; shift 2 + ;; --dry-run) DRY_RUN=1; shift ;; --offline) OFFLINE=1; shift ;; -h|--help) sed -n '2,8p' "$0"; exit 0 ;; @@ -60,7 +66,7 @@ fetch_sha() { echo "error: archive ${archive} returned HTTP ${archive_status}" >&2 exit 1 fi - curl -sSf "${BASE_URL}/${archive}.sha256" | parse_sha_from_stdin + curl -sSf "${BASE_URL}/${archive}.sha256" | "$0" --parse-sha-stdin } MAC_X64_SHA=$(fetch_sha "x86_64-apple-darwin") From e1a43350ee4715a7ced940df1da11bbbe29f77d7 Mon Sep 17 00:00:00 2001 From: Scott Gerlach Date: Tue, 21 Apr 2026 15:46:22 -0600 Subject: [PATCH 15/15] feat: hash tarballs directly and align with current brew audit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The release pipeline in stackhawk/hawkop does not publish .sha256 sidecar files alongside the tarballs — mirroring the hawkscan pattern of hashing the artifact locally. Update the renderer to download each tarball and compute SHA256 with shasum/sha256sum instead of fetching a sidecar. Also: - Drop the explicit `version` field from the formula template. Homebrew derives version from `hawkop-v-.tar.gz` and flags the explicit field as redundant under `brew audit --strict`. - Update the PR test workflow to symlink the checkout into the Taps dir and invoke `brew audit` / `brew install` / `brew test` by the tap- qualified name `stackhawk/hawkop/hawkop`. Current Homebrew (5.1+) disabled path-based audit. - Remove the renderer's optional local brew-audit hook for the same reason; developers should tap the checkout and run audit manually. - Update the probe step to extract the version from the URL pattern. Verified end-to-end locally against v0.6.1: render, audit (clean), install, and `brew test` all pass; `hawkop --version` reports 0.6.1. --- .github/workflows/test.yml | 20 ++++++++++++---- scripts/formula-template.rb | 1 - scripts/test-update-formula.bats | 5 +++- scripts/update-formula.sh | 41 +++++++++++++++++++++----------- 4 files changed, 47 insertions(+), 20 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 81a5cb5..c0f1b6c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,9 +31,19 @@ jobs: echo "skip=0" >> "$GITHUB_OUTPUT" fi + - name: Register checkout as local tap + if: steps.guard.outputs.skip == '0' + run: | + # Modern `brew audit` and `brew install` require a tap-qualified name, + # not a file path. Symlink the checked-out repo into the Taps dir so + # `stackhawk/hawkop/hawkop` resolves to the formula in this branch. + tap_parent="$(brew --repository)/Library/Taps/stackhawk" + mkdir -p "$tap_parent" + ln -sfn "$GITHUB_WORKSPACE" "$tap_parent/homebrew-hawkop" + - name: Audit if: steps.guard.outputs.skip == '0' - run: brew audit --strict --online Formula/hawkop.rb + run: brew audit --strict --online stackhawk/hawkop/hawkop - name: Probe tarball availability if: steps.guard.outputs.skip == '0' @@ -47,7 +57,9 @@ jobs: Linux-ARM64) target="aarch64-unknown-linux-gnu" ;; *) echo "unsupported runner: ${{ runner.os }}-${{ runner.arch }}"; exit 1 ;; esac - version=$(grep -E '^\s*version ' Formula/hawkop.rb | head -n1 | sed -E 's/.*"([^"]+)".*/\1/') + # Formula has no explicit `version` field — it's derived from the URL by + # Homebrew. Extract it from the first hawkop-v- pattern we see. + version=$(grep -oE 'hawkop-v[0-9][0-9A-Za-z.-]+' Formula/hawkop.rb | head -n1 | sed 's/^hawkop-v//' | sed -E 's/-(x86_64|aarch64).*$//') url="https://download.stackhawk.com/hawkop/cli/hawkop-v${version}-${target}.tar.gz" code=$(curl -sSIo /dev/null -w '%{http_code}' "$url") echo "url=$url" @@ -60,11 +72,11 @@ jobs: - name: Install if: steps.guard.outputs.skip == '0' && steps.probe.outputs.installable == '1' - run: brew install --verbose Formula/hawkop.rb + run: brew install --verbose stackhawk/hawkop/hawkop - name: Test if: steps.guard.outputs.skip == '0' && steps.probe.outputs.installable == '1' - run: brew test hawkop + run: brew test stackhawk/hawkop/hawkop lint-scripts: name: Shellcheck + bats diff --git a/scripts/formula-template.rb b/scripts/formula-template.rb index 8984b39..7390e16 100644 --- a/scripts/formula-template.rb +++ b/scripts/formula-template.rb @@ -3,7 +3,6 @@ class Hawkop < Formula desc "CLI companion for the StackHawk AppSec Intelligence Platform" homepage "https://www.stackhawk.com/" - version "${version}" license "MIT" on_macos do diff --git a/scripts/test-update-formula.bats b/scripts/test-update-formula.bats index b036778..005a408 100755 --- a/scripts/test-update-formula.bats +++ b/scripts/test-update-formula.bats @@ -53,8 +53,11 @@ teardown() { run "$SCRIPT" --version 1.2.3 --dry-run --offline [ "$status" -eq 0 ] [[ "$output" == *'class Hawkop < Formula'* ]] - [[ "$output" == *'version "1.2.3"'* ]] + # Version comes from the URL, not an explicit `version` field — Homebrew + # derives it from `hawkop-v-.tar.gz` to avoid `brew audit` + # flagging redundancy. [[ "$output" == *'hawkop-v1.2.3-x86_64-apple-darwin.tar.gz'* ]] + [[ "$output" == *'hawkop-v1.2.3-aarch64-unknown-linux-gnu.tar.gz'* ]] # No unsubstituted placeholders remain [[ "$output" != *'${version}'* ]] [[ "$output" != *'${mac_x64_sha256}'* ]] diff --git a/scripts/update-formula.sh b/scripts/update-formula.sh index bca8dc5..5789719 100755 --- a/scripts/update-formula.sh +++ b/scripts/update-formula.sh @@ -1,6 +1,6 @@ #!/bin/sh # Render scripts/formula-template.rb to Formula/hawkop.rb for a given hawkop version. -# Fetches SHA256 sidecars from download.stackhawk.com. +# Downloads each tarball from download.stackhawk.com and computes its SHA256. # # Usage: # scripts/update-formula.sh --version 0.6.2 @@ -11,7 +11,7 @@ set -eu parse_sha_from_stdin() { sha=$(tr -d '\r' | grep -Eo '[0-9a-fA-F]{64}' | head -n1) if [ -z "$sha" ]; then - echo "error: expected 64-char hex in sha256 sidecar" >&2 + echo "error: expected 64-char hex on stdin" >&2 return 1 fi printf '%s' "$sha" @@ -54,6 +54,19 @@ fi BASE_URL="https://download.stackhawk.com/hawkop/cli" +sha256_of_file() { + # Compute SHA256 of the given file. Prefer shasum (ships on macOS and most + # Linux distros); fall back to sha256sum. + if command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | awk '{print $1}' + elif command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + else + echo "error: neither shasum nor sha256sum is available" >&2 + return 1 + fi +} + fetch_sha() { target="$1" if [ "$OFFLINE" -eq 1 ]; then @@ -61,12 +74,20 @@ fetch_sha() { return 0 fi archive="hawkop-v${VERSION}-${target}.tar.gz" - archive_status=$(curl -sSIo /dev/null -w '%{http_code}' "${BASE_URL}/${archive}") - if [ "$archive_status" != "200" ]; then - echo "error: archive ${archive} returned HTTP ${archive_status}" >&2 + tmpfile=$(mktemp) + trap 'rm -f "$tmpfile"' EXIT INT TERM + if ! curl -sSfL -o "$tmpfile" "${BASE_URL}/${archive}"; then + echo "error: failed to download ${archive} from ${BASE_URL}" >&2 exit 1 fi - curl -sSf "${BASE_URL}/${archive}.sha256" | "$0" --parse-sha-stdin + sha=$(sha256_of_file "$tmpfile") + rm -f "$tmpfile" + trap - EXIT INT TERM + if [ -z "$sha" ] || [ "${#sha}" -ne 64 ]; then + echo "error: got unexpected hash for ${archive}: '$sha'" >&2 + exit 1 + fi + printf '%s' "$sha" } MAC_X64_SHA=$(fetch_sha "x86_64-apple-darwin") @@ -100,11 +121,3 @@ else render > Formula/hawkop.rb echo "wrote Formula/hawkop.rb (version $VERSION)" >&2 fi - -# --- Optional local audit --- -# If brew is on PATH and we actually wrote a file, run audit. Skip in dry-run -# or offline mode (offline produces zeroed SHAs that would fail audit). -if [ "$DRY_RUN" -eq 0 ] && [ "$OFFLINE" -eq 0 ] && command -v brew >/dev/null 2>&1; then - echo "running: brew audit --strict --online Formula/hawkop.rb" >&2 - brew audit --strict --online Formula/hawkop.rb -fi