diff --git a/.github/actions/setup-cli/action.yaml b/.github/actions/setup-cli/action.yaml index 6a78ccf8..bc071a3e 100644 --- a/.github/actions/setup-cli/action.yaml +++ b/.github/actions/setup-cli/action.yaml @@ -96,8 +96,8 @@ runs: # Robustly capture a single-line version, sanitized for GHA's output # file format. Older form `echo "version=$RAW" >> $GITHUB_OUTPUT` # produced `##[error]Invalid format 'unknown'` against the v1.1.0 - # binary — likely a GHA file-command parser quirk where certain - # values get mis-interpreted as a format directive. Strip CR/newlines + # binary (likely a GHA file-command parser quirk where certain + # values get mis-interpreted as a format directive). Strip CR/newlines # and non-printable characters, fall back to "unknown" on empty/error, # cap length, and use printf instead of echo to avoid backslash # interpretation surprises. diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 1eb0c8af..41941488 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -1,9 +1,9 @@ # End-to-end test workflow # Triggers: -# push:tags — every release tag (existing) -# workflow_dispatch — manual run against any ref (existing) -# merge_group — runs as a merge-queue gate before merging to main -# schedule — nightly at 07:00 UTC (low-traffic window) against main +# push:tags every release tag (existing) +# workflow_dispatch manual run against any ref (existing) +# merge_group runs as a merge-queue gate before merging to main +# schedule nightly at 07:00 UTC (low-traffic window) against main # # E2E uses act + gitea testcontainers and is too slow + flaky to run per PR. # Run locally (`go test -v ./e2e/...`) before pushing instead. @@ -73,7 +73,7 @@ jobs: # enough headroom. Override per-dispatch as needed. E2E_TIMEOUT: ${{ github.event.inputs.timeout || '60m' }} # Cap subtest parallelism. The GitHub runner has 4 cores / ~7.9GB - # RAM. Each scenario spins up gitea + act + N job containers — at + # RAM. Each scenario spins up gitea + act + N job containers; at # the default GOMAXPROCS=4, four scenarios concurrently exhaust # memory and the test process is OOM-killed (silent FAIL with no # per-test output, see #104). diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 17f9d209..1efa94fc 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -1,6 +1,6 @@ # Pull Request validation workflow # Runs unit tests + lint on PRs. E2E runs separately (e2e.yaml) on -# release tags and manual dispatch — too slow + flaky to run per PR. +# release tags and manual dispatch. Too slow and flaky to run per PR. # # This workflow triggers on every PR to main so the PR Gate job always # reports a status. The expensive jobs (Unit Tests, Lint) are gated on a diff --git a/.github/workflows/promote.yaml b/.github/workflows/promote.yaml index fae900dd..47b3f4ee 100644 --- a/.github/workflows/promote.yaml +++ b/.github/workflows/promote.yaml @@ -255,7 +255,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ steps.release-data.outputs.sem_version }} run: | - # Only dispatch on real GitHub — in act/gitea e2e environments + # Only dispatch on real GitHub. In act/gitea e2e environments # GITHUB_SERVER_URL is http://gitea:3000 and the Release workflow # doesn't exist, so skip silently. if [[ "$GITHUB_SERVER_URL" != "https://github.com" ]]; then diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index c6abfb57..42b8ecc1 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -18,7 +18,7 @@ on: release: types: [published, released] # Explicitly triggered by the promote workflow's finalize job after - # publishing. This is the reliable path — event-based triggers don't + # publishing. This is the reliable path; event-based triggers don't # fire when releases are managed via API calls in a workflow run. workflow_dispatch: diff --git a/CHANGELOG.md b/CHANGELOG.md index 90c5d2a0..a079f72c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,7 +62,7 @@ release that bumps `schema_version`. `actions/upload-artifact` v7, `goreleaser/goreleaser-action` v7, `github.com/spf13/cobra` v1.10.2. (#48, #49, #52, #53, #54) -## [0.1.0] — 2026-06-09 +## [0.1.0] - 2026-06-09 Initial release of cascade: a trunk-based CI/CD orchestrator for GitHub Actions. diff --git a/README.md b/README.md index de679807..52c64cda 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

cascade

- + @@ -31,7 +31,7 @@ ## How it works - + The **manifest** (`.github/manifest.yaml`) is the single source of truth. It holds both the pipeline configuration and the live deployment state for every environment. You run `cascade generate-workflow` once; after that the generated workflows own their own execution. @@ -64,7 +64,7 @@ Merge to trunk └─────────────────────────────────────────────────────────────┘ ``` - + --- @@ -146,20 +146,20 @@ cascade is a metadata courier. You construct the registry and deploy operations cascade generates workflows that handle the orchestration layer. Your callback workflows handle the domain logic. The manifest gives you control over: -- **Change detection** — builds and deploys only run when their declared `triggers` match changed paths -- **Dependency ordering** — `depends_on` chains builds and deploys in the right order -- **Matrix builds** — fan out a single build over a matrix of inputs -- **Per-job runner selection** — `runs_on` at the config or per-build/deploy level -- **Concurrency control** — configurable group and cancel-in-progress on orchestrate, promote, release, and external-update workflows -- **Extra triggers** — attach `schedule`, `repository_dispatch`, `workflow_run`, and `merge_group` events to orchestration -- **Dispatch inputs** — expose operator-facing manual-run inputs on the generated `workflow_dispatch` -- **PR plan preview** — a comment on each PR shows which builds and deploys would run -- **Merge queue lane** — a dedicated gate job runs before merge to protect trunk -- **Action pinning** — `pin_mode: sha` emits pinned SHA references for all cascade-managed action calls; override individual actions via `action_pins` -- **Breaking-change gate** — `feat!:` or `BREAKING CHANGE:` commits block the prerelease-to-release boundary unless explicitly overridden -- **Artifact passing** — `artifact_id` output from build callbacks is stored in state and forwarded to deploys and the publish callback -- **Publish callback** — after a release is published, a separate workflow call lets you retag RC artifacts in your registry -- **Schema version enforcement** — `schema_version` on the manifest is checked on every CLI invocation; incompatible manifests are rejected with a clear error +- **Change detection**: builds and deploys run only when their declared `triggers` match changed paths. +- **Dependency ordering**: `depends_on` chains builds and deploys in the right order. +- **Matrix builds**: fan out a single build over a matrix of inputs. +- **Per-job runner selection**: set `runs_on` at the config or per-build/deploy level. +- **Concurrency control**: configurable group and cancel-in-progress on orchestrate, promote, release, and external-update workflows. +- **Extra triggers**: attach `schedule`, `repository_dispatch`, `workflow_run`, and `merge_group` events to orchestration. +- **Dispatch inputs**: expose operator-facing manual-run inputs on the generated `workflow_dispatch`. +- **PR plan preview**: a comment on each PR shows which builds and deploys would run. +- **Merge queue lane**: a dedicated gate job runs before merge to protect trunk. +- **Action pinning**: `pin_mode: sha` emits pinned SHA references for all cascade-managed action calls. Override individual actions via `action_pins`. +- **Breaking-change gate**: `feat!:` or `BREAKING CHANGE:` commits block the prerelease-to-release boundary unless you override them. +- **Artifact passing**: the `artifact_id` output from build callbacks is stored in state and forwarded to deploys and the publish callback. +- **Publish callback**: once a release is published, a separate workflow call lets you retag RC artifacts in your registry. +- **Schema version enforcement**: every CLI invocation checks `schema_version` on the manifest and rejects incompatible manifests with a clear error. For a no-environment project (library or CLI), omit `environments` entirely. Commits produce RC pre-releases; a `promote` dispatch publishes the final release. @@ -250,11 +250,11 @@ Full flag reference: [docs/cli-reference.md](docs/cli-reference.md). ## Roadmap to stable -cascade is functional and self-hosted — the releases page shows the full pipeline running end to end. The remaining work before the v1.0.0 schema freeze falls into two areas: +cascade is functional and self-hosted. Its own releases page shows the full pipeline running end to end. The remaining work before the v1.0.0 schema freeze falls into two areas. -**Schema coverage** — a handful of GitHub Actions capabilities are modeled in the manifest shape but not yet emitted by the generator: environment gates, OIDC token configuration, and per-environment runner overrides. These are on the direct path to v1.0.0. +**Schema coverage.** A few GitHub Actions capabilities are modeled in the manifest shape but not yet emitted by the generator: environment gates, OIDC token configuration, and per-environment runner overrides. These sit on the direct path to v1.0.0. -**Hardening** — schema version enforcement (shipped), compatibility docs ([docs/versioning.md](docs/versioning.md)), and further e2e coverage to confirm that the generated workflows behave correctly under edge cases (empty builds, cross-repo coordination, rollback to N-1). +**Hardening.** This covers schema version enforcement (shipped), compatibility docs ([docs/versioning.md](docs/versioning.md)), and more e2e coverage. The added tests confirm that the generated workflows behave correctly under edge cases such as empty builds, cross-repo coordination, and rollback to N-1. The manifest schema field shapes were frozen in v0.1.0 as the v1 contract baseline. Minor versions between now and v1.0.0 may add new optional fields; no existing fields will be removed or renamed before v1.0.0. @@ -266,10 +266,10 @@ Open work is tracked in [GitHub Issues](https://github.com/stablekernel/cascade/ 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; existing manifest files continue to work across minor version bumps -- **Conventional commits** — commit messages follow `type: subject` (e.g., `feat:`, `fix:`, `docs:`); the changelog generator reads this format -- **Callback isolation** — generated workflows call your workflows via `workflow_call`; 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 +- **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. --- diff --git a/SECURITY.md b/SECURITY.md index 3e670b98..c5899b4c 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,7 +4,7 @@ | Version | Supported | | --- | --- | -| 0.x (latest) | Yes — security fixes backported promptly | +| 0.x (latest) | Yes, security fixes backported promptly | | Older 0.x tags | No | The `0.x` line is the active release line. Only the most recent tag receives diff --git a/docs/architecture.md b/docs/architecture.md index 860e216d..e1c7b6ee 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -4,11 +4,11 @@ System design and internals of cascade. ## Design Principles -1. **Build once, deploy everywhere** - Single artifact promoted through environments -2. **Change-driven** - Only build/deploy what changed -3. **Trunk-based** - Single main branch, short-lived feature branches -4. **Callback contract** - Framework orchestrates, adopting repos own build/deploy -5. **State tracking** - Manifest tracks what's deployed where +1. Build once, deploy everywhere. One artifact is promoted through every environment. +2. Change-driven. We build and deploy only what changed. +3. Trunk-based. A single main branch backs short-lived feature branches. +4. Callback contract. The framework orchestrates and adopting repos own build and deploy. +5. State tracking. The manifest records what is deployed where. ## System Overview @@ -561,21 +561,21 @@ For satellite repos with notify config: 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 — all configured inside GitHub, not -in the manifest. No cascade code calls the Deployments REST API or the -Environments REST API directly. +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 currently 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 for free without cascade owning that call. + deployment records without cascade owning that call. -- **Environments REST configuration sync** — cascade does not read or write +- 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. @@ -583,10 +583,10 @@ Two capabilities are intentionally out of scope for v1: 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 satisfy the common -case; adding programmatic control before there is a clear adopter need would -add complexity without a demonstrable benefit. If those APIs change shape, -cascade would need to track the change even though nothing in v1 depends on them. +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 @@ -598,7 +598,7 @@ without a breaking change: ```yaml config: - environments: [dev, test, prod] # ordered list — source of truth, unchanged + environments: [dev, test, prod] # ordered list (source of truth), unchanged environment_config: # reserved; omitting it is valid today prod: gha_environment: production # maps to the GHA environment name diff --git a/docs/callback-contract.md b/docs/callback-contract.md index 3c4f4287..c9a6840e 100644 --- a/docs/callback-contract.md +++ b/docs/callback-contract.md @@ -390,7 +390,7 @@ jobs: docker push myrepo/${{ inputs.build_name }}:${{ inputs.new_version }} ``` -The framework only carries metadata. The publish callback is responsible for the registry operation. If `artifact_id` is provided, prefer it over `old_version` for unambiguous identification. +The framework only carries metadata. The publish callback performs the registry operation. When `artifact_id` is present, use it instead of `old_version` so the target is unambiguous. ## Custom Inputs diff --git a/docs/cli-reference.md b/docs/cli-reference.md index b43d4a68..b79bf145 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -8,7 +8,7 @@ Complete reference for the `cascade` command-line tool. # Latest stable go install github.com/stablekernel/cascade/cmd/cascade@latest -# Bleeding edge from master +# Latest build from master go install github.com/stablekernel/cascade/cmd/cascade@master # Specific version @@ -35,7 +35,7 @@ These flags are available on all commands: | Flag | Type | Description | |------|------|-------------| -| `--dry-run` | bool | Preview mode — show what would happen without making changes | +| `--dry-run` | bool | Preview mode: show what would happen without making changes | | `--trace` | bool | Enable TRACE-level logging for detailed internals | | `--json` | bool | Output structured JSON for workflow consumption | @@ -189,7 +189,7 @@ cascade generate-workflow - **Output chaining**: passes outputs from one callback to dependents - **Per-callback policies**: respects `run_policy`, `on_failure`, `retries` - **Environment overrides**: applies `env_inputs` per environment -- **Publish step**: when `publish:` is configured, the promote workflow dispatches the callback once per build at the prerelease→release boundary +- **Publish step**: when `publish:` is configured, the promote workflow dispatches the callback once per build at the boundary where a prerelease becomes a release ### manage-release @@ -390,9 +390,9 @@ cascade next-version \ | `--json` | bool | No | Output as JSON | Bump rules: -- Breaking change (`feat!`, `BREAKING CHANGE:`) → major bump -- Feature (`feat`) → minor bump -- Fix (`fix`) → patch bump +- Breaking change (`feat!`, `BREAKING CHANGE:`) triggers a major bump +- Feature (`feat`) triggers a minor bump +- Fix (`fix`) triggers a patch bump - Pre-release environments append an RC suffix (e.g., `v1.3.0-rc.0`) ### external diff --git a/docs/configuration.md b/docs/configuration.md index d1354f31..f8549411 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -13,7 +13,7 @@ ci: environments: [dev, test, prod] # builds, deploys, etc. - state: # Deployment tracking (managed by the framework — do not edit) + state: # Deployment tracking (managed by the framework, do not edit) dev: sha: "abc123" version: "v1.2.0-rc.3" @@ -57,7 +57,7 @@ Controls which CLI version the generated workflows install via setup-cli: | Value | Behavior | |-------|----------| | `latest` | Most recent stable release (default) | -| `beta` | Bleeding edge from `master` branch | +| `beta` | Latest build from the `master` branch | | `vX.Y.Z` | Specific version (e.g., `v2.0.4`) | Pin to a specific version for reproducibility. Use `beta` for early access. @@ -122,7 +122,7 @@ ci: | `env_inputs` | map | {} | Per-environment input overrides | | `run_policy` | string | default | Execution policy | | `on_failure` | string | abort | Failure handling | -| `retries` | int | 0 | Retry attempts (0–3) | +| `retries` | int | 0 | Retry attempts (0-3) | ### builds Section @@ -156,7 +156,7 @@ ci: | `env_inputs` | map | No | Per-environment input overrides | | `run_policy` | string | No | Execution policy | | `on_failure` | string | No | Failure handling | -| `retries` | int | No | Retry attempts (0–3) | +| `retries` | int | No | Retry attempts (0-3) | The build's `artifact_id` output (if declared) is captured automatically into state. Any other declared outputs are forwarded to dependent deploys as inputs. @@ -194,7 +194,7 @@ ci: | `env_inputs` | map | No | Per-environment overrides | | `run_policy` | string | No | Execution policy | | `on_failure` | string | No | Failure handling | -| `retries` | int | No | Retry attempts (0–3) | +| `retries` | int | No | Retry attempts (0-3) | ### Deploy Types @@ -210,7 +210,7 @@ Build-linked deploys inherit the build's triggers for change detection during pr ### publish Section -The publish callback runs once per build when a release is published (RC → final semver). Use it to retag artifacts that still carry their RC version. +The publish callback runs once per build when a release is published, at the point where an RC version becomes a final semver. Use it to retag artifacts that still carry their RC version. ```yaml ci: @@ -265,8 +265,8 @@ ci: | `deploys[].triggers` | list | No | File patterns for change detection | **Workflow paths:** -- Local (`.github/workflows/deploy.yaml`) — calls a workflow in the primary repo -- External (`org/repo/.github/workflows/deploy.yaml@ref`) — calls a workflow in the external repo +- Local (`.github/workflows/deploy.yaml`) calls a workflow in the primary repo +- External (`org/repo/.github/workflows/deploy.yaml@ref`) calls a workflow in the external repo When external deploys are configured, the generated promote workflow includes deploy jobs for each external deploy and the finalize job tracks their state. @@ -331,7 +331,7 @@ Omit this section to use the built-in conventional commit parser. ## State Section -The `state` section tracks deployment state per environment plus a synthetic `release` slot. It is automatically managed by the framework — do not hand-edit. +The `state` section tracks deployment state per environment plus a synthetic `release` slot. The framework manages it automatically. Do not hand-edit. ### Structure @@ -430,7 +430,7 @@ deploys: stack_version: "v2.1.0" ``` -This enables diff-based change detection during promotions — only deployables with actual file changes are redeployed. +This enables diff-based change detection during promotions. Only deployables with actual file changes are redeployed. ### External Deploy Tracking @@ -477,7 +477,7 @@ Controls when a callback executes: ### retries -Number of retry attempts if the callback fails (0–3). +Number of retry attempts if the callback fails (0-3). ## Trigger Patterns @@ -493,9 +493,9 @@ Triggers use glob patterns: | `deploy/k8s/**` | All files under deploy/k8s/ | Special characters: -- `*` — matches any characters except `/` -- `**` — matches any path segments -- `?` — matches a single character +- `*` matches any characters except `/` +- `**` matches any path segments +- `?` matches a single character ## Input Inheritance diff --git a/docs/getting-started.md b/docs/getting-started.md index 10ef96a1..d12083b2 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -24,7 +24,7 @@ go install github.com/stablekernel/cascade/cmd/cascade@v2.0.4 cascade version ``` -In GitHub Actions, generated workflows install the CLI for you via the setup action — you don't need to add it explicitly. To pin a version, set `cli_version` in your manifest. +In GitHub Actions, generated workflows install the CLI for you via the setup action, so you don't need to add it explicitly. To pin a version, set `cli_version` in your manifest. If you need to invoke it manually: @@ -83,7 +83,7 @@ ci: prod: {} ``` -The framework owns `state:` and `latest_release:`. The `state: { dev: {}, ... }` skeleton is enough — the workflows fill in details on every run. +The framework owns `state:` and `latest_release:`. The `state: { dev: {}, ... }` skeleton is enough. The workflows fill in the details on every run. See [Configuration Reference](configuration.md) for every field. @@ -260,8 +260,8 @@ cascade generate-workflow --force ``` This creates: -- `.github/workflows/orchestrate.yaml` — runs on merge to trunk -- `.github/workflows/promote.yaml` — manual promotion between environments +- `.github/workflows/orchestrate.yaml` runs on merge to trunk +- `.github/workflows/promote.yaml` handles manual promotion between environments ## Step 5: Validate diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..08750892 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,38 @@ +# Cascade + +**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. + +## How it works + +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. + +A merge to trunk runs the orchestrate workflow: it detects what changed, computes +the next release candidate, builds and deploys to the first environment, and writes +state back to the manifest. A `workflow_dispatch` then promotes the same artifacts +forward, one environment at a time, until the release is published and the RC tags +are cleaned up. + +## Where to go next + +- **[Getting Started](getting-started.md)** walks through a first manifest and the + generated workflows. +- **[Manifest Reference](configuration.md)** documents every field. +- **[Callback Contract](callback-contract.md)** covers the inputs and outputs your + build, deploy, and publish workflows exchange with cascade. +- **[Workflows](workflows.md)** explains orchestrate, promote, and release. +- **[CLI Reference](cli-reference.md)** lists the commands and flags. +- **[Architecture](architecture.md)** describes the design and what cascade does and + does not own. +- **[Versioning & Schema](versioning.md)** sets out the schema compatibility policy. + +## 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). diff --git a/docs/versioning.md b/docs/versioning.md index 9c28c354..d8dad99f 100644 --- a/docs/versioning.md +++ b/docs/versioning.md @@ -1,8 +1,8 @@ # Versioning and schema compatibility -The cascade manifest is the public contract between your repository and the -cascade CLI. This document describes how the manifest schema is versioned and -how the CLI decides whether it can read a given manifest. +The cascade manifest is the contract between your repository and the cascade +CLI. This document describes how the manifest schema is versioned and how the +CLI decides whether it can read a given manifest. ## `schema_version` @@ -16,8 +16,8 @@ ci: # ... ``` -`schema_version` is a single monotonic integer — a "schema major" — not a semver -string. It identifies which breaking-change generation of the schema the +`schema_version` is a single monotonic integer, the "schema major". It is not a +semver string. It identifies which breaking-change generation of the schema the manifest is written for. ### Why an integer @@ -25,8 +25,8 @@ manifest is written for. The manifest evolves additively. New capabilities arrive as new optional fields, new enum values, or new nested blocks, each with a sensible default. An older CLI ignores fields it does not recognize, and a newer CLI fills in defaults for -fields an older manifest omits. Because of this, **additive changes never change -`schema_version`**. The integer only moves when a change is genuinely breaking: +fields an older manifest omits. Because of this, additive changes never change +`schema_version`. The integer only moves when a change is genuinely breaking: - a field is removed, - a field is re-typed, @@ -39,16 +39,16 @@ additive-only design, never need to exist. The CLI knows two bounds: -- `CurrentSchemaVersion` — the highest schema version this CLI understands. A +- `CurrentSchemaVersion` is the highest schema version this CLI understands. A manifest that omits `schema_version` is assumed to target this version. -- `MinSchemaVersion` — the oldest schema version this CLI still reads. +- `MinSchemaVersion` is the oldest schema version this CLI still reads. On load, the CLI applies the following rules: | Manifest `schema_version` | CLI behavior | | --- | --- | | equal to `CurrentSchemaVersion` | Accepted silently. | -| omitted or `0` | Accepted with a warning; assumed to be `CurrentSchemaVersion`. Pin it explicitly. Because `schema_version` is an `int` field with `omitempty`, an explicit `schema_version: 0` is encoded identically to an absent field and is treated the same way — as omitted. | +| omitted or `0` | Accepted with a warning; assumed to be `CurrentSchemaVersion`. Pin it explicitly. Because `schema_version` is an `int` field with `omitempty`, an explicit `schema_version: 0` is encoded identically to an absent field and is treated the same way, as omitted. | | between `MinSchemaVersion` and `CurrentSchemaVersion - 1` | Accepted with a warning; the CLI still reads it. See the migration table below. | | below `MinSchemaVersion` (and not `0`) | Rejected. The schema generation is no longer supported; follow the migration table. | | above `CurrentSchemaVersion` | Rejected. The manifest needs a newer CLI; upgrade the `cli_version` pin. A newer schema may rely on changed semantics this CLI would mis-handle, so it does not guess. | @@ -83,14 +83,18 @@ current schema version is the first. ## Supported release line -**0.x (current)** — the active development line. Bug fixes, security patches, -and new capabilities all land here. No stability guarantee is made for the CLI -command surface or the manifest schema between 0.x releases; additive changes -arrive without a `schema_version` bump, but breaking changes (field removals, -type changes, behaviour changes) increment `schema_version` and carry a -`Migration` entry in [CHANGELOG.md](https://github.com/stablekernel/cascade/blob/main/CHANGELOG.md). +### 0.x (current) -**1.0** — when cascade reaches v1.0 the following guarantees apply: +This is the active development line. Bug fixes, security patches, and new +capabilities all land here. No stability guarantee is made for the CLI command +surface or the manifest schema between 0.x releases. Additive changes arrive +without a `schema_version` bump. Breaking changes (field removals, type changes, +behaviour changes) increment `schema_version` and carry a `Migration` entry in +[CHANGELOG.md](https://github.com/stablekernel/cascade/blob/main/CHANGELOG.md). + +### 1.0 + +When cascade reaches v1.0 the following guarantees apply: - The CLI command surface (flags, subcommands, exit codes, JSON output shapes) follows semver: breaking changes require a major version bump. diff --git a/docs/workflows.md b/docs/workflows.md index d425ebed..5d0c7b7f 100644 --- a/docs/workflows.md +++ b/docs/workflows.md @@ -39,7 +39,7 @@ Merge to Trunk ### Triggering -The orchestrate workflow is generated to fire on `push` to the trunk branch. There is no need to wrap it — the generator emits the trigger directly: +The orchestrate workflow is generated to fire on `push` to the trunk branch. You don't need to wrap it. The generator emits the trigger directly: ```yaml # .github/workflows/orchestrate.yaml (generated) @@ -50,7 +50,7 @@ on: ### Standard Inputs -The orchestrate workflow has no manual inputs by default — it runs automatically on push. +The orchestrate workflow has no manual inputs by default. It runs automatically on push. ### Outputs @@ -198,7 +198,7 @@ on: ### Atomic Promotions with Rollback -The promote workflow supports atomic promotions where successful deploys are automatically rolled back if any deploy fails: +The promote workflow can run atomic promotions. If any deploy fails, the deploys that already succeeded are rolled back: ```yaml # Enabled by default @@ -209,7 +209,8 @@ When enabled: 1. Preflight captures the target environment's current SHA as `rollback_sha` 2. If any deploy job fails, rollback jobs trigger for successful deploys 3. Rollback jobs redeploy using the `rollback_sha` -4. This ensures all-or-nothing promotion semantics + +The result is all-or-nothing promotion: either every deploy lands or none does. Disable for non-atomic promotions: ```yaml @@ -278,7 +279,7 @@ The framework drops the RC suffix when crossing the prerelease→release boundar ## Hotfix -Hotfix is currently handled via the standard promote workflow with `dry_run: false` and a deploy-list filter. A first-class hotfix workflow is on the roadmap (issue #94 — direct promotion to prod with branch ancestry checks). +Hotfix is currently handled via the standard promote workflow with `dry_run: false` and a deploy-list filter. A first-class hotfix workflow is tracked in issue #94 (direct promotion to prod with branch ancestry checks). ## Workflow Permissions diff --git a/e2e/harness/act.go b/e2e/harness/act.go index 11e7775a..d0fe1c12 100644 --- a/e2e/harness/act.go +++ b/e2e/harness/act.go @@ -75,7 +75,7 @@ func NewActRunner(ctx context.Context, giteaURL, giteaToken, networkName string, return nil, fmt.Errorf("failed to install act: %w", err) } - // Network override is passed on the CLI as `--network=` — act's + // Network override is passed on the CLI as `--network=`. Act's // dedicated flag drives ContainerNetworkMode; --container-options is // appended after docker create and cannot override the network mode. actrc := `mkdir -p /root/.config/act && cat > /root/.config/act/actrc <<'EOF' @@ -91,7 +91,7 @@ EOF` // Pre-pull the act job-container image into the host docker daemon (via // the bind-mounted docker socket). Without this, every parallel scenario // triggers its own `docker pull` and races to fetch from registry-1.docker.io - // — under proxied or rate-limited connections (e.g., Docker Desktop on + // Under proxied or rate-limited connections (e.g., Docker Desktop on // macOS) those pulls intermittently time out and fail the workflow. // The pull is best-effort: if it fails (offline, image already present, // proxy hiccup), we proceed; act will retry on its own when needed. @@ -282,7 +282,7 @@ func (a *ActRunner) RunWorkflowFromRepo(ctx context.Context, opts RunOpts) (*Ext // that targeted a specific workflow file but produced zero parsed jobs is also // a failure: act emitted no job events because it could not find or load the // workflow (e.g. a missing orchestrate.yaml). Without this, such a run -// masqueraded as Conclusion="success" with 0 jobs — a missing workflow showing +// masqueraded as Conclusion="success" with 0 jobs. A missing workflow showing // up as a green-but-empty scenario (#25). func normalizeWorkflowResult(result *ExtendedWorkflowResult, workflowPath string, exitCode int) { if exitCode != 0 { diff --git a/e2e/harness/context.go b/e2e/harness/context.go index 6398b21d..d2fa4fba 100644 --- a/e2e/harness/context.go +++ b/e2e/harness/context.go @@ -165,7 +165,7 @@ func (c *ExecutionContext) RecordState(env, sha, version string) { // ClearState removes all env state. Used by sync routines that need to // rebuild ctx from an authoritative source (the manifest), so deletions in -// that source — e.g. finalize wiping state[prerelease] on publish — are +// that source (e.g. finalize wiping state[prerelease] on publish) are // reflected rather than leaving stale ctx entries. func (c *ExecutionContext) ClearState() { c.mu.Lock() diff --git a/e2e/harness/gitea.go b/e2e/harness/gitea.go index 3b2dba06..04c8a570 100644 --- a/e2e/harness/gitea.go +++ b/e2e/harness/gitea.go @@ -127,7 +127,7 @@ func (g *GiteaContainer) setupAdmin(ctx context.Context) error { // The CLI writes the user to SQLite directly while the gitea API server // runs in a separate process. /api/v1/version (used as the readiness probe) - // returns OK before the API can authenticate the freshly-written admin — + // returns OK before the API can authenticate the freshly-written admin. // observed in CI as "401 user does not exist [uid: 0, name: testadmin]" // on the first request. Poll an authenticated endpoint until it succeeds. return g.waitForAdminAuth(ctx, 30*time.Second) diff --git a/e2e/harness/harness.go b/e2e/harness/harness.go index 82ee232d..188479bc 100644 --- a/e2e/harness/harness.go +++ b/e2e/harness/harness.go @@ -117,7 +117,7 @@ func (h *Harness) StageRepoFromConfig(ctx context.Context, config Config) error // Create stub workflow files for builds and deploys. // These are reusable workflows that the CLI reads to discover inputs/outputs. // We tag each stub's `name:` with a per-scenario suffix so act's - // auto-generated job container hash diverges across parallel scenarios — + // auto-generated job container hash diverges across parallel scenarios. // otherwise multiple scenarios with the same build/deploy names race on // /act-Build-app-app-app- at the host docker level. scenarioTag := scenarioTagFromTestName(h.t.Name()) @@ -287,7 +287,7 @@ func scenarioTagFromTestName(name string) string { // generatePublishStubWorkflow creates a minimal workflow_dispatch stub for the // publish callback. Unlike build/deploy stubs (which are workflow_call), the // publish workflow is invoked via `gh workflow run` (dispatch) from the promote -// finalize job. The stub accepts the standard publish inputs but does nothing — +// finalize job. The stub accepts the standard publish inputs but does nothing; // in the e2e harness the dispatch is a no-op because GITHUB_SERVER_URL points // to gitea. func generatePublishStubWorkflow(scenarioTag string) string { @@ -409,7 +409,7 @@ func (h *Harness) GenerateWorkflows(ctx context.Context) error { // a non-zero exit, so a run that exited 0 but wrote no workflow (e.g. the // "No changes"/skip path) left no trace and the missing orchestrate.yaml // only surfaced much later as a `cat: ... No such file` in the orchestrate - // step — a different scenario per parallel dispatch (#25). Keep the output + // step. A different scenario per parallel dispatch (#25). Keep the output // for diagnostics. var genOutput bytes.Buffer if reader != nil { @@ -423,7 +423,7 @@ func (h *Harness) GenerateWorkflows(ctx context.Context) error { // can exit 0 without emitting .github/workflows/orchestrate.yaml; if we // commit and push that empty set, the downstream orchestrate step runs act // against a non-existent `-W` path and act (with --detect-event) reports no - // jobs while still exiting success — the run then masquerades as a passing + // jobs while still exiting success. The run then masquerades as a passing // scenario with 0 jobs (#25). Fail here, at the source, with the generate // output and a workflow-dir listing so the real cause is obvious. if err := h.assertOrchestrateGenerated(ctx, genOutput.String()); err != nil { @@ -436,12 +436,12 @@ func (h *Harness) GenerateWorkflows(ctx context.Context) error { // - 'uses: build.yaml' → 'uses: ./build.yaml' // // Earlier this was a fire-and-forget shell sed loop with three blank - // receivers — under heavy CI parallelism (28 scenarios × act + gitea + // receivers. Under heavy CI parallelism (28 scenarios × act + gitea // containers) the exec would occasionally fail or no-op and leave // un-localized refs in the workflow files. The act job container would // then try to fetch `stablekernel/cascade@latest` from the test // gitea (where that repo doesn't exist), failing with `authentication - // required` — cf. #78. Now we check the exit code, verify no + // required` (cf. #78). Now we check the exit code, verify no // `stablekernel/cascade` references remain, and retry on transient // failure. if err := h.localizeWorkflows(ctx); err != nil { @@ -613,7 +613,7 @@ func (h *Harness) SyncRepoToActContainer(ctx context.Context) error { // under heavy parallel load against the per-scenario gitea; bind the fetch // to origin/main explicitly and chain the reset so a partial sync surfaces // as a non-zero exit instead of silently resetting to a stale tree (which - // would drop the just-pushed orchestrate.yaml — #25). + // would drop the just-pushed orchestrate.yaml (#25). syncCmd := []string{ "bash", "-c", "cd /tmp/repo && git fetch origin main && git reset --hard origin/main && (git branch -f master HEAD 2>/dev/null || true)", diff --git a/e2e/harness/multistep.go b/e2e/harness/multistep.go index 74b027a8..54c7d5ec 100644 --- a/e2e/harness/multistep.go +++ b/e2e/harness/multistep.go @@ -65,7 +65,7 @@ type StepExpect struct { // WorkflowFileExpect asserts a generated workflow file contains/excludes // specific substrings. Verifies manifest fields make it into the emitted -// YAML — orthogonal to behavior checks (state/jobs/etc.) which observe the +// YAML, orthogonal to behavior checks (state/jobs/etc.) which observe the // run outcome. Used for features whose effect is purely the generated // workflow shape (#92 concurrency, #97 timeout-minutes, #101/#102 push // retry loops). diff --git a/e2e/harness/runner.go b/e2e/harness/runner.go index 8b46455f..c099d623 100644 --- a/e2e/harness/runner.go +++ b/e2e/harness/runner.go @@ -498,7 +498,7 @@ func (r *Runner) syncStateFromGitea(ctx context.Context, config Config) error { // This ensures deleted tags are properly removed from tracking r.ctx.ClearTags() - // An RC is a draft only while it's exclusive to the first env (e.g., dev) — + // An RC is a draft only while it's exclusive to the first env (e.g., dev). // once it's been promoted into any later env, it's "blessed" and the // workflow's prerelease step would have flipped its draft flag. Collect // the set of RC versions that are present in any non-firstEnv state. @@ -562,7 +562,7 @@ func (r *Runner) syncStateFromGitea(ctx context.Context, config Config) error { } // Clear ctx state so deletions in the manifest (e.g. finalize wiping - // state[prerelease] on publish) are reflected — otherwise stale entries + // state[prerelease] on publish) are reflected. Otherwise stale entries // from prior steps make wiped: true assertions fail. r.ctx.ClearState() @@ -593,7 +593,7 @@ func (r *Runner) syncStateFromGitea(ctx context.Context, config Config) error { // readPromotedRCVersions reads the manifest and returns the set of RC versions // that appear in any state[env] beyond the first env. Once an RC has been // promoted past dev (the first env), the workflow's prerelease step would have -// flipped its draft flag — so the harness should treat it as non-draft. +// flipped its draft flag, so the harness should treat it as non-draft. func (r *Runner) readPromotedRCVersions(ctx context.Context, envs []string) map[string]bool { promoted := make(map[string]bool) if len(envs) < 2 { @@ -698,7 +698,7 @@ func (r *Runner) assertStep(ctx context.Context, step *Step, preState *Execution // Returns errors for missing-substring or unexpected-substring matches. func (r *Runner) assertWorkflowFile(ctx context.Context, expect WorkflowFileExpect) []error { if r.harness == nil || r.harness.act == nil { - // In unit-test mode there's no act container — skip silently. + // In unit-test mode there's no act container; skip silently. return nil } if expect.Path == "" { diff --git a/e2e/scenarios/05-publish-callback.yaml b/e2e/scenarios/05-publish-callback.yaml index c0eeeb6b..0a249f01 100644 --- a/e2e/scenarios/05-publish-callback.yaml +++ b/e2e/scenarios/05-publish-callback.yaml @@ -27,7 +27,7 @@ steps: src/app.ts: | export function main() { console.log("v0.1.0 feature"); } - - name: "Orchestrate — build runs, state.prerelease created" + - name: "Orchestrate: build runs, state.prerelease created" action: orchestrate expect: state: @@ -37,7 +37,7 @@ steps: jobs: app: success - - name: "Promote to release — publish step runs (dispatch no-op in harness)" + - name: "Promote to release: publish step runs (dispatch no-op in harness)" action: promote promote: mode: default diff --git a/e2e/scenarios/06-callback-timeout.yaml b/e2e/scenarios/06-callback-timeout.yaml index 49b18b90..cdb1d63b 100644 --- a/e2e/scenarios/06-callback-timeout.yaml +++ b/e2e/scenarios/06-callback-timeout.yaml @@ -3,7 +3,7 @@ description: | Verifies the timeout_minutes field on validate/builds/deploys propagates into the generated orchestrate.yaml as a job-level timeout-minutes (#97). - This is a generator-output verification scenario — assertion runs on the + This is a generator-output verification scenario. Assertion runs on the staged repo after StageRepoFromConfig generates workflows but before any orchestrate runs. We don't run orchestrate because act doesn't honor timeout-minutes on `uses:` reusable workflow callers and would fail the diff --git a/e2e/scenarios/08-state-push-retry.yaml b/e2e/scenarios/08-state-push-retry.yaml index 6fece5f7..0bd2428d 100644 --- a/e2e/scenarios/08-state-push-retry.yaml +++ b/e2e/scenarios/08-state-push-retry.yaml @@ -4,7 +4,7 @@ description: | fetch+reset+reapply+push retry loop (#101) instead of a single git push. Generator-output verification. Reproducing the actual concurrent-push race - needs real GHA — out of scope for the act+gitea harness. + needs real GHA and is out of scope for the act+gitea harness. config: trunk_branch: main diff --git a/e2e/scenarios/09-inline-run-callback.yaml b/e2e/scenarios/09-inline-run-callback.yaml index 6672a121..bf8393fc 100644 --- a/e2e/scenarios/09-inline-run-callback.yaml +++ b/e2e/scenarios/09-inline-run-callback.yaml @@ -4,7 +4,7 @@ description: | inline run: step in the generated orchestrate.yaml, while a sibling callback declaring workflow: still emits a jobs..uses reusable-workflow call (#36). - This is a generator-output verification scenario — assertion runs on the + This is a generator-output verification scenario. Assertion runs on the staged repo after StageRepoFromConfig generates workflows but before any orchestrate runs. diff --git a/e2e/scenarios/10-inline-job-attributes.yaml b/e2e/scenarios/10-inline-job-attributes.yaml index 0651c089..068443e0 100644 --- a/e2e/scenarios/10-inline-job-attributes.yaml +++ b/e2e/scenarios/10-inline-job-attributes.yaml @@ -5,7 +5,7 @@ description: | generated orchestrate.yaml (#12, #35, #15, #17). A sibling reusable-workflow callback (workflow:) must not carry those - attributes — GHA forbids runs-on/concurrency on jobs..uses calls. + attributes; GHA forbids runs-on/concurrency on jobs..uses calls. Generator-output verification scenario; assertion runs on the staged repo after StageRepoFromConfig generates workflows but before any orchestrate runs. diff --git a/e2e/scenarios/11-job-timeouts-and-optional-deps.yaml b/e2e/scenarios/11-job-timeouts-and-optional-deps.yaml index ab380199..b7e6b799 100644 --- a/e2e/scenarios/11-job-timeouts-and-optional-deps.yaml +++ b/e2e/scenarios/11-job-timeouts-and-optional-deps.yaml @@ -2,17 +2,17 @@ name: "Owned-job timeouts and optional dependencies" description: | Verifies two generator features in the emitted orchestrate.yaml: - #37 — cascade-owned jobs (setup, finalize) carry a timeout-minutes from the + #37: cascade-owned jobs (setup, finalize) carry a timeout-minutes from the config-level job_timeout_minutes (here 20), so cascade's own jobs do not inherit GHA's 360-minute default. Reusable-workflow callbacks (jobs..uses) - get no timeout — the called workflow owns its own. + get no timeout; the called workflow owns its own. - #18 — optional_depends_on sequences a deploy AFTER a build (adds it to needs:) + #18: optional_depends_on sequences a deploy AFTER a build (adds it to needs:) but does NOT skip-gate it: a skipped optional dep must not skip the deploy. The emitted shape that yields this is the optional dep present in needs: and absent from the if: skip-gate, while a hard depends_on still gates. - This is a generator-output verification scenario — assertions run on the staged + This is a generator-output verification scenario. Assertions run on the staged repo after StageRepoFromConfig generates workflows. We don't run orchestrate because act does not honor timeout-minutes on `uses:` callers; real timeout-firing belongs to real-GHA validation. diff --git a/internal/changelog/parse.go b/internal/changelog/parse.go index 295e9816..542dc937 100644 --- a/internal/changelog/parse.go +++ b/internal/changelog/parse.go @@ -20,7 +20,7 @@ var ( // Per Conventional Commits, `BREAKING CHANGE:` is a footer that appears // at the start of a line. Anchor with `(?m)^` and use `[ \t]+` between // BREAKING and CHANGE so word-wrapped narrative (`...BREAKING\nCHANGE:...`) - // doesn't match — earlier `\s+` accepted newlines and flagged unrelated + // doesn't match. Earlier `\s+` accepted newlines and flagged unrelated // commits as breaking. breakingBodyPattern = regexp.MustCompile(`(?im)^BREAKING[ \t]+CHANGE\s*:`) // Matches PR references like (#123) or (org/repo#123) @@ -268,7 +268,7 @@ func formatCommitLine(c ConventionalCommit, repo string) string { // Build attribution var attribution string if c.GitHubUsername != "" { - attribution = fmt.Sprintf(" — @%s", c.GitHubUsername) + attribution = fmt.Sprintf(" (@%s)", c.GitHubUsername) } // Escape @branch patterns to prevent GitHub from treating them as user mentions @@ -291,7 +291,7 @@ func formatOtherCommitLine(c git.Commit, repo string) string { var attribution string if c.GitHubUsername != "" { - attribution = fmt.Sprintf(" — @%s", c.GitHubUsername) + attribution = fmt.Sprintf(" (@%s)", c.GitHubUsername) } // Escape @branch patterns to prevent GitHub from treating them as user mentions diff --git a/internal/changelog/parse_test.go b/internal/changelog/parse_test.go index 1c7bda44..937ca202 100644 --- a/internal/changelog/parse_test.go +++ b/internal/changelog/parse_test.go @@ -349,11 +349,11 @@ func TestFormatMarkdown_WithContributors(t *testing.T) { result := FormatMarkdown(nil, features, fixes, nil, "owner/repo", "base123", "head456") - // Check inline attribution exists (new format: — @username) - if !strings.Contains(result, "— @alice") { + // Check inline attribution exists (the "(@username)" format). + if !strings.Contains(result, "(@alice)") { t.Error("Missing inline contributor attribution for alice") } - if !strings.Contains(result, "— @bob") { + if !strings.Contains(result, "(@bob)") { t.Error("Missing inline contributor attribution for bob") } diff --git a/internal/config/types.go b/internal/config/types.go index 18d716d4..dc977427 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -167,8 +167,8 @@ func (c *TrunkConfig) GetSchemaVersion() int { // validateSchemaVersion is the testable core of the compatibility check. It // evaluates v against the provided min and current bounds and returns a warning // string (non-empty means warn-and-accept) or an error (means reject). Callers -// that need to exercise the full matrix — including branches that are currently -// unreachable when min == current — should call this directly. +// that need to exercise the full matrix (including branches that are currently +// unreachable when min == current) should call this directly. // // Rules: // @@ -189,7 +189,7 @@ func validateSchemaVersion(v, min, current int) (warning string, err error) { case v > current: return "", fmt.Errorf( "manifest requires schema version %d but this CLI supports schema versions up to %d; "+ - "upgrade the CLI (cli_version) — see docs/versioning.md", v, current) + "upgrade the CLI (cli_version); see docs/versioning.md", v, current) case v < min: return "", fmt.Errorf( "manifest schema version %d is no longer supported (minimum %d); "+ @@ -340,7 +340,7 @@ type ValidateConfig struct { // v1 reserved-shape per-callback fields (parse + structural validation only). // The validate gate is a singleton, so the spec scopes optional_depends_on - // (§2.11) and auto_commits (§5.5) to builds/deploys only — not here. + // (§2.11) and auto_commits (§5.5) to builds/deploys only; not here. Secrets *SecretsConfig `yaml:"secrets,omitempty" json:"secrets,omitempty"` Permissions map[string]string `yaml:"permissions,omitempty" json:"permissions,omitempty"` RunsOn *RunsOn `yaml:"runs_on,omitempty" json:"runs_on,omitempty"` diff --git a/internal/generate/action_pins.go b/internal/generate/action_pins.go index 476e4c8c..1b509b4c 100644 --- a/internal/generate/action_pins.go +++ b/internal/generate/action_pins.go @@ -48,10 +48,10 @@ var defaultActionPins = map[string]actionPin{ // uniformly and no ref is missed. // // Resolution order: -// 1. config.action_pins[action] — explicit per-action override (any ref/sha), +// 1. config.action_pins[action]: explicit per-action override (any ref/sha), // applied regardless of pin_mode. Use this for forks or org-mirrored actions. -// 2. pin_mode: sha — emit @ # from the built-in table. -// 3. pin_mode: tag (default) — emit @, today's behavior, never +// 2. pin_mode: sha: emit @ # from the built-in table. +// 3. pin_mode: tag (default): emit @, today's behavior, never // @latest for a third-party action. // // An action not present in the built-in table and not overridden falls back to diff --git a/internal/generate/dry_run_test.go b/internal/generate/dry_run_test.go index 4fc47e1c..39fa2f7a 100644 --- a/internal/generate/dry_run_test.go +++ b/internal/generate/dry_run_test.go @@ -31,7 +31,7 @@ func TestPromote_SupportsDryRun_SingleDeploy(t *testing.T) { content, err := gen.Generate() require.NoError(t, err) - // The job must NOT gate on dry_run != 'true' — the callback runs regardless. + // The job must NOT gate on dry_run != 'true'; the callback runs regardless. assert.NotContains(t, content, "github.event.inputs.dry_run != 'true' && contains(fromJSON(needs.preflight.outputs.deploys_to_run), 'app')", "supports_dry_run deploy should not be skipped by dry_run guard") diff --git a/internal/generate/env_gates_test.go b/internal/generate/env_gates_test.go index 9e4373cf..4bed3da0 100644 --- a/internal/generate/env_gates_test.go +++ b/internal/generate/env_gates_test.go @@ -67,7 +67,7 @@ func TestEnvGates_Orchestrate_DeployJob_WithoutGHAEnvironment(t *testing.T) { Deploys: []config.DeployConfig{ {Name: "svc", Workflow: ".github/workflows/deploy.yaml", Triggers: []string{"src/**"}}, }, - // No EnvironmentConfig — no gha_environment. + // No EnvironmentConfig; no gha_environment. } gen := NewGenerator(cfg, tmpDir) diff --git a/internal/generate/generator.go b/internal/generate/generator.go index 1791bdc5..b9435afb 100644 --- a/internal/generate/generator.go +++ b/internal/generate/generator.go @@ -584,7 +584,7 @@ func (g *Generator) writeExtraTriggers(sb *strings.Builder, et *config.ExtraTrig // writeConcurrency emits a top-level concurrency: block. Two rapid pushes to // trunk used to fire concurrent orchestrate runs, which raced on state writes // and produced duplicate RC tags + non-fast-forward push failures (#92). -// Default: cancel an older in-progress run when a newer push lands — +// Default: cancel an older in-progress run when a newer push lands. // the older run's work is obsolete. Override via config.concurrency. func (g *Generator) writeConcurrency(sb *strings.Builder) { sb.WriteString("concurrency:\n") @@ -749,7 +749,7 @@ func (g *Generator) writeCallbackJob(sb *strings.Builder, info CallbackInfo, wor fmt.Fprintf(sb, " needs: [%s]\n", strings.Join(needs, ", ")) // if: condition based on run_policy. Optional deps are intentionally not - // passed here — they sequence the job without gating it. + // passed here; they sequence the job without gating it. g.writeIfCondition(sb, info, needs) switch { @@ -768,7 +768,7 @@ func (g *Generator) writeCallbackJob(sb *strings.Builder, info CallbackInfo, wor g.writeStrategyBlock(sb, info.Matrix) } - // environment: — emitted on deploy jobs when the config declares a + // environment: emitted on deploy jobs when the config declares a // gha_environment for at least one environment. The job-level environment: // key wires the job to a GitHub Environment so that the environment's // protection rules (required reviewers, wait timers, deployment branch @@ -792,7 +792,7 @@ func (g *Generator) writeCallbackJob(sb *strings.Builder, info CallbackInfo, wor sb.WriteString(" continue-on-error: true\n") } - // Inline run: callback — emit a cascade-owned job with an inline run: step + // Inline run: callback. Emit a cascade-owned job with an inline run: step // instead of a jobs..uses reusable-workflow call. Standard inputs reach // the step as env: variables rather than reusable-workflow with: inputs. if info.Run != "" { @@ -1434,7 +1434,7 @@ func (g *Generator) writeManifestUpdateStep(sb *strings.Builder, sorted []string } // Add env vars for build artifact IDs. Only emitted when the build - // workflow declares an `artifact_id` output — the generator discovers + // workflow declares an `artifact_id` output. The generator discovers // this via discoverOutputsAndInputs. When present, finalize captures // the immutable identifier (e.g., a Docker image digest) so it can be // stored in state and later passed to the publish callback on release. diff --git a/internal/generate/generator_test.go b/internal/generate/generator_test.go index ecba9b03..51758581 100644 --- a/internal/generate/generator_test.go +++ b/internal/generate/generator_test.go @@ -216,7 +216,7 @@ func TestGenerator_CallbackTimeoutMinutes(t *testing.T) { // TestGenerator_CallbackTimeoutOmittedWhenZero asserts no timeout-minutes is // emitted on a reusable-workflow callback (jobs..uses) when its -// timeout_minutes is unset — those callers own their own timeout. Cascade-owned +// timeout_minutes is unset; those callers own their own timeout. Cascade-owned // jobs (setup/finalize) still receive the owned-job default (#37), so the check // is scoped to the callback job block, not the whole workflow. func TestGenerator_CallbackTimeoutOmittedWhenZero(t *testing.T) { @@ -1389,7 +1389,7 @@ on: } // ============================================================================= -// Publish callback (#39) — artifact_id tracking in orchestrate finalize +// Publish callback (#39): artifact_id tracking in orchestrate finalize // ============================================================================= func TestGenerator_BuildArtifactIDTracked(t *testing.T) { @@ -1603,7 +1603,7 @@ func TestGenerator_ExtraTriggers_MergeGroup(t *testing.T) { require.NoError(t, err) assert.Contains(t, result, " merge_group:\n", "merge_group trigger must be emitted when MergeGroup is non-nil") - // Lane behavior is a separate issue — no merge_queue: config involved here. + // Lane behavior is a separate issue; no merge_queue: config involved here. assert.NotContains(t, result, "merge_queue:", "lane behavior config must not appear from trigger emission alone") } @@ -1712,7 +1712,7 @@ func TestGenerator_BuildMatrix_MaxParallelOmittedWhenZero(t *testing.T) { Triggers: []string{"src/**"}, Matrix: &config.MatrixConfig{ Dimensions: map[string][]string{"os": {"linux"}}, - // MaxParallel zero and FailFast nil — neither should appear. + // MaxParallel zero and FailFast nil: neither should appear. }, }, }, @@ -1783,7 +1783,7 @@ func TestGenerator_PassthroughArtifact_InlineUpload(t *testing.T) { // The path must match the declared upload path. assert.Contains(t, result, "path: dist/", "upload step must set path to artifact.upload value") - // No download step — this build has no downloads configured. + // No download step; this build has no downloads configured. assert.NotContains(t, result, "uses: actions/download-artifact@v4", "build without artifact.downloads must not emit download-artifact step") } @@ -1860,7 +1860,7 @@ func TestGenerator_PassthroughArtifact_NoArtifactNoSteps(t *testing.T) { // (no release config is set, so the finalize job emits no artifact steps either). assert.NotContains(t, result, "actions/upload-artifact@v4", "build without artifact: must not emit upload-artifact") - // download-artifact only appears in finalize when HasReleaseArtifacts — it is + // download-artifact only appears in finalize when HasReleaseArtifacts. It is // absent here because no release artifacts are declared and no passthrough is set. assert.NotContains(t, result, "actions/download-artifact@v4", "build without artifact: must not emit download-artifact") diff --git a/internal/generate/graph.go b/internal/generate/graph.go index 5439772e..eb13d700 100644 --- a/internal/generate/graph.go +++ b/internal/generate/graph.go @@ -14,7 +14,7 @@ type DependencyGraph struct { // OptionalEdges holds optional_depends_on edges (job ID -> dependencies as // job IDs). Optional deps add to a job's needs: for ordering but do NOT - // contribute a skip-gate to its if: condition — the job still runs when an + // contribute a skip-gate to its if: condition. The job still runs when an // optional dep was skipped because its triggers didn't match (#18). OptionalEdges map[string][]string } diff --git a/internal/generate/input_passthrough_test.go b/internal/generate/input_passthrough_test.go index 6a739509..63886c7c 100644 --- a/internal/generate/input_passthrough_test.go +++ b/internal/generate/input_passthrough_test.go @@ -12,7 +12,7 @@ import ( ) // TestOrchestrateInputExpressionPassthrough verifies that operator-authored -// inputs survive into the orchestrate callback with: block — vars.* +// inputs survive into the orchestrate callback with: block. Vars.* // passthrough expressions emit verbatim, literals emit as-is, and matrix.* // placeholders are not treated as passthrough. func TestOrchestrateInputExpressionPassthrough(t *testing.T) { diff --git a/internal/generate/job_attributes_test.go b/internal/generate/job_attributes_test.go index d7a58d0d..9b2523c9 100644 --- a/internal/generate/job_attributes_test.go +++ b/internal/generate/job_attributes_test.go @@ -264,7 +264,7 @@ func TestInlineRunOmittedAttributesUnchanged(t *testing.T) { // TestReusableWorkflowCallbackHasNoJobAttributes asserts that a reusable // workflow: callback (jobs..uses) never carries runs-on / permissions / -// concurrency on the job — GHA forbids them there and schema validation rejects +// concurrency on the job. GHA forbids them there and schema validation rejects // runs_on/concurrency on reusable callbacks. Only permissions is structurally // accepted on the config, but the generator must not emit any of the three on a // uses: job. diff --git a/internal/generate/job_control_test.go b/internal/generate/job_control_test.go index 0323a744..3837c69e 100644 --- a/internal/generate/job_control_test.go +++ b/internal/generate/job_control_test.go @@ -89,7 +89,7 @@ func TestGenerator_OwnedJobTimeoutConfigurable(t *testing.T) { } // TestGenerator_TimeoutNotOnReusableCallback asserts a reusable-workflow -// callback (jobs..uses) does NOT receive the owned-job timeout — the called +// callback (jobs..uses) does NOT receive the owned-job timeout. The called // workflow owns its own timeout. Inline run: callbacks, which are cascade-owned, // DO get it. func TestGenerator_TimeoutNotOnReusableCallback(t *testing.T) { @@ -100,9 +100,9 @@ func TestGenerator_TimeoutNotOnReusableCallback(t *testing.T) { TrunkBranch: "main", Environments: []string{"dev"}, Builds: []config.BuildConfig{ - // Reusable-workflow callback (uses:) — no timeout-minutes. + // Reusable-workflow callback (uses:): no timeout-minutes. {Name: "reusable", Workflow: ".github/workflows/build.yaml", Triggers: []string{"src/**"}}, - // Inline run: callback — cascade-owned, gets the default. + // Inline run: callback: cascade-owned, gets the default. {Name: "inline", Run: "go test ./...", Triggers: []string{"src/**"}}, }, } @@ -177,7 +177,7 @@ func TestGenerator_OptionalDependsOnAddsNeedsWithoutGating(t *testing.T) { assert.Regexp(t, `needs:.*build-app`, deploy, "hard dep in needs:") assert.Regexp(t, `needs:.*build-migrations`, deploy, "optional dep in needs: for ordering") - // if: must skip-gate on the HARD dep but NOT on the optional dep — a skipped + // if: must skip-gate on the HARD dep but NOT on the optional dep. A skipped // optional dep must not skip this job. assert.Contains(t, deploy, "needs.build-app.result == 'success'", "hard depends_on still contributes a skip-gate") diff --git a/internal/generate/merge_queue.go b/internal/generate/merge_queue.go index df168852..a5d4c9b6 100644 --- a/internal/generate/merge_queue.go +++ b/internal/generate/merge_queue.go @@ -13,8 +13,8 @@ import ( // workflow that validates the prospective trunk commit with cascade's own // logic: it runs `cascade parse-config` as a validity gate and a dry-run // `cascade orchestrate setup` to preview the build/deploy decisions against the -// merge-group candidate ref. The lane is read-only — no state writes, no -// releases, no deploys — and reports a status the merge queue can require. +// merge-group candidate ref. The lane is read-only (no state writes, no +// releases, no deploys) and reports a status the merge queue can require. // // This generator owns the LANE behavior. The raw merge_group trigger itself is // expressible separately under extra_triggers.merge_group; the two are diff --git a/internal/generate/promote.go b/internal/generate/promote.go index 0f373fb5..3237de35 100644 --- a/internal/generate/promote.go +++ b/internal/generate/promote.go @@ -748,7 +748,7 @@ func (g *PromoteGenerator) writeDeployJobs(sb *strings.Builder) { fmt.Fprintf(sb, " deploy-%s:\n", d.Name) if d.Run != "" { - // Inline run: deploy callback — cascade-owned job with an inline run: + // Inline run: deploy callback. This is a cascade-owned job with an inline run: // step. Inline callbacks declare their inputs via the manifest inputs: // keys (no reusable-workflow with: matrix); the standard environment/ // sha/image_tag inputs reach the step as env: vars. @@ -761,7 +761,7 @@ func (g *PromoteGenerator) writeDeployJobs(sb *strings.Builder) { } else { fmt.Fprintf(sb, " if: ${{ github.event.inputs.dry_run != 'true' && contains(fromJSON(needs.preflight.outputs.deploys_to_run), '%s') }}\n", d.Name) } - // environment: — wires the job to a GitHub Environment so that the + // environment: wires the job to a GitHub Environment so that the // environment's protection rules apply when gha_environment is configured // for any env. The target env is resolved at runtime by preflight. if anyEnvHasGHAConfig(g.config) { @@ -831,7 +831,7 @@ func (g *PromoteGenerator) writeDeployJobs(sb *strings.Builder) { } else { fmt.Fprintf(sb, " if: ${{ github.event.inputs.dry_run != 'true' && contains(fromJSON(needs.preflight.outputs.deploys_to_run), '%s') }}\n", d.Name) } - // environment: — wires the job to a GitHub Environment so that the + // environment: wires the job to a GitHub Environment so that the // environment's protection rules apply when gha_environment is configured // for any env. The target env is resolved at runtime by preflight. if anyEnvHasGHAConfig(g.config) { @@ -866,7 +866,7 @@ func (g *PromoteGenerator) writeDeployJobs(sb *strings.Builder) { } else { sb.WriteString(" if: ${{ github.event.inputs.dry_run != 'true' && needs.preflight.outputs.has_prod_deployment == 'true' }}\n") } - // environment: — the prod deploy job always targets a single known env + // environment: The prod deploy job always targets a single known env // (the final environment in the pipeline), so we can resolve the GitHub // Environment name statically from gha_environment when configured. if ec, ok := g.config.EnvironmentConfig[finalEnv]; ok && ec.GHAEnvironment != "" { @@ -1247,7 +1247,7 @@ func (g *PromoteGenerator) writeFinalizeJob(sb *strings.Builder) { fmt.Fprintf(sb, " GITHUB_TOKEN: %s\n", g.getReleaseTokenRef()) sb.WriteString(" TAG: ${{ steps.release-data.outputs.sem_version }}\n") sb.WriteString(" run: |\n") - sb.WriteString(" # Only dispatch on real GitHub — in act/gitea e2e environments\n") + sb.WriteString(" # Only dispatch on real GitHub. In act/gitea e2e environments\n") sb.WriteString(" # GITHUB_SERVER_URL is http://gitea:3000 and the Release workflow\n") sb.WriteString(" # doesn't exist, so skip silently.\n") sb.WriteString(" if [[ \"$GITHUB_SERVER_URL\" != \"https://github.com\" ]]; then\n") @@ -1297,7 +1297,7 @@ func (g *PromoteGenerator) writeFinalizeJob(sb *strings.Builder) { // // Each deploy job's conclusion is passed in as DEPLOY_RESULT_, derived // from `needs.deploy-.result`. finalize reads these env vars to know - // which deploys succeeded — more reliable than the legacy + // which deploys succeeded. More reliable than the legacy // `gh api ... /jobs` query, which can't reach the GitHub API in act/Gitea // test environments. sb.WriteString(" - name: Finalize Promotion\n") diff --git a/internal/generate/release.go b/internal/generate/release.go index 71a0cf41..63d382f8 100644 --- a/internal/generate/release.go +++ b/internal/generate/release.go @@ -401,8 +401,8 @@ func (g *ReleaseGenerator) writeFinalizeJob(sb *strings.Builder) { // Same retry-with-rebase pattern as the orchestrate finalize Update // Manifest step (#101). release.yaml is workflow_dispatch-only so the // race window is smaller than orchestrate's, but a concurrent - // orchestrate state push can still reject this push as non-fast-forward - // — see #102. + // orchestrate state push can still reject this push as non-fast-forward. + // See #102. sb.WriteString(" echo \"Updating latest_release state\"\n") sb.WriteString(" for attempt in 1 2 3 4 5; do\n") sb.WriteString(" git fetch origin \"$BRANCH\"\n") diff --git a/internal/generate/secrets_test.go b/internal/generate/secrets_test.go index 4df85a7a..44950c6f 100644 --- a/internal/generate/secrets_test.go +++ b/internal/generate/secrets_test.go @@ -172,7 +172,7 @@ func TestOrchestrateDeployCallbackJob_ExplicitSecretsMap(t *testing.T) { } // TestOrchestrateCallbackJob_InlineRunUnaffected verifies that an inline-run -// callback (run: ...) does not emit any secrets: key at all — inline jobs are +// callback (run: ...) does not emit any secrets: key at all; inline jobs are // cascade-owned, not reusable-workflow calls. func TestOrchestrateCallbackJob_InlineRunUnaffected(t *testing.T) { cfg := &config.TrunkConfig{ @@ -183,7 +183,7 @@ func TestOrchestrateCallbackJob_InlineRunUnaffected(t *testing.T) { Name: "smoke", Run: "go test ./...", Triggers: []string{"**/*.go"}, - // Secrets field set on an inline-run callback — must be ignored. + // Secrets field set on an inline-run callback; must be ignored. Secrets: &config.SecretsConfig{ Map: map[string]string{"SOME_TOKEN": "MY_TOKEN"}, }, @@ -195,7 +195,7 @@ func TestOrchestrateCallbackJob_InlineRunUnaffected(t *testing.T) { result, err := gen.Generate() require.NoError(t, err) - // Inline run: jobs are cascade-owned steps — no secrets: key of any form. + // Inline run: jobs are cascade-owned steps; no secrets: key of any form. assert.NotContains(t, result, "secrets:") } diff --git a/internal/orchestrate/orchestrator.go b/internal/orchestrate/orchestrator.go index 0012c2f7..3b2291de 100644 --- a/internal/orchestrate/orchestrator.go +++ b/internal/orchestrate/orchestrator.go @@ -384,11 +384,11 @@ func (o *Orchestrator) calculateVersion() (string, error) { // // 1. Multi-env intermediate: next env's state SHA (e.g., dev's changelog // is "what's new vs test"). Preserves the existing per-env progression -// model — the changelog shows what's about to be promoted forward. +// model. The changelog shows what's about to be promoted forward. // 2. Last published release: state["release"].SHA, falling back to // latest_release.SHA. This is the right base whenever there's no -// "next env" to compare against — i.e., no-env library/CLI projects -// after their first publish, OR the terminal env in a multi-env list. +// "next env" to compare against (i.e., no-env library/CLI projects +// after their first publish, OR the terminal env in a multi-env list). // Without this, a freshly-published-then-orchestrated repo would // compute a changelog covering its entire git history (cf. #80). // 3. Initial commit: only when nothing has been released yet (truly the diff --git a/internal/orchestrate/orchestrator_test.go b/internal/orchestrate/orchestrator_test.go index 855952bb..e6dad8c2 100644 --- a/internal/orchestrate/orchestrator_test.go +++ b/internal/orchestrate/orchestrator_test.go @@ -291,7 +291,7 @@ func TestNewOrchestratorInvalidYAML(t *testing.T) { // 2. No-env or last env → state["release"] (or latest_release) // 3. Nothing released → initial commit (git fallback, not exercised here) // -// The git fallback isn't exercised — it requires a real repo and the rest of +// The git fallback isn't exercised; it requires a real repo and the rest of // the priority ladder is what we're really fixing. func TestCalculateChangelogRefs(t *testing.T) { tests := []struct { diff --git a/internal/promote/auto_commit_sha_test.go b/internal/promote/auto_commit_sha_test.go index fd3646b1..90a2d422 100644 --- a/internal/promote/auto_commit_sha_test.go +++ b/internal/promote/auto_commit_sha_test.go @@ -98,7 +98,7 @@ func TestFinalizer_NoAutoCommits_KeepsPromotionSHA(t *testing.T) { Version: "v1.0.0-2", }}, }) - // No SetHeadSHA call — standard path. + // No SetHeadSHA call; standard path. require.NoError(t, fin.Run()) @@ -144,7 +144,7 @@ func TestFinalizer_AutoCommits_EmptySHA_FallsBackToPromotion(t *testing.T) { Version: "v1.0.0-3", }}, }) - fin.SetHeadSHA("") // explicit empty — must behave like unset + fin.SetHeadSHA("") // explicit empty: must behave like unset require.NoError(t, fin.Run()) diff --git a/internal/promote/finalize.go b/internal/promote/finalize.go index bf0a0682..e62e7370 100644 --- a/internal/promote/finalize.go +++ b/internal/promote/finalize.go @@ -155,7 +155,7 @@ func (f *Finalizer) updateState() { for _, promo := range f.promotionResult.Promotions { if promo.Environment == "" || promo.Environment == "release" { - // Skip the release marker — it tracks publish state, not deploys. + // Skip the release marker; it tracks publish state, not deploys. continue } if promo.SHA == "" { diff --git a/internal/promote/preflight.go b/internal/promote/preflight.go index 5f33c509..fb96b8a0 100644 --- a/internal/promote/preflight.go +++ b/internal/promote/preflight.go @@ -260,9 +260,9 @@ func (p *Preflighter) detectDeployChanges(sourceSHA, targetEnv string) ([]string // Get target deploy state. Per-deploy state is the most precise // comparison point ("when was this specific deploy last run for this - // env"). When unavailable — typical for first-promotion scenarios + // env"). When unavailable (typical for first-promotion scenarios // where the env has been promoted to some SHA but no deploy has run - // yet — fall back to the env-level SHA. That captures the semantic + // yet), fall back to the env-level SHA. That captures the semantic // "if the env is at SHA X, treat the deploy as having seen X" so // trigger filters still apply rather than unconditionally including. var targetSHA string @@ -276,7 +276,7 @@ func (p *Preflighter) detectDeployChanges(sourceSHA, targetEnv string) ([]string } // If neither per-deploy nor env-level state exists, include - // unconditionally — this is a never-deployed env. + // unconditionally: this is a never-deployed env. if targetSHA == "" { localDeploys = append(localDeploys, d.Name) continue @@ -493,7 +493,7 @@ func (p *Preflighter) getEnvCurrentSHA(env string) string { // (baseSHA, headSHA] is a breaking change per the conventional-commit spec // (`feat!:`, `fix!:`, or any commit body containing `BREAKING CHANGE:`). // -// On any failure to read git history, returns false (fail-open) — the gate +// On any failure to read git history, returns false (fail-open). The gate // is a guardrail, not a hard correctness check, and surfacing parse errors // at preflight time would make the CLI brittle in environments with shallow // or partially fetched histories. diff --git a/internal/promote/promote.go b/internal/promote/promote.go index 87e46e3d..a2b954ea 100644 --- a/internal/promote/promote.go +++ b/internal/promote/promote.go @@ -134,7 +134,7 @@ func (p *Promoter) defaultPromotion() (*PromotionResult, error) { return p.noEnvironmentPromotion() } - // Snapshot current state — include "release" since it's a virtual env + // Snapshot current state; include "release" since it's a virtual env // in the chain but isn't necessarily in the env list. preState := make(map[string]*config.EnvState) for _, env := range envs { @@ -153,7 +153,7 @@ func (p *Promoter) defaultPromotion() (*PromotionResult, error) { var skippedEnvs []string // Determine prerelease and prod positions. The implicit chain is - // [envs..., "release", prodEnv] — "release" is a virtual env between + // [envs..., "release", prodEnv]. "release" is a virtual env between // the prerelease env and prod where the publish action lands. Each // `mode: default` invocation advances state through the chain by one // logical step, stopping at the publish boundary. @@ -174,14 +174,14 @@ func (p *Promoter) defaultPromotion() (*PromotionResult, error) { } // Sequential walk through env pairs. Stops at the publish boundary - // (sourceEnv == prereleaseEnv && targetEnv == prodEnv) — that crossing + // (sourceEnv == prereleaseEnv && targetEnv == prodEnv): that crossing // produces a "release" promotion that advances state[release] only; // state[prodEnv] is advanced by a subsequent default-mode invocation. for i := 0; i < len(envs)-1; i++ { sourceEnv := envs[i] targetEnv := envs[i+1] - // Skip "release" as a target if it's literally in the env list — the + // Skip "release" as a target if it's literally in the env list. The // release marker is advanced via the publish path, not as a normal env. if targetEnv == "release" { continue @@ -208,7 +208,7 @@ func (p *Promoter) defaultPromotion() (*PromotionResult, error) { // (state marker + publish action). State[prodEnv] is left untouched; // a subsequent default-mode invocation handles release→prod. if sourceEnv == prereleaseEnv && targetEnv == prodEnv { - // Skip if state[release] is already at sourceState.SHA — the + // Skip if state[release] is already at sourceState.SHA. The // publish was completed by a prior invocation. Without this, a // repeat default-mode call would re-emit a no-op publish promo // instead of falling through to the release→prod fallback below. @@ -315,7 +315,7 @@ func (p *Promoter) defaultPromotion() (*PromotionResult, error) { // Fallback: if the env walk produced nothing AND state[release] is ahead // of state[prodEnv], deploy release → prod. This is the second step of - // the two-step "publish then deploy" flow — runs only when no upstream + // the two-step "publish then deploy" flow. Runs only when no upstream // advance is pending. Order matters: if uat is ahead of release, we // publish first; if release is ahead of prod and there's no upstream // work, we deploy. (When "release" is in the env list literally, the @@ -533,7 +533,7 @@ func (p *Promoter) cascadePromotion(target string) (*PromotionResult, error) { // Build promotions for envs[sourceIdx+1..targetIdx]. Materialize "release" // as its own promotion either when it's in the env list or when crossing // into prodEnv from prereleaseEnv (publish boundary). The release - // promotion advances state[release] only — no deploy. + // promotion advances state[release] only; no deploy. var promotions []EnvPromotion for i := sourceIdx + 1; i <= targetIdx; i++ { env := envs[i] diff --git a/mkdocs.yml b/mkdocs.yml index fda052ad..f0ef02f0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,6 +1,6 @@ site_name: Cascade site_description: >- - Manifest-driven release orchestration for GitHub Actions — coordinate + Manifest-driven release orchestration for GitHub Actions: coordinate builds, promotions, and releases across any environment topology. site_url: https://stablekernel.github.io/cascade/ repo_url: https://github.com/stablekernel/cascade @@ -61,6 +61,7 @@ markdown_extensions: permalink: true nav: + - Home: index.md - Getting Started: getting-started.md - Manifest Reference: configuration.md - Callback Contract: callback-contract.md