@mutmutco/codex-plugin 4.3.5 → 4.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/skills/bootstrap/SKILL.md +57 -68
- package/skills/hotfix/SKILL.md +51 -126
- package/skills/rcand/SKILL.md +40 -188
- package/skills/release/SKILL.md +38 -919
package/skills/release/SKILL.md
CHANGED
|
@@ -5,964 +5,83 @@ description: Ship rc or direct-track development to main and production.
|
|
|
5
5
|
|
|
6
6
|
**Host-native invocation:** Claude `/mmi:release` · Codex `$mmi:release` · Cursor `/release` · jervcode `/release` · Hermes skill_view. A backticked `/name` in this doc names the matching workflow (this skill or a sibling), not a literal command.
|
|
7
7
|
|
|
8
|
-
**Argument
|
|
9
|
-
`--flag` or env var. Map it to `MMI_BUMP_INTENT` exported for the whole run (the train and
|
|
10
|
-
`next-version.mjs` both read it). No argument → `patch`: a `/release patch` from development is a
|
|
11
|
-
legitimate ordinary release; `/hotfix` remains the cherry-pick promotion lane.
|
|
8
|
+
**Argument:** `/release minor`, `/release major`, or `/release patch` — a bare bump word, never a `--flag` or env var. No argument → `patch`; a `/release patch` from development is a legitimate ordinary release, `/hotfix` stays the cherry-pick promotion lane.
|
|
12
9
|
|
|
13
10
|
# /release — ship to main + prod
|
|
14
11
|
|
|
15
|
-
Full-track repos ship
|
|
16
|
-
`vX.Y.0`, publish a GitHub Release, dispatch the Hub central tenant deploy workflow for prod, then roll
|
|
17
|
-
`development` forward. Direct-track repos — product repos with `releaseTrack: direct`, plus MMI-Hub via the `isHubControlRepo` special-case (its `releaseTrack` stays unset) — skip rc: release
|
|
18
|
-
merges `development → main`, tags, publishes the GitHub Release, and the release event fires the repo's own
|
|
19
|
-
deploy/publish workflow (for MMI-Hub, `deploy.yml` + `publish.yml`).
|
|
20
|
-
`rc` is **ephemeral**: `/rcand` creates the rc runtime, `/release` retires it — after a confirmed full-track
|
|
21
|
-
prod deploy the train stops the rc stage (reported as `rcRetirement` in the result; never fatal to the
|
|
22
|
-
release). Full-track repos may also pass **`--dev`** to release `development → main` directly, skipping rc —
|
|
23
|
-
the default stays `rc → main`. `--dev` **fails closed** when `origin/rc` carries content not yet in
|
|
24
|
-
`development` (a dev → main release would drop it), and is a friendly no-op on direct-track repos.
|
|
25
|
-
**Train-authority gated (D14):** the repo's project-admin or the master; the Hub repo's train is
|
|
26
|
-
master-only. This is the **only** sanctioned prod path; a prod release still needs the authorized human's
|
|
27
|
-
explicit per-turn go — an agent never self-initiates it. The board needs no
|
|
28
|
-
touch — items reached `Done` when their PRs merged to `development`; `rc`/`main` are deploy stages, not lanes.
|
|
12
|
+
Full-track repos ship exactly what is on `rc` (never `development`): merge `rc → main`, tag `vX.Y.0`, publish the GitHub Release, deploy prod, roll `development` forward. Direct-track repos ship `development → main`, and the release event fires the repo's own deploy/publish workflow (MMI-Hub: `deploy.yml` + `publish.yml`). `rc` is ephemeral: `/rcand` creates its runtime, `/release` retires it after a confirmed prod deploy (`rcRetirement`). A prod release needs the authorized human's explicit go in the current turn — an agent never self-initiates it. The board needs no touch: items reached `Done` when their PRs merged to `development`.
|
|
29
13
|
|
|
30
|
-
|
|
31
|
-
to the protected `main` branch, whose per-repo allowlist carries the same people (master + that repo's
|
|
32
|
-
project-admins; Hub: master + App only). Gate ordering: the tag lands the release SHA for checks, and
|
|
33
|
-
nothing deploys before the protected `main` push accepts that checked SHA.
|
|
14
|
+
## Lane
|
|
34
15
|
|
|
35
|
-
|
|
16
|
+
Read META and name the lane before describing or executing anything:
|
|
36
17
|
|
|
37
|
-
- **Never squash-merge a tagged commit.** A squash re-mints the SHA, so the tag's commit vanishes from the
|
|
38
|
-
branch's lineage and every tag-anchored check (coverage trailers, misalignment guard, version probes)
|
|
39
|
-
re-flags or mis-reads forever after. Any merge that carries an already-tagged commit — the Step 5
|
|
40
|
-
`main → development` roll-forward, any alignment PR — lands as a **true merge** (`--merge`), never squash.
|
|
41
|
-
`--wait` does not relax this: use `mmi-cli devops pr merge <n> --wait --merge` (or the policy-gated
|
|
42
|
-
`--auto --merge`). `--wait` alone still defaults the CLI merge method to squash and must not land an
|
|
43
|
-
alignment / tagged-commit-carrying PR.
|
|
44
|
-
- **A refused train is a stop, never a license to finish by hand.** Every fail-closed halt below — authority
|
|
45
|
-
probe, coverage guard, untolerated conflict, required checks, stray tag — ends the run. The recovery is
|
|
46
|
-
always to fix the cause and re-run the train; it is never hand-resolving on `main`, bare-pushing a train
|
|
47
|
-
branch, acking to save time, or bypassing a check.
|
|
48
|
-
- **Version probes anchor on the latest Release tag, not a branch.** `next-version.mjs` and the fold derive
|
|
49
|
-
the next version from tags; a branch manifest or a stale local tag is not a version source. (This is why a
|
|
50
|
-
stray tag must be deleted locally too — a surviving local tag silently mints the *next* version, Step 3.)
|
|
51
|
-
- **One `/release` at a time on a repo (#5380).** Two overlapping `--apply` runs race the shared tag namespace:
|
|
52
|
-
the slower train can tag a SHA that never got a fold commit, while the other sees that tag mid-flight and
|
|
53
|
-
mints the *next* version (a skip). Publish then fails verify (`root package and lockfile versions must
|
|
54
|
-
equal …`) and the broken GitHub Release can briefly hold **Latest**. `mmi-cli devops release --apply`
|
|
55
|
-
fail-closes post-fold / pre-tag when the computed tag (or a newer release tag) appears on origin since
|
|
56
|
-
the plan-phase probe — stop, do not hand-finish, do not start a second train. Recovery is below (Step 3).
|
|
57
|
-
|
|
58
|
-
## Step 0 — confirm + probe
|
|
59
|
-
|
|
60
|
-
Before describing or executing any release lane, read the project META and name the lane:
|
|
61
18
|
```bash
|
|
62
19
|
mmi-cli oracle org project get {owner}/{repo} --json
|
|
63
20
|
```
|
|
64
|
-
`releaseTrack: direct` means `development → main` and **no `/rcand`**. An absent `releaseTrack` means full-track
|
|
65
|
-
(`rc → main`) except for MMI-Hub itself, whose Hub-control special case is direct. Stop on an unreadable META record;
|
|
66
|
-
never infer the lane from branch names, release history, or the existence of an `rc` branch.
|
|
67
|
-
|
|
68
|
-
Then confirm the human holding train authority for THIS repo authorized a prod release this turn. Probe:
|
|
69
|
-
```bash
|
|
70
|
-
mmi-cli oracle org access role {owner}/{repo} --json # Hub-verified: { role, train }
|
|
71
|
-
```
|
|
72
|
-
`train: false` — or any error (fail closed) → stop: a product repo's train belongs to that repo's
|
|
73
|
-
project-admin or the master; the Hub train is master-only. **Three exceptions to fail-closed here, and all
|
|
74
|
-
mean "unverified", never an authority verdict:**
|
|
75
|
-
|
|
76
|
-
- A `repository … does not exist or is not visible to the org App` error (HTTP 404) means the probe target
|
|
77
|
-
is not a resolvable repo — a package name, a typo, a repo never created (#4778). It is UNKNOWN, not a
|
|
78
|
-
denial, and retrying will never change it: fix the `{owner}/{repo}` argument, then re-probe. Before the
|
|
79
|
-
fix this string came back `role: master, train: true, verified: true` for anything owner/name-shaped.
|
|
80
|
-
|
|
81
|
-
- An `unknown command 'oracle'` error is a STALE CLI — the house-prefixed namespaces ship in newer
|
|
82
|
-
trains, so an old installed CLI can't even parse the probe. Jump to Step 0a, heal the CLI, then re-run
|
|
83
|
-
this probe; never conclude the command form is wrong from a pre-heal failure (#3150). The mirror-image
|
|
84
|
-
failure is a Wave-3 flat alias on a CURRENT CLI: the un-housed `org …`, `secrets …`, and
|
|
85
|
-
`release …` forms were removed in #4316 and fail closed with a house-prefix error — rewrite to the
|
|
86
|
-
house form (`mmi-cli oracle org …`, `mmi-cli vault secrets …`, `mmi-cli devops release …`); that is
|
|
87
|
-
never a stale CLI and never an authority verdict.
|
|
88
|
-
- A **TIMEOUT or network error** (`operation aborted due to timeout`, DNS/socket failures) is **not** a
|
|
89
|
-
`train: false` denial (#3321). Read the probe's own `verified` field: `verified: false` means the verdict
|
|
90
|
-
could not be established, so the `train: false` beside it carries NO authority meaning. The client
|
|
91
|
-
already retries transient failures internally, so a *persistent* timeout points at the local network
|
|
92
|
-
rather than the Hub — resolve the Hub API host first (fails in seconds and rules out the whole
|
|
93
|
-
local-network class), then re-run the probe. Conclude "no train authority" ONLY from a `verified: true`
|
|
94
|
-
response. Aborting a legitimate release on an unverified probe is the failure this carve-out exists to
|
|
95
|
-
prevent.
|
|
96
|
-
|
|
97
|
-
Then
|
|
98
|
-
preconditions: clean tree; full-track repos run from `rc` (or from `development` with `--dev`), while
|
|
99
|
-
direct-track repos run from `development`. Ordinary `/release` takes its bump intent from the bare
|
|
100
|
-
argument (`/release minor|major|patch` → `MMI_BUMP_INTENT`, exported for the whole run); unstated, the
|
|
101
|
-
train defaults to `patch` (#4929). An invalid intent fails `--apply` closed at intent resolution
|
|
102
|
-
(`MMI_BUMP_INTENT must be one of major|minor|patch`), before any merge, tag, or fold.
|
|
103
|
-
|
|
104
|
-
**Run from the checkout that already has that branch — never a fresh isolated worktree (#2770).** This is
|
|
105
|
-
a shared-branch train operation (merge/tag/push against origin's protected `main`/`rc`), not an isolated
|
|
106
|
-
feature-branch edit. A background-job harness that isolates every task into a fresh worktree by default
|
|
107
|
-
breaks here: the new worktree's branch is never literally named `development`/`rc`, and `git checkout
|
|
108
|
-
development` inside it fails outright when `development` is already checked out in the primary checkout (git
|
|
109
|
-
worktrees cannot have the same branch checked out twice). If you are in such a worktree, exit it first and
|
|
110
|
-
run the release from the primary checkout.
|
|
111
|
-
|
|
112
|
-
The clean-tree check rejects UNTRACKED scratch too, not just modified tracked files. When `--apply` or
|
|
113
|
-
`--resume` stops with `working tree must be clean before …`, run `git status --porcelain` on the paths it
|
|
114
|
-
named and read BOTH status columns before touching anything (#1472, #4004):
|
|
115
|
-
|
|
116
|
-
- `??` — untracked scratch. For `?? .jerv/` / `?? .pi/` (org-managed agent scratch), run
|
|
117
|
-
`mmi-cli doctor --apply` to write the managed `.gitignore` block — do not hand-edit `.gitignore`
|
|
118
|
-
(#5409). The train CLI already ignores those live-session dirs (#5397); doctor is the durable heal.
|
|
119
|
-
For other `??` paths, gitignore them, or move them to a gitignored path like `tmp/`, then retry.
|
|
120
|
-
- exactly ` M` (blank staged column) — the only state that can be churn rather than work. Test it with
|
|
121
|
-
`git diff HEAD --numstat -- <paths>`: non-empty is a real edit to commit or stash; empty means the
|
|
122
|
-
worktree normalizes straight back to HEAD (an LF↔CRLF rewrite, say), and `git checkout -- <paths>`
|
|
123
|
-
then clears the refusal without discarding anything.
|
|
124
|
-
- anything else — `M `, `MM`, `A`, `D`, `R`, `T`, or any `U` conflict — is real work or an exceptional
|
|
125
|
-
index state. Commit, stash or resolve it; do not try to classify it.
|
|
126
|
-
|
|
127
|
-
Do not substitute plain `git diff --numstat` for the `HEAD` form. It compares the worktree against the
|
|
128
|
-
INDEX, so a change that is merely staged prints nothing at all, and reading that emptiness as
|
|
129
|
-
"line-ending churn" throws the change away. The remedy is the index form of `git checkout` for the same
|
|
130
|
-
reason: `git checkout HEAD -- <paths>` would overwrite a staged edit that was misread as churn.
|
|
131
|
-
|
|
132
|
-
Full-track repos:
|
|
133
|
-
```bash
|
|
134
|
-
git fetch origin
|
|
135
|
-
git rev-list --count origin/main..origin/rc
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
Direct-track repos (e.g. MMI-Hub):
|
|
139
|
-
```bash
|
|
140
|
-
git fetch origin
|
|
141
|
-
git rev-list --count origin/main..origin/development
|
|
142
|
-
```
|
|
143
|
-
`0` → stop ("nothing to release").
|
|
144
|
-
|
|
145
|
-
**Checkout topology — verify the lane's refs exist locally before apply (#5624).** A fresh single-branch
|
|
146
|
-
`development` clone has no local `main`; the train then fails or rolls back at `git checkout main`. Before
|
|
147
|
-
any merge or `--apply`, confirm every ref the lane will touch is present as a local tracking branch:
|
|
148
|
-
|
|
149
|
-
```bash
|
|
150
|
-
git branch --list development main # direct-track (and full-track --dev)
|
|
151
|
-
git branch --list rc main # full-track rc → main
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
A missing ref is a topology gap, not a train failure — materialize it from origin, never force past it:
|
|
155
|
-
|
|
156
|
-
```bash
|
|
157
|
-
git fetch origin main:main # (or development:development / rc:rc) — creates the local ref
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
Re-run the `--list` until every ref the lane touches is present, then proceed.
|
|
161
|
-
|
|
162
|
-
## Step 0a — stale CLI preflight (#1410)
|
|
163
|
-
|
|
164
|
-
Before local gates or `release --apply`, ensure the repo-local / PATH `mmi-cli` matches the released train
|
|
165
|
-
path — a stale checkout (e.g. 2.32.0 while 2.32.4 is released) fails release gates with opaque errors.
|
|
166
|
-
Read the gap on the compatibility-safe no-repository-write lane:
|
|
167
|
-
```bash
|
|
168
|
-
mmi-cli doctor --no-repo-writes
|
|
169
|
-
```
|
|
170
|
-
**Read the checks, not the exit code.** `doctor` exits non-zero on any failed check it could have healed —
|
|
171
|
-
not just a version gap — so a red exit here does not by itself mean the train is blocked (#2962). The
|
|
172
|
-
inverse is also true: a run can exit **0** while printing ✗ lines (report-only rows, #3485). Neither
|
|
173
|
-
direction is a gate. Read the rows.
|
|
174
|
-
|
|
175
|
-
- **Blocking:** a red `mmi-cli` / `Claude plugin` version line, or a reported `minClientVersion` gap.
|
|
176
|
-
**Doctor does not update the CLI (#4954).** `mmi-hub` owns release-gated version convergence; doctor
|
|
177
|
-
reports lag and last-run evidence only. The remedy is `mmi-hub update`, not a doctor re-run or a bare
|
|
178
|
-
CLI install. Plain doctor/`--no-repo-writes` may safely repair active plugin wiring but never repository
|
|
179
|
-
state. Do **not**
|
|
180
|
-
proceed to Step 0b while the installed CLI is behind the Hub's `minClientVersion`.
|
|
181
|
-
- **Not blocking:** every other red check is hygiene, not a train gate. Stale branches and scratch require
|
|
182
|
-
explicit `mmi-cli doctor --apply` after the release; `plugin cache` needs a different verb,
|
|
183
|
-
`mmi-cli plugin prune --apply` — saying doctor cleared it was wrong. Both still exit 1 until you run the
|
|
184
|
-
right verb. Read them, then proceed.
|
|
185
|
-
|
|
186
|
-
Treating doctor's exit code as the gate halts a healthy train on cosmetic drift — or, worse, teaches an
|
|
187
|
-
operator to ignore doctor's exit code entirely.
|
|
188
|
-
|
|
189
|
-
**`mmi-cli devops release --apply` and `mmi-cli devops rcand --apply` now run an active npm-major preflight
|
|
190
|
-
before any branch/tag mutation (#5666).** They read the repo's expected CI npm, in this order (#5866,
|
|
191
|
-
#5893): an explicit `npm-version:` on the gate's `runner-node-toolchain` step; else an explicit
|
|
192
|
-
`npm install -g npm@<major>` step in gate.yml; else the infra gate's exact node pin through the
|
|
193
|
-
bundled-npm map (only for an estate that still carries a retired pre-#5870 pin); else the publish
|
|
194
|
-
workflow's own npm pin (`npm install -g npm@12`, the #5870 retirement shape). Local `npm -v`
|
|
195
|
-
must match that npm **major**; a
|
|
196
|
-
mismatch refuses `--apply` before any repository write.
|
|
197
|
-
|
|
198
|
-
**Both gate shapes are a declaration; only a gate that declares NEITHER falls through (#5893).** A lane
|
|
199
|
-
that installs its own npm has stated its npm as plainly as an `npm-version:` input — the shape differs, the
|
|
200
|
-
intent does not. Reading only `npm-version:` skipped Jerv-JervCode's gate (floating `node-version: 24`
|
|
201
|
-
plus `npm install -g npm@12`), fell through to publish.yml's stale npm 11, and prescribed a downgrade the
|
|
202
|
-
repo had already retired (#2845, MMI-Hub#5871); the publish smoke then failed under npm 11 (`Cannot find
|
|
203
|
-
module jiti/package.json` — a packed global install materializes no dependency closure under npm 11 once
|
|
204
|
-
the root declares `bundleDependencies`), shipping v1.57.7 to main with a Release but no npm publish.
|
|
205
|
-
**When gate.yml and publish.yml disagree on the npm major, that is a repo defect** — one lane will run the
|
|
206
|
-
wrong toolchain whichever the preflight believes. The preflight names it and proceeds; fix it with a PR
|
|
207
|
-
moving both pins together, never by silently trusting publish.yml.
|
|
208
|
-
|
|
209
|
-
**Check which side is wrong before repairing (#5862).** The receipt names one direction — move the machine
|
|
210
|
-
to the pin — and that is only right when the pin is current. Compare it against the registry first:
|
|
211
|
-
|
|
212
|
-
```bash
|
|
213
|
-
npm view npm version # what npm is actually at
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
A pin **behind** latest is fixed by a PR moving the pin, not by a machine-global downgrade that every
|
|
217
|
-
release host then inherits. The pin is coordinated, so move all of it together: on Jerv-Hub the same
|
|
218
|
-
version lives in `publish.yml`, `DISTRIBUTION_NPM_VERSION`, `package.json` `packageManager`, and a
|
|
219
|
-
workflow-contract test. Prefer declaring a bare **major** (`npm@12`) over an exact patch — an exact pin
|
|
220
|
-
goes stale and then prescribes downgrades, and `latest` silently takes the next major.
|
|
221
21
|
|
|
222
|
-
|
|
223
|
-
between majors (npm ≤10 a top-level array, npm ≥11 a name-keyed object) and npm 12 prints the `prepare`
|
|
224
|
-
lifecycle script's stdout ahead of the document, so `JSON.parse(stdout)` throws. An inline
|
|
225
|
-
`const [entry] = JSON.parse(pack.json)` in a workflow step is the hazard — on Jerv-Hub it failed **after**
|
|
226
|
-
`npm publish` had already succeeded, leaving the GitHub Release assets and the Gateway deploy undone. Repo
|
|
227
|
-
scripts that already tolerate both shapes (`scripts/distribution-assembly.mjs`, #4741) are the fix; audit
|
|
228
|
-
inline workflow parsing before bumping.
|
|
22
|
+
`releaseTrack: direct` → `development → main`, no `/rcand`. Absent → full (`rc → main`), except MMI-Hub, which is direct by the Hub-control special case. Full-track `--dev` ships `development → main` skipping rc and fails closed when `origin/rc` carries content not in `development`. Unreadable META → stop; never infer the lane from branch names, tags or release history.
|
|
229
23
|
|
|
230
|
-
|
|
231
|
-
`node -v && npm -v`, then rerun the same release command) preserves Node and avoids routing a
|
|
232
|
-
machine-global mutation through the train.
|
|
24
|
+
## Preflight
|
|
233
25
|
|
|
234
|
-
**A local `npm ci` failure during the fold on a lockfile CI already accepted is an npm-major mismatch, not a
|
|
235
|
-
bad lockfile (#4578).** Before touching the lockfile, compare toolchains:
|
|
236
26
|
```bash
|
|
237
|
-
|
|
238
|
-
gh api repos/{owner}/{repo}/actions/runs --jq '.workflow_runs[0].id' # or read the gate log's
|
|
239
|
-
# `node -v && npm -v` toolchain-preflight line (#3446) for the npm major CI ran with
|
|
240
|
-
# Read the gate's OWN declared npm first — `npm-version:` or `npm install -g npm@X` (#5893);
|
|
241
|
-
# only a gate that declares neither falls back to the publish workflow npm pin (#5666).
|
|
27
|
+
mmi-cli devops train doctor --lane release --heal --json
|
|
242
28
|
```
|
|
243
|
-
A different npm MAJOR (e.g. local 12 vs. CI's 11) resolves the same lockfile differently and throws `Missing:
|
|
244
|
-
<pkg> from lock file` on a lockfile that is not actually broken. Align the local npm major
|
|
245
|
-
(`npm install -g npm@<CI major>`) and re-run `npm ci` before rewriting or regenerating the lockfile.
|
|
246
29
|
|
|
247
|
-
|
|
248
|
-
libnpmpublish cannot find its declared sigstore dependency (`MODULE_NOT_FOUND`) — is toolchain
|
|
249
|
-
corruption / an npm-major mismatch, not a malformed publish surface (#5616).** Compare toolchains
|
|
250
|
-
BEFORE inspecting package contents (deleted dir, malformed `package.json`, missing `files`):
|
|
251
|
-
```bash
|
|
252
|
-
npm -v
|
|
253
|
-
# green gate npm from the gate log's `node -v && npm -v` toolchain-preflight line (#3446),
|
|
254
|
-
# the gate's declared `npm-version:` or its `npm install -g npm@X` step (#5893), or an exact
|
|
255
|
-
# node pin that maps to a bundled npm, or the publish workflow npm pin when the gate declares
|
|
256
|
-
# no npm of its own (#5666)
|
|
257
|
-
```
|
|
258
|
-
When local npm differs from the green gate (e.g. local npm 11 vs CI's 12), align local npm
|
|
259
|
-
(`npm install -g npm@<gate npm>`) and rerun the train — do not rewrite the publish surface for an
|
|
260
|
-
npm-internal missing module.
|
|
30
|
+
Read `ready`, then `findings[]`: each carries `code`, `severity` (`blocker` · `warning` · `healed` · `info`), `source` (`local` · `origin`), `remedy` and `anchor`. `ready: false` → stop and quote every blocker's `code` and `remedy`; an unread origin is never green. `--apply` re-runs this doctor as its step 0 with heal on.
|
|
261
31
|
|
|
262
|
-
|
|
263
|
-
toolchain problem (#4841).** The signature is the kill, not error text: no `Missing:` line, no lockfile
|
|
264
|
-
complaint at all — the fold's child-process budget expired while a cold cache installed the full tree.
|
|
265
|
-
The npm-major note above is inert here (majors match, the lockfile is fine), and following it tempts the
|
|
266
|
-
one thing it warns against — regenerating the lockfile for a problem that has nothing to do with it.
|
|
267
|
-
Warm the cache with a plain `npm ci` in the release checkout, then rerun the identical train command; the
|
|
268
|
-
rollback the failed fold already ran makes the rerun clean. Never regenerate or rewrite the lockfile in
|
|
269
|
-
response to a SIGTERM kill.
|
|
32
|
+
## Apply
|
|
270
33
|
|
|
271
|
-
|
|
272
|
-
not a lockfile failure (#4982).** Rename `node_modules` aside once into a gitignored `tmp/` path, then rerun
|
|
273
|
-
the identical train command. `npm ci` extracts into the now-empty tree and performs no unlinks; do not retry
|
|
274
|
-
this workaround repeatedly.
|
|
34
|
+
Run from the primary checkout on the lane's start branch (`rc`; `development` for direct-track and `--dev`). Export the bump intent once from the bare argument — `MMI_BUMP_INTENT=minor|major|patch`, unset → `patch`; `MMI_RELEASE_VERSION=X.Y.Z` only for an exact target the tag math cannot derive.
|
|
275
35
|
|
|
276
|
-
|
|
277
|
-
problem (#5365).** App-style folds run `npm version`, which fires `preversion` / `version` / `postversion`
|
|
278
|
-
(e.g. sync-workspace-version + refresh-distribution-bom); Hub folds run `release-distribution.mjs prepare`,
|
|
279
|
-
which likewise executes repo scripts. When those fail, the train surfaces only
|
|
280
|
-
`fold failed; local main was reset` — the real error is one layer down. Open the newest npm debug log
|
|
281
|
-
(path printed by npm, typically under the npm cache `_logs/`) and read the verbose title / argv: if it
|
|
282
|
-
names `npm version …` (or another non-`ci` command) and a `runScript` / lifecycle command failed, run that
|
|
283
|
-
script standalone (e.g. `node scripts/refresh-distribution-bom.mjs`) to get the real error in seconds.
|
|
284
|
-
Do this **before** suspecting npm major (#4578), cold cache (#4841), EPERM/EBUSY (#4982), or regenerating
|
|
285
|
-
the lockfile — those classes apply to install failures, not to a script that already had `node_modules`.
|
|
36
|
+
MMI-Hub only: write a fresh 3–6 line neutral summary to `f=$(mktemp tmp/release-summary.XXXXXX)` — sourced from Hub PR titles only (`origin/main..origin/development`), rewritten in Hub-subsystem terms, never a product or brand name. Hub scope is only `mutmutco/MMI-Hub`: never a product's board, `ds-propagate.yml`, or a product's deploy state.
|
|
286
37
|
|
|
287
|
-
**Then check the version yourself — do not rely on doctor having printed a row (#3674).** "Read the rows"
|
|
288
|
-
resolves to "proceed" when the row is *absent*, and doctor's version row is conditional. On 2026-07-27 this
|
|
289
|
-
step reported healthy with no version line while the installed CLI was 3.70.0 against a released 3.71.0;
|
|
290
|
-
`release --apply` refused one command later. Two lines, no dependency on another command's reporting
|
|
291
|
-
completeness:
|
|
292
38
|
```bash
|
|
293
|
-
|
|
294
|
-
|
|
39
|
+
r=$(mktemp tmp/release-receipt.XXXXXX)
|
|
40
|
+
mmi-cli devops release --apply --json --out "$r" # product repos
|
|
41
|
+
mmi-cli devops release --apply --announce-summary-file "$f" --json --out "$r" # MMI-Hub
|
|
295
42
|
```
|
|
296
|
-
Mismatch → run `mmi-hub update`, then repeat both reads before Step 0b, **regardless of what doctor's
|
|
297
|
-
rows said**. This is the check whose absence Step 0a's own title promises to cover, so it is not optional.
|
|
298
|
-
|
|
299
|
-
## Step 0b — registry + main secret-name preflight
|
|
300
|
-
|
|
301
|
-
Resolve the project META first; its `deployModel` decides the deploy path. `tenant-container` repos use the
|
|
302
|
-
central tenant deployer and therefore need DEPLOY# coords. `hub-serverless` (MMI-Hub), `serverless`,
|
|
303
|
-
`registry-publish`, and `solo-container` repos deploy from their own branch/release-triggered or model
|
|
304
|
-
workflow, so do **not** dispatch `tenant-deploy.yml` for them.
|
|
305
|
-
Verify META + required SSM secret names before touching `main`:
|
|
306
|
-
```bash
|
|
307
|
-
mmi-cli oracle org project get {owner}/{repo}
|
|
308
|
-
mmi-cli vault secrets preflight --stage main --repo {owner}/{repo}
|
|
309
|
-
```
|
|
310
|
-
An enumerated missing META row or missing secret name → stop and repair the registry/secrets first. An
|
|
311
|
-
HTTP 5xx, timeout, DNS, socket, or other transport failure is **unverified**, not evidence that a name is
|
|
312
|
-
missing: retry the read/preflight and repair connectivity if it persists. Never provision or rename a
|
|
313
|
-
secret from a transport-error response.
|
|
314
|
-
|
|
315
|
-
**GitHub Actions billing/spending (#5604).** Those checks do not prove a *hosted* Actions job can start.
|
|
316
|
-
The train dispatches `actions-job-start-canary.yml` on MMI-Hub (Linux hosted, not `windows-latest`) and
|
|
317
|
-
refuses to mint a tag if the canary job never starts (empty steps / billing refusal). If a tag is already
|
|
318
|
-
on origin, do **not** recut: `mmi-cli devops release --retry-publish <run-id> --apply` retries that exact
|
|
319
|
-
failed or cancelled run. A failed run retries failed jobs; a cancelled run reruns the whole idempotent
|
|
320
|
-
publish workflow because GitHub does not classify cancelled jobs as failed (#5797).
|
|
321
|
-
|
|
322
|
-
## Step 0c — hotfix-coverage guard (fail closed, #839, #958)
|
|
323
|
-
|
|
324
|
-
Full-track repos only. Direct-track repos skip this specific guard — not because they are exposed to no
|
|
325
|
-
risk, but because the risk this guard checks structurally cannot arise there: a direct-track `/hotfix`
|
|
326
|
-
cherry-picks straight from `development` onto `main`, so every hotfix trailer's source sha is *by
|
|
327
|
-
construction* an ancestor of `origin/development` — a trailer-ancestry check would be permanently green and
|
|
328
|
-
add no signal (#2786 — a literal port was considered and rejected as a false-confidence guard).
|
|
329
|
-
|
|
330
|
-
Hotfixes are cherry-picked from `development` to `main` with **no back-merge** (see `/hotfix`), so a
|
|
331
|
-
candidate cut *before* a fix landed on `development` would silently revert that hotfix in prod. The guard
|
|
332
|
-
proves every main-only commit is in the candidate before the `rc → main` merge.
|
|
333
|
-
|
|
334
|
-
**Direct-track repos are not risk-free, though — they carry a different, real failure mode:** a hotfix
|
|
335
|
-
freezes a snapshot of the files it cherry-picks (e.g. docs, a test assertion) while `development` keeps
|
|
336
|
-
evolving those same files afterward. Step 1's `development → main` merge preflight (untolerated-path
|
|
337
|
-
conflict check) is what actually catches this — a genuine content conflict on the **next** release is
|
|
338
|
-
expected when a hotfix's cherry-picked commits touched files `development` went on to rewrite
|
|
339
|
-
incompatibly. That is routine, working-as-designed friction, not a fluke or a misconfiguration (incident:
|
|
340
|
-
hotfix v3.16.1 #2765 cherry-picked #2762/#2768's doc sweep onto `main`; `development` then rewrote the same
|
|
341
|
-
files further via #2767/#2771; `/release` correctly stopped on 4 conflicted files, resolved by confirming
|
|
342
|
-
`development`'s side already represented the correct, current content and landing alignment PR #2784 — see
|
|
343
|
-
#2786). When Step 1 stops this way: run `git merge-tree` (or read the conflict content directly) to confirm
|
|
344
|
-
which side carries the correct current content, then land a true-merge alignment PR (never hand-resolve on
|
|
345
|
-
`main`) before rerunning release.
|
|
346
|
-
|
|
347
|
-
It runs **automatically inside `mmi-cli devops release --apply`** (Step 1+ below) — built into the CLI so it
|
|
348
|
-
works in every product repo with no repo-local script. You do not invoke it separately.
|
|
349
|
-
|
|
350
|
-
Per main-only commit it accepts: the `(cherry picked from commit <sha>)` trailer with that dev SHA an
|
|
351
|
-
ancestor of `origin/rc` (immune to conflict-resolved ports); a matching `git patch-id` on the rc side
|
|
352
|
-
(trailer-less picks); or a distribution-manifest-only bump (exempt — rc carries its own). Anything else
|
|
353
|
-
**fails the release closed** → **stop**: the right fix is a re-cut `/rcand` from `development`. Only when
|
|
354
|
-
the authorized human has manually verified the content is in the candidate, rerun with
|
|
355
|
-
`mmi-cli devops release --apply --ack <sha>[,<sha>…]` — the ack is recorded in the verdict. Never ack to save time.
|
|
356
|
-
|
|
357
|
-
## Step 0d — no docs-keeping on the release train (Hub#4164)
|
|
358
|
-
|
|
359
|
-
Scheduled docs-janitor / wiki-keeper / living-docs freshness passes are **retired** (#4117/#4119).
|
|
360
|
-
The release train does **not** open docs-only PRs, wait on README/architecture auto-merges, or
|
|
361
|
-
treat prose refresh as a gate.
|
|
362
|
-
|
|
363
|
-
Current state comes from code and live CLI verbs. Durable WHY stays in `docs/decisions/`.
|
|
364
|
-
If shipping code made a hand-written surface wrong, fix it on `development` as an ordinary PR
|
|
365
|
-
**outside** the train — never as a release Step.
|
|
366
|
-
|
|
367
|
-
## Step 1 — merge to main (never force)
|
|
368
|
-
|
|
369
|
-
Full-track repos:
|
|
370
|
-
|
|
371
|
-
```bash
|
|
372
|
-
git checkout main
|
|
373
|
-
git pull --ff-only origin main
|
|
374
|
-
git merge rc --no-edit
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
Direct-track repos (e.g. MMI-Hub):
|
|
378
|
-
```bash
|
|
379
|
-
git checkout main
|
|
380
|
-
git pull --ff-only origin main
|
|
381
|
-
git merge development --no-edit
|
|
382
|
-
```
|
|
383
|
-
Conflict → abort + stop (the train is misaligned — investigate; don't hand-resolve on `main`). On a
|
|
384
|
-
direct-track repo, a conflict here right after a hotfix is expected friction, not a fluke — see Step 0c for
|
|
385
|
-
why (a hotfix freezes a snapshot that `development` can keep rewriting).
|
|
386
|
-
Alignment PRs are the exception to the org's squash default: land them with a true merge —
|
|
387
|
-
`mmi-cli devops pr merge <n> --auto --merge` (not squash; a squash discards the merge parentage, so the
|
|
388
|
-
misalignment guard re-flags the same divergence on the next run). `--auto` clears the checks the PR
|
|
389
|
-
triggers, which block an immediate merge right after a release. When awaiting checks inline, keep
|
|
390
|
-
`--merge`: `mmi-cli devops pr merge <n> --wait --merge` — `--wait` alone is not enough (merge floor above).
|
|
391
|
-
|
|
392
|
-
**Exception — version-manifest and `.gitignore` paths.** `mmi-cli devops release --apply` tolerates
|
|
393
|
-
conflicts confined to the version-fold paths (Step 1b) and `.gitignore` (#1037 — a
|
|
394
|
-
repo bootstrapped before the managed-gitignore era still carries the legacy file on `main`; the candidate
|
|
395
|
-
carries the Hub-managed copy with project-local entries preserved). The org spine is no longer delivered per
|
|
396
|
-
repo (hub-v3 WS4), so a spine-path conflict is treated like any other untolerated path. A hotfix bumps the manifests +
|
|
397
|
-
committed CLI bundle on `main` only, so the next merge re-conflicts there even when the train is healthy —
|
|
398
|
-
and the fold rewrites those exact paths right after the merge. For all tolerated paths the CLI takes the
|
|
399
|
-
incoming side deterministically and continues. Any other conflicted path → abort + stop as above.
|
|
400
|
-
|
|
401
|
-
## Step 1b — version fold (automatic, inside `mmi-cli devops release --apply`)
|
|
402
43
|
|
|
403
|
-
|
|
404
|
-
before the tag, the CLI bumps the version manifests to the release version and commits — **unconditionally,
|
|
405
|
-
changed or not** — then the tag-first push (Step 3) earns that fresh commit its required checks. There is
|
|
406
|
-
no separate bump PR, no `development` prep cycle, and `development` never sits ahead of the published
|
|
407
|
-
version (the back-merge in Step 5 carries the bump back).
|
|
44
|
+
Add `--dev` for a full-track dev → main release and `--watch` to block on the deploy/publish runs. `mmi-cli devops release --apply --ack <sha>[,<sha>…]` only when the authorized human has verified that a main-only commit the hotfix-coverage guard refused is already in the candidate — never ack to save time. The one command merges, runs the version fold (automatic, inside `mmi-cli devops release --apply`) that bumps the manifests unconditionally before the tag, pushes the tag first (`gate.yml` runs on `v*` tags, so the fold commit earns its required checks), waits for the required contexts, pushes `main`, publishes the Release, dispatches or correlates the deploy (`tenant-deploy.yml` only for tenant-container), announces (Hub), re-syncs project info, and rolls `development` and `rc` forward. Nothing here is done by hand.
|
|
408
45
|
|
|
409
|
-
|
|
410
|
-
- **Hub (`hub-serverless`):** the full locked distribution set via `scripts/release-distribution.mjs
|
|
411
|
-
prepare` — spine dogfood (`scripts/spine-dogfood.mjs`: verify docs/surfaces + the managed `.gitignore` block),
|
|
412
|
-
every registry-declared version holder, adapter payload synchronization, build output, and the public
|
|
413
|
-
artifact bill of materials — then verifies the set (`verify --skip-npm-view`). That prepare path runs
|
|
414
|
-
`scripts/check-hook-contract.mjs`, which executes host adapter commands as a native argv without a
|
|
415
|
-
shell (#5605), so a checkout path that contains spaces is supported; the spaced-path argv regression
|
|
416
|
-
is pinned in `infra/plugin-set.test.mjs`. Publication and staging both derive from `surfaces.json`;
|
|
417
|
-
there is no second package list. Claude, Codex, Cursor, jervcode, and Hermes are active.
|
|
418
|
-
- **App-style repos with a root `package.json`** (most products): the manifest + lockfile version via
|
|
419
|
-
`npm version --no-git-tag-version`, kept in lockstep with the release tag.
|
|
420
|
-
- **Repos with neither:** nothing to fold — the tag is the version.
|
|
46
|
+
## Verify
|
|
421
47
|
|
|
422
|
-
|
|
48
|
+
Read the receipt at `$r`, never the exit code (exit `2` = shipped with a follow-up pending; exit `1` can also mean a follow-up failed after promotion):
|
|
423
49
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
50
|
+
- `releaseVerdict.releaseStatus` + `followUpStatus` — `succeeded` + `pending` is shipped with the follow-up unresolved: neither failed nor done.
|
|
51
|
+
- `deployStatus` — `pending` is never terminal; `promoted: true` holds even when a deploy failed.
|
|
52
|
+
- `workflowRuns` — every run on the release SHA (deploy-model runs, any `push: main` deploy, the repo's own `gate`). Watch each non-success entry to conclusion: `gh run watch <run-id> --repo {owner}/{repo} --exit-status`. Name which run is red: `deploy.yml` / `publish.yml` / `tenant-deploy.yml` / `jerv-gateway` is a deploy or publish verdict; a `gate` push run is ordinary CI sharing the SHA.
|
|
53
|
+
- `announceNote` (Hub) — `announced`, `skipped`, or the failure note.
|
|
54
|
+
- `devRollForward` / `rcAlignment` — `pushed`, or `pr-pending` with the alignment PR to land by true merge (Merge floor).
|
|
55
|
+
- `versionFold`, `rcRetirement`, `checkout` — `returned`, or the named reason you are still on `main`.
|
|
427
56
|
|
|
428
|
-
|
|
57
|
+
Then the bounded Latest Release read; its `tagName` must equal the resolved tag:
|
|
429
58
|
|
|
430
|
-
Full-track repos drop the `-rc.N` suffix from the open cycle. Direct-track repos use the next cycle directly
|
|
431
|
-
because they have no rc tag — `cycle` mode reads `MMI_BUMP_INTENT` (the `/release` argument; unset →
|
|
432
|
-
`patch`, #4929). The Step 0 export is what covers the apply run — the inline prefix below only resolves
|
|
433
|
-
the tag for display; `mmi-cli devops release --apply` re-derives intent itself and fail-closes before any
|
|
434
|
-
merge, tag, or fold when the value is invalid:
|
|
435
|
-
```bash
|
|
436
|
-
TAG=$(node scripts/next-version.mjs release) # full-track repos -> vX.Y.0
|
|
437
|
-
TAG=$(MMI_BUMP_INTENT=minor node scripts/next-version.mjs cycle) # direct-track repos -> vX.Y.0
|
|
438
|
-
git tag "$TAG"
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
## Step 3 — push tag, wait for the REQUIRED checks, then push main (the gate)
|
|
442
|
-
|
|
443
|
-
`mmi-cli devops release --apply`'s exit code is **not** the release verdict: exit `2` can mean the release
|
|
444
|
-
shipped successfully while protected-branch alignment remains pending, and exit `1` can mean a
|
|
445
|
-
post-release follow-up failed after promotion — **or that the follow-up is merely unresolved** (#4939):
|
|
446
|
-
a verdict of `releaseStatus=succeeded` with `followUpStatus=pending` (the release-triggered runs — e.g.
|
|
447
|
-
the Hub's `deploy.yml` + `publish.yml` — had only just auto-fired) also exits nonzero, and a
|
|
448
|
-
background-task harness surfacing exit codes misreads it as "release failed" when the release actually
|
|
449
|
-
shipped. Classify from the verdict fields, never the exit code alone: `succeeded` + `pending` means
|
|
450
|
-
shipped with the follow-up unresolved — watch the enumerated `workflowRuns` to conclusion; only a
|
|
451
|
-
resolved failure there is a failed follow-up. Read the live release verdict and verify these four
|
|
452
|
-
facts instead: the `main..development` count, the tag on `origin`, the bounded Latest Release read in
|
|
453
|
-
Step 6, and the runs on the release SHA.
|
|
454
|
-
|
|
455
|
-
Required status checks are **per-repo branch protection, not a fixed list** — MMI-Hub's `main` requires
|
|
456
|
-
`cli` · `infra` · `docs`, but a product repo may require different contexts or none at all (#1045). The
|
|
457
|
-
release SHA is always fresh (the Step 1b fold commits on local `main`), so when checks ARE required,
|
|
458
|
-
pushing the branch *first* is structurally rejected until CI catches up. Push the **tag first** (it lands
|
|
459
|
-
the SHA and triggers the repo's CI — in MMI-Hub `gate.yml` runs on `v*` tags — without touching the
|
|
460
|
-
protected ref), then probe what `main` actually requires and wait only for those contexts:
|
|
461
|
-
|
|
462
|
-
```bash
|
|
463
|
-
git push origin "vX.Y.0" # lands the SHA + triggers the repo's CI
|
|
464
|
-
SHA=$(git rev-parse main)
|
|
465
|
-
# discover the REQUIRED contexts on main (classic protection + rulesets; 404 = none from that source):
|
|
466
|
-
gh api repos/{owner}/{repo}/branches/main/protection/required_status_checks --jq '[.contexts[]]'
|
|
467
|
-
gh api repos/{owner}/{repo}/rules/branches/main \
|
|
468
|
-
--jq '[.[]|select(.type=="required_status_checks")|.parameters.required_status_checks[].context]'
|
|
469
|
-
# ZERO required contexts -> push main immediately (the GitHub push gate is the backstop).
|
|
470
|
-
# Otherwise poll until every required context is "success" on $SHA — never a hard-coded list, and bound
|
|
471
|
-
# the wait (~10 min): on timeout, stop with a clear failure naming the pending/failed contexts.
|
|
472
|
-
gh api repos/{owner}/{repo}/commits/$SHA/check-runs \
|
|
473
|
-
--jq '[.check_runs[]|{name:.name,conclusion:.conclusion}]'
|
|
474
|
-
git push origin main
|
|
475
|
-
```
|
|
476
|
-
(`mmi-cli devops release --apply` performs this discovery + bounded wait itself.) A required context can only ever
|
|
477
|
-
resolve on a tag SHA if its workflow runs on `push: tags` (or is otherwise SHA-addressable) — a PR/issue-event
|
|
478
|
-
job (e.g. a stale hand-added `add-to-project` / `mark-merged-pr-done` board-automation check) structurally
|
|
479
|
-
never produces a check-run there. The train recognizes that fixed pair by name and, after a short grace
|
|
480
|
-
window, treats it as satisfied-for-the-tag instead of burning the full ~10-minute budget on a check-run that
|
|
481
|
-
can never appear (#2404). Any OTHER required context still waits the full budget and, on timeout, names
|
|
482
|
-
exactly which contexts never materialized vs. which were merely pending. If your repo's ruleset requires a
|
|
483
|
-
PR-only job outside that pair, fix the ruleset (drop it, or replace with a real gate) — don't rely on the
|
|
484
|
-
grace window for anything not on the allowlist.
|
|
485
|
-
Rejected → stop (nothing released).
|
|
486
|
-
|
|
487
|
-
**Halted on a FAILED required check (not merely pending)?** That is a **stray unreleased tag** (#2734): the
|
|
488
|
-
tag is pushed but `main` was not, no GitHub Release exists, and nothing deployed/published off it — so it can
|
|
489
|
-
never go forward (a failed check will not pass on that SHA). This is the **opposite** of a *resumable* partial
|
|
490
|
-
state (checks passing/pending, only the branch push / Release / deploy left, where the recovery is to finish
|
|
491
|
-
forward and you must NOT delete the tag). Here: fix the cause on `development` via a CI-gated PR, then **delete
|
|
492
|
-
the stray tag** — `git push origin --delete vX.Y.0` **and** `git tag -d vX.Y.0` (delete the local tag too; a
|
|
493
|
-
surviving local tag makes `next-version cycle` silently mint the *next* version) — and re-run `mmi-cli devops release
|
|
494
|
-
--apply` (the fold re-tags the same version on the fixed HEAD). **Tag deletion is deletion-class → it needs the
|
|
495
|
-
authorized human's explicit per-turn go.** Exception: if a publish/deploy DID run off the stray tag, do **not**
|
|
496
|
-
delete — mint the next version with `MMI_RELEASE_VERSION` instead. The CLI already classifies this
|
|
497
|
-
(`isStrayUnreleasedTag`, `cli/src/train-apply.ts`); this note surfaces the doctrine so a halted operator does
|
|
498
|
-
not re-derive it from source.
|
|
499
|
-
|
|
500
|
-
**Halted by a concurrent-train refusal, or a publish verify that says package/lockfile versions ≠ the tag
|
|
501
|
-
(#5380)?** Same deletion class as a stray tag, plus the GitHub Release if one was created (it may still show
|
|
502
|
-
as **Latest** while broken). Evidence shape: two tags one patch apart from overlapping `/release` runs (e.g.
|
|
503
|
-
`v3.1.11` on a no-fold SHA + `v3.1.12` minted because that tag landed mid-flight), and verify text like
|
|
504
|
-
`root package and lockfile versions must equal 3.1.11`. Do **not** leave the skip-version tag and continue —
|
|
505
|
-
with the authorized human's go: delete the broken Release (`gh release delete vX.Y.Z --yes`), delete the
|
|
506
|
-
stray tag locally and on origin, confirm `next-version` would re-derive the *same* version (not the skip),
|
|
507
|
-
then re-run a **single** `mmi-cli devops release --apply`. If the newer skip-version already published
|
|
508
|
-
cleanly, keep it and only remove the broken older Release/tag.
|
|
509
|
-
|
|
510
|
-
**Resuming a resumable partial state? The recovery form is `--resume --watch --json`, and nothing
|
|
511
|
-
else (#5815).** `--resume` and `--apply` are **mutually exclusive** and the CLI refuses the pair:
|
|
512
|
-
`--apply cuts the NEXT version, --resume finishes the immutable tag already on origin`
|
|
513
|
-
(`cli/src/index.ts`, the #3851 guard). A continuation that reaches for
|
|
514
|
-
`--resume --apply --watch` is asking the train to mint a new version while finishing an old one —
|
|
515
|
-
it refuses before any mutation, so nothing is lost, but the operator has spent a turn on a form that
|
|
516
|
-
can never run. `--apply` belongs to a **new** release cut, never to recovery.
|
|
517
|
-
```bash
|
|
518
|
-
mmi-cli devops release --resume --watch --json --repo <owner/repo>
|
|
519
|
-
```
|
|
520
|
-
|
|
521
|
-
## Step 4 — GitHub Release + start prod deploy (non-blocking)
|
|
522
|
-
|
|
523
|
-
For `tenant-container` repos, publish the GitHub Release and dispatch the central tenant deploy.
|
|
524
|
-
```bash
|
|
525
|
-
gh release create "vX.Y.0" --target main --generate-notes --latest
|
|
526
|
-
gh workflow run tenant-deploy.yml --repo mutmutco/MMI-Hub \
|
|
527
|
-
-f slug={slug} -f repo={owner}/{repo} -f ref=main -f stage=main
|
|
528
|
-
```
|
|
529
|
-
|
|
530
|
-
On PowerShell, read the run as a structured receipt and filter it natively; do not nest a
|
|
531
|
-
`gh run list` query inside `gh run watch`:
|
|
532
|
-
```powershell
|
|
533
|
-
$runs = gh run list --workflow tenant-deploy.yml --limit 10 --json databaseId,createdAt,url,status,conclusion | ConvertFrom-Json
|
|
534
|
-
$run = $runs |
|
|
535
|
-
Where-Object { $_.databaseId -and $_.createdAt } |
|
|
536
|
-
Sort-Object { [DateTimeOffset]$_.createdAt } -Descending |
|
|
537
|
-
Select-Object -First 1
|
|
538
|
-
if (-not $run) { throw 'tenant-deploy workflow run was not found' }
|
|
539
|
-
$run | Select-Object databaseId,createdAt,url,status,conclusion | ConvertTo-Json -Compress
|
|
540
|
-
# Run this in the BACKGROUND (PowerShell Start-Job / task runner).
|
|
541
|
-
gh run watch $run.databaseId --exit-status
|
|
542
|
-
```
|
|
543
|
-
|
|
544
|
-
When several run receipts need collecting, assign the loop output before serializing it; a top-level
|
|
545
|
-
`foreach (...) { ... } | ConvertTo-Json` is a PowerShell parser trap:
|
|
546
|
-
```powershell
|
|
547
|
-
$out = foreach ($run in $runs) {
|
|
548
|
-
gh run view $run.databaseId --json databaseId,url,status,conclusion,createdAt | ConvertFrom-Json
|
|
549
|
-
}
|
|
550
|
-
$out | ConvertTo-Json -Compress
|
|
551
|
-
```
|
|
552
|
-
|
|
553
|
-
For `hub-serverless` (MMI-Hub), publish the GitHub Release but do **not** dispatch `tenant-deploy.yml`:
|
|
554
|
-
the release event auto-fires `deploy.yml` for prod and `publish.yml` for the
|
|
555
|
-
plugin/CLI package. A missing `DEPLOY#main` registry row for MMI-Hub is expected, not a tenant stack repair
|
|
556
|
-
task. Watch/report the release-triggered `deploy.yml` and `publish.yml` runs instead.
|
|
557
|
-
|
|
558
|
-
For other direct-track repos, the train dispatches nothing centrally: a `registry-publish` repo's release
|
|
559
|
-
event fires its own `publish.yml` (npm / plugin marketplace); a `solo-container` repo deploys via its own
|
|
560
|
-
workflow. Publish the GitHub Release, then watch/report that repo's own release-triggered run.
|
|
561
|
-
|
|
562
|
-
**Jerv-Hub has one additional fail-closed operator-host phase (#5699).** Only for exact repository
|
|
563
|
-
`mutmutco/Jerv-Hub`, after its release-triggered `publish.yml` is proven green, the train runs
|
|
564
|
-
`scripts/jerv-gateway-release-deploy.sh --tag <resolved-tag>`. That repository helper keeps root SSH in
|
|
565
|
-
Jerv Vault custody, deploys the exact tagged commit to jerv-central, and requires systemd, `/livez`, and
|
|
566
|
-
`/readyz` health. Its validated receipt is appended to `workflowRuns` as `jerv-gateway`; a missing helper,
|
|
567
|
-
refusal, malformed receipt, unhealthy activation, or failed rollback sets `deployStatus: failure`. An
|
|
568
|
-
unwatched/pending publish defers Gateway deployment and keeps `deployStatus: pending` — it never runs the
|
|
569
|
-
host mutation before publication is proven. `release --resume --watch` re-derives the historical publish
|
|
570
|
-
verdict and invokes the idempotent helper once it is green; it never re-tags or republishes the release.
|
|
571
|
-
Do not move this phase into GitHub Actions: no supported Actions identity reaches personal jerv-central
|
|
572
|
-
without moving root SSH custody out of its owning vault.
|
|
573
|
-
|
|
574
|
-
`mmi-cli devops release --apply --json` returns the relevant run id/url data with `deployStatus`; `--watch` blocks
|
|
575
|
-
on the run(s) and resolves `deployStatus` to `success`/`failure`. For tenant-container repos, that is the
|
|
576
|
-
dispatched `tenant-deploy.yml` run. For Hub serverless, that is the auto-fired release `deploy.yml` and
|
|
577
|
-
`publish.yml` workflow pair — watched on `mutmutco/MMI-Hub`. For `registry-publish`, that is the target
|
|
578
|
-
repo's own release-triggered `publish.yml` run — watched on that repo, never dispatched, and never the
|
|
579
|
-
central `tenant-publish.yml` (#2428: dispatching both is a guaranteed npm E409 double-publish, since the
|
|
580
|
-
repo's own release-triggered publish always wins the version race).
|
|
581
|
-
`promoted: true` stays set even on a failed deploy — promotion and deploy are separate outcomes.
|
|
582
|
-
|
|
583
|
-
**`deployStatus: 'pending'` is NEVER a terminal, reportable state (#3322).** A release that returns
|
|
584
|
-
`pending` has an UNRESOLVED deploy, not a clean one. The report now prints `deploy: UNVERIFIED` instead of
|
|
585
|
-
omitting the deploy line entirely — which is exactly how a failed deploy once hid behind a "successful"
|
|
586
|
-
release (Jerv-PowerTools v1.22.0: the release-triggered `publish.yml` went green, a separate `push: main`
|
|
587
|
-
SAM deploy FAILED, and the train returned `deployStatus: pending`, so the run read as clean). Resolve it,
|
|
588
|
-
or report it as unverified-RED. Never as success.
|
|
589
|
-
|
|
590
|
-
**Read the `--apply` result's `workflowRuns` array — the train already enumerates every run on the release
|
|
591
|
-
SHA (#3699).** That enumeration includes `push: main` infra deploys (`deploy.yml`, SAM) the deploy model does
|
|
592
|
-
not name, and `deployStatus` is held at `pending` until they resolve. Any non-success entry — including
|
|
593
|
-
`pending` — keeps the release unverified: watch those listed runs to conclusion:
|
|
594
|
-
```bash
|
|
595
|
-
gh run watch <run-id> --repo {owner}/{repo} --exit-status
|
|
596
|
-
```
|
|
597
|
-
The one remaining caveat: `--watch` blocks only on deploy-model-named runs, not extra push-triggered
|
|
598
|
-
deploys — those you conclude from the `workflowRuns` list yourself. Any `failure` there is a failed release
|
|
599
|
-
deploy even when the train reported success — flag it loudly and fix it before calling the release healthy.
|
|
600
|
-
|
|
601
|
-
**Identify WHICH run is red before believing the wording (#5872).** The release pushes the tag and the
|
|
602
|
-
branch seconds apart, so an ordinary repo CI gate lands on the release SHA too — it is enumerated
|
|
603
|
-
alongside the deploy and publish runs, and a red one reads out as
|
|
604
|
-
`deploy/publish on the release SHA FAILED`. On Jerv-Hub v3.4.4 that sentence appeared while `publish.yml`
|
|
605
|
-
was green and npm was already serving 3.4.4: the red entry was a `gate` push run that had raced the
|
|
606
|
-
shared runner's npm cache (the same SHA's tag-push gate passed, and a re-run passed). A red build gate
|
|
607
|
-
**holds the release unverified but is not a publish failure** — and on Jerv-Hub it defers the fail-closed
|
|
608
|
-
Gateway phase (#5699) until the gate is green, so it must be re-run and the release resumed, not
|
|
609
|
-
re-tagged. Read the run NAMES in `workflowRuns` first:
|
|
610
|
-
|
|
611
|
-
- `deploy.yml` / `publish.yml` / `tenant-deploy.yml` / `jerv-gateway` — a real deploy or publish verdict.
|
|
612
|
-
- `gate` (or any repo CI workflow) on a `push` event — ordinary CI that happens to share the SHA. Re-run
|
|
613
|
-
it; nothing about the published artifact is in question.
|
|
614
|
-
|
|
615
|
-
**Hub releases announce to Slack (#883).** Hub scope is **only** `mutmutco/MMI-Hub`: the commits/PRs on
|
|
616
|
-
`origin/main..origin/development`, this repo's `deploy.yml` + `publish.yml`, and Hub tooling (`mmi-cli`,
|
|
617
|
-
skills, plugin, registry, central workflows). Do **not** read another repo's board (`mmi-cli oracle board` / `/mmi`
|
|
618
|
-
on a product), watch or dispatch `ds-propagate.yml`, run `design-system` / `doctor` design-system heals for
|
|
619
|
-
consumers, or treat a product's deploy state as part of this release — those belong to that product's own
|
|
620
|
-
`/release` or `/rcand`, never a Hub train.
|
|
621
|
-
|
|
622
|
-
Before running `--apply` for MMI-Hub, resolve the real tag first:
|
|
623
|
-
`TAG=$(MMI_BUMP_INTENT=minor node scripts/next-version.mjs cycle)` — always print `$TAG` (e.g.
|
|
624
|
-
`v2.43.0`) in summaries, Slack, chat, and the final report; never a placeholder like `vX.Y.0` or
|
|
625
|
-
`v0.x.0`.
|
|
626
|
-
|
|
627
|
-
**Write the announcement from the actual release evidence** — summarize the user-visible change, required action, and recovery path before posting. Key rules:
|
|
628
|
-
|
|
629
|
-
- **Audience is the whole org** — no issue/PR numbers, repo names, commit SHAs, or internal tool jargon.
|
|
630
|
-
- **Scale length and depth by release type** (major > minor > patch > hotfix): a major is a broad,
|
|
631
|
-
migration-focused announcement; a minor is scannable grouped bullets; a patch is factual and minimal;
|
|
632
|
-
a hotfix is incident-style (impact/status/workaround) with a concise durable note.
|
|
633
|
-
- **Front-load**: one lead sentence stating the most important thing, then grouped bold sections;
|
|
634
|
-
breaking changes and required actions get their own visible section.
|
|
635
|
-
- **Benefits, active voice, addressed to the reader** — no hype, no internal identifiers.
|
|
636
|
-
|
|
637
|
-
Write the curated summary to a fresh temp file (`f=$(mktemp tmp/release-summary.XXXXXX)`, so a stale prior
|
|
638
|
-
summary is never reused) and point `--out` at an equally fresh result receipt
|
|
639
|
-
(`r=$(mktemp tmp/release-receipt.XXXXXX)`, #5983). Source from **Hub PR titles only** (`origin/main..origin/development` on
|
|
640
|
-
`mutmutco/MMI-Hub`), but **rewrite** each line in neutral Hub-subsystem terms (CLI, skills, plugin,
|
|
641
|
-
workflows, registry, deploy hub) and to the release-type depth the guide demands — **never** a
|
|
642
|
-
product or brand name (FoFu, Katip, etc.) anywhere in the summary file, Slack post, chat, or release
|
|
643
|
-
report. Product names are allowed only when releasing **that product's repo**. Then pass the file through:
|
|
644
|
-
`mmi-cli devops release --apply --announce-summary-file "$f" --json --out "$r"`. After the GitHub Release publishes, the CLI posts
|
|
645
|
-
the summary to the org alerts channel as the MMI-Future Slack app (token + channel from SSM at run time).
|
|
646
|
-
For a new MMI-Hub `--apply`, the CLI refuses before promotion when the file is missing, unreadable, or does
|
|
647
|
-
not contain 3–6 non-empty lines; generated-note fallback is not an agent release path. `--resume` never
|
|
648
|
-
requires or republishes a summary, and non-Hub repos skip the announcement automatically. After a valid
|
|
649
|
-
summary is accepted, Slack delivery remains best-effort: a transport failure is reported in the result and
|
|
650
|
-
never rolls back an otherwise completed release.
|
|
651
|
-
|
|
652
|
-
**Write the result receipt before bounded monitoring (#5983).** A background-task harness that bounds its
|
|
653
|
-
output retains only the tail of the apply run — exactly where the accepted announcement outcome
|
|
654
|
-
(`announceNote`) can be dropped. With `--out <path>`, the CLI itself writes the full apply result to the
|
|
655
|
-
file as BOM-free UTF-8 (never a shell `>` redirect — see #5802), so the receipt survives any monitor
|
|
656
|
-
bound. `--resume` proves the Release and deploy but does not and should not re-announce, so this receipt
|
|
657
|
-
is the only durable record of the accepted Slack delivery — verify it in Step 6.
|
|
658
|
-
|
|
659
|
-
**Don't block on the deploy.** Start the watch as a background task and proceed to Steps 4b–5 (docs, project
|
|
660
|
-
info, branch alignment) while prod deploys. The verdict is collected in Step 6 — verification is not
|
|
661
|
-
skipped, only un-blocked. Deploy failure → report plainly, then **retry the existing promoted ref by deploy
|
|
662
|
-
model** once the runtime is repaired — never a re-tag or republish (`main` is already at the release, which is
|
|
663
|
-
correct). Read the model from the registry (`mmi-cli oracle org project get {owner}/{repo}` → `deployModel`):
|
|
664
|
-
- **tenant-container** — the central tenant deployer owns the redeploy: `mmi-cli devops runtime tenant redeploy
|
|
665
|
-
{owner}/{repo} main --watch`.
|
|
666
|
-
- **repository-owned serverless / registry-publish** (e.g. Jerv-PowerTools' `Deploy Jerv Memory`) — there is
|
|
667
|
-
no `runtime tenant redeploy` for these; re-run the repo's OWN declared `workflow_dispatch` deploy workflow at
|
|
668
|
-
the promoted-equivalent ref: `gh workflow run <deploy-workflow> --repo {owner}/{repo} --ref <ref>`.
|
|
669
|
-
- **hub-serverless** (MMI-Hub) — repair the named deploy failure, then rerun the failed deploy run. Do not re-tag.
|
|
670
|
-
|
|
671
|
-
Name the sanctioned dispatch mechanism you used in the train report.
|
|
672
|
-
|
|
673
|
-
**Cut over after hook changes.** If this release changes or retires plugin hooks, stop this seat here.
|
|
674
|
-
Complete the host's declared session/workspace reload or process restart, resume the train in that fresh
|
|
675
|
-
seat, and run read-only `mmi-hub status` before any Step 4b shell/git command.
|
|
676
|
-
|
|
677
|
-
## Step 4b — re-sync project info (no in-repo docs pass)
|
|
678
|
-
|
|
679
|
-
In-repo README/architecture freshness is **not** a train step (Step 0d retired, Hub#4164).
|
|
680
|
-
`mmi-cli devops release --apply` re-syncs the GitHub **Project** short description + thin README from the
|
|
681
|
-
released commit and the registry member list through the command ladder. For a manual repair or preview use:
|
|
682
|
-
```bash
|
|
683
|
-
mmi-cli oracle org project sync-info --apply # omit --apply for the read-only plan
|
|
684
|
-
```
|
|
685
|
-
|
|
686
|
-
## Step 5 — roll development forward
|
|
687
|
-
|
|
688
|
-
- Keep branches aligned: `mmi-cli devops release --apply` back-merges the released `main` (incl. the version
|
|
689
|
-
fold) into `development` and reports it as `devRollForward`. When `development` has no required checks it
|
|
690
|
-
pushes directly (`status: pushed`). When `development` *requires* checks (e.g. MMI-Hub needs
|
|
691
|
-
`cli`/`infra`/`docs`), the fresh merge commit carries no passing checks, so a direct push is structurally
|
|
692
|
-
rejected — the train instead **opens an alignment PR** `main → development` (`status: pr-pending`) and the
|
|
693
|
-
release report prints the exact land command. The release itself has already shipped; **land the alignment
|
|
694
|
-
PR with a true merge** — `mmi-cli devops pr merge <number> --auto --merge` (never squash — a squash drops the merge
|
|
695
|
-
parentage and the misalignment guard re-flags the divergence). `--auto` is what makes it land right after a
|
|
696
|
-
release: the alignment PR's own `cli`/`infra`/`docs` checks are still running, so a plain immediate merge is
|
|
697
|
-
policy-blocked — `--auto` merges once they pass. If you await with `--wait` instead, still pass `--merge`
|
|
698
|
-
(`mmi-cli devops pr merge <number> --wait --merge`); `--wait` alone must not land the alignment PR. Never force.
|
|
699
|
-
A pre-#5903 CLI can true-merge this PR and still exit 1 with `mergeStatus: partial-cleanup` after GitHub
|
|
700
|
-
refuses to delete its protected `main` head. Classify that legacy receipt from the live PR: `MERGED` with
|
|
701
|
-
merge method `MERGE` means alignment succeeded and protected-main retention is expected. Never delete
|
|
702
|
-
`main`, retry the release, or call the alignment failed. Current CLIs report the same retention as
|
|
703
|
-
`remoteBranch.status: retained-protected` with `mergeStatus: merged`.
|
|
704
|
-
- Full-track repos: `mmi-cli devops release --apply` already aligned `rc` to the released `main` (#1036 — the
|
|
705
|
-
push runs inside the authority-gated train; the result reports it as `rcAlignment`). No manual `rc`
|
|
706
|
-
push — if the result reports a failed alignment, investigate and rerun via the train, never bare-push.
|
|
707
|
-
- **Local branches never left stale (#2582).** A release advances `main`/`development`/`rc` on **origin**;
|
|
708
|
-
`mmi-cli devops release --apply` also fast-forwards the LOCAL train branches to match, so the checkout you
|
|
709
|
-
released from does not lag what you just pushed. The train restores + fast-forwards the start branch
|
|
710
|
-
(`checkout`, #2340) and fast-forwards the others (`localSync`) — non-destructively: only a true
|
|
711
|
-
fast-forward moves a branch; a diverged local branch is reported and left untouched (never a force).
|
|
712
|
-
The one branch it **cannot** sync at release time is a check-gated `development` whose roll-forward is a
|
|
713
|
-
`pr-pending` alignment PR (Hub) — that PR merges on origin *after* the release returns. Once that PR
|
|
714
|
-
lands, fast-forward the local branch yourself (`git pull --ff-only` on `development`) — #5908 retired
|
|
715
|
-
the session-start self-heal along with the rest of the hook-era plumbing. Nothing here ever blocks or
|
|
716
|
-
fails the release.
|
|
717
|
-
- **Read the `checkout:` clause — it does not always say "returned" (#4006).** The report always prints
|
|
718
|
-
one, and restoration is deliberately skipped rather than forced when the repo is not in a fit state.
|
|
719
|
-
A release leaves the repo on `main` until the train moves it back, so a non-`returned` outcome means
|
|
720
|
-
you are still there and must return by hand once the cause is cleared:
|
|
721
|
-
- `checkout restoration skipped: working tree changed after release` — the tree went dirty DURING the
|
|
722
|
-
train, so the train declined to move you. Diagnose those paths with the Step 0 clean-tree rules,
|
|
723
|
-
then `git checkout <start branch>` yourself. The release itself already shipped; this is a
|
|
724
|
-
deliberate, harmless decline, not a failed release.
|
|
725
|
-
- `checkout restoration failed while returning to …` / `origin/<branch> fast-forward failed` — the
|
|
726
|
-
checkout or the pull errored. Read the appended git message; the release is unaffected.
|
|
727
|
-
|
|
728
|
-
`--resume` uses the same `checkout:` clause after every branch-mutating terminal path. Direct-track
|
|
729
|
-
resumes return to `development`; full-track resumes return to `rc`. A skipped restoration names the
|
|
730
|
-
dirty tree, checkout failure, or fast-forward failure and still leaves the shipped release intact.
|
|
731
|
-
|
|
732
|
-
## Step 6 — collect deploy verdict + report
|
|
733
|
-
|
|
734
|
-
Verify the Slack announcement from the receipt, not from memory (#5983). The `--out` receipt written at
|
|
735
|
-
Step 4 is the durable record of the initial apply result; read its `announceNote` field before the final
|
|
736
|
-
report and state the delivery verdict (`announced`, `skipped`, or the failure note). A monitor that kept
|
|
737
|
-
only the apply output's tail may have dropped that line — the receipt is what keeps that from losing the
|
|
738
|
-
outcome.
|
|
739
|
-
|
|
740
|
-
The `release --apply` exit code is **not** the release verdict. A nonzero exit with
|
|
741
|
-
`releaseStatus=succeeded` and `followUpStatus=pending` is the unresolved-follow-up case from Step 3 —
|
|
742
|
-
the release shipped; the runs enumerated in `workflowRuns` are the outstanding verdict, so watch them
|
|
743
|
-
to conclusion here rather than re-running or alarming. Report the live release verdict from
|
|
744
|
-
the four checks that matter: the `main..development` count, the tag on `origin`, the bounded Latest
|
|
745
|
-
Release read, and the runs on the release SHA; an alignment PR is normal follow-up work when those
|
|
746
|
-
checks confirm the release shipped.
|
|
747
|
-
|
|
748
|
-
Use only this bounded Latest Release read:
|
|
749
59
|
```bash
|
|
750
60
|
gh api repos/{owner}/{repo}/releases/latest --jq '{tagName:.tag_name,targetCommitish:.target_commitish,publishedAt:.published_at,url:.html_url}'
|
|
751
61
|
```
|
|
752
|
-
Require its `tagName` to equal the expected `$TAG`; a mismatch means Latest does not identify this
|
|
753
|
-
release and is unverified.
|
|
754
62
|
|
|
755
|
-
|
|
756
|
-
`gh release view`: the field is not in its JSON schema, so gh rejects the whole read and lists its
|
|
757
|
-
supported fields — the check errors instead of answering, and Latest stays unverified. This bounded
|
|
758
|
-
read is the supported Latest probe; `gh release list --json <fields>,isLatest` is the supported form
|
|
759
|
-
only when a list receipt is genuinely the right shape.
|
|
63
|
+
Four facts close the verdict: the `origin/main..origin/development` count, the tag on origin, Latest, and green runs on the release SHA.
|
|
760
64
|
|
|
761
|
-
|
|
762
|
-
healthy (the central deploy workflow smoke step / a health check); **red** → report the failure prominently and flag
|
|
763
|
-
that the release shipped on a failed deploy (re-run just the deploy — `main` is already correct).
|
|
65
|
+
## Recovery
|
|
764
66
|
|
|
765
|
-
|
|
766
|
-
`deployStatus` for every run on the release SHA, so a flaky repo CI gate that shares that SHA is reported
|
|
767
|
-
in the language of a failed publication — the operator is told publication failed while npm is serving
|
|
768
|
-
the version. Before writing that into a report, name the workflow: a red `deploy.yml` / `publish.yml` /
|
|
769
|
-
`tenant-deploy.yml` / `jerv-gateway` is a real deploy or publish failure; a red `gate` on a `push` event
|
|
770
|
-
is ordinary CI. Both hold the release unverified and both are fixed by re-running that run and resuming —
|
|
771
|
-
neither is ever fixed by re-tagging — but only the first means anything about the artifact.
|
|
67
|
+
Every refusal and every doctor blocker names its `code` and `anchor`: open `docs/Guides/train-troubleshooting.md#<code>` and follow that entry, nothing else. A partial release (tag on origin, later phases pending or failed) continues with `mmi-cli devops release --resume --watch --json --out <fresh-receipt>` — the only continuation form: `--resume` never re-tags, republishes or re-announces, and `--apply` belongs to a new cut only. A refused train is a stop (Merge floor): fix the cause on `development` through a normal PR, then rerun.
|
|
772
68
|
|
|
773
|
-
|
|
774
|
-
generated artifacts. Non-Hub `registry-publish` repositories must not run `release-distribution.mjs
|
|
775
|
-
verify`: use their own release-workflow evidence and the publish-visibility contract below.
|
|
69
|
+
## Merge floor
|
|
776
70
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
```powershell
|
|
783
|
-
$runs = gh run list --workflow publish.yml --event release --limit 10 --json databaseId,createdAt,url,status,conclusion | ConvertFrom-Json
|
|
784
|
-
$run = $runs |
|
|
785
|
-
Where-Object { $_.databaseId -and $_.createdAt } |
|
|
786
|
-
Sort-Object { [DateTimeOffset]$_.createdAt } -Descending |
|
|
787
|
-
Select-Object -First 1
|
|
788
|
-
if (-not $run) { throw 'release-triggered publish workflow run was not found' }
|
|
789
|
-
$run | Select-Object databaseId,createdAt,url,status,conclusion | ConvertTo-Json -Compress
|
|
790
|
-
gh run watch $run.databaseId --exit-status
|
|
791
|
-
node scripts/release-distribution.mjs verify "X.Y.0" # bare semver — asserts registry versions, BOM identities, and published npm artifacts
|
|
792
|
-
```
|
|
793
|
-
**verify takes bare semver (#5379).** Pass `X.Y.Z`, not the git-tag form `vX.Y.Z`. Checkout still
|
|
794
|
-
uses the tag (`git checkout "vX.Y.0"`); the distribution script's version argument is strict semver
|
|
795
|
-
without a leading `v`.
|
|
796
|
-
Run that verify from a checkout **at the tag** (`main` right after the release merge, `git checkout
|
|
797
|
-
"vX.Y.0"`, or a worktree at the tag) — verify checks the working tree and refuses any other commit once
|
|
798
|
-
the tag exists, so a stale `development`/`rc` checkout can't masquerade as a broken release.
|
|
71
|
+
- **Never squash-merge a tagged commit (#3167).** A squash re-mints the SHA and orphans the tag from the branch lineage, so every tag-anchored check mis-reads forever after. Any merge that carries an already-tagged commit — the `main → development` roll-forward, an alignment PR, a hotfix fold PR — lands as a true merge: `mmi-cli devops pr merge <number> --auto --merge` (or `--wait --merge`); `--wait` alone still squashes. The hotfix main-base PR itself may squash: its tag lands after, on the merged `main` HEAD, and the `-x` trailer must survive the squash message.
|
|
72
|
+
- **A refused train is a stop, never a license to finish by hand.** Every fail-closed halt — doctor blocker, authority probe, coverage guard, untolerated conflict, required checks, stray tag, ledger — ends the run. Fix the cause on `development` through a normal PR and rerun the train; never hand-resolve on `main`, hand-cherry-pick, bare-push a train branch, ack to save time, or bypass a check.
|
|
73
|
+
- **Version probes anchor on the latest Release tag, not a branch (#2734).** A branch manifest or a stale local tag is not a version source; a surviving local tag silently mints the next version.
|
|
74
|
+
- **One train at a time on a repo (#5380).** Two overlapping runs race the shared tag namespace; the ledger refuses the second — never start one to help the first.
|
|
75
|
+
- **Tag and Release deletion are deletion-class:** the authorized human's explicit per-turn go, named in the guide entry that prescribes it.
|
|
799
76
|
|
|
800
|
-
|
|
801
|
-
`--skip-npm-view`) runs a live `npm pack` per public surface, and `npm pack` refuses a package whose
|
|
802
|
-
declared `bin` target is absent — e.g. `updater publishes bin jerv-hub -> dist/index.cjs, which npm pack
|
|
803
|
-
does not ship — run npm run build`. Whether that bites depends on the repo: a build output that is
|
|
804
|
-
**tracked** (MMI-Hub commits `updater/dist/index.cjs`) is present in a clean tag checkout, one that is
|
|
805
|
-
**gitignored** is not, and a fresh clone has no `node_modules` either way. So provision the tag worktree
|
|
806
|
-
first, and check the declared bin targets rather than assuming:
|
|
807
|
-
```bash
|
|
808
|
-
git worktree add "$PROOF" "vX.Y.0" # $PROOF outside the repo — see the scratch note below
|
|
809
|
-
cd "$PROOF" && npm ci && npm run build # build only if a declared bin target is missing from the checkout
|
|
810
|
-
node scripts/release-distribution.mjs verify "X.Y.0"
|
|
811
|
-
```
|
|
812
|
-
Removing that proof tree afterwards: `npm ci` writes `node_modules`, which `git status --porcelain`
|
|
813
|
-
never shows and which makes `git worktree remove` refuse with `Directory not empty`. Delete
|
|
814
|
-
`node_modules` first, then remove the worktree. On Windows a global install made from a worktree leaves
|
|
815
|
-
a junction there, so delete the directory rather than following it. In a guarded agent shell pass the
|
|
816
|
-
**literal absolute** proof path to `git worktree remove` — the checkout guard refuses a variable-held
|
|
817
|
-
target (`Blocked git worktree remove with an unanalyzable variable-held target`, #2492 / #6060); only an
|
|
818
|
-
interactive shell may pass `"$PROOF"`.
|
|
819
|
-
Release-blocking for Hub tooling changes: every public artifact must match the registry version and BOM
|
|
820
|
-
identity. Manual fallback if CI can't publish: `node scripts/release-distribution.mjs publish "X.Y.0"`
|
|
821
|
-
from a machine with npm auth.
|
|
77
|
+
## Report
|
|
822
78
|
|
|
823
|
-
|
|
824
|
-
headless and CI hosts keep using automation tokens. When any npm write requires a human desktop passkey on
|
|
825
|
-
Windows, **never run it from the agent shell**. Write the package loop to a durable `.cmd` outside disposable
|
|
826
|
-
worktrees (for example, `C:\Users\<you>\Documents\mmi-npm-write.cmd`):
|
|
827
|
-
```bat
|
|
828
|
-
@echo off
|
|
829
|
-
set "failures=%~dpn0.failures.txt"
|
|
830
|
-
del "%failures%" 2>nul
|
|
831
|
-
for %%P in (@scope/package-a @scope/package-b) do (
|
|
832
|
-
call npm deprecate "%%P@*" "replacement message"
|
|
833
|
-
if errorlevel 1 >>"%failures%" echo %%P
|
|
834
|
-
)
|
|
835
|
-
if exist "%failures%" type "%failures%"
|
|
836
|
-
```
|
|
837
|
-
Launch it visibly and leave the window open, then let the human tap once for each npm invocation:
|
|
838
|
-
```powershell
|
|
839
|
-
Start-Process cmd -ArgumentList '/k', 'C:\Users\<you>\Documents\mmi-npm-write.cmd'
|
|
840
|
-
```
|
|
841
|
-
Ignore any EOTP browser URL npm prints; it does not complete this passkey flow. Treat the failure file as the
|
|
842
|
-
per-package receipt and verify the corresponding registry state, for example
|
|
843
|
-
`npm view <pkg> deprecated --prefer-online` after `npm deprecate`.
|
|
79
|
+
Title first, verdict first. Name the resolved tag (never a placeholder like `vX.Y.0`) and the Release URL, the receipt's status fields (`releaseStatus`, `followUpStatus`, `deployStatus`, `promoted`), every `workflowRuns` entry with its conclusion and URL — saying which run is red, and whether it is a deploy/publish verdict or ordinary CI — `announceNote`, the alignment note (`devRollForward`, `rcAlignment`, or the hotfix fold PR and whether it landed), the `checkout` clause, and the sanctioned dispatch mechanism you used. A pending follow-up is reported as unverified, never as success. Product names appear only when releasing that product's repo.
|
|
844
80
|
|
|
845
|
-
|
|
846
|
-
compares the registry's served `dist.integrity` (SSRI) against a live `npm pack` on THIS machine
|
|
847
|
-
(`packedIntegrityBySurface`). A tarball SSRI covers file modes and packer (npm version) behavior, so
|
|
848
|
-
that identity step is authoritative **only** when run in the publish environment (`publish.yml` on the
|
|
849
|
-
self-hosted Linux runner) — or on an operator machine with the **same OS and npm major** as that
|
|
850
|
-
runner. Elsewhere (Windows, a clean Linux container with a different packer, a mismatched npm major) a
|
|
851
|
-
`tarball identity mismatch` can false-fail even when the published bytes match the tag: three different
|
|
852
|
-
SSRIs (registry / Windows pack / container pack) with content-identical unpacked trees is the expected
|
|
853
|
-
cross-environment shape for surfaces with no publish-time staged payload, not a substituted artifact.
|
|
854
|
-
Off the publish environment, either:
|
|
855
|
-
- fall back to an **unpacked-content** proof — download the registry tarball (`npm pack <pkg>@X.Y.Z`
|
|
856
|
-
from the registry, or `npm view … dist.tarball`), `npm pack` from the tag checkout, unpack both, and
|
|
857
|
-
compare the trees (do not alarm on SSRI alone). On Windows unpack with the native
|
|
858
|
-
`%SystemRoot%\System32\tar.exe` named explicitly — a bare `tar.exe` can resolve to Git's GNU tar
|
|
859
|
-
through PATH, which reads a drive-letter path as a remote host (`Cannot connect to C: resolve
|
|
860
|
-
failed`, #6059) — and run `npm pack` from the package directory (`cd` there; no positional path
|
|
861
|
-
and no `--pack-destination` under a path with spaces), since routing npm through `cmd.exe /c`
|
|
862
|
-
reparses quoted paths (#6058). Hub host-plugin npm surfaces stage the canonical
|
|
863
|
-
`skills/` tree and declared hook-policy `scripts/` into the package at publish time via
|
|
864
|
-
`assembleDeclaredPayloads` (#5471); those paths are gitignored and absent from a plain tag checkout,
|
|
865
|
-
so a local `npm pack` without staging cannot reproduce them and an empty `diff -r` is an unreachable
|
|
866
|
-
bar (#5677), not evidence of substitution. For those surfaces, prove identity in two parts:
|
|
867
|
-
1. **Committed package content** — `diff -r` the unpacked trees while excluding each publish-time
|
|
868
|
-
staged path relative to the package root (for `@mutmutco/claude-plugin`:
|
|
869
|
-
`diff -r -x skills -x scripts …`; list all staged targets with
|
|
870
|
-
`node --input-type=module -e "import { generatedPayloadPaths } from './scripts/distribution-assembly.mjs'; console.log(generatedPayloadPaths().join('\\n'))"` and strip each surface's `assembly.rootPath` prefix). Empty diff ⇒ the committed package matches.
|
|
871
|
-
2. **Staged payload** — for each excluded path, compare the registry unpacked copy against the tag
|
|
872
|
-
checkout repo source named in that surface's `surfaces.json` assembly sync (`directories` sync:
|
|
873
|
-
`diff -r reg/package/skills skills/`; `files` sync: `cmp` each `include` entry against
|
|
874
|
-
`<sourcePath>/<name>`). Alternatively, run
|
|
875
|
-
`node --input-type=module -e "import { assembleDeclaredPayloads } from './scripts/distribution-assembly.mjs'; assembleDeclaredPayloads()"` on the tag checkout before the local `npm pack` so a plain `diff -r` compares the full publish payload.
|
|
876
|
-
Surfaces with no publish-time staged payload still use a plain `diff -r` (empty diff ⇒ identity OK), or
|
|
877
|
-
- skip the registry identity read with
|
|
878
|
-
`node scripts/release-distribution.mjs verify "X.Y.0" --skip-npm-view` and verify published content
|
|
879
|
-
by hand the same way (including the staged-payload split above when applicable).
|
|
81
|
+
## Retro
|
|
880
82
|
|
|
881
|
-
|
|
882
|
-
including staged `skills/` into whatever directory you choose. Those copies are **not** registered skill
|
|
883
|
-
payloads — if they remain anywhere scannable under the repo root when the next release fold runs
|
|
884
|
-
`check-skill-payload`, the gate fails with `unregistered authored or copied skills`. Prefer unpacking
|
|
885
|
-
**outside** the repository:
|
|
886
|
-
```bash
|
|
887
|
-
PROOF=$(mktemp -d "${TMPDIR:-/tmp}/mmi-release-proof.XXXXXX")
|
|
888
|
-
# unpack both tarballs under $PROOF, diff -r, then:
|
|
889
|
-
rm -rf "$PROOF"
|
|
890
|
-
```
|
|
891
|
-
If you must unpack inside the checkout, use the org-managed gitignored scratch at repo-root `tmp/`
|
|
892
|
-
(`mktemp -d tmp/mmi-release-proof.XXXXXX`) — **not** `.jerv/tmp/` (agent session scratch still lives on
|
|
893
|
-
disk and must not carry npm proof trees between releases). Remove the proof tree **immediately** after the
|
|
894
|
-
diff succeeds; do not leave it for the next `--apply`.
|
|
83
|
+
Before the final report, answer one question honestly: did this skill's own instructions misfire this run — ambiguous wording, a misleading message, or an environment failure it should have warned about? Process only, never the user's code or task. If yes, file one lesson and move on; a clean run is silent (hard cap: one per run). It lands on the Hub board, deduplicated, and is fixed only through a reviewed PR — never edit the skill live. The retro is advisory: if the call fails, note it and continue.
|
|
895
84
|
|
|
896
|
-
Before the next `release --apply`, confirm no leftover proof copies remain outside org scratch:
|
|
897
85
|
```bash
|
|
898
|
-
|
|
86
|
+
mmi-cli learning skill-lesson --skill release --title "<what misfired>" --body "<what; evidence; proposed amendment>"
|
|
899
87
|
```
|
|
900
|
-
A failure naming `unregistered authored or copied skills` means a proof tree (or other rogue copy) is
|
|
901
|
-
still on disk — delete it and rerun the contract check before retrying the train.
|
|
902
|
-
|
|
903
|
-
Do not treat an off-runner SSRI mismatch as a broken release, and do not change how the runner publishes
|
|
904
|
-
to paper over a local packer difference.
|
|
905
|
-
|
|
906
|
-
**Private packages (#2405) — never trust a bare `npm view` to mean "not published".** An unauthenticated
|
|
907
|
-
`npm view <pkg>` returns a hard 404 on any package published with `publishConfig.access=restricted`
|
|
908
|
-
(private) — indistinguishable from "not published" — because npm masks private packages from anonymous
|
|
909
|
-
reads. Before verifying ANY npm publish (Hub's own, a `registry-publish` product repo's own `publish.yml`,
|
|
910
|
-
or a `publishRequired` product repo's companion `tenant-publish.yml`), check that surface's
|
|
911
|
-
`publishVisibility` in the target repo's `surfaces.json`
|
|
912
|
-
(`node -e "console.log(require('./surfaces.json').surfaces.find(s=>s.id==='<id>').publishVisibility)"`, or
|
|
913
|
-
the repo-agnostic reader `scripts/release-distribution.mjs` exports as `npmVerifyModeFor`):
|
|
914
|
-
- `public` → an unauthenticated `npm view <pkg>@<version> version` is a trustworthy verify signal.
|
|
915
|
-
- `private` → do **not** run a bare `npm view`. Verify from an authenticated source instead: the
|
|
916
|
-
release-triggered run's conclusion (`gh run watch … --exit-status` — the repo's own `publish.yml` for
|
|
917
|
-
`registry-publish`, #2428; the central `tenant-publish.yml` for a `publishRequired` companion publish) and
|
|
918
|
-
its logged `+ pkg@ver` / `"already on npm — nothing to do"` / `"already on npm ... treating as idempotent
|
|
919
|
-
success"` output (`gh run view <id> --log`), or `npm view` with a registry auth token in env. A 404 here is
|
|
920
|
-
"unverifiable from here", never "not published" — report it as such, don't block or alarm on it.
|
|
921
|
-
- `n/a` → nothing publishes to npm for that surface; skip.
|
|
922
|
-
Hub's registry-declared npm surfaces are public today, so `release-distribution.mjs verify` uses a bare
|
|
923
|
-
`npm view` for them — this changes only if a package's declared visibility changes.
|
|
924
|
-
|
|
925
|
-
**tenant-publish.yml E409 belt-and-braces (#2428).** The central `tenant-publish.yml` lane (the
|
|
926
|
-
`publishRequired` companion publish only — never dispatched for `registry-publish`, see above) already
|
|
927
|
-
treats a lost `npm publish` race as idempotent inside the run (a re-check `npm view` after a failed publish;
|
|
928
|
-
see the workflow file). `mmi-cli devops release --apply --watch` mirrors that at the CLI layer: a watched
|
|
929
|
-
`tenant-publish.yml` run that concludes `failure` is re-checked against that SAME run's own log (bounded
|
|
930
|
-
retries, visibility-agnostic — never a bare `npm view`) for the idempotent-success marker before the CLI
|
|
931
|
-
reports the publish as failed. A run whose log never shows the version landed stays a loud failure.
|
|
932
|
-
|
|
933
|
-
Report: Release `$TAG` (the resolved tag from Step 2 — never a placeholder) + the GitHub Release URL · prod
|
|
934
|
-
deploy run + URL + **green/red** · branch-alignment note · npm publish run + CLI version (Hub releases).
|
|
935
|
-
For Jerv-Hub, also report the `jerv-gateway` exact-commit deployment and health verdict; npm green without
|
|
936
|
-
Gateway green is a failed follow-up, never a complete Jerv-Hub release.
|
|
937
|
-
|
|
938
|
-
## Notes
|
|
939
|
-
|
|
940
|
-
- `/hotfix` stays the cherry-pick promotion lane (a hotfix always skips rc — it cherry-picks
|
|
941
|
-
`development → main` directly), but a `/release patch` from development is a legitimate ordinary
|
|
942
|
-
release (#4929). Never force-push `main`.
|
|
943
|
-
- **`MMI_BUMP_INTENT=major|minor|patch` comes from the bare `/release` argument; unstated it defaults to
|
|
944
|
-
`patch`** (#4929). Export it for the whole run, not a one-off prefix on `next-version.mjs cycle`:
|
|
945
|
-
`mmi-cli devops release --apply` (and `rcand --apply`) resolve intent themselves **before any merge,
|
|
946
|
-
tag, or fold** — direct-track included — defaulting to `patch` when the env is absent and failing closed
|
|
947
|
-
there with `MMI_BUMP_INTENT must be one of major|minor|patch` on an invalid value, so the tree stays
|
|
948
|
-
clean on the start branch. `MMI_RELEASE_VERSION`
|
|
949
|
-
below is a different knob: it overrides the computed version; it does not replace the intent export
|
|
950
|
-
unless you set that exact-target env instead.
|
|
951
|
-
- **`--dev` (full-track only):** releases `development → main` skipping rc, with the same fold/tag/Release/
|
|
952
|
-
deploy machinery plus the post-release rc retirement and rc alignment. Refuses (fail closed) when
|
|
953
|
-
`origin/rc` carries commits not in `development`; on direct-track repos it's a no-op (they already
|
|
954
|
-
release from `development`).
|
|
955
|
-
- **MAJOR / exact-target release:** to ship a version the tag math can't derive (a MAJOR like `2.0.0`, or
|
|
956
|
-
skipping a version already on npm), product repos export `MMI_RELEASE_VERSION=X.Y.Z` for **both** `/rcand`
|
|
957
|
-
and `/release`; MMI-Hub exports it for `/release` only. Steps 1b/2 then fold and tag exactly that
|
|
958
|
-
version. Unset, the train uses the declared `MMI_BUMP_INTENT` increment, defaulting to `patch` when
|
|
959
|
-
neither the argument nor the env declares one (#4929).
|
|
960
|
-
|
|
961
|
-
## Retro — one check before you finish
|
|
962
|
-
Before your final report, answer one question honestly: did **this skill's own instructions** misfire
|
|
963
|
-
this run — ambiguous wording, a misleading message, or an environment failure it should have warned
|
|
964
|
-
about? (Process only — never the user's code or task; e.g. a misleading authority or gate message, or an
|
|
965
|
-
ambiguous version-fold or back-merge step.) If yes, file **one** lesson and move on; a clean run is silent
|
|
966
|
-
(hard cap: one per run). It lands on the Hub board (deduped) and is fixed only via a reviewed PR — never
|
|
967
|
-
edit the skill live; the retro is advisory, so if the call fails, note it and continue:
|
|
968
|
-
`mmi-cli learning skill-lesson --skill release --title "<what misfired>" --body "<what; evidence; proposed amendment>"`
|