A cascade fleet example that exercises deploy retry semantics: a deploy whose first attempt fails and whose retry succeeds must leave the environment deployed and the run green, and a dependent deploy gated on that retried deploy must still run. These are runtime behaviors that only real GitHub Actions can show, because they depend on how live job results flow through the generated workflow.
When a deploy declares retries, cascade emits a retry shim job
(Deploy (app) - Retry 1) that re-invokes the deploy callee when the base
attempt fails. Downstream jobs must key on the deploy's effective result (the
base attempt or any retry succeeded), not on the immutable result of the base
attempt. This repo proves both halves of that contract:
- A deploy that fails once and then succeeds on the retry leaves the environment deployed and the orchestrate run green: the base attempt concludes failure, the retry concludes success, the environment's deployed sha advances, and the overall run is green.
- A dependent deploy (
notify) thatdepends_onthe retried deploy runs rather than being skipped, because its gate reads the effective result of the base deploy, not the failed result of the first attempt.
Two environments (staging, prod) and two deploys:
| Deploy | Role | Callee |
|---|---|---|
app |
environment deploy with retries: 1; fails once, then succeeds |
deploy-app.yaml |
notify |
dependent deploy with depends_on: [app]; must still run |
deploy-notify.yaml |
A stub build callback gives the repo a valid build and promote chain.
deploy-app.yaml fails on its first attempt and succeeds on its retry using an
Actions cache entry as a one-shot marker. The cache key is
deploy-app-attempted-<run id>-<environment>:
- On the first attempt the cache misses (no marker), so the callee writes the marker, then fails. The retry shim re-invokes the same callee, the cache now hits, and the callee succeeds.
- The key is scoped to
github.run_id, so the marker is unique to a single run and resets on its own: a fresh run always starts with a cache miss, so the first attempt fails again. No state carries between runs, so the marker can never wedge a later run into a stuck state.
This marker needs no secret and no committed state; it lives entirely in the Actions cache for the duration of one run.
scenario-suite.yaml merges a source change to drive an orchestrate run, then
reads that run's jobs and the committed deploy state and asserts:
- the base
Deploy (app)attempt concluded failure andDeploy (app) - Retry 1concluded success (the retry rescued the deploy); - the dependent
Deploy (notify)job ran rather than being skipped; - the orchestrate run concluded success; and
- the
stagingdeploy sha forappadvanced to the merge sha.
Every run the suite causes is recorded in the fleet ledger, and the final reconcile job fails closed on any unregistered run.
.github/manifest.yaml cascade manifest (config plus live state)
.github/workflows/build.yaml build callee stub
.github/workflows/deploy-app.yaml fail-once deploy callee
.github/workflows/deploy-notify.yaml dependent deploy callee
.github/workflows/orchestrate.yaml generated CI on trunk merges
.github/workflows/promote.yaml generated environment promotion
.github/workflows/scenario-suite.yaml driver and assertions
The orchestrate, promote, hotfix, and rollback workflows and the
manage-release action are generated by cascade from the manifest. Run
cascade generate-workflow after editing the manifest.