From 0f276800b2b68ff4a5c9fe2c04b1c61d2f477563 Mon Sep 17 00:00:00 2001 From: Joshua Temple Date: Wed, 15 Jul 2026 19:49:12 -0400 Subject: [PATCH] fix(config): bump DefaultCLIVersion to v0.16.1 and guard it against lagging releases Signed-off-by: Joshua Temple --- CONTRIBUTING.md | 1 + internal/config/default_cli_version_test.go | 67 +++++++++++++++++++ internal/config/types.go | 5 +- ...hub__workflows__cascade-hotfix.yaml.golden | 16 ++--- ...b__workflows__cascade-rollback.yaml.golden | 8 +-- ...ub__workflows__external-update.yaml.golden | 4 +- ...github__workflows__orchestrate.yaml.golden | 8 +-- .../.github__workflows__promote.yaml.golden | 12 ++-- 8 files changed, 95 insertions(+), 26 deletions(-) create mode 100644 internal/config/default_cli_version_test.go diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8dacd32f..84069c11 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -75,6 +75,7 @@ cascade owns the third-party action pins it emits into generated workflows, and - A machine-authored commit (a bot or CI job writing on the project's behalf) stages an explicit pathspec allowlist naming exactly the files it intends to change. It never uses a blanket `git add -A` or `git add .`, so an unrelated working-tree change can never ride along. - Generated files are targets, never sources: a pin (or any other value) is read from the manifest and written into generated output, never read back out of a generated file. This keeps generation a pure, offline function of the manifest, which is what makes a regenerate reproducible and a diff meaningful. - cascade's own self-heal companion is generated, not hand-written. `.github/workflows/pin-reconcile.yaml` is produced by the same reconcile generator that emits a downstream user's companion, in its own-repo variant, and is drift-locked byte-for-byte by a test so a hand-edit fails the suite. The own-repo variant differs from the user emission in exactly three ways: it installs the latest non-prerelease cascade release (never an rc or a draft, so cascade's own CI cannot self-install a prerelease), it scans both the workflow and composite-action trees for a moved pin, and it commits the regenerated workflows alongside the updated `action_pins.yaml`. Change the generator and regenerate the file; never edit the workflow by hand. +- `DefaultCLIVersion` in `internal/config/types.go` is the setup-cli pin baked into every generated workflow whose manifest leaves `cli_version` unset, so it must always name the newest stable release tag. Bump it in the same body of work that cuts a release, then regenerate the byte-identical baseline goldens (`go test ./internal/generate/ -run TestByteIdenticalBaseline -update`). `TestDefaultCLIVersion_MatchesLatestReleaseTag` fails any full clone whose const lags the repository's tags. ## Tag grammar diff --git a/internal/config/default_cli_version_test.go b/internal/config/default_cli_version_test.go new file mode 100644 index 00000000..18ae2ab4 --- /dev/null +++ b/internal/config/default_cli_version_test.go @@ -0,0 +1,67 @@ +package config + +import ( + "os/exec" + "regexp" + "strconv" + "strings" + "testing" +) + +// stableTagPattern matches an immutable stable release tag (vX.Y.Z with no +// prerelease suffix). DefaultCLIVersion must always be one of these: it is +// baked into generated workflows as the setup-cli pin for every adopter that +// leaves cli_version unset, so a mutable ref or prerelease here weakens the +// supply-chain posture of all default configurations. +var stableTagPattern = regexp.MustCompile(`^v(\d+)\.(\d+)\.(\d+)$`) + +// TestDefaultCLIVersion_IsStableReleaseTag guards the shape of the default: +// a plain vX.Y.Z tag, never "latest", "beta", a branch name, or an rc/dryrun +// prerelease. +func TestDefaultCLIVersion_IsStableReleaseTag(t *testing.T) { + if !stableTagPattern.MatchString(DefaultCLIVersion) { + t.Fatalf("DefaultCLIVersion %q is not a stable vX.Y.Z release tag", DefaultCLIVersion) + } +} + +// TestDefaultCLIVersion_MatchesLatestReleaseTag guards against the default +// silently rotting behind the released CLI. It shipped as v0.1.0 for fifteen +// releases, pinning every default adopter to a pre-hardening CLI. +// +// The expectation is derived from the repository's own stable tags, so there +// is no network dependency: each release must bump DefaultCLIVersion in the +// same body of work that cuts the tag (see CONTRIBUTING.md). Shallow CI +// checkouts without tags skip; any full clone (every local inner loop and +// fetch-depth: 0 workflow) enforces the match. +func TestDefaultCLIVersion_MatchesLatestReleaseTag(t *testing.T) { + out, err := exec.Command("git", "tag", "--list", "v*").Output() + if err != nil { + t.Skipf("git tags unavailable (%v); cannot derive the latest release tag", err) + } + + latest := "" + var latestParts [3]int + for _, tag := range strings.Fields(string(out)) { + m := stableTagPattern.FindStringSubmatch(tag) + if m == nil { + continue // prerelease (rc/dryrun) or foreign tag + } + var parts [3]int + for i := 0; i < 3; i++ { + parts[i], _ = strconv.Atoi(m[i+1]) + } + if latest == "" || parts[0] > latestParts[0] || + (parts[0] == latestParts[0] && (parts[1] > latestParts[1] || + (parts[1] == latestParts[1] && parts[2] > latestParts[2]))) { + latest, latestParts = tag, parts + } + } + if latest == "" { + t.Skip("no stable release tags visible (shallow checkout); cannot derive the latest release tag") + } + + if DefaultCLIVersion != latest { + t.Fatalf("DefaultCLIVersion %q lags the latest stable release tag %q; bump the const in internal/config/types.go and regenerate the byte-identical baseline goldens (go test ./internal/generate/ -run TestByteIdenticalBaseline -update)", + DefaultCLIVersion, latest) + } +} diff --git a/internal/config/types.go b/internal/config/types.go index 24de9bc2..a0d0439f 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -324,8 +324,9 @@ func (c *TrunkConfig) ValidateSchemaVersion() (warnings []string, fatalErr error // DefaultCLIVersion is the immutable cascade release tag pinned into generated // workflows when cli_version is unset (or set to the mutable "latest"). Bump this -// with each cascade release so generated pipelines track the newest stable tag. -const DefaultCLIVersion = "v0.1.0" +// with each cascade release so generated pipelines track the newest stable tag; +// TestDefaultCLIVersion_MatchesLatestReleaseTag fails when it lags the repo's tags. +const DefaultCLIVersion = "v0.16.1" // GetCLIVersion returns the configured CLI version, resolving mutable refs to the // immutable pinned default for supply-chain integrity. diff --git a/internal/generate/testdata/byte_identical_baseline/.github__workflows__cascade-hotfix.yaml.golden b/internal/generate/testdata/byte_identical_baseline/.github__workflows__cascade-hotfix.yaml.golden index 10ec78a3..e2c92750 100644 --- a/internal/generate/testdata/byte_identical_baseline/.github__workflows__cascade-hotfix.yaml.golden +++ b/internal/generate/testdata/byte_identical_baseline/.github__workflows__cascade-hotfix.yaml.golden @@ -81,9 +81,9 @@ jobs: with: fetch-depth: 0 - name: Setup CLI - uses: stablekernel/cascade/.github/actions/setup-cli@v0.1.0 + uses: stablekernel/cascade/.github/actions/setup-cli@v0.16.1 with: - version: v0.1.0 + version: v0.16.1 token: ${{ github.token }} - name: Fetch env branches and tags run: | @@ -131,9 +131,9 @@ jobs: with: fetch-depth: 0 - name: Setup CLI - uses: stablekernel/cascade/.github/actions/setup-cli@v0.1.0 + uses: stablekernel/cascade/.github/actions/setup-cli@v0.16.1 with: - version: v0.1.0 + version: v0.16.1 token: ${{ github.token }} - name: Fetch env branches and tags run: | @@ -253,9 +253,9 @@ jobs: with: fetch-depth: 0 - name: Setup CLI - uses: stablekernel/cascade/.github/actions/setup-cli@v0.1.0 + uses: stablekernel/cascade/.github/actions/setup-cli@v0.16.1 with: - version: v0.1.0 + version: v0.16.1 token: ${{ github.token }} - name: Validate manifest run: | @@ -399,9 +399,9 @@ jobs: with: fetch-depth: 0 - name: Setup CLI - uses: stablekernel/cascade/.github/actions/setup-cli@v0.1.0 + uses: stablekernel/cascade/.github/actions/setup-cli@v0.16.1 with: - version: v0.1.0 + version: v0.16.1 token: ${{ github.token }} - name: Fetch env branches and tags run: | diff --git a/internal/generate/testdata/byte_identical_baseline/.github__workflows__cascade-rollback.yaml.golden b/internal/generate/testdata/byte_identical_baseline/.github__workflows__cascade-rollback.yaml.golden index 8611ed95..2d0a9ded 100644 --- a/internal/generate/testdata/byte_identical_baseline/.github__workflows__cascade-rollback.yaml.golden +++ b/internal/generate/testdata/byte_identical_baseline/.github__workflows__cascade-rollback.yaml.golden @@ -60,10 +60,10 @@ jobs: with: fetch-depth: 0 - name: Setup CLI - uses: stablekernel/cascade/.github/actions/setup-cli@v0.1.0 + uses: stablekernel/cascade/.github/actions/setup-cli@v0.16.1 with: token: ${{ secrets.GITHUB_TOKEN }} - version: v0.1.0 + version: v0.16.1 - name: Resolve Target id: preflight env: @@ -116,10 +116,10 @@ jobs: with: fetch-depth: 0 - name: Setup CLI - uses: stablekernel/cascade/.github/actions/setup-cli@v0.1.0 + uses: stablekernel/cascade/.github/actions/setup-cli@v0.16.1 with: token: ${{ secrets.GITHUB_TOKEN }} - version: v0.1.0 + version: v0.16.1 - name: Finalize Rollback env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/internal/generate/testdata/byte_identical_baseline/.github__workflows__external-update.yaml.golden b/internal/generate/testdata/byte_identical_baseline/.github__workflows__external-update.yaml.golden index f648c02d..96d941a5 100644 --- a/internal/generate/testdata/byte_identical_baseline/.github__workflows__external-update.yaml.golden +++ b/internal/generate/testdata/byte_identical_baseline/.github__workflows__external-update.yaml.golden @@ -51,10 +51,10 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} - name: Setup CLI - uses: stablekernel/cascade/.github/actions/setup-cli@v0.1.0 + uses: stablekernel/cascade/.github/actions/setup-cli@v0.16.1 with: token: ${{ secrets.GITHUB_TOKEN }} - version: v0.1.0 + version: v0.16.1 - name: Configure Git run: | diff --git a/internal/generate/testdata/byte_identical_baseline/.github__workflows__orchestrate.yaml.golden b/internal/generate/testdata/byte_identical_baseline/.github__workflows__orchestrate.yaml.golden index 137548a3..bf8e8962 100644 --- a/internal/generate/testdata/byte_identical_baseline/.github__workflows__orchestrate.yaml.golden +++ b/internal/generate/testdata/byte_identical_baseline/.github__workflows__orchestrate.yaml.golden @@ -60,10 +60,10 @@ jobs: with: fetch-depth: 0 - name: Setup CLI - uses: stablekernel/cascade/.github/actions/setup-cli@v0.1.0 + uses: stablekernel/cascade/.github/actions/setup-cli@v0.16.1 with: token: ${{ secrets.GITHUB_TOKEN }} - version: v0.1.0 + version: v0.16.1 - name: Run Setup id: setup env: @@ -192,10 +192,10 @@ jobs: echo "_No outputs produced_" >> "$GITHUB_STEP_SUMMARY" fi - name: Setup CLI - uses: stablekernel/cascade/.github/actions/setup-cli@v0.1.0 + uses: stablekernel/cascade/.github/actions/setup-cli@v0.16.1 with: token: ${{ secrets.GITHUB_TOKEN }} - version: v0.1.0 + version: v0.16.1 - name: Generate Changelog id: changelog env: diff --git a/internal/generate/testdata/byte_identical_baseline/.github__workflows__promote.yaml.golden b/internal/generate/testdata/byte_identical_baseline/.github__workflows__promote.yaml.golden index df3450f3..8deb565d 100644 --- a/internal/generate/testdata/byte_identical_baseline/.github__workflows__promote.yaml.golden +++ b/internal/generate/testdata/byte_identical_baseline/.github__workflows__promote.yaml.golden @@ -118,10 +118,10 @@ jobs: with: fetch-depth: 0 - name: Setup CLI - uses: stablekernel/cascade/.github/actions/setup-cli@v0.1.0 + uses: stablekernel/cascade/.github/actions/setup-cli@v0.16.1 with: token: ${{ secrets.GITHUB_TOKEN }} - version: v0.1.0 + version: v0.16.1 - name: Run Preflight id: preflight env: @@ -155,10 +155,10 @@ jobs: steps: - uses: actions/checkout@v7 - name: Setup CLI - uses: stablekernel/cascade/.github/actions/setup-cli@v0.1.0 + uses: stablekernel/cascade/.github/actions/setup-cli@v0.16.1 with: token: ${{ secrets.GITHUB_TOKEN }} - version: v0.1.0 + version: v0.16.1 - name: Validate Promotion env: MODE: ${{ github.event.inputs.mode }} @@ -352,10 +352,10 @@ jobs: with: fetch-depth: 0 - name: Setup CLI - uses: stablekernel/cascade/.github/actions/setup-cli@v0.1.0 + uses: stablekernel/cascade/.github/actions/setup-cli@v0.16.1 with: token: ${{ secrets.GITHUB_TOKEN }} - version: v0.1.0 + version: v0.16.1 - name: Generate Changelog id: changelog env: