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
7 changes: 6 additions & 1 deletion e2e/harness/harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,9 @@ func generateStubWorkflow(name, scenarioTag string) string {
// generator discovers them and threads the matching preflight outputs through
// each deploy's with: block. image_tag drives the source_image_tag passthrough
// and image_digest drives the source_image_digest passthrough in the generated
// promote workflow.
// promote workflow. dry_run is declared so that deploys with supports_dry_run
// enabled receive the null-safe passthrough from the orchestrate workflow.
// Declaring it here is harmless for callbacks that do not use it.
return fmt.Sprintf(`name: %s
on:
workflow_call:
Expand All @@ -400,6 +402,9 @@ on:
image_digest:
required: false
type: string
dry_run:
required: false
type: boolean
jobs:
%s:
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions e2e/harness/scenario.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ type DeployConfig struct {
DependsOn []string `yaml:"depends_on"`
OptionalDependsOn []string `yaml:"optional_depends_on,omitempty"`
TimeoutMinutes int `yaml:"timeout_minutes,omitempty"`
SupportsDryRun bool `yaml:"supports_dry_run,omitempty"`
RunsOn any `yaml:"runs_on,omitempty"`
Permissions map[string]string `yaml:"permissions,omitempty"`
Concurrency *ConcurrencySpec `yaml:"concurrency,omitempty"`
Expand Down
47 changes: 47 additions & 0 deletions e2e/scenarios/orchestrate/dry-run-input-expression.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: "Dry-Run Passthrough Uses Null-Safe Expression"
description: |
Verifies that the generator emits the null-safe github.event.inputs.dry_run
accessor when a deploy callback has supports_dry_run: true.

The orchestrate workflow is triggered by push, schedule, and workflow_run
events -- none of which populate the inputs context. A bare inputs.dry_run
expression renders as an empty string on those triggers, which breaks the
boolean dispatch to the reusable callback workflow. The generator must use
github.event.inputs.dry_run instead so the value is null (and the boolean
coercion to false) rather than an empty string that fails type validation.

Generator-output verification scenario; the assertion runs on the staged repo
after StageRepoFromConfig generates workflows but before any orchestrate run,
so Docker and act are not required to validate the generated YAML.

config:
trunk_branch: main
environments: ["dev", "prod"]
builds:
- name: app
workflow: build-app.yaml
triggers: ["src/**"]
deploys:
- name: app
workflow: deploy-app.yaml
triggers: []
supports_dry_run: true

steps:
- name: "Initial commit; assert dry_run passthrough uses null-safe accessor"
action: commit
commit:
message: "feat: add app build and deploy with dry-run support"
files:
src/main.go: |
package main
func main() {}
expect:
workflow_files:
- path: ".github/workflows/orchestrate.yaml"
contains:
# The null-safe accessor works across push, schedule, and workflow_run triggers.
- "dry_run: ${{ github.event.inputs.dry_run }}"
not_contains:
# The bare inputs accessor renders empty on non-dispatch triggers and must not appear.
- "dry_run: ${{ inputs.dry_run }}"
9 changes: 6 additions & 3 deletions internal/generate/dry_run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,12 @@ on:
content, err := gen.Generate()
require.NoError(t, err)

// dry_run must be forwarded in the with: block to the deploy job.
// dry_run must be forwarded in the with: block to the deploy job using the
// null-safe github.event.inputs accessor. The bare inputs.dry_run form renders
// empty on push/schedule/workflow_run, which is invalid for a boolean callback
// input and fails the reusable-workflow dispatch.
assert.Contains(t, content,
"dry_run: ${{ inputs.dry_run }}",
"dry_run: ${{ github.event.inputs.dry_run }}",
"orchestrate generator must pass dry_run to a supports_dry_run callback")
}

Expand Down Expand Up @@ -219,7 +222,7 @@ on:

deploySection := extractJobSection(t, content, "deploy-app:")
assert.NotContains(t, deploySection,
"dry_run: ${{ inputs.dry_run }}",
"dry_run: ${{ github.event.inputs.dry_run }}",
"non-supports_dry_run callback must not receive dry_run in orchestrate")
}

Expand Down
9 changes: 8 additions & 1 deletion internal/generate/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -1091,8 +1091,15 @@ func (g *Generator) writeWithInputs(sb *strings.Builder, info CallbackInfo) {

// When a callback opts in to dry-run emulation, pass the dry_run dispatch
// input through so the callback can emulate internally instead of being skipped.
// Use github.event.inputs.dry_run rather than the bare inputs.dry_run context:
// orchestrate is triggered by push/schedule/workflow_run as well as dispatch,
// and on the non-dispatch events the inputs context is null so ${{ inputs.dry_run }}
// renders empty. Passing "" into a callback's boolean dry_run input fails the
// reusable-workflow dispatch. The github.event.inputs accessor is null-safe on
// those events (the callback falls back to its dry_run default), and on dispatch
// it still forwards the operator's value.
if info.SupportsDryRun {
inputs = append(inputs, " dry_run: ${{ inputs.dry_run }}")
inputs = append(inputs, " dry_run: ${{ github.event.inputs.dry_run }}")
}

// For build callbacks with a matrix, pass each dimension's current value to
Expand Down
Loading