diff --git a/e2e/harness/multistep.go b/e2e/harness/multistep.go index 7a2e873..f9c2eff 100644 --- a/e2e/harness/multistep.go +++ b/e2e/harness/multistep.go @@ -70,15 +70,18 @@ type StepExpect struct { } // WorkflowFileExpect asserts a generated workflow file contains/excludes -// specific substrings. Verifies manifest fields make it into the emitted -// YAML, orthogonal to behavior checks (state/jobs/etc.) which observe the -// run outcome. Used for features whose effect is purely the generated -// workflow shape (#92 concurrency, #97 timeout-minutes, #101/#102 push -// retry loops). +// specific substrings, or asserts the file is absent entirely. Verifies +// manifest fields make it into the emitted YAML, orthogonal to behavior +// checks (state/jobs/etc.) which observe the run outcome. NotExists covers +// the conditional-generation case where a feature suppresses a whole file +// (for example the hotfix workflow when fewer than two environments exist). +// Used for features whose effect is purely the generated workflow shape +// (#92 concurrency, #97 timeout-minutes, #101/#102 push retry loops). type WorkflowFileExpect struct { Path string `yaml:"path"` // Path inside the test repo (e.g., ".github/workflows/orchestrate.yaml") Contains []string `yaml:"contains,omitempty"` // Substrings that must appear NotContains []string `yaml:"not_contains,omitempty"` // Substrings that must NOT appear + NotExists bool `yaml:"not_exists,omitempty"` // When true, the file must NOT exist } // StateExpect defines expected state for an environment diff --git a/e2e/harness/runner.go b/e2e/harness/runner.go index 022cd67..9158452 100644 --- a/e2e/harness/runner.go +++ b/e2e/harness/runner.go @@ -725,6 +725,12 @@ func (r *Runner) assertWorkflowFile(ctx context.Context, expect WorkflowFileExpe if reader != nil { _, _ = io.Copy(&content, reader) } + if expect.NotExists { + if exitCode == 0 { + return []error{fmt.Errorf("workflow file %s should not exist but was found", expect.Path)} + } + return nil + } if exitCode != 0 { return []error{fmt.Errorf("workflow file not found: %s", expect.Path)} } diff --git a/e2e/harness/scenario.go b/e2e/harness/scenario.go index b6bca5d..dc3329f 100644 --- a/e2e/harness/scenario.go +++ b/e2e/harness/scenario.go @@ -45,6 +45,18 @@ type Config struct { // job-level environment: key. Mirrors internal/config EnvironmentConfig. // Keyed by env name so future per-env keys extend additively. EnvironmentConfig map[string]EnvEnvironmentConfig `yaml:"environment_config,omitempty"` + // Validate, ValidateCheck, MergeQueue, PRPreview, Notify, and External carry + // the optional generator features through to the generated manifest untouched. + // Each uses a generic map (rather than a typed struct) so the harness stays + // decoupled from the generator's struct shapes while preserving every key + // across the marshal round-trip. As the generator gains new keys under any of + // these blocks, scenarios can exercise them without a harness change. + Validate map[string]any `yaml:"validate,omitempty"` + ValidateCheck map[string]any `yaml:"validate_check,omitempty"` + MergeQueue map[string]any `yaml:"merge_queue,omitempty"` + PRPreview map[string]any `yaml:"pr_preview,omitempty"` + Notify map[string]any `yaml:"notify,omitempty"` + External []map[string]any `yaml:"external,omitempty"` } // EnvEnvironmentConfig mirrors internal/config.EnvironmentConfig's gha_environment diff --git a/e2e/scenarios/14-validate-check.yaml b/e2e/scenarios/14-validate-check.yaml new file mode 100644 index 0000000..d4ca84d --- /dev/null +++ b/e2e/scenarios/14-validate-check.yaml @@ -0,0 +1,38 @@ +name: "Validate Check Workflow" +description: | + Verifies that enabling validate_check emits the standalone + cascade-validate.yaml workflow that runs the manifest parse on pull + requests touching the manifest file (#90). + + Generator-output verification only. + +config: + trunk_branch: main + environments: [dev] + builds: + - name: app + workflow: build.yaml + triggers: ["src/**"] + deploys: [] + validate_check: + enabled: true + +steps: + - name: "Initial commit; assert cascade-validate.yaml is generated" + action: commit + commit: + message: "feat: add app" + files: + src/app.go: | + package main + func main() {} + expect: + workflow_files: + - path: ".github/workflows/cascade-validate.yaml" + contains: + - "name: Validate Manifest" + - "pull_request:" + - "paths:" + - ".github/manifest.yaml" + - "validate-manifest:" + - "cascade parse-config" diff --git a/e2e/scenarios/15-merge-queue.yaml b/e2e/scenarios/15-merge-queue.yaml new file mode 100644 index 0000000..5d8ee60 --- /dev/null +++ b/e2e/scenarios/15-merge-queue.yaml @@ -0,0 +1,37 @@ +name: "Merge Queue Workflow" +description: | + Verifies that enabling merge_queue emits the cascade-merge-queue.yaml + workflow that validates the manifest and sets up orchestration on + merge_group events (#91). + + Generator-output verification only. + +config: + trunk_branch: main + environments: [dev] + builds: + - name: app + workflow: build.yaml + triggers: ["src/**"] + deploys: [] + merge_queue: + enabled: true + +steps: + - name: "Initial commit; assert cascade-merge-queue.yaml is generated" + action: commit + commit: + message: "feat: add app" + files: + src/app.go: | + package main + func main() {} + expect: + workflow_files: + - path: ".github/workflows/cascade-merge-queue.yaml" + contains: + - "merge_group" + - "contents: read" + - "merge-queue-validate:" + - "cascade parse-config" + - "cascade --dry-run orchestrate setup" diff --git a/e2e/scenarios/16-pr-preview.yaml b/e2e/scenarios/16-pr-preview.yaml new file mode 100644 index 0000000..82f2385 --- /dev/null +++ b/e2e/scenarios/16-pr-preview.yaml @@ -0,0 +1,37 @@ +name: "PR Preview Workflow" +description: | + Verifies that enabling pr_preview emits the cascade-pr-preview.yaml + workflow that plans a preview on pull requests, with a per-PR + concurrency group (#93). + + Generator-output verification only. + +config: + trunk_branch: main + environments: [dev] + builds: + - name: app + workflow: build.yaml + triggers: ["src/**"] + deploys: [] + pr_preview: + enabled: true + +steps: + - name: "Initial commit; assert cascade-pr-preview.yaml is generated" + action: commit + commit: + message: "feat: add app" + files: + src/app.go: | + package main + func main() {} + expect: + workflow_files: + - path: ".github/workflows/cascade-pr-preview.yaml" + contains: + - "name: Cascade PR Preview" + - "pull_request:" + - "cascade-pr-preview-${{ github.event.pull_request.number }}" + - "preview:" + - "Plan Preview" diff --git a/e2e/scenarios/17-validate-callback.yaml b/e2e/scenarios/17-validate-callback.yaml new file mode 100644 index 0000000..85ca070 --- /dev/null +++ b/e2e/scenarios/17-validate-callback.yaml @@ -0,0 +1,37 @@ +name: "Validate Callback Gate" +description: | + Verifies that a top-level validate callback adds a validate job to the + generated orchestrate.yaml and gates the build jobs on its success via a + needs.validate.result == 'success' condition (#94). + + Generator-output verification only. + +config: + trunk_branch: main + environments: [dev] + builds: + - name: app + workflow: build.yaml + triggers: ["src/**"] + deploys: + - name: deploy-dev + workflow: deploy.yaml + triggers: ["src/**"] + validate: + run: "echo validate" + +steps: + - name: "Initial commit; assert validate gate 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: + - "validate:" + - "needs.validate.result == 'success'" diff --git a/e2e/scenarios/hotfix/hotfix-generation-threshold.yaml b/e2e/scenarios/hotfix/hotfix-generation-threshold.yaml new file mode 100644 index 0000000..674b77e --- /dev/null +++ b/e2e/scenarios/hotfix/hotfix-generation-threshold.yaml @@ -0,0 +1,51 @@ +name: "Hotfix Generation Threshold" +description: | + Verifies that a repository with two or more environments generates the + cascade-hotfix.yaml workflow with its full set of jobs and the finalize + step that records the merge, fix, and base SHAs (#88). + + Generator-output verification only. + +config: + trunk_branch: main + environments: [dev, prod] + builds: + - name: app + workflow: build.yaml + triggers: ["src/**"] + deploys: + - name: deploy-dev + workflow: deploy.yaml + triggers: ["src/**"] + - name: deploy-prod + workflow: deploy.yaml + triggers: ["src/**"] + +steps: + - name: "Initial commit; assert cascade-hotfix.yaml is generated" + action: commit + commit: + message: "feat: add app" + files: + src/app.go: | + package main + func main() {} + expect: + workflow_files: + - path: ".github/workflows/cascade-hotfix.yaml" + contains: + - "workflow_dispatch:" + - "type: choice" + - "target_env:" + - "pull_request:" + - "types: [closed]" + - "'env/*'" + - "group: hotfix-" + - " plan:" + - " apply:" + - " check:" + - " context:" + - " finalize:" + - "--merge-sha" + - "--fix-sha" + - "--base-sha" diff --git a/e2e/scenarios/hotfix/hotfix-no-hotfix-single-env.yaml b/e2e/scenarios/hotfix/hotfix-no-hotfix-single-env.yaml new file mode 100644 index 0000000..316e5c3 --- /dev/null +++ b/e2e/scenarios/hotfix/hotfix-no-hotfix-single-env.yaml @@ -0,0 +1,33 @@ +name: "Hotfix Single Env Absent" +description: | + Verifies that a repository with a single environment does not generate + the cascade-hotfix.yaml workflow, since the hotfix flow only applies once + two or more environments exist (#88). + + Generator-output verification only. + +config: + trunk_branch: main + environments: [dev] + builds: + - name: app + workflow: build.yaml + triggers: ["src/**"] + deploys: + - name: deploy-dev + workflow: deploy.yaml + triggers: ["src/**"] + +steps: + - name: "Initial commit; assert cascade-hotfix.yaml is absent" + action: commit + commit: + message: "feat: add app" + files: + src/app.go: | + package main + func main() {} + expect: + workflow_files: + - path: ".github/workflows/cascade-hotfix.yaml" + not_exists: true