Skip to content
Merged
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
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
67 changes: 67 additions & 0 deletions internal/config/default_cli_version_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
5 changes: 3 additions & 2 deletions internal/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down Expand Up @@ -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: |
Expand Down Expand Up @@ -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: |
Expand Down Expand Up @@ -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: |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 }}
Expand Down Expand Up @@ -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:
Expand Down