Skip to content
Open
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: 22 additions & 0 deletions e2e/scenarios/28-drift-check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,28 @@ steps:
package main

func main() {}
expect:
workflow_files:
# The fork-safe comment companion runs on workflow_run with secrets in
# scope, so its top-level token must default to nothing and each job opts
# into only the scope it needs. Pinning the empty top-level block proves
# the least-privilege posture the security model depends on.
- path: ".github/workflows/cascade-drift-comment.yaml"
contains:
- "permissions: {}"
# The single comment job opts into only what it needs: write to PRs
# and read the source run; no broader scope leaks in.
- " permissions:\n pull-requests: write\n actions: read\n"
not_contains:
# The companion must never grant itself write to repo contents.
- "contents: write"
# The pull_request check lane runs on fork-controlled input, so its
# top-level token is read-only and re-stated read-only at the job.
- path: ".github/workflows/cascade-drift-check.yaml"
contains:
- "permissions:\n contents: read\n"
not_contains:
- "\npermissions:\n contents: write"

- name: "Verify clean against pristine generated output"
action: verify
Expand Down
43 changes: 43 additions & 0 deletions internal/generate/promote_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1830,3 +1830,46 @@ func TestPromoteGenerator_ConcurrencyOverride(t *testing.T) {
assert.Contains(t, content, "group: my-custom-promote", "custom group must propagate to promote")
assert.Contains(t, content, "cancel-in-progress: true", "custom cancel_in_progress must propagate to promote")
}

// TestPromoteGenerator_SelectiveDeploysAndForcePassthrough pins the generator
// emission that the selective-deploy filter and the force flag actually reach
// the preflight CLI. The runtime effect of --deploys (run only the named
// deploys) is unit-asserted in internal/promote (TestPreflight_DeploysFilter),
// and the force-continue semantics in TestPreflight_ForceFlag; this guards the
// workflow plumbing that carries both inputs from the dispatch form into the
// CLI invocation, which was previously unasserted at generation time.
func TestPromoteGenerator_SelectiveDeploysAndForcePassthrough(t *testing.T) {
cfg := &config.TrunkConfig{
TrunkBranch: "main",
Environments: []string{"dev", "prod"},
Deploys: []config.DeployConfig{
{Name: "infra", Workflow: "deploy-infra.yaml"},
{Name: "app", Workflow: "deploy-app.yaml"},
},
}

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

// The dispatch form exposes both the deploys filter and the force toggle.
assert.Contains(t, content, " deploys:")
assert.Contains(t, content, " force:")

// Both inputs are threaded into the preflight environment...
assert.Contains(t, content, " DEPLOYS: ${{ github.event.inputs.deploys }}")
assert.Contains(t, content, " PROMOTION_FORCE: ${{ github.event.inputs.force }}")

// ...and forwarded to the preflight CLI flags that perform the filtering and
// the force-continue behavior. Defaulting deploys to "all" preserves the
// promote-everything path when the input is left blank.
assert.Contains(t, content, `--deploys="${DEPLOYS:-all}"`)
assert.Contains(t, content, `--force="${PROMOTION_FORCE:-false}"`)

// Each selected deploy renders as its own reusable-workflow job gated on the
// resolved deploys_to_run set, so an unnamed deploy is skipped at runtime
// while finalize still commits via its always() guard, the plumbing that lets
// promote continue past a failed deploy.
assert.Contains(t, content, "contains(fromJSON(needs.preflight.outputs.deploys_to_run), 'infra')")
assert.Contains(t, content, "contains(fromJSON(needs.preflight.outputs.deploys_to_run), 'app')")
}
Loading