From c0158f8fc11878ffb9dadfe0e447173eedc1620e Mon Sep 17 00:00:00 2001 From: Joshua Temple Date: Thu, 18 Jun 2026 12:57:34 -0400 Subject: [PATCH] docs: join hard-wrapped prose into single lines Signed-off-by: Joshua Temple --- CONTRIBUTING.md | 26 ++-- README.md | 4 +- SECURITY.md | 36 ++---- docs/src/content/docs/adoption.md | 10 +- docs/src/content/docs/architecture.md | 54 ++------- docs/src/content/docs/callback-contract.md | 16 +-- docs/src/content/docs/cli-reference.md | 9 +- docs/src/content/docs/comparison.md | 116 +++++------------- docs/src/content/docs/configuration.md | 42 ++----- docs/src/content/docs/getting-started.md | 15 +-- docs/src/content/docs/index.mdx | 12 +- docs/src/content/docs/security/hardening.md | 128 +++++--------------- docs/src/content/docs/workflows.md | 8 +- 13 files changed, 109 insertions(+), 367 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9e27370d..a7b72faa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,21 +1,16 @@ # Contributing to cascade -Thanks for your interest in contributing. This document covers what you need to -get set up and land a change. +Thanks for your interest in contributing. This document covers what you need to get set up and land a change. ## Developer Certificate of Origin -cascade uses the [Developer Certificate of Origin](https://developercertificate.org/). -Every commit must be signed off, which certifies you wrote the change or have the -right to submit it under the project's license: +cascade uses the [Developer Certificate of Origin](https://developercertificate.org/). Every commit must be signed off, which certifies you wrote the change or have the right to submit it under the project's license: ```bash git commit -s -m "your message" ``` -The sign-off adds a `Signed-off-by: Your Name ` line using your -`git config user.name` and `user.email`. Pull requests with unsigned commits will -be asked to amend. +The sign-off adds a `Signed-off-by: Your Name ` line using your `git config user.name` and `user.email`. Pull requests with unsigned commits will be asked to amend. ## Development setup @@ -40,21 +35,14 @@ golangci-lint run ./... 1. Open an issue first for anything non-trivial so we can agree on the approach. 2. Branch from `main`. 3. Keep changes focused. One logical change per pull request. -4. Add or update tests. New manifest fields and generator features need an `e2e/` - scenario, not just a unit test on generated output. +4. Add or update tests. New manifest fields and generator features need an `e2e/` scenario, not just a unit test on generated output. 5. Run `go test ./...` and `golangci-lint run ./...` before pushing. -6. Use [Conventional Commits](https://www.conventionalcommits.org/) for commit - messages (`feat:`, `fix:`, `docs:`, `chore:`, ...). cascade derives changelogs - and version bumps from them. +6. Use [Conventional Commits](https://www.conventionalcommits.org/) for commit messages (`feat:`, `fix:`, `docs:`, `chore:`, ...). cascade derives changelogs and version bumps from them. ## API design -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. +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. ## Reporting bugs -Open an issue with the manifest config, the generated workflow (if relevant), and -what you expected versus what happened. A minimal reproduction helps a lot. +Open an issue with the manifest config, the generated workflow (if relevant), and what you expected versus what happened. A minimal reproduction helps a lot. diff --git a/README.md b/README.md index 9c818ffd..755d48d1 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,7 @@

Declarative trunk-based CI/CD for GitHub Actions.

- Define what to build and where to deploy in one manifest.
- cascade generates the GitHub Actions wiring, tracks deployment state, manages releases,
- and cascades promotions through your environments. + Define what to build and where to deploy in one manifest.
cascade generates the GitHub Actions wiring, tracks deployment state, manages releases,
and cascades promotions through your environments.

--- diff --git a/SECURITY.md b/SECURITY.md index 20af08f1..57b65eea 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -7,48 +7,30 @@ | Latest release line | Yes, security fixes shipped promptly | | Older releases | No | -The latest release line is the active one. Only the most recent release receives -security patches. Upgrade to the latest release to stay covered. +The latest release line is the active one. Only the most recent release receives security patches. Upgrade to the latest release to stay covered. -The schema-version compatibility policy (which CLI versions read which manifest -versions) is documented separately in -[versioning and schema compatibility](https://stablekernel.github.io/cascade/versioning/). +The schema-version compatibility policy (which CLI versions read which manifest versions) is documented separately in [versioning and schema compatibility](https://stablekernel.github.io/cascade/versioning/). ## Reporting a vulnerability Please do **not** open a public GitHub issue for security vulnerabilities. -Report them privately via -[GitHub Security Advisories](https://github.com/stablekernel/cascade/security/advisories/new) -using the "Report a vulnerability" button on the repository's Security tab. This -needs no email and keeps the report private until a fix is coordinated. +Report them privately via [GitHub Security Advisories](https://github.com/stablekernel/cascade/security/advisories/new) using the "Report a vulnerability" button on the repository's Security tab. This needs no email and keeps the report private until a fix is coordinated. -Include a description of the issue, steps to reproduce, and any relevant version -information. +Include a description of the issue, steps to reproduce, and any relevant version information. **Response expectations** - You will receive an acknowledgement within 3 business days. - We aim to triage and confirm the issue within 7 days. -- A fix or mitigation will be released as soon as practicable, typically within - 30 days for high-severity findings. +- A fix or mitigation will be released as soon as practicable, typically within 30 days for high-severity findings. -We follow -[coordinated disclosure](https://en.wikipedia.org/wiki/Coordinated_vulnerability_disclosure): -please allow us reasonable time to address the issue before making it public. +We follow [coordinated disclosure](https://en.wikipedia.org/wiki/Coordinated_vulnerability_disclosure): please allow us reasonable time to address the issue before making it public. ## Security model -Cascade is a build-time tool that generates GitHub Actions workflows you commit -and review in your own repository. The generated workflows run under your own -runners, branch protection, and environment gates, and cross-repo coordination -uses a same-organization, shared-token model where a dispatch token you provision -is the trust boundary. Deploying cascade safely is therefore a shared -responsibility between cascade and your organization's GitHub and cloud -configuration. - -See the -[security and hardening guide](https://stablekernel.github.io/cascade/security/hardening/) -for the full model and a step-by-step hardening checklist. +Cascade is a build-time tool that generates GitHub Actions workflows you commit and review in your own repository. The generated workflows run under your own runners, branch protection, and environment gates, and cross-repo coordination uses a same-organization, shared-token model where a dispatch token you provision is the trust boundary. Deploying cascade safely is therefore a shared responsibility between cascade and your organization's GitHub and cloud configuration. + +See the [security and hardening guide](https://stablekernel.github.io/cascade/security/hardening/) for the full model and a step-by-step hardening checklist. diff --git a/docs/src/content/docs/adoption.md b/docs/src/content/docs/adoption.md index 3e8282c1..72cf04a7 100644 --- a/docs/src/content/docs/adoption.md +++ b/docs/src/content/docs/adoption.md @@ -22,20 +22,14 @@ The flow in one line: you write a manifest plus callback workflows, run `cascade ## Build a pipeline from scratch -**Fast path:** `cascade init` does the first three steps below for you. It -scaffolds the manifest and the callback stubs, verifies them through the real -generator, and writes them into your repository: +**Fast path:** `cascade init` does the first three steps below for you. It scaffolds the manifest and the callback stubs, verifies them through the real generator, and writes them into your repository: ```bash cascade init --topology two-env # dev, prod cascade init --envs staging,production # your own ordered names ``` -Pick a preset with `--topology` (`no-env`, `two-env`, `three-env`, `four-env`) -or supply your own ordered list with `--envs`. Then jump to step 4 to generate -and commit. The walkthrough below explains each piece `init` produces, so you -understand what you are filling in. See the [CLI Reference](/cascade/cli-reference/#init) -for every flag. +Pick a preset with `--topology` (`no-env`, `two-env`, `three-env`, `four-env`) or supply your own ordered list with `--envs`. Then jump to step 4 to generate and commit. The walkthrough below explains each piece `init` produces, so you understand what you are filling in. See the [CLI Reference](/cascade/cli-reference/#init) for every flag. ### 1. Choose your environments diff --git a/docs/src/content/docs/architecture.md b/docs/src/content/docs/architecture.md index f37beeae..ac23ea9b 100644 --- a/docs/src/content/docs/architecture.md +++ b/docs/src/content/docs/architecture.md @@ -564,43 +564,25 @@ For satellite repos with notify config: ### What cascade does today -The generator emits an `environment: ` key on each deploy job whenever the -manifest includes an `environments` list. That single key is enough for GitHub -Actions to attach deployment records, honour required-reviewer gates, apply -wait timers, and scope environment secrets. You configure all of that inside -GitHub, not in the manifest. No cascade code calls the Deployments REST API or -the Environments REST API directly. +The generator emits an `environment: ` key on each deploy job whenever the manifest includes an `environments` list. That single key is enough for GitHub Actions to attach deployment records, honour required-reviewer gates, apply wait timers, and scope environment secrets. You configure all of that inside GitHub, not in the manifest. No cascade code calls the Deployments REST API or the Environments REST API directly. ### What is deferred Two capabilities are intentionally out of scope for v1: -- Programmatic Deployments API status. cascade does not call - `POST /repos/{owner}/{repo}/deployments` or - `POST /repos/{owner}/{repo}/deployments/{id}/statuses`. GitHub Actions creates - these records automatically when a job carries `environment:`, so adopters get - deployment records without cascade owning that call. +- Programmatic Deployments API status. cascade does not call `POST /repos/{owner}/{repo}/deployments` or `POST /repos/{owner}/{repo}/deployments/{id}/statuses`. GitHub Actions creates these records automatically when a job carries `environment:`, so adopters get deployment records without cascade owning that call. -- Environments REST configuration sync. cascade does not read or write - environment protection rules (required reviewers, wait timers, branch policies) - via the REST API. That configuration lives in GitHub today. +- Environments REST configuration sync. cascade does not read or write environment protection rules (required reviewers, wait timers, branch policies) via the REST API. That configuration lives in GitHub today. ### Why deferred -Keeping cascade out of these APIs in v1 bounds the surface area and avoids -coupling the tool to GitHub API semantics that are still evolving. The -auto-created deployment records from `environment:` already cover the common -case. Adding programmatic control before an adopter needs it would buy -complexity and nothing else. If those APIs change shape, cascade would have to -track the change even though nothing in v1 depends on them. +Keeping cascade out of these APIs in v1 bounds the surface area and avoids coupling the tool to GitHub API semantics that are still evolving. The auto-created deployment records from `environment:` already cover the common case. Adding programmatic control before an adopter needs it would buy complexity and nothing else. If those APIs change shape, cascade would have to track the change even though nothing in v1 depends on them. ### How the design reserves the extension points -The schema already carries the hooks needed to add both capabilities later -without a breaking change: +The schema already carries the hooks needed to add both capabilities later without a breaking change: -**`environment_config` reserved shape.** The manifest schema reserves an -`environment_config` block at the `config:` level, keyed by environment name: +**`environment_config` reserved shape.** The manifest schema reserves an `environment_config` block at the `config:` level, keyed by environment name: ```yaml config: @@ -614,31 +596,15 @@ config: # branch_policy: protected ``` -The `environments` list stays a plain ordered `[]string`; the separate -`environment_config` map carries per-env settings. Adding fields under -`environment_config.` is additive and never touches the ordering semantics -of `environments`. A manifest that omits `environment_config` entirely is valid -and equivalent to today's behaviour. +The `environments` list stays a plain ordered `[]string`; the separate `environment_config` map carries per-env settings. Adding fields under `environment_config.` is additive and never touches the ordering semantics of `environments`. A manifest that omits `environment_config` entirely is valid and equivalent to today's behaviour. -**Single finalize seam.** The `orchestrate.Finalize` and `promote.Finalize` -functions are the only places that write state after a deployment completes. -A future Deployments API call attaches at one of those two points, not scattered -across the generator. That code constraint is already in place. +**Single finalize seam.** The `orchestrate.Finalize` and `promote.Finalize` functions are the only places that write state after a deployment completes. A future Deployments API call attaches at one of those two points, not scattered across the generator. That code constraint is already in place. -**Generator delegates environment semantics to GitHub.** Because the generator -emits `environment:` and nothing more, it does not embed logic about what that -environment means. Programmatic status reporting slots in at finalize time; -Environments REST configuration sync is a separate operational concern that never -needs to touch the generator. +**Generator delegates environment semantics to GitHub.** Because the generator emits `environment:` and nothing more, it does not embed logic about what that environment means. Programmatic status reporting slots in at finalize time; Environments REST configuration sync is a separate operational concern that never needs to touch the generator. ### Forward-compatibility guarantee -Both capabilities, when they arrive, will follow the same additive-only policy -described in [Versioning & Schema](/cascade/versioning/): new optional fields under -`environment_config.`, new optional top-level blocks if needed, and no -removal or re-typing of existing fields. Neither will require a `schema_version` -bump. Manifests that do not opt in to the new fields continue to work exactly as -they do today. +Both capabilities, when they arrive, will follow the same additive-only policy described in [Versioning & Schema](/cascade/versioning/): new optional fields under `environment_config.`, new optional top-level blocks if needed, and no removal or re-typing of existing fields. Neither will require a `schema_version` bump. Manifests that do not opt in to the new fields continue to work exactly as they do today. ## Testing Strategy diff --git a/docs/src/content/docs/callback-contract.md b/docs/src/content/docs/callback-contract.md index df362a8c..907891a9 100644 --- a/docs/src/content/docs/callback-contract.md +++ b/docs/src/content/docs/callback-contract.md @@ -479,14 +479,9 @@ ci: ## Environment Protection -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. +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. -cascade passes the target environment name to your 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 @@ -498,12 +493,7 @@ jobs: - run: ./deploy.sh ``` -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, reminding you to -declare `environment:` inside the reusable workflow. +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, reminding you to declare `environment:` inside the reusable workflow. Configure protection in GitHub: **Settings -> Environments -> Add required reviewers**. diff --git a/docs/src/content/docs/cli-reference.md b/docs/src/content/docs/cli-reference.md index 7b552036..40296fdb 100644 --- a/docs/src/content/docs/cli-reference.md +++ b/docs/src/content/docs/cli-reference.md @@ -364,14 +364,7 @@ cascade promote preflight \ | `--rollback-on-failure` | bool | true | Revert successful deploys if any fails | | `--allow-downgrade` | bool | false | Permit promoting an older version onto an env (a downgrade). Blocked by default; prod always requires this flag | -A promotion that would place a strictly older semver version onto an env than the -version it currently holds is a downgrade. Preflight blocks it by default, naming -both versions and the env. Pass `--allow-downgrade` to permit it. The terminal -(prod) env always requires the flag, even when a lower env in the same cascade -already permitted the same downgrade. Equal versions are an idempotent -re-promote and are never treated as a downgrade. When either version is not -parseable as semver the gate fails open with a warning rather than blocking, so -non-semver pipelines keep working. +A promotion that would place a strictly older semver version onto an env than the version it currently holds is a downgrade. Preflight blocks it by default, naming both versions and the env. Pass `--allow-downgrade` to permit it. The terminal (prod) env always requires the flag, even when a lower env in the same cascade already permitted the same downgrade. Equal versions are an idempotent re-promote and are never treated as a downgrade. When either version is not parseable as semver the gate fails open with a warning rather than blocking, so non-semver pipelines keep working. ##### Output diff --git a/docs/src/content/docs/comparison.md b/docs/src/content/docs/comparison.md index 772e908f..a91e9373 100644 --- a/docs/src/content/docs/comparison.md +++ b/docs/src/content/docs/comparison.md @@ -3,76 +3,41 @@ title: Why Cascade description: What cascade is, who it is for, and how it relates to release tooling, promotion control planes, and CI-as-code generators. --- -This page helps you decide whether cascade fits your repository, and explains how it -relates to the adjacent tools you may already use. The short version: cascade is a -compiler, not a control plane. It reads a manifest and writes plain GitHub Actions -workflows that you own. There is no platform to run, no cluster, and no agent. +This page helps you decide whether cascade fits your repository, and explains how it relates to the adjacent tools you may already use. The short version: cascade is a compiler, not a control plane. It reads a manifest and writes plain GitHub Actions workflows that you own. There is no platform to run, no cluster, and no agent. ## What cascade is -cascade is a Go CLI that compiles a single declarative manifest into native GitHub -Actions workflows for multi-environment release and promotion. It also derives versions -and changelogs from your Conventional Commits. +cascade is a Go CLI that compiles a single declarative manifest into native GitHub Actions workflows for multi-environment release and promotion. It also derives versions and changelogs from your Conventional Commits. -You run `cascade generate-workflow` once. From then on the generated workflows own their -own execution. The output is ordinary YAML that lives in your repository under -`.github/workflows/`. If you stop using cascade tomorrow, the workflows it wrote keep -running exactly as they did, because they are yours. Nothing about them depends on a -hosted runtime. +You run `cascade generate-workflow` once. From then on the generated workflows own their own execution. The output is ordinary YAML that lives in your repository under `.github/workflows/`. If you stop using cascade tomorrow, the workflows it wrote keep running exactly as they did, because they are yours. Nothing about them depends on a hosted runtime. -That is the whole identity: cascade is build-time tooling that produces artifacts you -keep, not a system you adopt and depend on at runtime. +That is the whole identity: cascade is build-time tooling that produces artifacts you keep, not a system you adopt and depend on at runtime. ## Who it is for, and when to use it -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: +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**, from which cascade derives versions, - changelogs, and the breaking-change gate. +- 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**, from which cascade derives versions, changelogs, and the breaking-change gate. ## When not to use it -cascade is likely overkill for a single environment with a plain build-and-release on -push, or for a repository with no deployments at all. (The no-environment mode still -gives you Conventional-Commit versioning and releases if you want just that.) - -A few deliberate non-goals are worth stating plainly, because they shape what cascade -will and will not do for you: - -- **Trunk-based only.** cascade promotes *from trunk*: you merge to one trunk branch and - cascade promotes that line through your environments. If you run release branches or a - GitFlow model today, adopting cascade means moving promotion onto a trunk-based flow. - That is a deliberate shift. cascade is a practical vehicle for it, but it does not - model long-lived release branches. -- **You own the deploy logic.** Build, deploy, validate, and publish are *your* logic, - supplied as reusable (`workflow_call`) workflows that cascade calls with a fixed input - contract. cascade never runs your scripts inline and never reaches into your callback - logic. -- **It never rebuilds artifacts per stage.** cascade promotes the artifact that was - built once, pinning each promotion to a specific SHA. It does not rebuild between - environments. -- **It is a metadata courier.** cascade passes artifact identifiers and versions between - stages. It never touches your container registry, package registry, or deployment - target directly. You construct those operations yourself in your callbacks. - -If you need a tool that runs your deployments for you, manages a cluster, or owns the -runtime path to production, cascade is the wrong layer. See the next section for tools -built for that job. +cascade is likely overkill for a single environment with a plain build-and-release on push, or for a repository with no deployments at all. (The no-environment mode still gives you Conventional-Commit versioning and releases if you want just that.) + +A few deliberate non-goals are worth stating plainly, because they shape what cascade will and will not do for you: + +- **Trunk-based only.** cascade promotes *from trunk*: you merge to one trunk branch and cascade promotes that line through your environments. If you run release branches or a GitFlow model today, adopting cascade means moving promotion onto a trunk-based flow. That is a deliberate shift. cascade is a practical vehicle for it, but it does not model long-lived release branches. +- **You own the deploy logic.** Build, deploy, validate, and publish are *your* logic, supplied as reusable (`workflow_call`) workflows that cascade calls with a fixed input contract. cascade never runs your scripts inline and never reaches into your callback logic. +- **It never rebuilds artifacts per stage.** cascade promotes the artifact that was built once, pinning each promotion to a specific SHA. It does not rebuild between environments. +- **It is a metadata courier.** cascade passes artifact identifiers and versions between stages. It never touches your container registry, package registry, or deployment target directly. You construct those operations yourself in your callbacks. + +If you need a tool that runs your deployments for you, manages a cluster, or owns the runtime path to production, cascade is the wrong layer. See the next section for tools built for that job. ## How cascade relates to adjacent tools -The space around cascade is crowded, but most tools sit on a single axis. cascade sits -at the intersection of three, and on each axis it has a different goal from the -specialists there. None of the comparisons below are about better or worse; they are -about different jobs. In several cases the right answer is to use cascade *alongside* -one of these tools. +The space around cascade is crowded, but most tools sit on a single axis. cascade sits at the intersection of three, and on each axis it has a different goal from the specialists there. None of the comparisons below are about better or worse; they are about different jobs. In several cases the right answer is to use cascade *alongside* one of these tools. ### Release, versioning, and changelogs from Conventional Commits @@ -85,16 +50,11 @@ These tools turn your commit history into versions, changelogs, and releases. | [Changesets](https://github.com/changesets/changesets) | Author-written change files, strong for multi-package JS monorepos. | cascade reads Conventional Commits rather than change files, and centers environments rather than package graphs. | | [GoReleaser](https://goreleaser.com/) | Builds and publishes Go (and other) release artifacts and packages. | cascade does not build or publish artifacts itself; it can call GoReleaser as a build or publish callback. | -These tools and cascade are **complementary, not mutually exclusive.** You can point -cascade's changelog or release step at your own workflow, or switch that step off, and -let a tool like release-please or GoReleaser keep doing what it already does inside a -reusable-workflow callback while cascade owns the promotion across environments. See the -[Adoption Guide](/cascade/adoption/) for wiring this up. +These tools and cascade are **complementary, not mutually exclusive.** You can point cascade's changelog or release step at your own workflow, or switch that step off, and let a tool like release-please or GoReleaser keep doing what it already does inside a reusable-workflow callback while cascade owns the promotion across environments. See the [Adoption Guide](/cascade/adoption/) for wiring this up. ### Multi-environment promotion and progressive rollout -These tools move releases through environments at runtime, and several add progressive -rollout strategies. +These tools move releases through environments at runtime, and several add progressive rollout strategies. | Tool | What it does well | How cascade differs | |---|---|---| @@ -104,13 +64,7 @@ rollout strategies. | [Octopus Deploy](https://octopus.com/) | Release management and deployment automation across many targets. | cascade does not run deployments or hold a server-side release database; state lives in your manifest. | | [Harness](https://www.harness.io/) | A broad platform spanning CI, CD, and feature management. | cascade is a focused CLI, not a platform; it generates workflows and then steps out of the way. | -The important distinction across this whole row: these are **runtime control planes you -adopt.** You run them (or pay for them), they hold pipeline state, and they often assume -Kubernetes. cascade takes a different shape: it generates native GitHub Actions you keep, -holds state in your manifest in your repository, and has nothing running between -promotions. If you already operate one of these platforms and it serves you, cascade is -not trying to replace it. cascade is for teams who would rather stay inside GitHub -Actions than take on a separate runtime. +The important distinction across this whole row: these are **runtime control planes you adopt.** You run them (or pay for them), they hold pipeline state, and they often assume Kubernetes. cascade takes a different shape: it generates native GitHub Actions you keep, holds state in your manifest in your repository, and has nothing running between promotions. If you already operate one of these platforms and it serves you, cascade is not trying to replace it. cascade is for teams who would rather stay inside GitHub Actions than take on a separate runtime. ### CI-as-code generators @@ -122,30 +76,18 @@ These tools generate or run CI configuration so you do not hand-write it. | [Dagger](https://dagger.io/) | Portable pipelines as code, executed by a custom engine. | cascade emits plain Actions YAML that runs on stock GitHub runners, with no engine to run. | | [Earthly](https://earthly.dev/) | Repeatable, containerized build definitions. | cascade does not define builds; it orchestrates and promotes the builds your callbacks define. | -cascade overlaps with these on "do not hand-write your CI," but its goal is narrower and -more opinionated: it models multi-environment promotion specifically, and its output is -GitHub-native rather than a custom runtime or general scaffolding. +cascade overlaps with these on "do not hand-write your CI," but its goal is narrower and more opinionated: it models multi-environment promotion specifically, and its output is GitHub-native rather than a custom runtime or general scaffolding. ## What cascade generates -cascade emits ordinary GitHub Actions YAML and standard GitHub objects. As of today, a -generated pipeline includes: +cascade emits ordinary GitHub Actions YAML and standard GitHub objects. As of today, a generated pipeline includes: -- **Orchestrate, promote, release, and rollback workflows** that move a single artifact - through your environments, pinned to a specific SHA and never rebuilt per stage. -- **GitHub Releases**, including release-asset upload and the release lifecycle (draft, - prerelease, published) with release-candidate tag cleanup. +- **Orchestrate, promote, release, and rollback workflows** that move a single artifact through your environments, pinned to a specific SHA and never rebuilt per stage. +- **GitHub Releases**, including release-asset upload and the release lifecycle (draft, prerelease, published) with release-candidate tag cleanup. - **Merge queue** configuration on the trunk integration path. - **Concurrency** blocks so overlapping runs do not collide. -- **A GitHub Environment gate**, threaded to your deploy callback as the `environment` - input. (Because every deploy is a reusable-workflow caller job, the actual - `environment:` declaration lives inside the workflow you point cascade at; see the - [Callback Contract](/cascade/callback-contract/).) +- **A GitHub Environment gate**, threaded to your deploy callback as the `environment` input. (Because every deploy is a reusable-workflow caller job, the actual `environment:` declaration lives inside the workflow you point cascade at; see the [Callback Contract](/cascade/callback-contract/).) - **Run summaries** via `$GITHUB_STEP_SUMMARY` for plan and preview output. - **Top-level `GITHUB_TOKEN` permission scoping** on the generated workflows. -What cascade does not generate is just as important. cascade does not build or publish -your artifacts, does not run your deployments, and does not own any runtime path to -production. Those are your callbacks. For the exact inputs and outputs cascade exchanges -with your workflows, see the [Callback Contract](/cascade/callback-contract/); for the -full design and ownership boundary, see [Architecture](/cascade/architecture/). +What cascade does not generate is just as important. cascade does not build or publish your artifacts, does not run your deployments, and does not own any runtime path to production. Those are your callbacks. For the exact inputs and outputs cascade exchanges with your workflows, see the [Callback Contract](/cascade/callback-contract/); for the full design and ownership boundary, see [Architecture](/cascade/architecture/). diff --git a/docs/src/content/docs/configuration.md b/docs/src/content/docs/configuration.md index 041a12e8..08877ca9 100644 --- a/docs/src/content/docs/configuration.md +++ b/docs/src/content/docs/configuration.md @@ -91,21 +91,9 @@ ci: | `action_folder` | string | No | `manage-release` | Folder name for the manage-release action | :::note[Environment names are yours; roles are positional] -The `environments` list is fully configurable. cascade attaches no meaning to specific -labels: `dev`, `test`, `uat`, `staging`, and `prod` are illustrative examples used -throughout these docs, not reserved names. Roles are decided by position in the list, not -by name. The last environment is the release stage (prod), the second-to-last is the -prerelease environment, and the publish boundary is the final crossing into the last -environment. The count is structural too: zero environments is release-only, one -environment generates a single-environment Release workflow, and two or more enable the -full promote cascade. - -**Naming.** Environment, build, and deploy names become GitHub Actions job IDs and -output-variable keys, so keep them identifier-safe: use letters, digits, and underscores -(hyphens are read as subtraction in GitHub Actions expressions). The reserved -generator-owned names `environment` and `dry_run` cannot be used as `dispatch_inputs`. Any -`gha_environment` value maps to a real GitHub Environment, so GitHub's own naming rules -apply there. +The `environments` list is fully configurable. cascade attaches no meaning to specific labels: `dev`, `test`, `uat`, `staging`, and `prod` are illustrative examples used throughout these docs, not reserved names. Roles are decided by position in the list, not by name. The last environment is the release stage (prod), the second-to-last is the prerelease environment, and the publish boundary is the final crossing into the last environment. The count is structural too: zero environments is release-only, one environment generates a single-environment Release workflow, and two or more enable the full promote cascade. + +**Naming.** Environment, build, and deploy names become GitHub Actions job IDs and output-variable keys, so keep them identifier-safe: use letters, digits, and underscores (hyphens are read as subtraction in GitHub Actions expressions). The reserved generator-owned names `environment` and `dry_run` cannot be used as `dispatch_inputs`. Any `gha_environment` value maps to a real GitHub Environment, so GitHub's own naming rules apply there. ::: ### cli_version @@ -346,11 +334,7 @@ When external deploys are configured, the generated promote workflow includes de #### Deploy on update (opt-in) -By default the receiver is record-only: when a satellite reports a new version, -the primary records the new external state and stops. Setting -`on_update.deploy.workflow` on an external deploy opts that component in to a -scoped deploy that runs synchronously in the same receiver run, right after the -slot is recorded. +By default the receiver is record-only: when a satellite reports a new version, the primary records the new external state and stops. Setting `on_update.deploy.workflow` on an external deploy opts that component in to a scoped deploy that runs synchronously in the same receiver run, right after the slot is recorded. ```yaml ci: @@ -368,20 +352,10 @@ ci: Behavior: -- **Opt-in and additive.** Omit `on_update` and the receiver stays record-only, - byte-for-byte identical to before. No deploy job is generated. -- **Scoped to the updated component.** The generated receiver emits one - `deploy_` job per opted-in component, each gated on - `inputs.deploy_name` so a single receiver run deploys only the component that - was just recorded. Other components are untouched. -- **Synchronous and gated on the record.** The deploy job runs in the same - receiver run and only after the record step succeeds. A failed record never - triggers a deploy. -- **Reusable-workflow only.** Like `deploys[].workflow`, `on_update.deploy` - accepts a workflow path (local `.github/workflows/x.yaml` or - `org/repo/.github/...@ref`); inline `run:` and `shell:` are not supported. The - scoped deploy receives the recorded `environment`, `sha`, `version`, and - `deploy_name` as inputs and inherits secrets. +- **Opt-in and additive.** Omit `on_update` and the receiver stays record-only, byte-for-byte identical to before. No deploy job is generated. +- **Scoped to the updated component.** The generated receiver emits one `deploy_` job per opted-in component, each gated on `inputs.deploy_name` so a single receiver run deploys only the component that was just recorded. Other components are untouched. +- **Synchronous and gated on the record.** The deploy job runs in the same receiver run and only after the record step succeeds. A failed record never triggers a deploy. +- **Reusable-workflow only.** Like `deploys[].workflow`, `on_update.deploy` accepts a workflow path (local `.github/workflows/x.yaml` or `org/repo/.github/...@ref`); inline `run:` and `shell:` are not supported. The scoped deploy receives the recorded `environment`, `sha`, `version`, and `deploy_name` as inputs and inherits secrets. ### notify Section (Satellite Repos) diff --git a/docs/src/content/docs/getting-started.md b/docs/src/content/docs/getting-started.md index 4690f4d9..b9bed696 100644 --- a/docs/src/content/docs/getting-started.md +++ b/docs/src/content/docs/getting-started.md @@ -42,9 +42,7 @@ The setup action downloads the release archive (`tar.gz`) from GoReleaser and in ## Fast path: scaffold with `cascade init` -If you want a working configuration in one step, run `cascade init`. It renders -the manifest and the callback workflow stubs for you, verifies them through the -real generator, and writes them into your repository: +If you want a working configuration in one step, run `cascade init`. It renders the manifest and the callback workflow stubs for you, verifies them through the real generator, and writes them into your repository: ```bash # Two-environment pipeline (dev, prod) in the current directory @@ -57,16 +55,9 @@ cascade init --envs staging,production --name my-service cascade init --topology two-env --dry-run ``` -This produces `.github/manifest.yaml` plus build and deploy stubs under -`.github/workflows`. The manifest already carries a `$schema` directive, so your -editor gives you autocomplete and validation while you fill in the stubs. If a -target file already exists, `init` aborts and lists the conflicts unless you -pass `--force`. +This produces `.github/manifest.yaml` plus build and deploy stubs under `.github/workflows`. The manifest already carries a `$schema` directive, so your editor gives you autocomplete and validation while you fill in the stubs. If a target file already exists, `init` aborts and lists the conflicts unless you pass `--force`. -Once scaffolded, skip ahead to [Step 3](#step-3-create-callback-workflows) to -fill in the callbacks, then generate the orchestration workflows. The manual -walkthrough below covers the same files step by step if you would rather build -them yourself. +Once scaffolded, skip ahead to [Step 3](#step-3-create-callback-workflows) to fill in the callbacks, then generate the orchestration workflows. The manual walkthrough below covers the same files step by step if you would rather build them yourself. ## Step 2: Create the manifest diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx index 5c8e29e7..5d681a20 100644 --- a/docs/src/content/docs/index.mdx +++ b/docs/src/content/docs/index.mdx @@ -20,14 +20,9 @@ hero: import { Card, CardGrid } from '@astrojs/starlight/components'; -Define what to build and where to deploy in one manifest. Cascade generates the -GitHub Actions wiring, tracks deployment state, manages releases, and cascades -promotions through your environments. +Define what to build and where to deploy in one manifest. Cascade generates the GitHub Actions wiring, tracks deployment state, manages releases, and cascades promotions through your environments. -The manifest (`.github/manifest.yaml`) is the single source of truth. It holds the -pipeline configuration and the live deployment state for every environment. You run -`cascade generate-workflow` once. After that, the generated workflows own their -execution. +The manifest (`.github/manifest.yaml`) is the single source of truth. It holds the pipeline configuration and the live deployment state for every environment. You run `cascade generate-workflow` once. After that, the generated workflows own their execution. @@ -65,5 +60,4 @@ execution. ## Project -Cascade is open source under the Apache 2.0 license. The source, issue tracker, and -releases live at [github.com/stablekernel/cascade](https://github.com/stablekernel/cascade). +Cascade is open source under the Apache 2.0 license. The source, issue tracker, and releases live at [github.com/stablekernel/cascade](https://github.com/stablekernel/cascade). diff --git a/docs/src/content/docs/security/hardening.md b/docs/src/content/docs/security/hardening.md index eef0a117..8d97d1d1 100644 --- a/docs/src/content/docs/security/hardening.md +++ b/docs/src/content/docs/security/hardening.md @@ -3,34 +3,17 @@ title: Security and hardening description: Cascade's security model and a shared-responsibility guide to deploying it safely - what cascade provides secure by construction versus what your organization configures in GitHub and your cloud - plus a concrete hardening checklist. --- -Cascade generates GitHub Actions workflow definitions and coordinates promotion -across environments. Those workflows are committed to, and run inside, your own -repositories, under your own runners, branch protection, and environment gates. -Security is shared: cascade emits sound, reviewable workflow definitions, and -your organization configures the GitHub and cloud controls that decide what -those workflows are allowed to do. +Cascade generates GitHub Actions workflow definitions and coordinates promotion across environments. Those workflows are committed to, and run inside, your own repositories, under your own runners, branch protection, and environment gates. Security is shared: cascade emits sound, reviewable workflow definitions, and your organization configures the GitHub and cloud controls that decide what those workflows are allowed to do. -This page describes the security model, then lays out both halves of the -shared-responsibility split and gives you a checklist to harden a pipeline. +This page describes the security model, then lays out both halves of the shared-responsibility split and gives you a checklist to harden a pipeline. ## Security model -Cascade is a build-time tool. It reads your manifest and writes workflow YAML -that you commit and review in your own repository. There is no cascade-operated -service in the request path at run time, and no cascade-held credential: every -workflow runs under your repository's runners and your organization's policies. +Cascade is a build-time tool. It reads your manifest and writes workflow YAML that you commit and review in your own repository. There is no cascade-operated service in the request path at run time, and no cascade-held credential: every workflow runs under your repository's runners and your organization's policies. -Cross-repo coordination uses a same-organization, shared-token model. When one -repository hands off to another (for example, a satellite repository signaling -its primary), the handoff is driven by a dispatch token that you provision and -hold. That token is the trust boundary for cross-repo coordination: any party -that holds it can trigger the coordinated workflow. Securing the token, scoping -it tightly, and pairing it with the GitHub controls below is your -responsibility. Treat the token as a production credential. +Cross-repo coordination uses a same-organization, shared-token model. When one repository hands off to another (for example, a satellite repository signaling its primary), the handoff is driven by a dispatch token that you provision and hold. That token is the trust boundary for cross-repo coordination: any party that holds it can trigger the coordinated workflow. Securing the token, scoping it tightly, and pairing it with the GitHub controls below is your responsibility. Treat the token as a production credential. -Because the generated workflows live in your repository, they are deterministic -and reviewable: you can read every job before you adopt it, pin it to a commit, -and gate it with your own branch protection and environment rules. +Because the generated workflows live in your repository, they are deterministic and reviewable: you can read every job before you adopt it, pin it to a commit, and gate it with your own branch protection and environment rules. ## The shared-responsibility model @@ -38,33 +21,17 @@ and gate it with your own branch protection and environment rules. 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. -- **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 - environment gate must live in the called workflow, so the requirement is - explicit rather than silent. -- **Generated workflows and commits are deterministic and reviewable.** Output - is plain YAML committed to your repository. You can diff it, review it, and pin - it before it runs. -- **The artifact identifier is tracked end to end.** Cascade records an artifact - digest in pipeline state alongside the human-readable version tag and can - resolve it back later. +- **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. +- **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 environment gate must live in the called workflow, so the requirement is explicit rather than silent. +- **Generated workflows and commits are deterministic and reviewable.** Output is plain YAML committed to your repository. You can diff it, review it, and pin it before it runs. +- **The artifact identifier is tracked end to end.** Cascade records an artifact digest in pipeline state alongside the human-readable version tag and can resolve it back later. ### What is planned (roadmap, not available today) Treat these as future work rather than current guarantees: -- Built-in authenticated cross-repo coordination (identity or signature - verification on the receiver) beyond the shared dispatch token. Until then, - the token plus your GitHub controls are the boundary. +- Built-in authenticated cross-repo coordination (identity or signature verification on the receiver) beyond the shared dispatch token. Until then, the token plus your GitHub controls are the boundary. - Scoped, non-blanket secret passing by default for generated callers. - Deploying by immutable digest by default, rather than by version tag. - SHA-pinned third-party actions by default and provenance attestation. @@ -73,59 +40,26 @@ Treat these as future work rather than current guarantees: GitHub and your cloud own these controls; cascade cannot set them for you: -- **Branch protection** on your trunk: require reviews and status checks, - restrict who can push, and require signed commits where appropriate. -- **Tag protection or rulesets** so release and version tags cannot be moved or - forged. -- **Environment protection rules** with required reviewers, wait timers, and - deployment branch or tag policies on production environments. For reusable - deploys, place this gate in the called workflow. -- **CODEOWNERS on workflow files** (`.github/workflows/**`) so changes to the - pipeline itself require owner review. -- **Restricted Actions settings:** an allow-list of permitted actions and - reusable workflows, fork-PR run approval, and a default `GITHUB_TOKEN` that is - read-only. -- **An OIDC trust policy** in your cloud scoped to specific repository, - environment, and ref, issuing short-lived role sessions instead of long-lived - static credentials. -- **Environment-scoped secrets** so production credentials are available only to - the gated production job, not to every job in the repository. -- **Scoped, short-lived tokens** for cross-repo coordination: prefer a GitHub - App over a broad personal access token, and never replicate one long-lived - token across many repositories. -- **Artifact integrity controls** in your registry: immutable tags and registry - RBAC so a published artifact cannot be swapped after it is produced. +- **Branch protection** on your trunk: require reviews and status checks, restrict who can push, and require signed commits where appropriate. +- **Tag protection or rulesets** so release and version tags cannot be moved or forged. +- **Environment protection rules** with required reviewers, wait timers, and deployment branch or tag policies on production environments. For reusable deploys, place this gate in the called workflow. +- **CODEOWNERS on workflow files** (`.github/workflows/**`) so changes to the pipeline itself require owner review. +- **Restricted Actions settings:** an allow-list of permitted actions and reusable workflows, fork-PR run approval, and a default `GITHUB_TOKEN` that is read-only. +- **An OIDC trust policy** in your cloud scoped to specific repository, environment, and ref, issuing short-lived role sessions instead of long-lived static credentials. +- **Environment-scoped secrets** so production credentials are available only to the gated production job, not to every job in the repository. +- **Scoped, short-lived tokens** for cross-repo coordination: prefer a GitHub App over a broad personal access token, and never replicate one long-lived token across many repositories. +- **Artifact integrity controls** in your registry: immutable tags and registry RBAC so a published artifact cannot be swapped after it is produced. ## Hardening checklist -Work through this when standing up or reviewing a cascade pipeline. The order -moves from the cross-repo trust boundary outward to the surrounding controls. - -1. **Secure the cross-repo dispatch token.** Store it as an environment-scoped - secret, scope it to only the permissions the coordination needs, prefer a - GitHub App or a short-lived token over a broad personal access token, and - rotate it. Do not replicate one long-lived token across many repositories. -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.** 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. -5. **Restrict Actions settings.** Allow-list the specific actions and reusable - workflows your pipeline needs, require approval for fork-PR runs, and set the - default workflow token to read-only. -6. **Use OIDC with a tightly scoped trust policy.** Scope cloud trust to the - specific repository, environment, and ref, and issue short-lived sessions - instead of long-lived static credentials. -7. **Pin actions and reusable workflows.** Pin third-party actions and the - cascade action reference by commit SHA rather than tracking a moving tag. -8. **Protect artifact integrity in the registry.** Use immutable tags and - registry RBAC so a published artifact cannot be replaced, and deploy by the - recorded digest where your deploy supports it. -9. **Review the generated YAML before adopting it,** especially anything copied - from example repositories, and replace example defaults (mutable pins, - inherited secrets) with the hardened settings above. +Work through this when standing up or reviewing a cascade pipeline. The order moves from the cross-repo trust boundary outward to the surrounding controls. + +1. **Secure the cross-repo dispatch token.** Store it as an environment-scoped secret, scope it to only the permissions the coordination needs, prefer a GitHub App or a short-lived token over a broad personal access token, and rotate it. Do not replicate one long-lived token across many repositories. +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.** 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. +5. **Restrict Actions settings.** Allow-list the specific actions and reusable workflows your pipeline needs, require approval for fork-PR runs, and set the default workflow token to read-only. +6. **Use OIDC with a tightly scoped trust policy.** Scope cloud trust to the specific repository, environment, and ref, and issue short-lived sessions instead of long-lived static credentials. +7. **Pin actions and reusable workflows.** Pin third-party actions and the cascade action reference by commit SHA rather than tracking a moving tag. +8. **Protect artifact integrity in the registry.** Use immutable tags and registry RBAC so a published artifact cannot be replaced, and deploy by the recorded digest where your deploy supports it. +9. **Review the generated YAML before adopting it,** especially anything copied from example repositories, and replace example defaults (mutable pins, inherited secrets) with the hardened settings above. diff --git a/docs/src/content/docs/workflows.md b/docs/src/content/docs/workflows.md index 3ef84be0..646ea6cf 100644 --- a/docs/src/content/docs/workflows.md +++ b/docs/src/content/docs/workflows.md @@ -355,10 +355,7 @@ permissions: packages: write # Optional: only if your callbacks publish to GHCR ``` -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: +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: @@ -367,8 +364,7 @@ jobs: environment: ${{ inputs.environment }} # GitHub enforces approvals ``` -cascade prints a generate-time note when `gha_environment` is configured, -reminding you to declare `environment:` inside the reusable workflow. +cascade prints a generate-time note when `gha_environment` is configured, reminding you to declare `environment:` inside the reusable workflow. ## Concurrency Control