From 607b03587a661fc81b400f9efadc355bb0b5eea2 Mon Sep 17 00:00:00 2001 From: Joel Scheuner Date: Mon, 21 Sep 2026 18:40:52 +0200 Subject: [PATCH 1/2] Pin GoReleaser and gate the generated Homebrew cask in CI Co-Authored-By: Claude --- .github/workflows/ci.yml | 52 ++++++++++++++++++++++++++++---- .tool-versions | 1 + Makefile | 8 ++++- scripts/check-cask.sh | 65 ++++++++++++++++++++++++++++++++++++++++ scripts/lint-cask.sh | 48 +++++++++++++++++++++++++++++ 5 files changed, 168 insertions(+), 6 deletions(-) create mode 100755 scripts/check-cask.sh create mode 100755 scripts/lint-cask.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a84013e..d1d3d6ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,17 +110,26 @@ jobs: goreleaser-check: name: GoReleaser Check runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 15 steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Run GoReleaser check + - name: Set up Go + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + cache-dependency-path: go.sum + + - name: Install GoReleaser uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 with: distribution: goreleaser - version: "~> v2" - args: check + version-file: .tool-versions + install-only: true + + - name: Run GoReleaser check + run: goreleaser check # `goreleaser check` only validates config syntax, so it cannot see that # packaging bundled extensions and downloading them have to land @@ -131,6 +140,39 @@ jobs: - name: Test release scripts run: make test-scripts + # Nothing else reads the generated cask before a release publishes it. + # Render it and check what would ship. + - name: Check generated Homebrew cask + run: make check-cask + + - name: Upload generated cask + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: generated-cask + path: dist/homebrew/Casks/lstk.rb + if-no-files-found: error + + # The cask cops need Homebrew, absent on the Linux runners. Lints the bytes + # the job above rendered, not a second render that could diverge. Not a + # `release` gate yet -- the staged rollout govulncheck is on. + cask-lint: + name: Cask Lint + runs-on: macos-latest + needs: goreleaser-check + timeout-minutes: 15 + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Download generated cask + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: generated-cask + path: dist/homebrew/Casks + + - name: Run Homebrew linters + run: make lint-cask + test-unit: name: Unit Tests runs-on: ubuntu-latest @@ -376,7 +418,7 @@ jobs: uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 with: distribution: goreleaser - version: "~> v2" + version-file: .tool-versions args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.tool-versions b/.tool-versions index d4f4a9b6..3b3d4f37 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1,2 @@ golangci-lint 2.9.0 +goreleaser 2.18.2 diff --git a/Makefile b/Makefile index 14f08b8d..7e299496 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ endif BUILD_DIR=bin export CGO_ENABLED=0 -.PHONY: build clean test test-integration test-scripts lint govulncheck mock-generate otel +.PHONY: build clean test test-integration test-scripts check-cask lint-cask lint govulncheck mock-generate otel # Always invoke `go build` and let Go's build cache handle incrementality; a # file target on bin/lstk would be skipped when the binary exists, even with @@ -28,6 +28,12 @@ test-integration: build test-scripts: @./scripts/bundled-extensions/test-scripts.sh +check-cask: + @./scripts/check-cask.sh + +lint-cask: + @./scripts/lint-cask.sh + otel: docker compose -f docker-compose.tracing.yaml up -d diff --git a/scripts/check-cask.sh b/scripts/check-cask.sh new file mode 100755 index 00000000..0942907f --- /dev/null +++ b/scripts/check-cask.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# Render the Homebrew cask and check what a release would publish. +# +# Only a release writes the cask, into a tap with no CI, so a fault reaches +# users as their next `brew install` -- how the deprecated `postflight` stanza +# shipped (localstack/lstk#501, localstack/homebrew-tap#7). +# +# Homebrew's own linters need Homebrew, absent on the Linux runners, so they run +# against this output on macOS -- scripts/lint-cask.sh. +# +# Wipes dist/ and stubs bundled/; both paths come from the config. + +set -euo pipefail + +cd "$(dirname "$0")/.." + +CASK=dist/homebrew/Casks/lstk.rb + +if ! command -v goreleaser >/dev/null 2>&1; then + echo "goreleaser required on PATH: https://goreleaser.com/install/" >&2 + exit 1 +fi + +# The renderer's version is part of the artifact's definition, as `make lint` +# treats golangci-lint. +EXPECTED=$(awk '/^goreleaser/ {print $2}' .tool-versions) +INSTALLED=$(goreleaser --version 2>/dev/null | awk '/GitVersion:/ {print $2}' | sed 's/^v//') +if [ "$INSTALLED" != "$EXPECTED" ]; then + echo "goreleaser $EXPECTED required (found: ${INSTALLED:-none})" >&2 + exit 1 +fi + +# archives.files globs `bundled/`, so an empty tree fails the build. The cask +# ignores the bundle's contents and the real fetch needs a private-repo token, +# so stub it. +scripts/bundled-extensions/fetch-bundled-extensions.sh --stub + +goreleaser release --snapshot --clean + +[ -f "$CASK" ] || { echo "no cask generated at $CASK" >&2; exit 1; } + +fail() { echo "FAIL: $1" >&2; echo "--- $CASK ---" >&2; cat "$CASK" >&2; exit 1; } + +# Homebrew warns on every `brew` operation that loads a raw-Ruby flight block, +# and points the user at our tap. Matches only the deprecated spellings -- +# `_steps` leaves no trailing ` do` -- and ignores indentation, so an upstream +# reindent cannot silently empty the check. +grep -Eq '^[[:space:]]*(uninstall_)?(pre|post)flight do$' "$CASK" && + fail "cask uses a deprecated raw-Ruby flight stanza; use the *_steps form" + +# Our darwin binaries are unsigned, so Gatekeeper kills them unless the cask +# clears quarantine. Deleting the hook would satisfy the check above, so assert +# the step survives too. +grep -q 'postflight_steps do' "$CASK" || + fail "cask has no postflight_steps stanza" +grep -q 'com.apple.quarantine' "$CASK" || + fail "cask no longer clears com.apple.quarantine" + +# Homebrew's tokens share GoReleaser's delimiters, so `{{staged_path}}` must +# survive the template pass. An unescaped token fails the release; a broken +# escape would emit a literal that resolves to the wrong path. +grep -q '"{{staged_path}}"' "$CASK" || + fail "quarantine step does not target the whole staged dir" + +echo "OK: $CASK" diff --git a/scripts/lint-cask.sh b/scripts/lint-cask.sh new file mode 100755 index 00000000..3341482d --- /dev/null +++ b/scripts/lint-cask.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Run Homebrew's own linters over a generated cask. +# +# check-cask.sh asserts what we know to look for; these cops encode what +# Homebrew wants, catching deprecations nobody thought to check -- +# `Cask/InstallSteps` is what flags localstack/lstk#501. They need Homebrew, +# absent on the Linux runners, so CI renders there and lints here on macOS. +# +# Usage: scripts/lint-cask.sh [path/to/lstk.rb] + +set -euo pipefail + +cd "$(dirname "$0")/.." + +CASK="${1:-dist/homebrew/Casks/lstk.rb}" + +[ -f "$CASK" ] || { echo "no cask at $CASK -- run 'make check-cask' first" >&2; exit 1; } + +if ! command -v brew >/dev/null 2>&1; then + echo "brew required on PATH: https://brew.sh" >&2 + exit 1 +fi + +export HOMEBREW_NO_AUTO_UPDATE=1 + +# `brew audit` refuses a bare path, and the cask cops only fire under `Casks/`, +# so this needs a tap. Build a throwaway one and drop it however we exit. +TAP=lstkci/caskcheck +untap() { brew untap "$TAP" >/dev/null 2>&1 || true; } +trap untap EXIT +untap +brew tap-new --no-git "$TAP" >/dev/null +TAP_CASKS="$(brew --repository "$TAP")/Casks" +mkdir -p "$TAP_CASKS" +cp "$CASK" "$TAP_CASKS/lstk.rb" + +# TODO(#512): drop --except-cops once hooks.post.install_steps replaces +# custom_block, which renders the stanza first and so trips Cask/StanzaOrder on +# every stanza after it. Scoped to that one cop, so the deprecation cops this +# exists for still fire. +brew style --except-cops=Cask/StanzaOrder "$TAP_CASKS/lstk.rb" + +# Passes on a deprecated stanza, so it guards other faults, not #501. The +# `--online` form checks urls and checksums, but a rendered cask points at an +# unpublished release -- that belongs in the tap. +brew audit --cask "$TAP/lstk" + +echo "OK: Homebrew linters clean for $CASK" From 7e5504e67bb780eb07859691ade07c51efc5002e Mon Sep 17 00:00:00 2001 From: Joel Scheuner Date: Tue, 22 Sep 2026 12:10:28 +0200 Subject: [PATCH 2/2] Use hooks.post.install_steps for the cask quarantine step Co-Authored-By: Claude --- .goreleaser.yaml | 25 +++++++++++-------------- .tool-versions | 2 +- scripts/check-cask.sh | 6 +++--- scripts/lint-cask.sh | 6 +----- 4 files changed, 16 insertions(+), 23 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index bcef1288..f3fbab7d 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -83,17 +83,14 @@ homebrew_casks: bash: completions/lstk.bash zsh: completions/lstk.zsh fish: completions/lstk.fish - # hooks.post.install emits Homebrew's deprecated `postflight`; the - # replacement arrives as hooks.post.install_steps in GoReleaser v2.19 - # (goreleaser/goreleaser#6873). Until then custom_block writes the stanza, - # escaping `{{staged_path}}` past GoReleaser's template pass. It renders - # first in the cask, so `brew style` reports stanza-order offences. - custom_block: | - postflight_steps do - on_macos do - # The whole staged dir, not only lstk: the bundled extensions binary - # sits next to it and would otherwise be blocked by Gatekeeper on its - # first run. - run "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "{{ "{{staged_path}}" }}"] - end - end + hooks: + post: + # `.StagedPath` is GoReleaser's field; it renders to Homebrew's own + # `{{staged_path}}` token, which resolves at install time. + install_steps: | + on_macos do + # The whole staged dir, not only lstk: the bundled extensions binary + # sits next to it and would otherwise be blocked by Gatekeeper on its + # first run. + run "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "{{ .StagedPath }}"] + end diff --git a/.tool-versions b/.tool-versions index 3b3d4f37..9e370a1f 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ golangci-lint 2.9.0 -goreleaser 2.18.2 +goreleaser 2.19.0 diff --git a/scripts/check-cask.sh b/scripts/check-cask.sh index 0942907f..d5b33c1f 100755 --- a/scripts/check-cask.sh +++ b/scripts/check-cask.sh @@ -56,9 +56,9 @@ grep -q 'postflight_steps do' "$CASK" || grep -q 'com.apple.quarantine' "$CASK" || fail "cask no longer clears com.apple.quarantine" -# Homebrew's tokens share GoReleaser's delimiters, so `{{staged_path}}` must -# survive the template pass. An unescaped token fails the release; a broken -# escape would emit a literal that resolves to the wrong path. +# `.StagedPath` must reach the cask as Homebrew's `{{staged_path}}` token, +# resolved at install time. A literal path would clear quarantine elsewhere, or +# nowhere. grep -q '"{{staged_path}}"' "$CASK" || fail "quarantine step does not target the whole staged dir" diff --git a/scripts/lint-cask.sh b/scripts/lint-cask.sh index 3341482d..ad6a14fa 100755 --- a/scripts/lint-cask.sh +++ b/scripts/lint-cask.sh @@ -34,11 +34,7 @@ TAP_CASKS="$(brew --repository "$TAP")/Casks" mkdir -p "$TAP_CASKS" cp "$CASK" "$TAP_CASKS/lstk.rb" -# TODO(#512): drop --except-cops once hooks.post.install_steps replaces -# custom_block, which renders the stanza first and so trips Cask/StanzaOrder on -# every stanza after it. Scoped to that one cop, so the deprecation cops this -# exists for still fire. -brew style --except-cops=Cask/StanzaOrder "$TAP_CASKS/lstk.rb" +brew style "$TAP_CASKS/lstk.rb" # Passes on a deprecated stanza, so it guards other faults, not #501. The # `--online` form checks urls and checksums, but a rendered cask points at an