Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 47 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 }}
Expand Down
25 changes: 11 additions & 14 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
golangci-lint 2.9.0
goreleaser 2.19.0
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
65 changes: 65 additions & 0 deletions scripts/check-cask.sh
Original file line number Diff line number Diff line change
@@ -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"

# `.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"

echo "OK: $CASK"
44 changes: 44 additions & 0 deletions scripts/lint-cask.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/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"

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
# unpublished release -- that belongs in the tap.
brew audit --cask "$TAP/lstk"

echo "OK: Homebrew linters clean for $CASK"
Loading