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
82 changes: 82 additions & 0 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,88 @@ cascade promote finalize \
| `--run-id` | string | No | Workflow run ID for job query |
| `--commit-push` | bool | No | Commit and push state changes |

### hotfix

Apply a trunk commit onto an environment pinned to an older base. A hotfix targets one environment on its `env/<env>` integration branch. The fix must already be on trunk; cascade refuses to apply a commit that is not an ancestor of trunk tip. The subcommands compute and validate the hotfix and write its final state; the cherry-pick, build, and deploy run in the generated `cascade-hotfix.yaml` workflow. See the Hotfix section of [workflows.md](workflows.md) for the full flow.

#### hotfix plan

Validate a hotfix request and compute the integration-branch plan. It enforces, in order: trunk ancestry of the fix, target-environment eligibility (a configured environment that is not the first; prod is allowed), no-op detection when the fix is already in the target, the single-flight open-pull-request gate, and `env/<env>` branch reconciliation. With `--dry-run` nothing is mutated (the env branch is planned but not created).

```bash
cascade hotfix plan \
--commit abc1234 \
--target-env test \
--gha-output
```

##### Flags

| Flag | Type | Required | Description |
|------|------|----------|-------------|
| `--config`, `-c` | string | No | Path to manifest file (default: `.github/manifest.yaml`) |
| `--key` | string | No | Top-level manifest key (default: `ci`) |
| `--commit` | string | Yes | Trunk commit (SHA or ref) carrying the fix |
| `--target-env` | string | Yes | Environment to hotfix |
| `--actor` | string | No | Actor recorded on the plan (default: `$GITHUB_ACTOR`) |
| `--remote` | string | No | Git remote env branches live on (default: `origin`) |
| `--repo` | string | No | `owner/repo` for single-flight pull-request lookup via `gh` (default: skip the check) |
| `--dry-run` | bool | No | Compute the plan without mutating anything |
| `--json` | bool | No | Output the plan as JSON |
| `--gha-output` | bool | No | Write outputs to `$GITHUB_OUTPUT` for workflow consumption |

##### Output

With `--json`:

```json
{
"target_env": "test",
"fix_sha": "abc1234...",
"branch": "env/test",
"base_sha": "def5678...",
"no_op": false,
"branch_created": true,
"hotfix_version_candidate": "v1.4.0-rc.2.hotfix.1",
"conflict_expected": false,
"protection_suggestions": ["gh api -X PUT repos/{owner}/{repo}/branches/env/test/protection ..."],
"dry_run": false
}
```

The GHA output writes `target_env`, `fix_sha`, `branch`, `base_sha`, `no_op`, `branch_created`, `hotfix_version_candidate`, `conflict_expected`, `dry_run`, and the `protection_suggestions` commands (as JSON and as multiline text).

#### hotfix finalize

Write the diverged state, tag, and release for a merged hotfix. Run after the resolution pull request merges and the build and deploy succeed. It cross-checks the merge SHA against the `env/<target>` branch tip, allocates the next free hotfix version, snapshots the prior state into the rollback ring, writes the divergence fields and substates, commits the manifest to trunk with the rebase-retry push, and creates the hotfix tag and release object. The verb is idempotent on identical inputs: a rerun after the state already records the merge SHA is a no-op.

```bash
cascade hotfix finalize \
--target-env test \
--merge-sha 1234abc \
--fix-sha abc1234 \
--base-sha def5678 \
--build-result app=success \
--deploy-result app=success
```

##### Flags

| Flag | Type | Required | Description |
|------|------|----------|-------------|
| `--config`, `-c` | string | No | Path to manifest file (default: `.github/manifest.yaml`) |
| `--key` | string | No | Top-level manifest key (default: `ci`) |
| `--target-env` | string | Yes | Environment to finalize |
| `--merge-sha` | string | Yes | Tip of `env/<target>` after the resolution pull request merged |
| `--fix-sha` | string | Yes | Trunk commit the hotfix carries |
| `--base-sha` | string | Yes | Trunk anchor the integration branch diverged from |
| `--actor` | string | No | Actor recorded on the state (default: `$GITHUB_ACTOR`) |
| `--dry-run` | bool | No | Validate and compute without writing state, tags, or releases |
| `--build-result` | string | No | Build result as `name=result` (repeatable) |
| `--deploy-result` | string | No | Deploy result as `name=result` (repeatable) |

Only successful build and deploy results update the per-build and per-deploy substates. For a prerelease-environment target the hotfix release is promoted to a GitHub prerelease, superseding that environment's current prerelease object; for other environments it stays a draft.

### next-version

Calculate the next semantic version.
Expand Down
32 changes: 32 additions & 0 deletions docs/versioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,38 @@ When cascade reaches v1.0 the following guarantees apply:
Older tags outside the current release line do not receive backported fixes.
See [SECURITY.md](https://github.com/stablekernel/cascade/blob/main/SECURITY.md) for the security-patch policy.

## Hotfix version segment

A hotfix applies a single trunk commit onto an environment pinned to an older trunk base (see the Hotfix section of [workflows.md](workflows.md)). The version cascade allocates for a hotfix depends on whether the environment's current version is still in flight (an rc) or already published.

### rc-based (unpublished) base

When the environment holds an rc version, the hotfix appends a nested `hotfix.M` segment:

```
v1.4.0-rc.2 -> v1.4.0-rc.2.hotfix.1 (first hotfix)
v1.4.0-rc.2.hotfix.1 -> v1.4.0-rc.2.hotfix.2 (second hotfix, stacked)
```

The dotted form is deliberate. Under semver precedence the pre-release field list for `v1.4.0-rc.2.hotfix.1` is `["rc", "2", "hotfix", "1"]`, which sorts strictly above `rc.2` and strictly below `rc.3`:

```
v1.4.0-rc.2 < v1.4.0-rc.2.hotfix.1 < v1.4.0-rc.2.hotfix.2 < v1.4.0-rc.3
```

A hotfix version therefore slots cleanly between its base rc and the next rc, and it never collides with the orchestrator's rc sequence. The rc-shaped tag and draft cleanup logic matches only `vX.Y.Z-rc.N`, so it is inert on hotfix tags; hotfix tags and drafts are cleaned up explicitly when the divergence ends.

### Published (no rc) base

When the environment holds a published version with no rc segment (for example `v1.3.0`), a hotfix is a **normal patch bump**, not a `-hotfix.M` shape:

```
v1.3.0 -> v1.3.1 (first hotfix)
v1.3.1 -> v1.3.2 (next free patch)
```

cascade allocates the next free patch by reconciling against existing tags, so the hotfix does not collide with a patch the normal release flow may also mint. There is no `vX.Y.Z-hotfix.M` form; the nested `hotfix.M` segment applies only to rc-based, still-in-flight versions.

## Version bump reference

| Change type | CLI semver impact | `schema_version` impact |
Expand Down
76 changes: 75 additions & 1 deletion docs/workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,81 @@ 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 tracked in issue #94 (direct promotion to prod with branch ancestry checks).
A hotfix applies a single trunk commit onto an environment that is pinned to an older trunk base, without dragging in the intervening commits. This is the case the standard promote flow cannot serve: promoting a pointer forward would advance the target environment past every commit between its base and the fix, which is exactly what an operator pinning that environment is trying to avoid.

### Roll forward on trunk first (the default)

The fix always lands on trunk first. cascade refuses to apply a commit that is not already an ancestor of trunk tip, so a hotfix never introduces a commit that exists only on a side branch. If the intervening commits between an environment's base and the fix are acceptable, the simplest answer is to merge the fix to trunk and run a normal cascade promotion: the target environment advances to a trunk SHA and nothing diverges. Reach for the hotfix workflow only when the environment must run `base + fix` and nothing else.

### Per-environment integration branches

When an environment genuinely needs to diverge, the hotfix is staged on a per-environment integration branch named `env/<env>` (for example `env/test`). The branch does not exist while an environment tracks trunk; it is created on demand at the environment's recorded state SHA. The cherry-pick of the fix is staged on a working branch `hotfix/<env>/<short-sha>` whose base is `env/<env>`, and a resolution pull request is opened with base `env/<env>`.

While an environment is diverged its state carries three additional fields, all additive and absent for environments that track trunk:

```yaml
state:
test:
sha: <merge SHA on env/test> # now possibly a non-trunk SHA
version: v1.4.0-rc.2.hotfix.1 # hotfix version segment
ref: env/test # the integration branch
base_sha: <trunk SHA> # the trunk anchor of the divergence
patches: [<sha of fix>, ...] # trunk commits applied on top
```

`cascade status` surfaces `ref`, `base_sha`, and `patches` only when they are set.

### Cherry-pick and resolution pull request

A clean cherry-pick opens a pull request labeled `cascade-hotfix` with auto-merge enabled. The required checks configured on `env/<env>` gate the merge, and the pull request is the audit record even when no human touches it.

On conflict, the conflicted tree is committed with its conflict markers intact, the branch is pushed, and the pull request is opened labeled `cascade-hotfix-conflict`. Committing the markers makes the resolution pull request a real, checkout-able branch: the diff shows exactly where the conflict is, and a human resolves it locally by force-pushing the head branch.

```
git fetch && git switch hotfix/<env>/<short-sha>
# resolve conflicts, then
git push --force-with-lease
```

The pushed resolution re-runs the checks, which unblock the merge. The pull request body also carries a machine-readable trailer block so the post-merge stages do not depend on branch-name parsing alone:

```
Cascade-Hotfix-Target: test
Cascade-Hotfix-Source: <fix SHA>
Cascade-Hotfix-Base: <base SHA>
```

When a conflict is resolved by hand, the resolution on `env/<env>` and the original fix on trunk can differ. Trunk's version wins long term: the divergence is discarded, not merged back. If the manual resolution embodies a real improvement, it needs its own trunk pull request; merging `env/<env>` back to trunk is wrong because it would introduce merge commits into a history that every SHA comparison in cascade assumes moves forward.

### Rejoin and cleanup

The divergence ends the next time the environment receives a normal promotion. Promote preflight verifies that the incoming trunk SHA contains every recorded patch (the regression gate). On success the divergence fields are cleared, the `env/<env>` branch is deleted, and the hotfix tags and release objects for that base are cleaned up. Promotion is refused from a diverged environment, and promoting an older trunk SHA that would drop a recorded patch is blocked unless explicitly forced with a loud annotation.

### Generated `cascade-hotfix.yaml` workflow

`cascade generate-workflow` emits `cascade-hotfix.yaml` for any repository that declares two or more environments. With a single environment there is no intermediate target to hotfix onto, so nothing is emitted.

The workflow carries two triggers in one file:

- `workflow_dispatch` with inputs `commit` (the trunk fix SHA), `target_env` (a choice over every configured environment except the first), `pr_number` (optional, to replay an existing resolution pull request), and `dry_run`.
- `pull_request` on `types: [closed]` against `branches: ['env/*']`, with the post-merge stages gated on the pull request having merged and carrying the `cascade-hotfix` label.

Its jobs:

| Job | Trigger | Role |
| --- | --- | --- |
| plan | dispatch | Fetch env branches and tags, run `cascade hotfix plan`, surface branch-protection suggestions as `::notice::` lines |
| apply | dispatch (not dry-run) | Cherry-pick onto `hotfix/<env>/<sha>`, open the resolution pull request (clean auto-merges; conflict opens the labeled resolution pull request) |
| check | open pull request to `env/*` | Validate the manifest while the hotfix pull request is open |
| build | merged hotfix | Build the merge SHA, since a cherry-picked commit has no prebuilt artifact |
| deploy | merged hotfix | Deploy to the target environment, paired with a rollback job mirroring the promote workflow |
| finalize | all deploys succeed | Run `cascade hotfix finalize` to write the diverged state, tag, and release |

Prod is a valid hotfix target. The deploy job binds to the GitHub `environment:` of the target environment, so organization protection rules (manual approval, required reviewers) apply to the hotfix deploy exactly as they do to a normal promotion. This is one mechanism, not a separate prod path.

Branch protection on `env/*` is the operator's responsibility: cascade never creates protection rules itself, because it does not assume an admin token. When no required status checks are configured on the target `env/*` branch, the workflow **warns** rather than blocks, and the `plan` verb prints ready-to-run `gh` and `gh api` command suggestions an operator can paste to put the protections in place.

> The `rollback_sha` output in the generated workflow is a disclosed placeholder today: the deploy and rollback jobs mirror the promote workflow's shape, and the rollback path activates once a CLI output supplies the prior SHA.

## Workflow Permissions

Expand Down
Loading