Files
Bo 8462893486 ci(release): goreleaser snapshot smoke between releases (#1030)
Closes the release-record commitment carried across three releases
(`docs/audits/release-readiness-v3.3.0.md`, "Process lesson"): **CI must
exercise the goreleaser release path between releases.** A retired
`.goreleaser.yml` `before.hooks` entry once sat broken for 20 days
because only a real `v*` tag runs that path.

## What lands

- `.github/workflows/release-path-smoke.yml` — full GoReleaser snapshot,
publishing skipped.
- `tests/scripts/lib/release-snapshot-smoke.sh` — the one smoke path,
shared by CI and its negative witness.
- `tests/scripts/release-path-smoke.bats` — the negative witness (10
tests).
- `.github/workflows/nightly.yml` — calls the smoke via `workflow_call`,
wired into the summary table + failure-issue condition.
- `CHANGELOG.md` + `docs/CHANGELOG.md` — `[Unreleased]` entry
(byte-identical mirror).

## Exact invocation, and why

```
goreleaser release --snapshot --clean --skip=publish --config <cfg> --timeout 20m
```

Installed with the **same** pinned
`goreleaser/goreleaser-action@f06c13b6… # v7` and `version: '~> v2'`
that `release.yml` uses (`install-only: true`), so the smoke exercises
the binary the publisher would actually get.

**`goreleaser check` is deliberately not the gate.** Verified against
the repo's config on goreleaser 2.16.0: `goreleaser check` exits **1**
on deprecation warnings alone (the config's `brews:` block), so it
cannot gate without changing release behavior — and it never runs hooks,
builds, archives, or the Homebrew formula template, which are precisely
the stages that broke. The snapshot exits 0 on the same config. So the
evidence below is from the **real snapshot**, not the cheap proxy.

## Acceptance → evidence

**1. Nightly + release-plumbing-input triggers.**
- Nightly: `nightly.yml` gains `release-path-smoke: uses:
./.github/workflows/release-path-smoke.yml`, added to `summary.needs`,
the summary table, and both failure conditions.
- PR/push: `paths:` filter on `.goreleaser.yml`,
`.github/workflows/release.yml`, `cli/go.mod`, `cli/Formula/**`, plus
the smoke script and its bats file (so edits to the gate re-run the
gate). A **dedicated workflow** rather than a `validate.yml` filter
branch — same isolation rationale as `workflow-scripts-syntax.yml` ("no
shared job registry to drift"), and it keeps `validate.yml`'s `changes`
output list untouched.
- **Runtime choice: full six-target matrix, no split.** Measured
locally: `11.5s` wall for `darwin/linux/windows × amd64/arm64`
end-to-end (build + archive + checksums + formula render). A
single-platform snapshot would leave five publisher targets unexercised
for no meaningful time saving. Job `timeout-minutes: 20`.

**2. Negative witness.** `tests/scripts/release-path-smoke.bats` runs
**the CI smoke path itself** against deliberately broken temp copies of
`.goreleaser.yml`:
- `red: a retired before-hook makes the release path FAIL` — reproduces
the exact 20-day bug (config prepended with `before: hooks: -
./scripts/this-script-was-retired.sh`), asserts `status -eq 1`.
- `red: a structurally invalid config makes the release path FAIL` —
asserts `status -eq 1`.
- Plus usage-error legs asserting **exit 2** so "goreleaser missing /
bad flags" is never mistaken for "release path broken".

```
$ AGENTOPS_RELEASE_SMOKE_FULL=1 bats tests/scripts/release-path-smoke.bats
1..10   … ok 1 … ok 10        # all 10 pass, both RED legs fire
```

The job is workflow-only (no new registry gate), so per the intent I
confirmed the ratchet stays green:

```
$ cd cli && go test ./internal/gates/checks/ -run TestBlockingGatesHaveProvenNegativeWitness
ok   (1 passed)
```

**3. Passes against current main's config.** The green leg is the real
snapshot, not `goreleaser check`:

```
$ bash tests/scripts/lib/release-snapshot-smoke.sh
… building binaries (6 targets) … archives … checksums … homebrew formula
release succeeded after 11s
OK    release path built end-to-end (publish skipped)     # rc=0
```

Also covered as the bats `green:` test (gated behind
`AGENTOPS_RELEASE_SMOKE_FULL=1` so the generic `bats
tests/scripts/*.bats` job stays fast; the smoke workflow sets it).

**4. CHANGELOG.** `[Unreleased] → Added` entry in `CHANGELOG.md`,
mirrored byte-identically to `docs/CHANGELOG.md` (`diff` clean).

## Local gates (all rc=0)

| Check | Result |
|---|---|
| `cd cli && go build ./...` | 0 |
| `cd cli && go vet ./...` | 0 |
| `cd cli && go test ./...` | 0 |
| `bash scripts/regen-all.sh --check` | 0 — "All generated projections
are current." |
| `bats --jobs 4 tests/scripts/*.bats` | 0 — 953 ok, 0 not ok |
| `ao gate check --full --workflow-coverage --require-workflow-parity` |
0 — 68/68 passed |
| `shellcheck tests/scripts/lib/release-snapshot-smoke.sh` | clean |
| YAML parse (both workflows) | OK |

## Scope

Write scope honored: `.github/workflows/**`, `tests/scripts/**`,
`CHANGELOG.md`, `docs/CHANGELOG.md`. **`.goreleaser.yml` is
read/verified only — not modified.** No `cli/**` Go code, no
`skills/**`.

## Residual risk

- The `brews:` deprecation is left in place (out of scope). It is a
warning today; if GoReleaser v3 removes it, this smoke is exactly what
will surface it between releases instead of at tag time — which is the
point.
- Nightly `workflow_call` inherits caller permissions; the reusable
workflow declares `permissions: contents: read` and the nightly job pins
the same.
2026-07-31 14:44:31 -04:00
..