diff --git a/e2e/harness/harness.go b/e2e/harness/harness.go index 2b053384..254b65bb 100644 --- a/e2e/harness/harness.go +++ b/e2e/harness/harness.go @@ -344,9 +344,23 @@ func generateStubWorkflow(name, scenarioTag string) string { if scenarioTag != "" { displayName = fmt.Sprintf("%s [scenario-%s]", name, scenarioTag) } + // Declare the inputs a real promote/orchestrate deploy callback accepts so the + // generator discovers them and threads the matching preflight outputs through + // each deploy's with: block. image_tag in particular drives the + // source_image_tag passthrough in the generated promote workflow. return fmt.Sprintf(`name: %s on: workflow_call: + inputs: + environment: + required: false + type: string + sha: + required: false + type: string + image_tag: + required: false + type: string jobs: %s: runs-on: ubuntu-latest diff --git a/e2e/scenarios/18-promote-source-image-tag.yaml b/e2e/scenarios/18-promote-source-image-tag.yaml new file mode 100644 index 00000000..3f1b06a5 --- /dev/null +++ b/e2e/scenarios/18-promote-source-image-tag.yaml @@ -0,0 +1,36 @@ +name: "Promote threads source image tag to deploys" +description: | + Verifies the generated promote.yaml wires the source image tag from the + preflight job into each deploy that accepts an image_tag input. The preflight + job's outputs block must declare source_image_tag, and a deploy whose reusable + workflow declares an image_tag input must receive + image_tag: ${{ needs.preflight.outputs.source_image_tag }} in its with: block. + Without the output declaration the reference resolves to an empty string on + real GitHub and the deploy receives a blank image tag. + + This is a generator-output verification scenario. Assertion runs on the staged + repo after StageRepoFromConfig generates workflows, before any run. + +config: + trunk_branch: main + environments: [dev, test, prod] + deploys: + - name: app + workflow: .github/workflows/deploy.yaml + triggers: ["src/**"] + +steps: + - name: "Initial commit generates workflows; promote threads source_image_tag to deploy-app" + action: commit + commit: + message: "feat: add app source" + files: + src/app.go: | + package main + func main() {} + expect: + workflow_files: + - path: ".github/workflows/promote.yaml" + contains: + - "source_image_tag: ${{ steps.preflight.outputs.source_image_tag }}" + - "image_tag: ${{ needs.preflight.outputs.source_image_tag }}" diff --git a/internal/generate/promote.go b/internal/generate/promote.go index b71de80f..eac27df7 100644 --- a/internal/generate/promote.go +++ b/internal/generate/promote.go @@ -626,6 +626,7 @@ func (g *PromoteGenerator) writePreflightJob(sb *strings.Builder) { sb.WriteString(" target_env: ${{ steps.preflight.outputs.target_env }}\n") sb.WriteString(" source_sha: ${{ steps.preflight.outputs.source_sha }}\n") sb.WriteString(" source_version: ${{ steps.preflight.outputs.source_version }}\n") + sb.WriteString(" source_image_tag: ${{ steps.preflight.outputs.source_image_tag }}\n") sb.WriteString(" changelog_base_sha: ${{ steps.preflight.outputs.changelog_base_sha }}\n") sb.WriteString(" rollback_sha: ${{ steps.preflight.outputs.rollback_sha }}\n") sb.WriteString(" rollback_on_failure: ${{ steps.preflight.outputs.rollback_on_failure }}\n") diff --git a/internal/generate/promote_test.go b/internal/generate/promote_test.go index 0a93c8f3..455fb74b 100644 --- a/internal/generate/promote_test.go +++ b/internal/generate/promote_test.go @@ -203,6 +203,26 @@ func TestPromoteGenerator_PublishOnProd(t *testing.T) { assert.Contains(t, content, "rc_version") } +// TestPromoteGenerator_PreflightDeclaresSourceImageTag verifies the preflight job +// declares a source_image_tag output. Deploy jobs reference +// needs.preflight.outputs.source_image_tag for the image_tag input, so the +// preflight outputs block must declare it or the reference resolves to an empty +// string on real GitHub (and actionlint flags an undefined property). +func TestPromoteGenerator_PreflightDeclaresSourceImageTag(t *testing.T) { + cfg := &config.TrunkConfig{ + TrunkBranch: "main", + Environments: []string{"dev", "test", "prod"}, + } + + gen := NewPromoteGenerator(cfg, "") + content, err := gen.Generate() + require.NoError(t, err) + + assert.Contains(t, content, + "source_image_tag: ${{ steps.preflight.outputs.source_image_tag }}", + "preflight outputs block must declare source_image_tag so deploy jobs resolve a non-empty image_tag") +} + func TestPromoteGenerator_DryRunSupport(t *testing.T) { cfg := &config.TrunkConfig{ TrunkBranch: "main", diff --git a/internal/promote/command_preflight.go b/internal/promote/command_preflight.go index 7dad5122..6bb8b2b0 100644 --- a/internal/promote/command_preflight.go +++ b/internal/promote/command_preflight.go @@ -160,6 +160,9 @@ func writePreflightGHAOutput(result *PreflightResult) error { w.Set("target_env", result.TargetEnv) w.Set("source_sha", result.SourceSHA) w.Set("source_version", result.SourceVersion) + // The promoted artifact version is the canonical image tag for the promotion. + // Deploy jobs consume this as their image_tag input. + w.Set("source_image_tag", result.SourceVersion) w.Set("changelog_base_sha", result.ChangelogBaseSHA) w.Set("rollback_sha", result.RollbackSHA) diff --git a/internal/promote/command_preflight_test.go b/internal/promote/command_preflight_test.go index ffc88dd1..c29692c9 100644 --- a/internal/promote/command_preflight_test.go +++ b/internal/promote/command_preflight_test.go @@ -259,6 +259,10 @@ func TestPreflightCommand_GHAOutput(t *testing.T) { require.NoError(t, err) require.Contains(t, string(output), "source_env=") require.Contains(t, string(output), "target_env=") + // Deploy jobs consume source_image_tag for their image_tag input. The promoted + // artifact version is the canonical image tag, so it must mirror source_version. + require.Contains(t, string(output), "source_image_tag=v1.0.0-rc.1", + "preflight must emit source_image_tag set to the source version") } // TestPreflightCommand_AllowBreaking tests --allow-breaking flag.