From 260bb75efd4d96d6eaa343b60a3ff6abea43fcf0 Mon Sep 17 00:00:00 2001 From: Joel Scheuner Date: Mon, 21 Sep 2026 18:40:52 +0200 Subject: [PATCH] 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 | 63 ++++++++++++++++++++++++++++++++++++++++ scripts/lint-cask.sh | 46 +++++++++++++++++++++++++++++ 5 files changed, 164 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..2c6d5580 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. Left out of + # `release`'s `needs:` for now, like govulncheck, until it has proven itself. + 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..80f00d19 --- /dev/null +++ b/scripts/check-cask.sh @@ -0,0 +1,63 @@ +#!/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 + +# A different GoReleaser can render a different cask, so the pin is part of the +# artifact's definition. +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, and the real fetch needs a private-repo token. +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. +# Only deprecated spellings match -- `_steps` leaves no trailing ` do` -- and +# indentation is ignored, so a reindent upstream cannot 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 quarantine +# is cleared. Deleting the hook would satisfy the check above, so assert the +# step survives. +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. An unescaped +# `{{staged_path}}` fails the release; a broken escape emits a literal pointing +# somewhere else. +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..356739f1 --- /dev/null +++ b/scripts/lint-cask.sh @@ -0,0 +1,46 @@ +#!/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 catch what we did +# not -- `Cask/InstallSteps` is what flags localstack/lstk#501. They need +# Homebrew, absent on the Linux runners, so CI renders there and lints here. +# +# 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 trips Cask/StanzaOrder on +# everything after it. Scoped to that cop so the deprecation cops still fire. +brew style --except-cops=Cask/StanzaOrder "$TAP_CASKS/lstk.rb" + +# Passes on a deprecated stanza, so this guards other faults, not #501. Its +# `--online` form checks urls and checksums, but only the tap has a published +# release to check against. +brew audit --cask "$TAP/lstk" + +echo "OK: Homebrew linters clean for $CASK"