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
13 changes: 13 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ cd e2e && go test -v -timeout 20m ./...

# Lint
golangci-lint run ./...

# Regenerate cascade's own workflows (cascade compiles its own)
go run ./cmd/cascade generate-workflow --config .github/manifest.yaml -f
```

## Making a change
Expand All @@ -44,6 +47,16 @@ golangci-lint run ./...

Public APIs follow a functional-options style: required inputs are positional and optional or extensible behavior arrives as a variadic `...Option` tail, so new capability is additive and never a breaking signature change. Cross-cutting concerns are small interfaces with no-op defaults rather than forced dependencies.

## Project conventions

cascade holds to a few conventions in its own codebase and in the workflows it generates:

- **Additive manifest changes**: new fields are always optional with sensible defaults, so existing manifest files keep working across minor version bumps.
- **Callback isolation**: generated workflows call your workflows via `workflow_call`, and cascade never reaches into your callback logic.
- **Metadata courier**: cascade passes artifact identifiers and versions between stages. It never touches your container registry, package registry, or the systems you deploy to directly.

Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/), as noted under [Making a change](#making-a-change); the changelog and version bumps are derived from them.

## Governed action pins

cascade owns the third-party action pins it emits into generated workflows, and that ownership rests on a few rules that any code touching pins, manifest paths, or machine-authored commits must keep:
Expand Down
87 changes: 11 additions & 76 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,6 @@ Read [How Cascade works](https://stablekernel.github.io/cascade/start/how-it-wor

---

## Is cascade for you?

cascade earns its keep when you promote a built artifact through a chain of environments. It is a strong fit when most of these hold:

- You deploy to **two or more environments** (say dev, test, prod) and want the *same* artifact promoted through them, never rebuilt per stage.
- You are on **GitHub Actions** and would rather own your deploy logic in reusable workflows than run a separate CD platform.
- You want **promotion gates, hotfix-to-any-environment, and rollback** without hand-wiring that state machine.
- You can adopt **conventional commits** (cascade derives versions, changelogs, and the breaking-change gate from them).

It is likely overkill for a single environment with a plain build-and-release on push. A repo with no deployments at all is a different case: the no-environment mode is a supported shape that still gives you conventional-commit versioning and releases.

Already running a pipeline? See the [adoption guide](https://stablekernel.github.io/cascade/guides/adopt/) for migrating without a rewrite.

---

## Quickstart

```bash
Expand Down Expand Up @@ -97,40 +82,22 @@ The full walkthrough, including `cascade init` scaffolding and the four topology

## What cascade generates

A `generate-workflow` run emits, unconditionally:
A single `generate-workflow` run compiles the manifest into the orchestrate, promote, hotfix, and rollback workflows plus the release composite action, with opt-in companions emitted only when their manifest block is present. See [Generated workflows](https://stablekernel.github.io/cascade/reference/generated-workflows/) for the full anatomy of each file.

| File | Purpose |
|---|---|
| `.github/workflows/orchestrate.yaml` | Runs on merge to trunk: validate, build, deploy to the first environment, finalize state. |
| `.github/workflows/promote.yaml` | Manually dispatched: cascades the same built artifact through the rest of the environment chain. |
| `.github/workflows/cascade-hotfix.yaml` | Patches a single environment out of band without touching the others. |
| `.github/workflows/cascade-rollback.yaml` | Rolls an environment back to its previous deployed version. |
| `.github/actions/manage-release/action.yaml` | Composite action that creates, updates, and publishes the GitHub release. |
---

Opt-in companions (drift-check, PR-preview, pin-reconcile) are emitted only when their manifest block is present. See [Generated workflows](https://stablekernel.github.io/cascade/reference/generated-workflows/) for the full anatomy of each file.
## Highlights

---
- **Compiler model.** One manifest compiles into a full multi-environment pipeline of native GitHub Actions workflows.
- **Single or multi-component.** A single-component repo is the default; declare more to version, promote, hotfix, and roll back each independently from one manifest, each in its own tag and state namespace. Monorepos are native, not bolted on. See [Components](https://stablekernel.github.io/cascade/guides/components/).
- **SHA-keyed promotion ladder.** Promote the exact bytes that passed the previous environment, never a per-stage rebuild. See [Promote a release](https://stablekernel.github.io/cascade/guides/promote/).
- **Security by construction.** Every caller job carries a per-callback least-privilege `permissions:` block, including OIDC `id-token: write`. See [Callback contract](https://stablekernel.github.io/cascade/reference/callbacks/).
- **Self-healing supply chain.** Third-party action pins live in one source of truth, and a reconcile companion adopts external pin bumps back into the manifest. See [Action pins](https://stablekernel.github.io/cascade/guides/action-pins/).
- **Hotfix and rollback, race-safe.** Patch or revert a single environment with correct, race-safe concurrency. See [Run a hotfix](https://stablekernel.github.io/cascade/guides/hotfix/) and [Roll back an environment](https://stablekernel.github.io/cascade/guides/rollback/).

## Capabilities
Preview a pipeline before you merge: [`simulate`](https://stablekernel.github.io/cascade/guides/simulate-and-verify/) traces what a change would build and deploy, and [`graph`](https://stablekernel.github.io/cascade/guides/visualize/) renders the environment chain.

| Capability | What it does |
|---|---|
| Change detection | Builds and deploys run only when their declared `triggers` match changed paths. |
| Dependency ordering | `depends_on` and `optional_depends_on` chain builds and deploys in the right order. |
| Matrix builds | Fan a single build out over a matrix of `dimensions`, with `max_parallel` and `fail_fast`. |
| Concurrency control | Configurable group and `cancel_in_progress` on orchestrate, promote, hotfix, rollback, release, and external-update workflows. |
| Extra triggers | Attach `schedule`, `repository_dispatch`, `workflow_run`, and `merge_group` events to orchestration. |
| Least-privilege permissions | Per-callback `permissions:` blocks scope each caller job, including OIDC `id-token: write`. |
| Action pinning | `pin_mode: tag` (default) or `sha`, with an embedded action-pins manifest and an opt-in reconcile companion. |
| PR plan preview | An opt-in comment on each PR shows which builds and deploys would run. |
| Breaking-change gate | `feat!:` or `BREAKING CHANGE:` commits block the prerelease-to-release boundary unless overridden. |
| Tag grammar | Release tags follow `vX.Y.Z-rc.N` by default; the prefix, prerelease token, and separator are configurable via `tag_grammar`, and an omitted block reproduces today's shape exactly. |
| Artifact passing | The `artifact_id` output from a build is stored in state and forwarded to deploys and publish. |
| GitHub Environments | The `environments` command emits per-environment config (`required_reviewers`, `wait_timer`, `branch_policy`) for you to apply. |
| Schema enforcement | Every CLI invocation checks `schema_version` and rejects incompatible manifests with a clear error. |
| Multi-component repos | A `components:` block versions, promotes, hotfixes, and rolls back several independent components from one manifest, each in its own tag and state namespace. Omit it for a single-component repo and output is byte-identical. |

A manifest that puts a few of these to work: `web` builds only after `api`, and each build and deploy runs only when its `triggers` match the changed paths.
A fuller manifest puts a few fields to work: `web` builds only after `api`, and each build and deploy runs only when its `triggers` match the changed paths.

```yaml
# .github/manifest.yaml
Expand Down Expand Up @@ -184,38 +151,6 @@ See the [full sidebar](https://stablekernel.github.io/cascade/) for the rest, in

---

## Conventions

cascade follows these conventions in its own codebase and in the generated workflows it produces:

- **Additive manifest changes**: new fields are always optional with sensible defaults, so existing manifest files keep working across minor version bumps.
- **Conventional commits**: commit messages follow `type: subject` (for example `feat:`, `fix:`, `docs:`), and the changelog generator reads this format.
- **Callback isolation**: generated workflows call your workflows via `workflow_call`, and cascade never reaches into your callback logic.
- **Metadata courier**: cascade passes artifact identifiers and versions between stages. It never touches your container registry, package registry, or deployment target directly.

---

## Development

```bash
# Build
go build -o cascade ./cmd/cascade

# Test (all packages)
go test ./...

# E2E tests (requires Docker)
cd e2e && go test -v -timeout 20m ./...

# Lint
golangci-lint run ./...

# Regenerate cascade's own workflows (uses itself)
go run ./cmd/cascade generate-workflow --config .github/manifest.yaml -f
```

---

## Contributing

Contributions are welcome. Please read [CONTRIBUTING.md](./CONTRIBUTING.md) for development setup and workflow details.
Expand Down
2 changes: 2 additions & 0 deletions docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ export default defineConfig({
{ label: 'Run a hotfix', link: '/guides/hotfix/' },
{ label: 'Roll back an environment', link: '/guides/rollback/' },
{ label: 'Simulate and verify', link: '/guides/simulate-and-verify/' },
{ label: 'Governance companions', link: '/guides/companions/' },
{ label: 'Pin and reconcile actions', link: '/guides/action-pins/' },
{ label: 'Coordinate multiple repos', link: '/guides/multi-repo/' },
{ label: 'Split a repo into components', link: '/guides/components/' },
{ label: 'Visualize the pipeline', link: '/guides/visualize/' },
Expand Down
79 changes: 79 additions & 0 deletions docs/src/content/docs/guides/action-pins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
title: Pin and reconcile actions
description: Pin every third-party action from one manifest source, choose tag or resolved-SHA mode, and adopt an external pin bump back into the manifest.
---

Cascade owns the third-party action references inside the workflows it generates. Rather than let `uses:` pins scatter and drift across many files, every pin resolves from one place: the manifest and a single committed pin table. This guide covers setting the pin style, overriding an individual action, and adopting an outside pin bump (for example a Dependabot update) back into the manifest so generated output agrees again. For the field definitions, see the [`pin_mode`](/cascade/reference/manifest/#action-pinning) and [`action_pins`](/cascade/reference/manifest/#action-pinning) reference.

## One source for every pin

Generated workflows are build output, so their action pins are build output too. Cascade emits each third-party action (for example `actions/checkout` and `actions/github-script`) from a single committed pin table, and you change a pin by editing the manifest, never by hand-editing the generated YAML. A hand-edited pin is reported as drift by the next [`cascade verify`](/cascade/reference/cli/#verify) and overwritten by the next regenerate. Keeping the source single is what makes the pins auditable: one table, one place to review, one place to bump.

## Choose tag or resolved-SHA mode

`pin_mode` sets the reference style for every third-party action Cascade emits:

```yaml
# .github/manifest.yaml
ci:
config:
pin_mode: sha
```

| Mode | Emits | Use it when |
|------|-------|-------------|
| `tag` (default) | `actions/checkout@v5` | You want readable, low-friction pins and trust the action major tag. |
| `sha` | `actions/checkout@0123...` with the version as a trailing comment | You want an immutable pin that a moved tag cannot repoint under you. |

Under `sha`, the pin resolves to a 40-character commit SHA and the human-readable version rides along as a comment, so a reviewer still sees which release the commit belongs to. Resolved-SHA mode is the stronger supply-chain posture and pairs naturally with `cli_version_sha` for the Cascade self-action pin.

## Override a single action

`action_pins` overrides the built-in reference for individual actions, keyed by action path. The value is the bare reference emitted after `@` for that action, a tag or a commit SHA, and it applies regardless of `pin_mode`. An override cannot repoint an action to a different owner or repository:

```yaml
ci:
config:
pin_mode: sha
action_pins:
actions/checkout: "0123456789abcdef0123456789abcdef01234567"
```

That emits `uses: actions/checkout@0123456789abcdef0123456789abcdef01234567`. An action that is neither in the built-in table nor overridden is emitted unchanged.

## Adopt an external pin bump

When an outside tool bumps a governed action in a generated workflow, that file now disagrees with the manifest. `cascade reconcile` closes the gap: it reads the changed source files, adopts the moved reference into the manifest's `action_pins`, and regenerates every workflow the manifest produces so the owned output agrees again. It reads only what you pass and the manifest; it never reads a pin back out of a generated file, and it never pushes, commits, or merges.

```bash
cascade reconcile \
--changed-file .github/workflows/orchestrate.yaml \
--changed-file .github/workflows/promote.yaml
```

Two flags shape the run:

- `--changed-file` names a changed source file to scan for a governed pin bump. Repeat it for each file the bump touched.
- `--check` runs the read-only detector instead: it reports whether the change is relevant and writes a data-only JSON artifact (path set by `--check-output`), writing nothing else. Use it to gate the adopting step in CI.
- `--dry-run` previews the adoption without writing.

## Automate it as a companion

Rather than run `reconcile` by hand, enable the reconcile companion so an external pin bump is detected and adopted in CI. Setting [`reconcile.enabled: true`](/cascade/reference/manifest/#reconcile) emits two workflows: a fork-safe read-only detector (`cascade-reconcile-check.yaml`) and an adopting companion (`cascade-reconcile-companion.yaml`) that writes the moved reference into `action_pins` and regenerates. Route the adoption commit with `reconcile.commit`: `append` pushes onto the triggering pull request branch, while `followup` opens a separate pull request, which is the safer choice when you automerge on green.

```yaml
ci:
config:
reconcile:
enabled: true
source: dependabot
commit: followup
```

## Wayfinding

**Prerequisite:** [Getting started](/cascade/start/getting-started/) for a manifest that already generates workflows.

**Related:** [Governance companions](/cascade/guides/companions/) for the drift and preview checks that keep generated output honest on every pull request.

**Reference:** [`pin_mode`](/cascade/reference/manifest/#action-pinning), [`action_pins`](/cascade/reference/manifest/#action-pinning), and [`cascade reconcile`](/cascade/reference/cli/#reconcile).
69 changes: 69 additions & 0 deletions docs/src/content/docs/guides/companions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
title: Governance companions
description: Turn on the opt-in pull-request companions that catch workflow drift, preview a plan before merge, validate the manifest, and guard a merge queue.
---

Cascade generates a working pipeline from the required manifest fields alone. The governance companions are optional extra workflows you turn on when you want the pipeline to police itself on every pull request: catch drift before it lands, show a plan before merge, validate the manifest as a check, and guard a merge queue. Each is a single manifest toggle. This guide covers what each one does and when it earns its place. For the exact fields, see [Companion workflows](/cascade/reference/manifest/#companion-workflows-opt-in); for the emitted files, see [Opt-in companions](/cascade/reference/generated-workflows/#opt-in-companions).

## Drift check: keep generated output honest

The generated workflows are build output. If someone hand-edits `.github/workflows/orchestrate.yaml` or the manifest changes without a regenerate, the committed files no longer match what the manifest says they should be. The drift check catches exactly that: it runs [`cascade verify`](/cascade/reference/cli/#verify) on every pull request and fails the check when committed workflows fall out of sync with the manifest.

```yaml
# .github/manifest.yaml
ci:
config:
drift_check:
enabled: true
comment: true
```

Enable it when the generated workflows are committed to the repository, which is the normal case, and you want a hard gate against silent hand-edits and forgotten regenerates. The check job is read-only (`contents: read`). Setting `comment: true` also emits a fork-safe companion that posts the result as a sticky pull request comment, so a contributor sees the drift inline rather than digging into a failed check. When you turn the comment on, consider [`pin_mode: sha`](/cascade/reference/manifest/#action-pinning) to remove floating-tag exposure on that one write-scoped job.

## PR preview: see the plan before merge

The preview companion renders what the pipeline would do for a pull request without deploying anything. It is read-only, so reviewers can read the plan on the pull request itself instead of reconstructing it from the manifest in their heads.

```yaml
ci:
config:
pr_preview:
enabled: true
comment: true
```

Enable it when your pull requests change pipeline behavior often enough that reviewers benefit from seeing the resolved plan in context. `comment: true` posts or updates a sticky preview comment so the latest plan is always the one shown. It changes no state and runs no deploys, so it is safe to leave on broadly.

## Validate check: fail fast on a broken manifest

The validate check runs manifest validation as its own pull-request check, so a malformed or invalid manifest fails early and clearly rather than surfacing later as a confusing generate or orchestrate error.

```yaml
ci:
config:
validate_check:
enabled: true
```

Enable it when more than one person edits the manifest, or when you want the manifest held to the schema on every change. It is the cheapest companion to run and the one that gives the clearest failure message when a manifest edit is wrong.

## Merge queue: validate the merge-group candidate

When the repository uses GitHub's merge queue, the merge-queue companion adds a `merge_group`-triggered lane that validates the queued candidate: it runs `cascade parse-config` and a dry-run `cascade orchestrate setup` against the merge-group commit before it is allowed to land.

```yaml
ci:
config:
merge_queue:
enabled: true
```

Enable it when you have turned on GitHub merge queues for the repository and want the same manifest and orchestration checks applied to the combined merge candidate, not just to each pull request in isolation. Wiring the merge-queue trigger itself is handled by the `merge_group` entry under [`extra_triggers`](/cascade/reference/manifest/#extra_triggers); this companion adds the validation that runs on it.

## Wayfinding

**Prerequisite:** [Getting started](/cascade/start/getting-started/) for a manifest that already generates a pipeline.

**Related:** [Simulate and verify](/cascade/guides/simulate-and-verify/) for running these same checks locally before you push, and [Pin and reconcile actions](/cascade/guides/action-pins/) for the reconcile companion that adopts external pin bumps.

**Reference:** [Companion workflows](/cascade/reference/manifest/#companion-workflows-opt-in) for every field and default.
Loading