From 8b928f0e279340873dbfa229f1f1fc073f1f08d4 Mon Sep 17 00:00:00 2001 From: Joshua Temple Date: Tue, 23 Jun 2026 13:30:33 -0400 Subject: [PATCH 1/2] docs: describe multi-commit, multi-env hotfix chains Signed-off-by: Joshua Temple --- README.md | 7 ++++--- docs/src/content/docs/cli-reference.md | 25 ++++++++++++++++++++----- docs/src/content/docs/versioning.md | 2 +- docs/src/content/docs/workflows.md | 14 ++++++++++---- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 1f5becf5..75e8cb03 100644 --- a/README.md +++ b/README.md @@ -120,9 +120,10 @@ flowchart TD Most pipelines can only hotfix the tip, which in practice means production. cascade hotfixes **any** environment: -- It stages the fix on a per-environment integration branch. -- It deploys that one environment with a clean `-rc.N.hotfix.M` version. -- It rejoins trunk the next time a trunk SHA that already contains the fix is promoted. +- It stages the fix or set of fixes on a per-environment integration branch. +- A hotfix can carry a set of commits and elevate them across the chain up to the target environment. +- It deploys the diverged environments with a clean `-rc.N.hotfix.M` version. +- It rejoins trunk the next time a trunk SHA that already contains the fixes is promoted. The example below lands a fix on **staging** while dev, test, and prod stay exactly where they are. diff --git a/docs/src/content/docs/cli-reference.md b/docs/src/content/docs/cli-reference.md index 65e78ba2..29649c12 100644 --- a/docs/src/content/docs/cli-reference.md +++ b/docs/src/content/docs/cli-reference.md @@ -583,11 +583,16 @@ cascade promote finalize \ ### hotfix -Apply a trunk commit onto an environment pinned to an older base. A hotfix targets one environment on its `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](/cascade/workflows/) for the full flow. +Apply one or more trunk commits onto an environment pinned to an older base. A hotfix elevates the commit set bottom-up across the environment chain, up to and including the target environment, on each environment's `env/` integration branch. The fixes must already be on trunk; cascade refuses to apply any 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](/cascade/workflows/) 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/` branch reconciliation. With `--dry-run` nothing is mutated (the env branch is planned but not created). +Validate a hotfix request and compute the integration-branch plan. It enforces, in order: trunk ancestry of every fix, target-environment eligibility (a configured environment that is not the first; prod is allowed), no-op detection when a fix is already present, the single-flight open-pull-request gate, and `env/` branch reconciliation. With `--dry-run` nothing is mutated (the env branches are planned but not created). + +Supply the fixes with one of two mutually exclusive flags, exactly one of which is required: + +- `--commit ` applies a single commit to the target environment. +- `--commits ` takes a comma-delimited set and elevates it bottom-up across the chain, from the environment above the first up to and including `--target-env`. On this path each (commit, environment) pair is skipped when the commit is already an ancestor of that environment's state SHA or already in its recorded `patches`; an environment whose whole set is already present is a no-op and the chain moves on. ```bash cascade hotfix plan \ @@ -596,13 +601,23 @@ cascade hotfix plan \ --gha-output ``` +To carry a set of commits and elevate them across the chain: + +```bash +cascade hotfix plan \ + --commits abc1234,def5678 \ + --target-env staging \ + --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 | +| `--commit` | string | One of `commit`/`commits` | Single trunk commit (SHA or ref) carrying the fix; single-env path | +| `--commits` | string | One of `commit`/`commits` | Comma-delimited trunk commits to elevate across the env chain up to `--target-env` | | `--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`) | @@ -630,7 +645,7 @@ With `--json`: } ``` -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). +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). On the `--commits` path the plan also writes `env_sequence` (the environments to walk bottom-up) and a `commits_` list per environment that the apply job replays in order. #### hotfix finalize @@ -654,7 +669,7 @@ cascade hotfix finalize \ | `--key` | string | No | Top-level manifest key (default: `ci`) | | `--target-env` | string | Yes | Environment to finalize | | `--merge-sha` | string | Yes | Tip of `env/` after the resolution pull request merged | -| `--fix-sha` | string | Yes | Trunk commit the hotfix carries | +| `--fix-sha` | string | Yes | Trunk commit(s) the hotfix carries; comma-delimited for a multi-commit set. Every commit is appended to the environment's recorded `patches`, so a multi-commit hotfix records all of its commits | | `--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 | diff --git a/docs/src/content/docs/versioning.md b/docs/src/content/docs/versioning.md index 29a6d2d2..e705517c 100644 --- a/docs/src/content/docs/versioning.md +++ b/docs/src/content/docs/versioning.md @@ -144,7 +144,7 @@ Older tags outside the current release line do not receive backported fixes. See ## 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](/cascade/workflows/)). The version cascade allocates for a hotfix depends on whether the environment's current version is still in flight (an rc) or already published. +A hotfix applies one or more trunk commits onto an environment pinned to an older trunk base (see the Hotfix section of [Workflows](/cascade/workflows/)). 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 diff --git a/docs/src/content/docs/workflows.md b/docs/src/content/docs/workflows.md index 973785ce..ad7ca831 100644 --- a/docs/src/content/docs/workflows.md +++ b/docs/src/content/docs/workflows.md @@ -263,12 +263,12 @@ flowchart TD CP -- "conflict" --> PRconf["resolution PR · cascade-hotfix-conflict
markers committed; human force-pushes head"] PRclean --> MERGE["on merge"] PRconf --> MERGE - MERGE --> FIN["build -> deploy one env -> finalize
vX.Y.Z-rc.N.hotfix.M · ref env/<env> · patches [fix]"] + MERGE --> FIN["build -> deploy one env -> finalize
vX.Y.Z-rc.N.hotfix.M · ref env/<env> · patches [fixes]"] FIN --> DIV["environment diverged
other environments untouched"] DIV == "promote a trunk SHA containing the fix
patch-containment guard refuses dropping it" ==> REJOIN["rejoin trunk
divergence cleared · env/<env> deleted"] ``` -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. +A hotfix applies one or more trunk commits 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 or set of fixes, which is exactly what an operator pinning that environment is trying to avoid. ### Roll forward on trunk first (the default) @@ -292,12 +292,18 @@ state: `cascade status` surfaces `ref`, `base_sha`, and `patches` only when they are set. +### Elevating across the chain + +A hotfix can carry a set of commits to a target environment higher in the chain. cascade elevates the set bottom-up across every environment from the one above the first up to and including the target, so each environment that must diverge ends up running its base plus the fixes. Per environment, any commit already present (an ancestor of that environment's state SHA, or already in its `patches`) is skipped; an environment whose whole set is already present is a no-op and the chain moves on. Every commit applied to an environment is recorded in that environment's `patches`, so a multi-commit hotfix records all of its commits, not just the first. The first environment is never a hotfix target: a fix reaches it by merging to trunk, not by hotfix. + ### Cherry-pick and resolution pull request A clean cherry-pick opens a pull request labeled `cascade-hotfix` and merges it as the configured `state_token`. The apply job polls the pull request until it is mergeable, so the required checks configured on `env/` still gate the merge, and the pull request is the audit record even when no human touches it. The merge runs as `state_token` rather than the default `GITHUB_TOKEN` on purpose: a merge authored by `GITHUB_TOKEN` does not emit the `pull_request` close event, so the build, deploy, and finalize stages would never run and the diverged state would never be recorded. Configure `state_token` with a trigger-capable token (the same one used for state writes) to get the post-merge stages after an automated hotfix. 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. +On the chain path a conflict halts the elevation: the environments still pending are listed in the resolution pull request body, and the later environments are left untouched. After the resolution merges, re-engage the hotfix workflow targeting the same environment to resume the chain from where it stopped. + ``` git fetch && git switch hotfix// # resolve conflicts, then @@ -324,7 +330,7 @@ The divergence ends the next time the environment receives a normal promotion. P 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`. +- `workflow_dispatch` with inputs `commit` (one or more trunk fix SHAs, comma-delimited), `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: @@ -332,7 +338,7 @@ 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//`, open the resolution pull request (clean polls until mergeable then merges as `state_token`; conflict opens the labeled resolution pull request) | +| apply | dispatch (not dry-run) | Cherry-pick the set onto each environment bottom-up; clean picks open the resolution pull request (polled until mergeable, then merged as `state_token`), a conflict opens the labeled resolution pull request and halts the chain | | 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 | From 36453dc25ee8baf7cf976d2576769dc4672d3dfe Mon Sep 17 00:00:00 2001 From: Joshua Temple Date: Tue, 23 Jun 2026 13:34:05 -0400 Subject: [PATCH 2/2] docs: clarify patches records the per-env applied commit set Signed-off-by: Joshua Temple --- docs/src/content/docs/cli-reference.md | 2 +- docs/src/content/docs/workflows.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/content/docs/cli-reference.md b/docs/src/content/docs/cli-reference.md index 29649c12..7db84196 100644 --- a/docs/src/content/docs/cli-reference.md +++ b/docs/src/content/docs/cli-reference.md @@ -669,7 +669,7 @@ cascade hotfix finalize \ | `--key` | string | No | Top-level manifest key (default: `ci`) | | `--target-env` | string | Yes | Environment to finalize | | `--merge-sha` | string | Yes | Tip of `env/` after the resolution pull request merged | -| `--fix-sha` | string | Yes | Trunk commit(s) the hotfix carries; comma-delimited for a multi-commit set. Every commit is appended to the environment's recorded `patches`, so a multi-commit hotfix records all of its commits | +| `--fix-sha` | string | Yes | Trunk commit(s) the hotfix carries; comma-delimited for a multi-commit set. Every commit applied to the environment is appended to its recorded `patches` (commits already present in that environment are skipped) | | `--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 | diff --git a/docs/src/content/docs/workflows.md b/docs/src/content/docs/workflows.md index ad7ca831..15b7c085 100644 --- a/docs/src/content/docs/workflows.md +++ b/docs/src/content/docs/workflows.md @@ -294,7 +294,7 @@ state: ### Elevating across the chain -A hotfix can carry a set of commits to a target environment higher in the chain. cascade elevates the set bottom-up across every environment from the one above the first up to and including the target, so each environment that must diverge ends up running its base plus the fixes. Per environment, any commit already present (an ancestor of that environment's state SHA, or already in its `patches`) is skipped; an environment whose whole set is already present is a no-op and the chain moves on. Every commit applied to an environment is recorded in that environment's `patches`, so a multi-commit hotfix records all of its commits, not just the first. The first environment is never a hotfix target: a fix reaches it by merging to trunk, not by hotfix. +A hotfix can carry a set of commits to a target environment higher in the chain. cascade elevates the set bottom-up across every environment from the one above the first up to and including the target, so each environment that must diverge ends up running its base plus the fixes. Per environment, any commit already present (an ancestor of that environment's state SHA, or already in its `patches`) is skipped; an environment whose whole set is already present is a no-op and the chain moves on. Every commit applied to an environment is recorded in that environment's `patches`, so the recorded set reflects every fix applied there, not just the first. The first environment is never a hotfix target: a fix reaches it by merging to trunk, not by hotfix. ### Cherry-pick and resolution pull request