From 91e784320daeff99c32617a6dea7a5081723c176 Mon Sep 17 00:00:00 2001 From: Joshua Temple Date: Wed, 24 Jun 2026 20:24:39 -0400 Subject: [PATCH 1/2] test(e2e): add emission coverage for triggers, action-pins, breaking-gate Adds e2e scenarios asserting generated-workflow constructs that the live fleet cannot drive: extra orchestrate triggers (schedule/repository_dispatch/workflow_run/merge_group), pin_mode sha vs default mutable refs plus per-action overrides, and the release/promote breaking-change gate. merge_queue emission was already covered (scenario 15 + unit tests). Closes #324, closes #327, closes #322. Signed-off-by: Joshua Temple --- e2e/harness/scenario.go | 14 ++++ .../34-extra-orchestrate-triggers.yaml | 65 +++++++++++++++++++ e2e/scenarios/35-action-pins-sha-mode.yaml | 40 ++++++++++++ e2e/scenarios/36-action-pins-default-tag.yaml | 40 ++++++++++++ e2e/scenarios/37-release-breaking-gate.yaml | 53 +++++++++++++++ ...8-promote-breaking-gate-release-build.yaml | 51 +++++++++++++++ e2e/scenarios/39-action-pins-override.yaml | 43 ++++++++++++ 7 files changed, 306 insertions(+) create mode 100644 e2e/scenarios/34-extra-orchestrate-triggers.yaml create mode 100644 e2e/scenarios/35-action-pins-sha-mode.yaml create mode 100644 e2e/scenarios/36-action-pins-default-tag.yaml create mode 100644 e2e/scenarios/37-release-breaking-gate.yaml create mode 100644 e2e/scenarios/38-promote-breaking-gate-release-build.yaml create mode 100644 e2e/scenarios/39-action-pins-override.yaml diff --git a/e2e/harness/scenario.go b/e2e/harness/scenario.go index 65623f9..1787552 100644 --- a/e2e/harness/scenario.go +++ b/e2e/harness/scenario.go @@ -108,6 +108,20 @@ type Config struct { // can declare any reserved release field without the harness needing to know // its structure. Release map[string]any `yaml:"release,omitempty"` + // ExtraTriggers carries the optional extra orchestrate triggers (schedule, + // repository_dispatch, workflow_run, merge_group) through to the generated + // manifest untouched, so a scenario can assert each extra on: entry is + // emitted into the orchestrate workflow. A generic map keeps the harness + // decoupled from the generator's ExtraTriggers shape. + ExtraTriggers map[string]any `yaml:"extra_triggers,omitempty"` + // PinMode carries the action pin mode (tag or sha) through to the generated + // manifest so a scenario can assert the sha-pinned uses: form versus the + // default tag form. + PinMode string `yaml:"pin_mode,omitempty"` + // ActionPins carries per-action ref overrides through to the generated + // manifest so a scenario can assert an overridden uses: ref is honored + // regardless of pin mode. + ActionPins map[string]string `yaml:"action_pins,omitempty"` } // PublishConfig defines a publish callback invoked after a release is published diff --git a/e2e/scenarios/34-extra-orchestrate-triggers.yaml b/e2e/scenarios/34-extra-orchestrate-triggers.yaml new file mode 100644 index 0000000..f149ccd --- /dev/null +++ b/e2e/scenarios/34-extra-orchestrate-triggers.yaml @@ -0,0 +1,65 @@ +name: "Extra orchestrate triggers" +description: | + Verifies that declaring extra_triggers in the manifest emits each extra + on: entry into the generated orchestrate workflow alongside the baseline + push and workflow_dispatch triggers (#324). + + Covers all four extra trigger kinds in one manifest: + - schedule with cron expressions + - repository_dispatch with event types + - workflow_run with workflows and types + - merge_group (bare presence) + + Generator-output verification only. + +config: + trunk_branch: main + environments: [dev] + builds: + - name: app + workflow: build.yaml + triggers: ["src/**"] + deploys: [] + extra_triggers: + schedule: + - cron: "0 2 * * *" + repository_dispatch: + types: + - external-update + - redeploy + workflow_run: + workflows: + - Upstream CI + types: + - completed + merge_group: {} + +steps: + - name: "Initial commit; assert every extra trigger is emitted in orchestrate.yaml" + action: commit + commit: + message: "feat: add app" + files: + src/app.go: | + package main + func main() {} + expect: + workflow_files: + - path: ".github/workflows/orchestrate.yaml" + contains: + # Baseline triggers remain present. + - " push:\n" + - " workflow_dispatch:\n" + # schedule extra trigger. + - " schedule:\n" + - " - cron: '0 2 * * *'\n" + # repository_dispatch extra trigger with both types. + - " repository_dispatch:\n" + - " - external-update\n" + - " - redeploy\n" + # workflow_run extra trigger with workflow name and type. + - " workflow_run:\n" + - " - 'Upstream CI'\n" + - " - completed\n" + # merge_group extra trigger (bare presence). + - " merge_group:\n" diff --git a/e2e/scenarios/35-action-pins-sha-mode.yaml b/e2e/scenarios/35-action-pins-sha-mode.yaml new file mode 100644 index 0000000..72eaadd --- /dev/null +++ b/e2e/scenarios/35-action-pins-sha-mode.yaml @@ -0,0 +1,40 @@ +name: "Action pins: sha mode pins to an immutable commit" +description: | + Verifies that pin_mode: sha rewrites third-party action refs to a 40-hex + commit SHA with a trailing version comment, instead of the default mutable + major tag (#327). + + Covers: + - pin_mode: sha emits actions/checkout@<40hex> # v6.0.3 + - the default mutable major tag (v6) no longer appears once sha-pinned + + Generator-output verification only. + +config: + trunk_branch: main + environments: [dev] + builds: + - name: app + workflow: build.yaml + triggers: ["src/**"] + deploys: [] + pin_mode: sha + +steps: + - name: "Initial commit; assert the sha-pinned uses: form in orchestrate.yaml" + action: commit + commit: + message: "feat: add app" + files: + src/app.go: | + package main + func main() {} + expect: + workflow_files: + - path: ".github/workflows/orchestrate.yaml" + contains: + # sha mode pins checkout to its 40-hex SHA with a version comment. + - "uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3" + not_contains: + # the mutable major tag must not appear once sha-pinned. + - "uses: actions/checkout@v6\n" diff --git a/e2e/scenarios/36-action-pins-default-tag.yaml b/e2e/scenarios/36-action-pins-default-tag.yaml new file mode 100644 index 0000000..1f1682c --- /dev/null +++ b/e2e/scenarios/36-action-pins-default-tag.yaml @@ -0,0 +1,40 @@ +name: "Action pins: default mode emits the mutable major tag" +description: | + Verifies the default pin behavior (no pin_mode): third-party action refs + are emitted at their built-in mutable major tag, with no sha pin (#327). + This is the baseline the sha mode in 35-action-pins-sha-mode.yaml contrasts + against. + + Covers: + - the default emits actions/checkout@v6 with no sha comment + - no 40-hex sha pin is emitted for checkout when pin_mode is unset + + Generator-output verification only. + +config: + trunk_branch: main + environments: [dev] + builds: + - name: app + workflow: build.yaml + triggers: ["src/**"] + deploys: [] + +steps: + - name: "Initial commit; assert the default mutable major tag in orchestrate.yaml" + action: commit + commit: + message: "feat: add app" + files: + src/app.go: | + package main + func main() {} + expect: + workflow_files: + - path: ".github/workflows/orchestrate.yaml" + contains: + # default behavior emits the built-in mutable major tag, no sha comment. + - "uses: actions/checkout@v6\n" + not_contains: + # without pin_mode: sha, no 40-hex sha pin for checkout is emitted. + - "uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10" diff --git a/e2e/scenarios/37-release-breaking-gate.yaml b/e2e/scenarios/37-release-breaking-gate.yaml new file mode 100644 index 0000000..65c4547 --- /dev/null +++ b/e2e/scenarios/37-release-breaking-gate.yaml @@ -0,0 +1,53 @@ +name: "Release workflow breaking-change gate, dry-run, concurrency" +description: | + Verifies that the single-environment release workflow emits its + breaking-change gate, the dry_run skip wiring, and the serialized + non-cancelling concurrency block (#322). + + A single-environment manifest generates a Release workflow into + promote.yaml (see 09-single-env-repo.yaml). This asserts the generated + shape of that workflow. + + Covers: + - allow_breaking_changes workflow_dispatch input + - the Check Breaking Changes preflight step and its can_proceed outputs + - the dry_run workflow_dispatch input and the release-job skip guard + - the top-level concurrency block with cancel-in-progress: false + + Generator-output verification only. + +config: + trunk_branch: main + environments: [prod] + deploys: + - name: app + workflow: .github/workflows/deploy.yaml + triggers: ["src/**"] + +steps: + - name: "Initial commit generates release-flavored promote.yaml; assert the breaking gate" + 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: + # Breaking-change gate input and detection step. + - " allow_breaking_changes:\n" + - " - name: Check Breaking Changes\n" + - " ALLOW_BREAKING: ${{ github.event.inputs.allow_breaking_changes }}\n" + - " echo \"has_breaking=$HAS_BREAKING\" >> \"$GITHUB_OUTPUT\"\n" + # The gate proceeds only when allowed, and blocks otherwise. + - " echo \"can_proceed=true\" >> \"$GITHUB_OUTPUT\"\n" + - " echo \"can_proceed=false\" >> \"$GITHUB_OUTPUT\"\n" + # dry_run input and the release-job skip guard. + - " dry_run:\n" + - " if: ${{ github.event.inputs.dry_run != 'true' }}\n" + # Serialized, non-cancelling concurrency block. + - "concurrency:\n" + - " cancel-in-progress: false\n" diff --git a/e2e/scenarios/38-promote-breaking-gate-release-build.yaml b/e2e/scenarios/38-promote-breaking-gate-release-build.yaml new file mode 100644 index 0000000..2d7dffc --- /dev/null +++ b/e2e/scenarios/38-promote-breaking-gate-release-build.yaml @@ -0,0 +1,51 @@ +name: "Promote breaking-change gate and release-build dispatch" +description: | + Verifies that the multi-environment promote workflow emits its + CLI-driven breaking-change gate and the follow-on release-build dispatch + when release.workflow is configured (#322). + + Covers: + - allow_breaking_changes workflow_dispatch input + - the cascade promote preflight run with the --allow-breaking flag + - the dry_run input and the promote-job skip guard + - the Trigger Release Build step dispatching the configured release-build + workflow after a final-env publication + + Generator-output verification only. + +config: + trunk_branch: main + environments: [dev, test, prod] + deploys: + - name: app + workflow: .github/workflows/deploy.yaml + triggers: ["src/**"] + release: + workflow: .github/workflows/release-build.yaml + +steps: + - name: "Initial commit generates promote.yaml; assert gate and release-build dispatch" + 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: + # Breaking-change gate input and CLI-driven preflight. + - " allow_breaking_changes:\n" + - " cascade promote preflight \\\n" + - " --allow-breaking=\"${ALLOW_BREAKING:-false}\" \\\n" + # dry_run input and the promote-job skip guard. + - " dry_run:\n" + - " if: ${{ github.event.inputs.dry_run != 'true' }}\n" + # Follow-on release-build dispatch against the published tag. + - " - name: Trigger Release Build\n" + - " gh workflow run ./.github/workflows/release-build.yaml \\\n" + not_contains: + # The promote path uses the CLI gate, not the release bash step. + - " - name: Check Breaking Changes\n" diff --git a/e2e/scenarios/39-action-pins-override.yaml b/e2e/scenarios/39-action-pins-override.yaml new file mode 100644 index 0000000..c585f5f --- /dev/null +++ b/e2e/scenarios/39-action-pins-override.yaml @@ -0,0 +1,43 @@ +name: "Action pins: per-action override is honored over the pin mode" +description: | + Verifies that a per-action action_pins override wins over the active pin + mode, emitting the override ref verbatim. This is both the per-action + override and the mutable-ref escape hatch: even under pin_mode: sha, an + override ref (here a mutable major tag) is emitted unchanged (#327). + + Covers: + - action_pins[actions/checkout] override wins under pin_mode: sha + - the override ref is emitted verbatim, not the built-in sha pin + + Generator-output verification only. + +config: + trunk_branch: main + environments: [dev] + builds: + - name: app + workflow: build.yaml + triggers: ["src/**"] + deploys: [] + pin_mode: sha + action_pins: + actions/checkout: "v6" + +steps: + - name: "Initial commit; assert the override ref wins over the sha pin" + action: commit + commit: + message: "feat: add app" + files: + src/app.go: | + package main + func main() {} + expect: + workflow_files: + - path: ".github/workflows/orchestrate.yaml" + contains: + # the override ref is emitted verbatim, as a mutable-ref opt-in. + - "uses: actions/checkout@v6\n" + not_contains: + # the built-in sha pin must not appear when overridden. + - "uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10" From b4c242611650a6589b3f0699a3d7e42672691ed1 Mon Sep 17 00:00:00 2001 From: Joshua Temple Date: Wed, 24 Jun 2026 21:57:27 -0400 Subject: [PATCH 2/2] fix(schema): allow release.workflow in the manifest schema ReleaseConfig has a Workflow field (the release-build callback workflow path), but the JSON schema's releaseConfig omitted it with additionalProperties:false, so a manifest using release.workflow failed schema validation despite cascade accepting it. Add the property and sync all three on-disk schema copies. Surfaced by the new promote-breaking-gate-release-build e2e scenario. Signed-off-by: Joshua Temple --- docs/public/manifest.schema.json | 1 + internal/schema/manifest.schema.json | 1 + schema/manifest.schema.json | 1 + 3 files changed, 3 insertions(+) diff --git a/docs/public/manifest.schema.json b/docs/public/manifest.schema.json index 3255a02..c2e52b2 100644 --- a/docs/public/manifest.schema.json +++ b/docs/public/manifest.schema.json @@ -485,6 +485,7 @@ "properties": { "disabled": { "type": "boolean", "description": "Disable cascade-managed releases." }, "tag": { "type": "string", "description": "callback.output reference for releases created by an external tool." }, + "workflow": { "type": "string", "description": "Path to a reusable release-build workflow dispatched after a release is published." }, "version_overrides": { "$ref": "#/definitions/versionOverridesConfig" } } }, diff --git a/internal/schema/manifest.schema.json b/internal/schema/manifest.schema.json index 3255a02..c2e52b2 100644 --- a/internal/schema/manifest.schema.json +++ b/internal/schema/manifest.schema.json @@ -485,6 +485,7 @@ "properties": { "disabled": { "type": "boolean", "description": "Disable cascade-managed releases." }, "tag": { "type": "string", "description": "callback.output reference for releases created by an external tool." }, + "workflow": { "type": "string", "description": "Path to a reusable release-build workflow dispatched after a release is published." }, "version_overrides": { "$ref": "#/definitions/versionOverridesConfig" } } }, diff --git a/schema/manifest.schema.json b/schema/manifest.schema.json index 3255a02..c2e52b2 100644 --- a/schema/manifest.schema.json +++ b/schema/manifest.schema.json @@ -485,6 +485,7 @@ "properties": { "disabled": { "type": "boolean", "description": "Disable cascade-managed releases." }, "tag": { "type": "string", "description": "callback.output reference for releases created by an external tool." }, + "workflow": { "type": "string", "description": "Path to a reusable release-build workflow dispatched after a release is published." }, "version_overrides": { "$ref": "#/definitions/versionOverridesConfig" } } },