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: 7 additions & 0 deletions .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ jobs:
go-version-file: go.mod
cache: true

# actionlint must be on PATH so the emitted-workflow enforcement guard
# (TestActionlint_FeatureMatrix and the census-driven sweep) genuinely runs
# instead of skipping. Pinned to the same SHA the workflow-lint job uses so
# the guard and the repo's own workflow lint agree on the actionlint version.
- name: Install actionlint
run: go install github.com/rhysd/actionlint/cmd/actionlint@914e7df21a07ef503a81201c76d2b11c789d3fca # v1.7.12

- name: Run unit tests
run: |
go list ./... | grep -v /e2e \
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/validate.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ jobs:
go-version-file: go.mod
cache: true

# actionlint must be on PATH so the emitted-workflow enforcement guard
# (TestActionlint_FeatureMatrix and the census-driven sweep) genuinely runs
# instead of skipping. Pinned to the same SHA the workflow-lint job uses so
# the guard and the repo's own workflow lint agree on the actionlint version.
- name: Install actionlint
run: go install github.com/rhysd/actionlint/cmd/actionlint@914e7df21a07ef503a81201c76d2b11c789d3fca # v1.7.12

- name: Run tests
run: go test -v -race -coverprofile=coverage.out ./...

Expand Down
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,24 @@ A `Migration` section is added to any release that bumps `schema_version`.
failed deploy. Per-deploy success rows are still recorded individually, so a
partial success is not lost. A promotion whose deploys all succeed, or that
advances an environment with no deploys, is unchanged.
- **generate:** A hotfix build callback no longer passes reusable-workflow inputs
the called workflow does not declare. The hotfix build job unconditionally sent
`sha` and `target_env` in its `with:` block, but GitHub Actions rejects a
reusable-workflow call that passes an undeclared `workflow_call` input, so every
hotfix build failed at parse on real GitHub. Each input is now gated on the
callback's declared inputs, mirroring the orchestrate path; a callee that
declares neither gets no `with:` block. Only the hotfix workflow's build `with:`
block changes, and only where the callee does not declare the input; all other
generated output is byte-identical.
- **generate:** `on_failure: continue` is now rejected at config validation
instead of emitting an invalid workflow. cascade emits every callback as a
reusable-workflow call (`jobs.<id>.uses`), and GitHub Actions forbids
`continue-on-error` on such a job, so the emitted workflow was rejected at parse
by real GitHub. Because a tolerated failure cannot be expressed for a
reusable-workflow-call job, `continue` is refused with a clear message; `abort`
(the default) is the only supported value. Tolerate a failure inside the
reusable workflow itself so the callback still concludes successfully. A
manifest that does not set `on_failure: continue` emits byte-identical output.
- **generate:** A callback declaring `retries` is now judged on its ladder's
effective result, so a deploy that fails and is then rescued by a retry no
longer fails the run or gets denied in recorded state. A GitHub Actions job
Expand Down
4 changes: 2 additions & 2 deletions docs/public/manifest.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -893,8 +893,8 @@
},
"onFailure": {
"type": "string",
"enum": ["abort", "continue"],
"description": "Behavior when the callback fails."
"enum": ["abort"],
"description": "Behavior when the callback fails. Only 'abort' is supported: cascade emits every callback as a reusable-workflow call and GitHub Actions forbids continue-on-error on such a job, so 'continue' is rejected at validation."
}
}
}
5 changes: 3 additions & 2 deletions docs/src/content/docs/reference/callbacks.md
Original file line number Diff line number Diff line change
Expand Up @@ -523,8 +523,9 @@ Callback failures are handled by the `on_failure` policy:

| Policy | Behavior |
|--------|----------|
| `abort` | Fail the entire workflow |
| `continue` | Other callbacks proceed |
| `abort` | Fail the entire workflow (the default, and the only supported value) |

`abort` is the only supported value. `on_failure: continue` is rejected at config validation: cascade emits every callback as a reusable-workflow call (`jobs.<id>.uses`), and GitHub Actions forbids `continue-on-error` on such a job, so a tolerated failure cannot be expressed without emitting a workflow GitHub rejects at parse. To tolerate a failure, handle it inside the reusable workflow itself so the callback still concludes successfully.

With `retries: N`, a failed callback is retried up to N times before it counts as a final failure. Each retry is a separate job that re-invokes the same reusable workflow, and the chain stops at the first attempt that succeeds. A callback that fails and is then rescued by a retry counts as a success: the run stays green and the deploy is recorded in state.

Expand Down
7 changes: 4 additions & 3 deletions docs/src/content/docs/reference/manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -1085,8 +1085,9 @@ To make a promotion resilient to a transient failure, handle the retry inside th

| `on_failure` | Behavior |
|--------------|----------|
| `abort` | Fail the entire workflow. |
| `continue` | Let other callbacks proceed. |
| `abort` | Fail the entire workflow. The default, and the only supported value. |

`on_failure: continue` is rejected at config validation. Every callback is emitted as a reusable-workflow call (`jobs.<id>.uses`), and GitHub Actions forbids `continue-on-error` on such a job, so a tolerated failure cannot be expressed without emitting a workflow GitHub rejects at parse. Tolerate a failure inside the reusable workflow itself instead, so the callback concludes successfully.

`retries` is the number of retry attempts on failure (0-3).

Expand Down Expand Up @@ -1223,7 +1224,7 @@ The implicit `release` slot tracks the most recently published (non-draft) GitHu

- `schema_version` should be `1`. Omitting it emits a warning.
- Environment, build, and deploy names must be identifier-safe (letters, digits, underscores). Within a section, two names that differ only by hyphen versus underscore are rejected: hyphens become underscores in job IDs and output keys, so those names would emit colliding outputs. The generator-owned names `environment` and `dry_run` are reserved and cannot be used as `dispatch_inputs`.
- `pin_mode` must be `tag` or `sha`; `run_policy` must be `default`, `always`, or `force`; `on_failure` must be `abort` or `continue`; `retries` must be 0-3.
- `pin_mode` must be `tag` or `sha`; `run_policy` must be `default`, `always`, or `force`; `on_failure` must be `abort` (the only supported value; `continue` is rejected); `retries` must be 0-3.
- A repository cannot set both `external` (primary) and `notify` (satellite).
- A per-callback `permissions` block is the complete permission set for that caller job and replaces the workflow default rather than merging.
- `cli_version_sha` takes effect only under `pin_mode: sha`.
Expand Down
53 changes: 53 additions & 0 deletions e2e/scenarios/errors/on-failure-abort-fails-run.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: "on_failure abort reds the run"
description: |
Exercises the builds on_failure field end to end, which no other scenario in
the corpus does. The app build is a reusable workflow whose inner job runs
"exit 1", and the build sets on_failure: abort explicitly. abort is the only
on_failure value cascade supports (on_failure: continue is rejected at config
validation because continue-on-error is invalid on a reusable-workflow-call
job), so this scenario pins the supported value: an aborting build that fails
concludes the orchestrate run in failure. expect_failure makes that failure the
success path, proving the emitter honors the field and the generated workflow
runs on act.

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

steps:
- name: "Commit source that triggers the aborting build"
action: commit
commit:
message: "feat: add source whose build aborts"
files:
src/app.go: |
package main
func main() {}
# Reusable build callback whose inner job exits non-zero, so the generated
# build-app job fails. Inner job id is failbuild so act keys it distinctly.
.github/workflows/build.yaml: |
name: build
on:
workflow_call:
inputs:
environment:
required: false
type: string
sha:
required: false
type: string
jobs:
failbuild:
runs-on: ubuntu-latest
steps:
- run: exit 1

- name: "Orchestrate reds because the aborting build fails"
action: orchestrate
expect_failure: true
16 changes: 13 additions & 3 deletions internal/config/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,7 @@ func TestValidate_NewFields(t *testing.T) {
{Name: "a", Workflow: "w.yaml", OnFailure: "invalid"},
},
},
wantErrs: []string{"builds[0].on_failure must be one of: abort, continue"},
wantErrs: []string{"builds[0].on_failure must be: abort"},
},
{
name: "retries out of range",
Expand Down Expand Up @@ -692,11 +692,21 @@ func TestValidate_ValidateBlockPolicyParity(t *testing.T) {
validate: &ValidateConfig{
Workflow: "w.yaml",
RunPolicy: "always",
OnFailure: "continue",
OnFailure: "abort",
Retries: intPtr(3),
},
wantErrs: nil,
},
{
name: "on_failure continue is rejected as unsupported",
validate: &ValidateConfig{
Workflow: "w.yaml",
OnFailure: "continue",
},
wantErrs: []string{"validate.on_failure: continue is not supported: cascade emits every " +
"callback as a reusable-workflow call and GitHub Actions forbids continue-on-error on such a job. " +
"Use on_failure: abort (the default), or tolerate the failure inside the reusable workflow itself"},
},
{
name: "unset retries is valid",
validate: &ValidateConfig{Workflow: "w.yaml"},
Expand All @@ -710,7 +720,7 @@ func TestValidate_ValidateBlockPolicyParity(t *testing.T) {
{
name: "invalid on_failure",
validate: &ValidateConfig{Workflow: "w.yaml", OnFailure: "invalid"},
wantErrs: []string{"validate.on_failure must be one of: abort, continue"},
wantErrs: []string{"validate.on_failure must be: abort"},
},
{
name: "retries above bound",
Expand Down
17 changes: 15 additions & 2 deletions internal/config/validate_v1.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,8 +253,21 @@ func validateCallbackPolicy(prefix, runPolicy, onFailure string, retries int) []
if runPolicy != "" && runPolicy != RunPolicyDefault && runPolicy != RunPolicyAlways && runPolicy != RunPolicyForce {
errs = append(errs, fmt.Sprintf("%s.run_policy must be one of: default, always, force", prefix))
}
if onFailure != "" && onFailure != OnFailureAbort && onFailure != OnFailureContinue {
errs = append(errs, fmt.Sprintf("%s.on_failure must be one of: abort, continue", prefix))
switch onFailure {
case "", OnFailureAbort:
// Unset or abort: the run fails when the callback fails, which is the
// only outcome expressible for a reusable-workflow-call job.
case OnFailureContinue:
// cascade emits every callback as a reusable-workflow call (jobs.<id>.uses).
// GitHub Actions forbids continue-on-error on such a job, and a failed job
// otherwise fails the whole run regardless of any downstream check, so a
// tolerated failure cannot be expressed without emitting a workflow GitHub
// rejects at parse. Reject it loudly here rather than emit invalid YAML.
errs = append(errs, fmt.Sprintf("%s.on_failure: continue is not supported: cascade emits every "+
"callback as a reusable-workflow call and GitHub Actions forbids continue-on-error on such a job. "+
"Use on_failure: abort (the default), or tolerate the failure inside the reusable workflow itself", prefix))
default:
errs = append(errs, fmt.Sprintf("%s.on_failure must be: abort", prefix))
}
if retries < 0 || retries > 3 {
errs = append(errs, fmt.Sprintf("%s.retries must be between 0 and 3", prefix))
Expand Down
Loading