Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build-cli.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
run: |
./cascade version
./cascade --help
./cascade parse-config --help
./cascade lint --help
./cascade detect-changes --help
./cascade generate-changelog --help
./cascade generate-workflow --help
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ cascade holds to a few conventions in its own codebase and in the workflows it g

- **Additive manifest changes**: new fields are always optional with sensible defaults, so existing manifest files keep working across minor version bumps.
- **Path fields reach every path sink**: a manifest field that widens which files a component reacts to must thread through all three places a path is consumed, or it is a silent bug. The emitted `on: push` paths filter fires the workflow, per-callback change detection decides which builds and deploys run, and the version commit range decides the bump. A field that reaches only some of these triggers a run that then no-ops, or bumps a version whose builds skip as unchanged. When you add such a field, add a test that asserts the shared path reaches each sink.
- **A breaking generator or validation change moves with the fleet, in the same change**: a change that makes a previously valid manifest invalid, such as rejecting a field `parse-config` used to accept, is breaking even when it ships as a `fix:`. The fleet repin re-stamps every example repository onto the release-candidate binary and regenerates its workflows before any suite runs, so a manifest still carrying the now-rejected shape fails that repin, not the intended test. Before landing a validation change that can reject something that used to pass, scan the fleet example repos for that shape and migrate any that use it in the same pull request, alongside the doc's migration note. This generalizes the existing rule that a fleet suite and the eligibility logic it exercises are one coupled unit (see [Making a change](#making-a-change)); it applies to validation, not only to eligibility.
- **A breaking generator or validation change moves with the fleet, in the same change**: a change that makes a previously valid manifest invalid, such as rejecting a field `lint` used to accept, is breaking even when it ships as a `fix:`. The fleet repin re-stamps every example repository onto the release-candidate binary and regenerates its workflows before any suite runs, so a manifest still carrying the now-rejected shape fails that repin, not the intended test. Before landing a validation change that can reject something that used to pass, scan the fleet example repos for that shape and migrate any that use it in the same pull request, alongside the doc's migration note. This generalizes the existing rule that a fleet suite and the eligibility logic it exercises are one coupled unit (see [Making a change](#making-a-change)); it applies to validation, not only to eligibility.
- **Every generated workflow kind carries executing coverage**: each workflow the generator emits (`orchestrate`, `promote`, `external-update`, and the `cascade-` lanes) is mapped to the e2e scenarios and fleet lanes that run it in `internal/coverage/registry.yaml`. The coverage gate derives the emitted kinds straight from the generator source and fails when an emitted kind has no registry entry, so a new generated workflow cannot ship without a scenario or lane that exercises it. When you add a generated workflow kind, add its entry pointing at the scenario or fleet lane that runs it; a referenced scenario or lane that does not exist also fails the gate.
- **Assert a runtime outcome, never the script that produces it**: an `e2e/` or fleet-suite assertion for a load-bearing behavior must compare against a runtime artifact that differs when the behavior regresses: a state leaf (`state.<env>` sha/version/ref), a job conclusion (`success`/`skipped`/`failure`), a preflight output, a tag, a release, a branch, a pull request, or a line the running job actually logged (`expect_log`). A passing assertion must be reachable only by the behavior working at run time. Never assert a behavior by grepping emitted script source for a marker that also appears in that source: a `workflow_files.contains`/`not_contains` check over a generated `.yaml` proves a string was rendered, not that the logic ran, and stays green when the runtime behavior is deleted because the marker text is still literally present in the file. As a cautionary example, the state-write retry loop was once "covered" by grepping the emitted `orchestrate.yaml` for `cascade-state-write: exhausted attempts=10`. That branch never runs on the happy path, yet the string is always present in the script, so the check was unconditionally green and a regression that replaced the whole loop with a single `git push` would have shipped green; the fix asserts the marker the running job emits (`cascade-state-write: ok attempt=1`) and proves it red-able by breaking the emission. Restrict `workflow_files` checks to behaviors whose entire effect is the generated shape (a `concurrency:` block, a `timeout-minutes:` value, a real-GitHub-only step act cannot execute) and label those scenarios generation-only in the header. When a behavior is genuinely un-runnable in act, its executing proof lives on the fleet, and a generation-only e2e cell is a ceiling that must be labeled as such, never credited as runtime coverage. The bar: a generator or behavior change adds or updates an assertion a reviewer can turn red by reverting the behavior alone, leaving the emitted string in place.
- **Callback isolation**: generated workflows call your workflows via `workflow_call`, and cascade never reaches into your callback logic.
Expand Down
14 changes: 7 additions & 7 deletions cmd/cascade/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func TestHelpCommand(t *testing.T) {
}
}

func TestParseConfigCommand(t *testing.T) {
func TestLintCommand(t *testing.T) {
// Create a temporary config file
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "trunk-config.yaml")
Expand All @@ -104,9 +104,9 @@ func TestParseConfigCommand(t *testing.T) {
t.Fatalf("Failed to write config file: %v", err)
}

stdout, stderr, err := runCLI("parse-config", "--config", configPath)
stdout, stderr, err := runCLI("lint", "--json", "--config", configPath)
if err != nil {
t.Fatalf("parse-config command failed: %v\nstderr: %s", err, stderr)
t.Fatalf("lint --json command failed: %v\nstderr: %s", err, stderr)
}

// Verify JSON output
Expand Down Expand Up @@ -135,7 +135,7 @@ func TestParseConfigCommand(t *testing.T) {
}
}

func TestParseConfigCommand_InvalidConfig(t *testing.T) {
func TestLintCommand_InvalidConfig(t *testing.T) {
// Create an invalid config file
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "invalid-config.yaml")
Expand All @@ -149,7 +149,7 @@ builds: []
t.Fatalf("Failed to write config file: %v", err)
}

stdout, _, err := runCLI("parse-config", "--config", configPath)
stdout, _, err := runCLI("lint", "--json", "--config", configPath)
// The command should succeed but report validation errors
if err != nil {
t.Logf("Command returned error (expected for invalid config): %v", err)
Expand All @@ -170,8 +170,8 @@ builds: []
}
}

func TestParseConfigCommand_FileNotFound(t *testing.T) {
stdout, _, err := runCLI("parse-config", "--config", "/nonexistent/path/config.yaml")
func TestLintCommand_FileNotFound(t *testing.T) {
stdout, _, err := runCLI("lint", "--json", "--config", "/nonexistent/path/config.yaml")

// CLI returns JSON with valid=false for file errors
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion docs/public/manifest.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://stablekernel.github.io/cascade/manifest.schema.json",
"title": "cascade manifest",
"description": "Structure, types, and enums for a cascade manifest file. The manifest is nested under a top key (default \"ci\", configurable via config.manifest_key) and carries the authoring surface (config), plus cascade-managed promotion state (state, latest_release). This schema powers editor autocomplete and hover docs; cascade parse-config remains the authority for semantic and cross-field rules.",
"description": "Structure, types, and enums for a cascade manifest file. The manifest is nested under a top key (default \"ci\", configurable via config.manifest_key) and carries the authoring surface (config), plus cascade-managed promotion state (state, latest_release). This schema powers editor autocomplete and hover docs; cascade lint remains the authority for semantic and cross-field rules.",
"type": "object",
"additionalProperties": false,
"properties": {
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/guides/companions.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Enable it when more than one person edits the manifest, or when you want the man

## Merge queue: validate the merge-group candidate

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

```yaml
ci:
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/internals/coverage-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ not a coverage gap.
| `cascade init` and scaffold | unit plus CLI | `internal/initcmd`, `internal/scaffold` | The scaffold self-checks through the real generator before any file is written |
| `cascade verify` (drift) | unit plus every harness scenario | `internal/verify` | Committed workflows match manifest-regenerated bytes; drift exits non-zero |
| `cascade plan` (diff preview) | unit plus CLI | `33-plan-diff`, `internal/plan` | The per-file unified diff is produced without writing files |
| `parse-config`, `schema`, `next-version`, `detect-changes`, `generate-changelog` | unit | per-package tests | Pure logic: parsing, version calculation, change detection, changelog assembly |
| `lint`, `schema`, `next-version`, `detect-changes`, `generate-changelog` | unit | per-package tests | Pure logic: parsing, version calculation, change detection, changelog assembly |
| `cascade status` and `status consistency` | unit plus harness | `27-verify-orphan`, `internal/status` | State is reported and orphan env branches are flagged, and deleted on the remote with `--fix` |
| `branch-protection` and `environments` emit | unit plus CLI | `internal/branchprotection`, `internal/environments` | The JSON body and env config are emitted for the operator (applying them is GitHub-side) |

Expand Down
26 changes: 16 additions & 10 deletions docs/src/content/docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ These flags are available on every command.

Most commands (`generate-workflow`, `verify`, `plan`, `graph`, `status`, and the
promotion lifecycle commands) auto-detect the manifest at `.github/manifest.yaml` when
`--config` is not given. Two commands are the exception: `parse-config` and
`--config` is not given. Two commands are the exception: `lint` and
`detect-changes` default `--config` to the literal path `cicd-config.yaml` and do NOT
auto-detect. Pass `--config` explicitly to point either one at a different file.

Expand All @@ -42,7 +42,7 @@ auto-detect. Pass `--config` explicitly to point either one at a different file.
Commands are grouped by how often you reach for them:

- **Everyday**: `version`, `init`, `generate-workflow`, `verify`, `plan`, `status`, `graph`
- **Preview and inspect**: `simulate`, `parse-config`, `detect-changes`
- **Preview and inspect**: `simulate`, `lint`, `detect-changes`
- **Promotion lifecycle**: `orchestrate`, `promote`, `hotfix`, `rollback`
- **Releases and versioning**: `next-version`, `generate-changelog`, `manage-release`
- **Multi-repo**: `external`
Expand Down Expand Up @@ -414,27 +414,33 @@ Subcommand-specific flags: `promote` takes `--mode` (`default` or `cascade`) and
`--target`; `rollback` takes `--env` (required), `--to`, and `--deployable`; `hotfix` takes
`--env` (required), `--fix`, and `--merge-sha`; `release` takes only the shared flags.

### parse-config
### lint

Parse and validate the manifest and print it as JSON. This command defaults `--config` to
the literal `cicd-config.yaml` path and does NOT auto-detect `.github/manifest.yaml`; pass
Parse a manifest, validate it, and report any errors or warnings. Unknown or misspelled
keys are rejected with a "did you mean" suggestion, so a typo or a field pasted at the wrong
level is a hard error rather than a silently ignored line. By default the report is
human-readable and the command exits non-zero when the manifest is invalid; pass `--json` to
emit the parsed config and validation result (`valid`/`errors`/`warnings`) as JSON, the
surface the generated validation lanes consume. This command defaults `--config` to the
literal `cicd-config.yaml` path and does NOT auto-detect `.github/manifest.yaml`; pass
`--config` to point it at another file.

```bash
cascade parse-config --config .github/manifest.yaml
cascade lint --config .github/manifest.yaml
cascade lint --json --config .github/manifest.yaml
```

#### Flags

| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--config`, `-c` | string | `cicd-config.yaml` | Path to the manifest file (no auto-detection) |
| `--output`, `-o` | string | `json` | Output format (`json`) |
| `--json` | bool | `false` | Emit the parsed config and validation result as JSON |

### detect-changes

Determine which builds and deploys are triggered by file changes between two commits. Like
`parse-config`, this command defaults `--config` to the literal `cicd-config.yaml` path and
`lint`, this command defaults `--config` to the literal `cicd-config.yaml` path and
does NOT auto-detect `.github/manifest.yaml`.

```bash
Expand Down Expand Up @@ -1032,7 +1038,7 @@ cascade schema --output manifest.schema.json
| `--output`, `-o` | string | stdout | Write the schema to a file instead of stdout |

The same schema is published at
`https://stablekernel.github.io/cascade/manifest.schema.json`. `parse-config` remains the
`https://stablekernel.github.io/cascade/manifest.schema.json`. `lint` remains the
authority for semantic and cross-field rules; the schema covers structure, types, enums,
and hover docs.

Expand Down Expand Up @@ -1325,7 +1331,7 @@ Many commands accept `--json` (or `--gha-output` inside Actions) for machine-rea
Raise log verbosity with the global `--trace` flag:

```bash
cascade --trace parse-config --config .github/manifest.yaml
cascade --trace lint --config .github/manifest.yaml
```

Trace logs include:
Expand Down
14 changes: 7 additions & 7 deletions docs/src/content/docs/reference/manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Every field below lives under `ci.config` unless stated otherwise. The `ci.state

### Editor support

cascade ships a hand-authored JSON Schema. Registering it gives autocomplete, type checking, enum hints, and hover docs while you author the manifest. The schema covers structure, types, and enums; `cascade parse-config` remains the authority for semantic and cross-field rules.
cascade ships a hand-authored JSON Schema. Registering it gives autocomplete, type checking, enum hints, and hover docs while you author the manifest. The schema covers structure, types, and enums; `cascade lint` remains the authority for semantic and cross-field rules.

The schema is published at:

Expand Down Expand Up @@ -139,7 +139,7 @@ ci:

**Relationship to `tag_prefix`.** `tag_prefix` still sets the prefix on its own when
`tag_grammar` is absent. When both `tag_prefix` and `tag_grammar.prefix` are set,
`tag_grammar.prefix` wins, and `cascade parse-config` emits a non-fatal warning naming both
`tag_grammar.prefix` wins, and `cascade lint` emits a non-fatal warning naming both
keys so the redundancy is visible. Resolution is well defined either way; the warning is
advisory only.

Expand Down Expand Up @@ -677,7 +677,7 @@ ci:
| `workflow_run` | emitted | Wires the `workflow_run` trigger. |
| `merge_group` | rejected | Not allowed. `extra_triggers` attaches to the side-effecting orchestrate workflow, which cuts release tags, publishes releases, and runs deploys while writing state, so a speculative merge-queue build could publish a real release from a candidate commit. cascade rejects `extra_triggers.merge_group` at validate. To gate pull requests inside a merge queue, set [`merge_queue.enabled`](#merge_queue), which emits a read-only validation lane. |

Migrating from a manifest that set `extra_triggers.merge_group`: remove that entry and set `merge_queue.enabled: true` instead. The read-only merge-queue lane runs `cascade parse-config` and a dry-run `cascade orchestrate setup` against the queued candidate without cutting tags, publishing releases, or writing state.
Migrating from a manifest that set `extra_triggers.merge_group`: remove that entry and set `merge_queue.enabled: true` instead. The read-only merge-queue lane runs `cascade lint` and a dry-run `cascade orchestrate setup` against the queued candidate without cutting tags, publishing releases, or writing state.

### rollback

Expand Down Expand Up @@ -752,15 +752,15 @@ Every Deployments API step carries an `if: ${{ github.server_url == 'https://git

| Field | Status | Type | Default | Description |
|-------|--------|------|---------|-------------|
| `enabled` | emitted | bool | false | Emit `.github/workflows/cascade-validate.yaml`, a `pull_request` check that runs `cascade parse-config` and fails on an invalid manifest. |
| `enabled` | emitted | bool | false | Emit `.github/workflows/cascade-validate.yaml`, a `pull_request` check that runs `cascade lint` and fails on an invalid manifest. |

The check validates cascade's own configuration only, requests `contents: read` alone, and has no dry-run or comment side effects.

### merge_queue

| Field | Status | Type | Default | Description |
|-------|--------|------|---------|-------------|
| `enabled` | emitted | bool | false | Emit `.github/workflows/cascade-merge-queue.yaml`, a `merge_group`-triggered lane that runs `cascade parse-config` and a dry-run `cascade orchestrate setup` against the merge-group candidate. |
| `enabled` | emitted | bool | false | Emit `.github/workflows/cascade-merge-queue.yaml`, a `merge_group`-triggered lane that runs `cascade lint` and a dry-run `cascade orchestrate setup` against the merge-group candidate. |

The lane is read-only, which is exactly what a merge queue needs: it validates the queued candidate without cutting tags, publishing releases, or writing state. This is the supported way to participate in a merge queue. Attaching the raw `merge_group` event to the side-effecting orchestrate workflow through `extra_triggers.merge_group` is rejected at validate, because a speculative merge-queue build could otherwise publish a real release from a candidate commit.

Expand Down Expand Up @@ -870,7 +870,7 @@ just each component's `path`, byte-identical to before the fields existed.

### Validation rules

`cascade parse-config` rejects a `components:` block that breaks isolation:
`cascade lint` rejects a `components:` block that breaks isolation:

- Each component must set `path` (relative, no `..`) and `tag_prefix`.
- Component names must be identifier-safe.
Expand Down Expand Up @@ -1000,7 +1000,7 @@ The implicit `release` slot tracks the most recently published (non-draft) GitHu

## Validation rules

`cascade parse-config` enforces the semantic rules the schema alone cannot:
`cascade lint` enforces the semantic rules the schema alone cannot:

- `schema_version` should be `1`. Omitting it emits a warning.
- Environment, build, and deploy names must be identifier-safe (letters, digits, underscores). The generator-owned names `environment` and `dry_run` are reserved and cannot be used as `dispatch_inputs`.
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/reference/versioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ On load, the CLI applies these rules:
| above `CurrentSchemaVersion` | Rejected. The manifest needs a newer CLI; upgrade the `cli_version` pin. |
| negative | Rejected as invalid. |

A rejected manifest is fatal: the CLI reports the error and produces no workflows. A warning is non-fatal and surfaces on stderr and in the `warnings` field of `parse-config` JSON output.
A rejected manifest is fatal: the CLI reports the error and produces no workflows. A warning is non-fatal and surfaces on stderr and in the `warnings` field of `lint --json` output.

### Schema-version to CLI-version matrix

Expand Down
Loading