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
36 changes: 17 additions & 19 deletions docs/src/content/docs/callback-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ description: Defines the inputs, outputs, and structural requirements for valida

The framework calls your workflows (callbacks) during CI/CD execution. This document defines the contract your workflows must follow.

Every callback (validate, build, deploy, publish) is a reusable workflow that you declare with `workflow:` in the manifest. The framework invokes it with `workflow_call`.

## Migrating from inline `run:`/`shell:` callbacks

Inline `run:`/`shell:` callbacks were removed. A callback can no longer carry a `run:` script or a `shell:` setting in the manifest; it must point at a reusable workflow via `workflow:`. The manifest still parses these keys, but validation now rejects them.

To migrate, move the script into a reusable workflow under `.github/workflows/`, expose it with `on: workflow_call` (declaring the standard `environment`, `sha`, and `dry_run` inputs), and replace the callback's `run:`/`shell:` with `workflow: .github/workflows/<name>.yaml`. The script text becomes a `run:` step inside that workflow's job. The sections below show the required structure for each callback type.

## Overview

Adopting repositories provide callback workflows that the framework invokes:
Expand Down Expand Up @@ -471,17 +479,14 @@ ci:

## Environment Protection

Use GitHub Environment protection for approval gates. Where you declare the
`environment:` key depends on whether the deploy is an external reusable
workflow or an inline `run:` callback, because GitHub Actions only allows a
job-level `environment:` key on a steps job, never on a job that calls a
reusable workflow with `uses:`.
Use GitHub Environment protection for approval gates. Because every deploy is a
reusable workflow, declare the `environment:` key on the job **inside your
reusable workflow**. GitHub Actions only allows a job-level `environment:` key on
a steps job, never on a job that calls a reusable workflow with `uses:`, so the
caller job cascade generates cannot carry it.

### External reusable-workflow deploys (`workflow:`)

For a deploy backed by an external reusable workflow, declare `environment:` on
the job **inside your reusable workflow**. cascade passes the target environment
name to that workflow as the `environment` input, so wire it through:
cascade passes the target environment name to your workflow as the `environment`
input, so wire it through:

```yaml
# your reusable deploy workflow
Expand All @@ -497,15 +502,8 @@ cascade cannot set `environment:` on the caller job it generates: GitHub Actions
rejects a workflow that puts `environment:` on a `uses:` job. cascade therefore
emits only the `with: environment:` input on the caller and relies on your
reusable workflow to apply the protection rules. cascade prints a generate-time
note when `gha_environment` is configured for an environment whose deploys are
external reusable workflows, reminding you to declare `environment:` inside the
reusable workflow.

### Inline `run:` deploys

For an inline `run:` deploy, cascade owns the job and emits the job-level
`environment:` key directly (resolved from `gha_environment` when configured),
so GitHub Environment protection applies without any extra wiring.
note when `gha_environment` is configured for an environment, reminding you to
declare `environment:` inside the reusable workflow.

Configure protection in GitHub: **Settings -> Environments -> Add required reviewers**.

Expand Down
21 changes: 9 additions & 12 deletions docs/src/content/docs/security/hardening.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,11 @@ These properties hold for generated output today:
- **Local reusable workflows are commit-pinned.** Workflows referenced as
`./.github/...` are pinned to the calling commit, so your own callbacks
resolve from a fixed commit rather than a moving branch.
- **Untrusted inputs are routed through `env:`.** Workflow inputs are exposed to
shell steps as environment variables rather than interpolated directly into
`run:` script text, which keeps input values as data instead of as part of the
script.
- **Inline deploys are environment-gated.** A deploy expressed as an inline
`run:` job receives a real job-level `environment:`, so the environment's
protection rules (required reviewers, wait timers, branch policies) apply to
it.
- **Every callback is a reusable workflow.** Validate, build, and deploy
callbacks run as reusable workflows referenced by `workflow:`. cascade does not
emit inline scripts on your behalf, so the script your pipeline runs is code you
author and review in a workflow file rather than text generated from the
manifest.
- **The reusable-deploy gate boundary is surfaced at generate time.** GitHub does
not allow a job-level `environment:` on a job that calls a reusable workflow.
When you wire a reusable deploy, cascade warns you at generation time that the
Expand Down Expand Up @@ -111,10 +108,10 @@ moves from the cross-repo trust boundary outward to the surrounding controls.
2. **Protect trunk and tags, and add CODEOWNERS.** Require review on trunk and on
`.github/workflows/**`, and protect release and version tags from being moved
or deleted.
3. **Gate every production deploy with a GitHub Environment.** For inline deploys,
confirm the job-level `environment:` is set. For reusable deploys, add the
`environment:` gate, required reviewers, and deployment branch or tag policy
inside the called workflow, since the calling job cannot carry the gate.
3. **Gate every production deploy with a GitHub Environment.** Deploys run as
reusable workflows, so add the `environment:` gate, required reviewers, and
deployment branch or tag policy inside the called workflow, since the calling
job cannot carry the gate.
4. **Scope secrets to the job that needs them.** Replace blanket secret
inheritance with an explicit list, and place production secrets behind an
environment so only the gated production job can read them.
Expand Down
13 changes: 6 additions & 7 deletions docs/src/content/docs/workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,11 +355,10 @@ permissions:
packages: write # Optional: only if your callbacks publish to GHCR
```

For environment protection on an external reusable-workflow deploy, set the
`environment:` key on the job inside your callback. cascade passes the target
environment name as the `environment` input and cannot set `environment:` on the
caller job it generates, because GitHub Actions disallows that key on a `uses:`
job:
Every deploy is a reusable workflow, so set the `environment:` key on the job
inside your callback. cascade passes the target environment name as the
`environment` input and cannot set `environment:` on the caller job it generates,
because GitHub Actions disallows that key on a `uses:` job:

```yaml
jobs:
Expand All @@ -368,8 +367,8 @@ jobs:
environment: ${{ inputs.environment }} # GitHub enforces approvals
```

For an inline `run:` deploy, cascade owns the job and emits the job-level
`environment:` key for you when `gha_environment` is configured.
cascade prints a generate-time note when `gha_environment` is configured,
reminding you to declare `environment:` inside the reusable workflow.

## Concurrency Control

Expand Down
37 changes: 0 additions & 37 deletions e2e/scenarios/06-callback-timeout.yaml

This file was deleted.

56 changes: 0 additions & 56 deletions e2e/scenarios/10-inline-job-attributes.yaml

This file was deleted.

42 changes: 0 additions & 42 deletions e2e/scenarios/12-inline-run-callback.yaml

This file was deleted.

39 changes: 0 additions & 39 deletions e2e/scenarios/13-inline-run-deploy-no-rollback.yaml

This file was deleted.

21 changes: 20 additions & 1 deletion e2e/scenarios/17-validate-callback.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ config:
workflow: deploy.yaml
triggers: ["src/**"]
validate:
run: "echo validate"
workflow: validate.yaml

steps:
- name: "Initial commit; assert validate gate in orchestrate.yaml"
Expand All @@ -29,6 +29,25 @@ steps:
src/app.go: |
package main
func main() {}
# Reusable validate callback the generated orchestrate.yaml invokes as a
# uses: job. Its inner job echoes "validate" so the gate has a real job to
# wait on; the orchestrate build jobs gate on needs.validate.result.
.github/workflows/validate.yaml: |
name: validate
on:
workflow_call:
inputs:
environment:
required: false
type: string
sha:
required: false
type: string
jobs:
runvalidate:
runs-on: ubuntu-latest
steps:
- run: echo "validate"
expect:
workflow_files:
- path: ".github/workflows/orchestrate.yaml"
Expand Down
32 changes: 25 additions & 7 deletions e2e/scenarios/errors/build-failure-stops-promotion.yaml
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
name: "Build failure stops orchestrate"
description: |
Drives a real build failure under act: the app build runs an inline
"exit 1" step, so the generated build-app job fails and the orchestrate run
concludes in failure. expect_failure on the orchestrate step makes that
failure the success path, proving the harness observes a genuinely failing
workflow rather than a simulated outcome.
Drives a real build failure under act: the app build is a reusable workflow
whose inner job runs "exit 1", so the generated build-app job fails and the
orchestrate run concludes in failure. expect_failure on the orchestrate step
makes that failure the success path, proving the harness observes a genuinely
failing workflow rather than a simulated outcome.

config:
trunk_branch: main
environments: [dev, prod]
builds:
- name: app
run: "exit 1"
shell: bash
workflow: build.yaml
triggers: ["src/**"]
deploys: []

Expand All @@ -25,6 +24,25 @@ steps:
src/app.go: |
package main
func main() {}
# Reusable build callback whose inner job exits non-zero on purpose, so
# the generated build-app job fails and the orchestrate run concludes in
# failure. 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 fails because the build exits non-zero"
action: orchestrate
Expand Down
Loading
Loading