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
8 changes: 7 additions & 1 deletion e2e/scenarios/16-pr-preview.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ description: |
workflow that plans a preview on pull requests, with a per-PR
concurrency group (#93).

The manifest declares multiple environments so the generated dry-run
step must target the first environment; orchestrate setup resolves the
version against that environment, and a bare environment would fail the
preview with `environment "" not found`.

Generator-output verification only.

config:
trunk_branch: main
environments: [dev]
environments: [staging, prod]
builds:
- name: app
workflow: build.yaml
Expand All @@ -35,3 +40,4 @@ steps:
- "cascade-pr-preview-${{ github.event.pull_request.number }}"
- "preview:"
- "Plan Preview"
- "--environment staging"
12 changes: 12 additions & 0 deletions internal/generate/pr_preview.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,13 @@ func (g *PRPreviewGenerator) writeDetectStep(sb *strings.Builder) {
// hard-coded, so no release is cut, no state is written, and no deploy is
// triggered. orchestrate setup only reports the head SHA, the version that would
// be cut, and which builds/deploys this merge would run.
//
// When the manifest declares environments the step targets the first (lowest)
// environment with --environment, mirroring how the orchestrate workflow
// defaults its setup environment. orchestrate setup resolves the version against
// that environment, so an empty value would fail with `environment "" not
// found`. A manifest with no environments omits the flag and runs the
// no-environment version calculation.
func (g *PRPreviewGenerator) writeDeployDryRunStep(sb *strings.Builder) {
sb.WriteString(" - name: Compute plan (dry-run)\n")
// pull_request.head.sha is attacker-influenceable on a fork PR; route it
Expand All @@ -169,6 +176,11 @@ func (g *PRPreviewGenerator) writeDeployDryRunStep(sb *strings.Builder) {
sb.WriteString(" # so no release is cut, no state is written, and no deploy is triggered.\n")
sb.WriteString(" cascade --dry-run orchestrate setup \\\n")
fmt.Fprintf(sb, " --config %s \\\n", g.config.GetManifestFile())
if len(g.config.Environments) > 0 {
// Target the first environment so version calculation resolves; the
// preview is read-only regardless of which environment it reports on.
fmt.Fprintf(sb, " --environment %s \\\n", g.config.Environments[0])
}
sb.WriteString(" --sha \"$HEAD_SHA\" \\\n")
sb.WriteString(" > cascade-plan.json\n")
sb.WriteString("\n")
Expand Down
40 changes: 40 additions & 0 deletions internal/generate/pr_preview_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,46 @@ func TestPRPreviewGenerator_PRFieldsAreNotInterpolatedIntoRun(t *testing.T) {
assert.Contains(t, content, "HEAD_SHA: ${{ github.event.pull_request.head.sha }}")
}

// TestPRPreviewGenerator_DryRunResolvesEnvironment asserts that the plan
// dry-run step targets a concrete environment so version calculation succeeds.
//
// orchestrate setup looks the --environment value up in the manifest's
// environments list; an empty value resolves to no match and fails with
// `environment "" not found`. For a manifest that declares environments the
// preview must pass --environment with the first (lowest) environment, mirroring
// how the orchestrate workflow defaults its setup environment. For a manifest
// with no environments the flag must be omitted entirely.
func TestPRPreviewGenerator_DryRunResolvesEnvironment(t *testing.T) {
t.Run("multi-env passes first environment", func(t *testing.T) {
cfg := prPreviewConfig(false)
cfg.Environments = []string{"staging", "prod"}

gen := NewPRPreviewGenerator(cfg, "")
content, err := gen.Generate()
require.NoError(t, err)

planBody := stepRunBody(t, content, "Compute plan (dry-run)")
assert.Contains(t, planBody, "--environment staging",
"plan dry-run must target the first environment so version calc succeeds")
assert.NotContains(t, planBody, "--environment \"\"",
"plan dry-run must never pass an empty environment")
})

t.Run("no-env omits the environment flag", func(t *testing.T) {
cfg := prPreviewConfig(false)
cfg.Environments = nil

gen := NewPRPreviewGenerator(cfg, "")
content, err := gen.Generate()
require.NoError(t, err)

planBody := stepRunBody(t, content, "Compute plan (dry-run)")
assert.NotContains(t, planBody, "--environment",
"no-env manifest must not pass --environment")
assert.Contains(t, planBody, "cascade --dry-run orchestrate setup")
})
}

func TestPRPreviewGenerator_Disabled(t *testing.T) {
// nil pr_preview
gen := NewPRPreviewGenerator(&config.TrunkConfig{TrunkBranch: "main"}, "")
Expand Down
Loading