From d1ae9bd9000f3d4f7787163b21c587f0cb135fae Mon Sep 17 00:00:00 2001 From: Joshua Temple Date: Wed, 24 Jun 2026 10:47:20 -0400 Subject: [PATCH] fix(generate): pass github.token to setup-cli so cold-cache CLI download authenticates The generated preview, merge-queue, validate-check, drift-check, and hotfix workflows invoked setup-cli without a token input, leaving GH_TOKEN empty. On a cold Actions tool-cache the setup-cli action falls back to gh release download, which then exits 4 (empty token) and reds the fleet reconcile gate. Pass the built-in github.token, sufficient to authenticate against the public stablekernel/cascade releases. Signed-off-by: Joshua Temple --- e2e/scenarios/14-validate-check.yaml | 1 + e2e/scenarios/15-merge-queue.yaml | 1 + e2e/scenarios/16-pr-preview.yaml | 1 + internal/generate/drift_check.go | 3 +++ internal/generate/drift_check_test.go | 13 +++++++++++++ internal/generate/hotfix.go | 3 +++ internal/generate/hotfix_test.go | 13 +++++++++++++ internal/generate/merge_queue.go | 3 +++ internal/generate/merge_queue_test.go | 17 +++++++++++++++++ internal/generate/pr_preview.go | 4 ++++ internal/generate/pr_preview_test.go | 14 ++++++++++++++ internal/generate/validate_check.go | 3 +++ internal/generate/validate_check_test.go | 17 +++++++++++++++++ 13 files changed, 93 insertions(+) diff --git a/e2e/scenarios/14-validate-check.yaml b/e2e/scenarios/14-validate-check.yaml index d4ca84d2..76de3d62 100644 --- a/e2e/scenarios/14-validate-check.yaml +++ b/e2e/scenarios/14-validate-check.yaml @@ -36,3 +36,4 @@ steps: - ".github/manifest.yaml" - "validate-manifest:" - "cascade parse-config" + - "token: ${{ github.token }}" diff --git a/e2e/scenarios/15-merge-queue.yaml b/e2e/scenarios/15-merge-queue.yaml index 5d8ee604..e24e66a3 100644 --- a/e2e/scenarios/15-merge-queue.yaml +++ b/e2e/scenarios/15-merge-queue.yaml @@ -35,3 +35,4 @@ steps: - "merge-queue-validate:" - "cascade parse-config" - "cascade --dry-run orchestrate setup" + - "token: ${{ github.token }}" diff --git a/e2e/scenarios/16-pr-preview.yaml b/e2e/scenarios/16-pr-preview.yaml index a74a0cbc..52555053 100644 --- a/e2e/scenarios/16-pr-preview.yaml +++ b/e2e/scenarios/16-pr-preview.yaml @@ -41,3 +41,4 @@ steps: - "preview:" - "Plan Preview" - "--environment staging" + - "token: ${{ github.token }}" diff --git a/internal/generate/drift_check.go b/internal/generate/drift_check.go index e5f6cf5c..ce5cbbda 100644 --- a/internal/generate/drift_check.go +++ b/internal/generate/drift_check.go @@ -136,6 +136,9 @@ func (g *DriftCheckGenerator) writeCheckJob(sb *strings.Builder) { fmt.Fprintf(sb, " uses: stablekernel/cascade/.github/actions/setup-cli@%s\n", g.getCLIRef()) sb.WriteString(" with:\n") fmt.Fprintf(sb, " version: %s\n", g.config.GetCLIVersion()) + // github.token is the built-in Actions token, sufficient to authenticate + // gh release download against the public stablekernel/cascade repository. + sb.WriteString(" token: ${{ github.token }}\n") sb.WriteString("\n") // Run verify, capturing stdout/stderr and the exit code without failing the diff --git a/internal/generate/drift_check_test.go b/internal/generate/drift_check_test.go index 7cb98795..fa6ce0bc 100644 --- a/internal/generate/drift_check_test.go +++ b/internal/generate/drift_check_test.go @@ -131,6 +131,19 @@ func TestDriftCheckGenerator_Deterministic(t *testing.T) { assert.Equal(t, comment1, comment2) } +// TestDriftCheckGenerator_SetupCLIPassesToken asserts that the setup-cli step +// passes github.token so that gh release download can authenticate on a cold +// tool-cache. Without the token: input the composite action's GH_TOKEN is +// empty and gh exits non-zero. +func TestDriftCheckGenerator_SetupCLIPassesToken(t *testing.T) { + gen := NewDriftCheckGenerator(driftCheckConfig(false), "") + check, err := gen.Generate() + require.NoError(t, err) + + assert.Contains(t, check, "token: ${{ github.token }}", + "setup-cli step must pass github.token so gh release download succeeds on a cold cache") +} + // TestDriftCheckGenerator_PinModeSHA proves third-party actions are SHA-pinned // when pin_mode is sha, matching how cascade pins actions elsewhere. func TestDriftCheckGenerator_PinModeSHA(t *testing.T) { diff --git a/internal/generate/hotfix.go b/internal/generate/hotfix.go index c1bde2c7..6d06ebbf 100644 --- a/internal/generate/hotfix.go +++ b/internal/generate/hotfix.go @@ -752,6 +752,9 @@ func (g *HotfixGenerator) writeSetupCLI(sb *strings.Builder) { fmt.Fprintf(sb, " uses: stablekernel/cascade/.github/actions/setup-cli@%s\n", g.getCLIRef()) sb.WriteString(" with:\n") fmt.Fprintf(sb, " version: %s\n", g.config.GetCLIVersion()) + // github.token is the built-in Actions token, sufficient to authenticate + // gh release download against the public stablekernel/cascade repository. + sb.WriteString(" token: ${{ github.token }}\n") } // writeFetchEnvBranches emits a step that fetches the env/* branches and tags so diff --git a/internal/generate/hotfix_test.go b/internal/generate/hotfix_test.go index 16b5eaae..4c783bf9 100644 --- a/internal/generate/hotfix_test.go +++ b/internal/generate/hotfix_test.go @@ -452,6 +452,19 @@ func TestHotfixGenerator_ValidYAML(t *testing.T) { assert.Contains(t, parsed, "permissions") } +// TestHotfixGenerator_SetupCLIPassesToken asserts that the setup-cli step +// passes github.token so that gh release download can authenticate on a cold +// tool-cache. Without the token: input the composite action's GH_TOKEN is +// empty and gh exits non-zero. +func TestHotfixGenerator_SetupCLIPassesToken(t *testing.T) { + gen := NewHotfixGenerator(threeEnvHotfixConfig(), "") + content, err := gen.Generate() + require.NoError(t, err) + + assert.Contains(t, content, "token: ${{ github.token }}", + "setup-cli step must pass github.token so gh release download succeeds on a cold cache") +} + // TestHotfixGenerator_PinModeSHA confirms third-party action refs route through // the shared pin helper rather than emitting a raw @v4. func TestHotfixGenerator_PinModeSHA(t *testing.T) { diff --git a/internal/generate/merge_queue.go b/internal/generate/merge_queue.go index f16c6a05..10b090a4 100644 --- a/internal/generate/merge_queue.go +++ b/internal/generate/merge_queue.go @@ -117,6 +117,9 @@ func (g *MergeQueueGenerator) writeJob(sb *strings.Builder) { fmt.Fprintf(sb, " uses: stablekernel/cascade/.github/actions/setup-cli@%s\n", g.getCLIRef()) sb.WriteString(" with:\n") fmt.Fprintf(sb, " version: %s\n", g.config.GetCLIVersion()) + // github.token is the built-in Actions token, sufficient to authenticate + // gh release download against the public stablekernel/cascade repository. + sb.WriteString(" token: ${{ github.token }}\n") // Validity gate: parse-config reports validity in its JSON output rather // than via exit code, so gate on the parsed result. diff --git a/internal/generate/merge_queue_test.go b/internal/generate/merge_queue_test.go index 63c7003d..ad254813 100644 --- a/internal/generate/merge_queue_test.go +++ b/internal/generate/merge_queue_test.go @@ -71,6 +71,23 @@ func TestMergeQueueGenerator_Steps(t *testing.T) { assert.NotContains(t, content, "action: publish") } +// TestMergeQueueGenerator_SetupCLIPassesToken asserts that the setup-cli step +// passes github.token so that gh release download can authenticate on a cold +// tool-cache. Without the token: input the composite action's GH_TOKEN is +// empty and gh exits non-zero. +func TestMergeQueueGenerator_SetupCLIPassesToken(t *testing.T) { + cfg := &config.TrunkConfig{ + TrunkBranch: "main", + MergeQueue: &config.MergeQueueConfig{Enabled: true}, + } + gen := NewMergeQueueGenerator(cfg, "") + content, err := gen.Generate() + require.NoError(t, err) + + assert.Contains(t, content, "token: ${{ github.token }}", + "setup-cli step must pass github.token so gh release download succeeds on a cold cache") +} + // TestMergeQueueGenerator_PinModeSHA confirms third-party action refs route // through the shared pin helper rather than emitting a raw @v4. func TestMergeQueueGenerator_PinModeSHA(t *testing.T) { diff --git a/internal/generate/pr_preview.go b/internal/generate/pr_preview.go index dc9e7990..ed24f6b8 100644 --- a/internal/generate/pr_preview.go +++ b/internal/generate/pr_preview.go @@ -115,6 +115,10 @@ func (g *PRPreviewGenerator) writeJob(sb *strings.Builder) { fmt.Fprintf(sb, " uses: stablekernel/cascade/.github/actions/setup-cli@%s\n", g.getCLIRef()) sb.WriteString(" with:\n") fmt.Fprintf(sb, " version: %s\n", g.config.GetCLIVersion()) + // github.token is the built-in Actions token. It is sufficient to + // authenticate gh release download against the public stablekernel/cascade + // repository and requires no adopter configuration. + sb.WriteString(" token: ${{ github.token }}\n") sb.WriteString("\n") g.writeValidateStep(sb) diff --git a/internal/generate/pr_preview_test.go b/internal/generate/pr_preview_test.go index 23d2e869..a58011e4 100644 --- a/internal/generate/pr_preview_test.go +++ b/internal/generate/pr_preview_test.go @@ -173,6 +173,20 @@ func TestPRPreviewGenerator_CommentAddsScriptAndPermission(t *testing.T) { assert.Contains(t, content, "createComment") } +// TestPRPreviewGenerator_SetupCLIPassesToken asserts that the setup-cli step in +// the generated preview workflow passes the built-in github.token so that +// gh release download can authenticate even on a cold tool-cache. Without the +// token: input the composite action's GH_TOKEN is empty and gh exits non-zero. +func TestPRPreviewGenerator_SetupCLIPassesToken(t *testing.T) { + gen := NewPRPreviewGenerator(prPreviewConfig(false), "") + content, err := gen.Generate() + require.NoError(t, err) + + // The setup-cli with: block must carry a token: line. + assert.Contains(t, content, "token: ${{ github.token }}", + "setup-cli step must pass github.token so gh release download succeeds on a cold cache") +} + func TestPRPreviewGenerator_ActionRefsPinUnderSHAMode(t *testing.T) { cfg := prPreviewConfig(true) cfg.PinMode = config.PinModeSHA diff --git a/internal/generate/validate_check.go b/internal/generate/validate_check.go index 809bbc34..4f0b3896 100644 --- a/internal/generate/validate_check.go +++ b/internal/generate/validate_check.go @@ -112,6 +112,9 @@ func (g *ValidateCheckGenerator) writeJob(sb *strings.Builder) { fmt.Fprintf(sb, " uses: stablekernel/cascade/.github/actions/setup-cli@%s\n", g.getCLIRef()) sb.WriteString(" with:\n") fmt.Fprintf(sb, " version: %s\n", g.config.GetCLIVersion()) + // github.token is the built-in Actions token, sufficient to authenticate + // gh release download against the public stablekernel/cascade repository. + sb.WriteString(" token: ${{ github.token }}\n") sb.WriteString(" - name: Validate Manifest\n") sb.WriteString(" run: |\n") diff --git a/internal/generate/validate_check_test.go b/internal/generate/validate_check_test.go index 8601006a..98a3c86c 100644 --- a/internal/generate/validate_check_test.go +++ b/internal/generate/validate_check_test.go @@ -71,6 +71,23 @@ func TestValidateCheckGenerator_Steps(t *testing.T) { assert.NotContains(t, content, "createComment") } +// TestValidateCheckGenerator_SetupCLIPassesToken asserts that the setup-cli +// step passes github.token so that gh release download can authenticate on a +// cold tool-cache. Without the token: input the composite action's GH_TOKEN is +// empty and gh exits non-zero. +func TestValidateCheckGenerator_SetupCLIPassesToken(t *testing.T) { + cfg := &config.TrunkConfig{ + TrunkBranch: "main", + ValidateCheck: &config.ValidateCheckConfig{Enabled: true}, + } + gen := NewValidateCheckGenerator(cfg, "") + content, err := gen.Generate() + require.NoError(t, err) + + assert.Contains(t, content, "token: ${{ github.token }}", + "setup-cli step must pass github.token so gh release download succeeds on a cold cache") +} + // TestValidateCheckGenerator_PinModeSHA confirms third-party action refs route // through the shared pin helper rather than emitting a raw @v4. func TestValidateCheckGenerator_PinModeSHA(t *testing.T) {