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
22 changes: 15 additions & 7 deletions internal/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,16 +243,24 @@ func (c *TrunkConfig) ValidateSchemaVersion() (warnings []string, fatalErr error
return nil, nil
}

// GetCLIVersion returns the configured CLI version.
// 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"

// GetCLIVersion returns the configured CLI version, resolving mutable refs to the
// immutable pinned default for supply-chain integrity.
// Supported values:
// - "" or "latest" → uses the "latest" tag (most recent stable release)
// - "beta" → uses "master" branch (bleeding edge, may be unstable)
// - "vX.Y.Z" → uses a specific version tag
// - "" or "latest" → DefaultCLIVersion (the pinned, immutable release tag)
// - "beta" → passes through; the generator maps it to "master" (bleeding edge)
// - "vX.Y.Z" → passes through as a specific version tag
func (c *TrunkConfig) GetCLIVersion() string {
if c.CLIVersion == "" {
return "latest"
switch c.CLIVersion {
case "", "latest":
return DefaultCLIVersion
default:
return c.CLIVersion
}
return c.CLIVersion
}

// GetTagPrefix returns the configured tag prefix or "v" if not specified
Expand Down
14 changes: 11 additions & 3 deletions internal/config/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,11 +391,19 @@ func TestGetTriggersForDeploy_BuildWithNoTriggers(t *testing.T) {
// =============================================================================

func TestGetCLIVersion(t *testing.T) {
// Default when not set
// Default when not set resolves to the pinned, immutable release tag.
cfg := &TrunkConfig{}
assert.Equal(t, "latest", cfg.GetCLIVersion())
assert.Equal(t, DefaultCLIVersion, cfg.GetCLIVersion())

// Configured value
// Explicit "latest" is a mutable ref and is pinned to the default tag.
cfg.CLIVersion = "latest"
assert.Equal(t, DefaultCLIVersion, cfg.GetCLIVersion())

// "beta" is the explicit opt-in escape hatch and passes through.
cfg.CLIVersion = "beta"
assert.Equal(t, "beta", cfg.GetCLIVersion())

// Configured version tag passes through unchanged.
cfg.CLIVersion = "v1.2.3"
assert.Equal(t, "v1.2.3", cfg.GetCLIVersion())
}
Expand Down
24 changes: 23 additions & 1 deletion internal/generate/action_pins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ func TestPinPolicy_E2E_OrchestrateEmitsPinnedRefs(t *testing.T) {
require.NoError(t, err)

// No mutable third-party tag survives. cascade's own setup-cli ref is
// intentionally excluded and may still carry @latest (its CLI version).
// intentionally excluded from SHA pinning and carries its version tag.
for _, action := range thirdPartyActions {
assert.NotContainsf(t, out, "uses: "+action+"@v", "third-party %s must be SHA-pinned, not tagged", action)
assert.NotContainsf(t, out, action+"@latest", "third-party %s must never be @latest", action)
Expand Down Expand Up @@ -184,6 +184,28 @@ func TestPinPolicy_E2E_OrchestrateEmitsPinnedRefs(t *testing.T) {
"cascade-owned action must not be SHA-pinned by the third-party pin table")
}

// TestGetCLIRef_DefaultIsPinnedNotMutable asserts that, under a default manifest
// with cli_version unset, the generated self-action ref is the immutable release
// tag (config.DefaultCLIVersion) and never the mutable @latest or @master refs.
// SHA-pinning the self-action is not done at generation time; the version tag is
// the supply-chain pin.
func TestGetCLIRef_DefaultIsPinnedNotMutable(t *testing.T) {
cfg, tmpDir := newPinE2EConfig(t)
cfg.PinMode = "" // default manifest, no pin config
// cli_version intentionally unset.

out, err := NewGenerator(cfg, tmpDir).Generate()
require.NoError(t, err)

const action = "stablekernel/cascade/.github/actions/setup-cli"
assert.Contains(t, out, action+"@"+config.DefaultCLIVersion,
"self-action must be pinned to the immutable release tag by default")
assert.NotContains(t, out, action+"@latest",
"self-action default ref must not be the mutable @latest")
assert.NotContains(t, out, action+"@master",
"self-action default ref must not be the mutable @master")
}

// TestPinPolicy_E2E_Deterministic confirms re-generating against the same pin
// config yields byte-identical output (no network, table-driven).
func TestPinPolicy_E2E_Deterministic(t *testing.T) {
Expand Down
16 changes: 7 additions & 9 deletions internal/generate/external.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,15 @@ func NewExternalUpdateGenerator(cfg *config.TrunkConfig, baseDir string) *Extern
}
}

// getCLIRef returns the Git ref to use for the cascade actions.
// getCLIRef returns the Git ref for the cascade self-action. The default
// (cli_version unset or "latest") resolves to config.DefaultCLIVersion, an
// immutable release tag, so consumers never run an unpinned mutable ref.
// "beta" is the explicit opt-in escape hatch to the "master" branch.
func (g *ExternalUpdateGenerator) getCLIRef() string {
version := g.config.GetCLIVersion()
switch version {
case "latest", "":
return "latest"
case "beta":
return "master"
default:
return version
if g.config.CLIVersion == "beta" {
return "master" // Explicit opt-in escape hatch to trunk.
}
return g.config.GetCLIVersion()
}

// getReleaseTokenRef returns the token expression for release operations.
Expand Down
21 changes: 9 additions & 12 deletions internal/generate/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,21 +177,18 @@ func (g *Generator) anyAutoCommits() bool {
return false
}

// getCLIRef returns the Git ref to use for the cascade actions.
// getCLIRef returns the Git ref to use for the cascade self-action. The default
// (cli_version unset or "latest") resolves to config.DefaultCLIVersion, an
// immutable release tag, so consumers never run an unpinned mutable ref.
// Supported values:
// - "latest" → uses the "latest" tag (updated with each stable release)
// - "beta" → uses "master" branch (bleeding edge, may be unstable)
// - "vX.Y.Z" → uses a specific version tag
// - unset / "latest" → config.DefaultCLIVersion (immutable, pinned default)
// - "beta" → "master" branch (explicit opt-in, bleeding edge, may be unstable)
// - "vX.Y.Z" → that specific version tag
func (g *Generator) getCLIRef() string {
version := g.config.GetCLIVersion()
switch version {
case "latest", "":
return "latest" // Points to the most recent stable release
case "beta":
return "master" // Bleeding edge from trunk
default:
return version // Specific version tag (e.g., v1.0.0)
if g.config.CLIVersion == "beta" {
return "master" // Explicit opt-in escape hatch to trunk.
}
return g.config.GetCLIVersion()
}

// getReleaseTokenRef returns the token expression for release operations.
Expand Down
15 changes: 6 additions & 9 deletions internal/generate/hotfix.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,14 @@ func (g *HotfixGenerator) targetEnvs() []string {
}

// getCLIRef mirrors the ref-resolution used by the other generators so the
// emitted setup-cli ref tracks config.cli_version.
// emitted setup-cli ref tracks config.cli_version. The default (cli_version
// unset or "latest") resolves to config.DefaultCLIVersion, an immutable release
// tag; "beta" is the explicit opt-in escape hatch to the "master" branch.
func (g *HotfixGenerator) getCLIRef() string {
version := g.config.GetCLIVersion()
switch version {
case "latest", "":
return "latest"
case "beta":
return "master"
default:
return version
if g.config.CLIVersion == "beta" {
return "master" // Explicit opt-in escape hatch to trunk.
}
return g.config.GetCLIVersion()
}

// getManifestFilePath returns the repo-relative manifest path for use in the
Expand Down
15 changes: 6 additions & 9 deletions internal/generate/merge_queue.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,14 @@ func (g *MergeQueueGenerator) Enabled() bool {
}

// getCLIRef mirrors the ref-resolution used by the other generators so the
// emitted setup-cli ref tracks config.cli_version.
// emitted setup-cli ref tracks config.cli_version. The default (cli_version
// unset or "latest") resolves to config.DefaultCLIVersion, an immutable release
// tag; "beta" is the explicit opt-in escape hatch to the "master" branch.
func (g *MergeQueueGenerator) getCLIRef() string {
version := g.config.GetCLIVersion()
switch version {
case "latest", "":
return "latest"
case "beta":
return "master"
default:
return version
if g.config.CLIVersion == "beta" {
return "master" // Explicit opt-in escape hatch to trunk.
}
return g.config.GetCLIVersion()
}

// getManifestFilePath returns the repo-relative manifest path for use in the
Expand Down
16 changes: 7 additions & 9 deletions internal/generate/pr_preview.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,15 @@ func NewPRPreviewGenerator(cfg *config.TrunkConfig, baseDir string) *PRPreviewGe
}
}

// getCLIRef returns the Git ref to use for the cascade actions.
// getCLIRef returns the Git ref for the cascade self-action. The default
// (cli_version unset or "latest") resolves to config.DefaultCLIVersion, an
// immutable release tag, so consumers never run an unpinned mutable ref.
// "beta" is the explicit opt-in escape hatch to the "master" branch.
func (g *PRPreviewGenerator) getCLIRef() string {
version := g.config.GetCLIVersion()
switch version {
case "latest", "":
return "latest"
case "beta":
return "master"
default:
return version
if g.config.CLIVersion == "beta" {
return "master" // Explicit opt-in escape hatch to trunk.
}
return g.config.GetCLIVersion()
}

// commentEnabled reports whether the preview should also post a PR comment.
Expand Down
21 changes: 9 additions & 12 deletions internal/generate/promote.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,21 +39,18 @@ func (g *PromoteGenerator) SetState(state map[string]*config.EnvState) {
g.state = state
}

// getCLIRef returns the Git ref to use for the cascade actions.
// getCLIRef returns the Git ref for the cascade self-action. The default
// (cli_version unset or "latest") resolves to config.DefaultCLIVersion, an
// immutable release tag, so consumers never run an unpinned mutable ref.
// Supported values:
// - "latest" → uses the "latest" tag (updated with each stable release)
// - "beta" → uses "master" branch (bleeding edge, may be unstable)
// - "vX.Y.Z" → uses a specific version tag
// - unset / "latest" → config.DefaultCLIVersion (immutable, pinned default)
// - "beta" → "master" branch (explicit opt-in, bleeding edge, may be unstable)
// - "vX.Y.Z" → that specific version tag
func (g *PromoteGenerator) getCLIRef() string {
version := g.config.GetCLIVersion()
switch version {
case "latest", "":
return "latest" // Points to the most recent stable release
case "beta":
return "master" // Bleeding edge from trunk
default:
return version // Specific version tag (e.g., v1.0.0)
if g.config.CLIVersion == "beta" {
return "master" // Explicit opt-in escape hatch to trunk.
}
return g.config.GetCLIVersion()
}

// getReleaseTokenRef returns the token expression for release operations.
Expand Down
21 changes: 9 additions & 12 deletions internal/generate/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,18 @@ func NewReleaseGenerator(cfg *config.TrunkConfig, baseDir string) *ReleaseGenera
}
}

// getCLIRef returns the Git ref to use for the cascade actions.
// getCLIRef returns the Git ref for the cascade self-action. The default
// (cli_version unset or "latest") resolves to config.DefaultCLIVersion, an
// immutable release tag, so consumers never run an unpinned mutable ref.
// Supported values:
// - "latest" → uses the "latest" tag (updated with each stable release)
// - "beta" → uses "master" branch (bleeding edge, may be unstable)
// - "vX.Y.Z" → uses a specific version tag
// - unset / "latest" → config.DefaultCLIVersion (immutable, pinned default)
// - "beta" → "master" branch (explicit opt-in, bleeding edge, may be unstable)
// - "vX.Y.Z" → that specific version tag
func (g *ReleaseGenerator) getCLIRef() string {
version := g.config.GetCLIVersion()
switch version {
case "latest", "":
return "latest" // Points to the most recent stable release
case "beta":
return "master" // Bleeding edge from trunk
default:
return version // Specific version tag (e.g., v1.0.0)
if g.config.CLIVersion == "beta" {
return "master" // Explicit opt-in escape hatch to trunk.
}
return g.config.GetCLIVersion()
}

// getReleaseTokenRef returns the token expression for release operations.
Expand Down
15 changes: 6 additions & 9 deletions internal/generate/validate_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,14 @@ func (g *ValidateCheckGenerator) Enabled() bool {
}

// getCLIRef mirrors the ref-resolution used by the other generators so the
// emitted setup-cli ref tracks config.cli_version.
// emitted setup-cli ref tracks config.cli_version. The default (cli_version
// unset or "latest") resolves to config.DefaultCLIVersion, an immutable release
// tag; "beta" is the explicit opt-in escape hatch to the "master" branch.
func (g *ValidateCheckGenerator) getCLIRef() string {
version := g.config.GetCLIVersion()
switch version {
case "latest", "":
return "latest"
case "beta":
return "master"
default:
return version
if g.config.CLIVersion == "beta" {
return "master" // Explicit opt-in escape hatch to trunk.
}
return g.config.GetCLIVersion()
}

// getManifestFilePath returns the repo-relative manifest path for use in the
Expand Down
Loading