@mutmutco/claude-plugin 3.131.0
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/.claude-plugin/plugin.json +11 -0
- package/.pi-plugin/package.json +23 -0
- package/bin/mmi-cli +6 -0
- package/bin/mmi-cli.cmd +3 -0
- package/bin/mmi-hook +2 -0
- package/bin/mmi-hook-console.cmd +10 -0
- package/bin/mmi-hook.exe +0 -0
- package/hooks/hooks.json +38 -0
- package/package.json +22 -0
- package/scripts/command-ladder-core.mjs +334 -0
- package/scripts/command-ladder-gate.mjs +126 -0
- package/scripts/deny-gate-crash.mjs +179 -0
- package/scripts/edit-tool-paths.mjs +113 -0
- package/scripts/env-write-lint.mjs +137 -0
- package/scripts/hook-io.mjs +22 -0
- package/scripts/hook-policy.mjs +73 -0
- package/scripts/hook-run.mjs +437 -0
- package/scripts/hook-trace.mjs +151 -0
- package/scripts/pretooluse-shell-gates.mjs +424 -0
- package/scripts/secret-echo-lint.mjs +177 -0
- package/scripts/secret-redact.mjs +552 -0
- package/scripts/throttle-core.mjs +324 -0
- package/scripts/validate-hook.mjs +156 -0
- package/scripts/vault-edit-gate.mjs +94 -0
- package/skills/bootstrap/SKILL.md +550 -0
- package/skills/bootstrap/seeds/Dockerfile.template +30 -0
- package/skills/bootstrap/seeds/README.template.md +37 -0
- package/skills/bootstrap/seeds/architecture.template.md +34 -0
- package/skills/bootstrap/seeds/decisions-readme.template.md +45 -0
- package/skills/bootstrap/seeds/docker-compose.template.yml +26 -0
- package/skills/bootstrap/seeds/gate.template.yml +85 -0
- package/skills/bootstrap/seeds/google-login.template.md +33 -0
- package/skills/bootstrap/seeds/manifest.json +26 -0
- package/skills/bootstrap/seeds/mmi-product-required-checks.template.json +23 -0
- package/skills/browser-automation/SKILL.md +95 -0
- package/skills/epic/SKILL.md +104 -0
- package/skills/hotfix/SKILL.md +165 -0
- package/skills/mmi/SKILL.md +404 -0
- package/skills/mmi-doctor/SKILL.md +63 -0
- package/skills/onboard/SKILL.md +85 -0
- package/skills/rcand/SKILL.md +208 -0
- package/skills/release/SKILL.md +599 -0
- package/skills/resume/SKILL.md +90 -0
- package/skills/secrets/SKILL.md +159 -0
- package/skills/stage/SKILL.md +153 -0
- package/skills/worktree/SKILL.md +151 -0
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: release
|
|
3
|
+
description: Ship rc or direct-track development to main and production.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
**Host-native invocation:** Claude `/mmi:release` · Codex `$mmi:release` · jervcode/Kimi `/skill:release` · Kilo `skill` tool. A backticked `/name` in this doc names the matching workflow (this skill or a sibling), not a literal command.
|
|
7
|
+
|
|
8
|
+
**Argument (#4929):** `/release minor`, `/release major`, or `/release patch` — a bare bump word, never a
|
|
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.
|
|
12
|
+
|
|
13
|
+
# /release — ship to main + prod
|
|
14
|
+
|
|
15
|
+
Full-track repos ship **exactly what is on `rc`** (never pulls `development`): merge `rc → main`, tag
|
|
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.
|
|
29
|
+
|
|
30
|
+
Authority is structural + server-checked: step 0 asks the Hub (`mmi-cli oracle org access role`), and step 3 pushes
|
|
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.
|
|
34
|
+
|
|
35
|
+
## Merge floor (#3167 — non-negotiable, applies to every step below)
|
|
36
|
+
|
|
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
|
+
- **A refused train is a stop, never a license to finish by hand.** Every fail-closed halt below — authority
|
|
42
|
+
probe, coverage guard, untolerated conflict, required checks, stray tag — ends the run. The recovery is
|
|
43
|
+
always to fix the cause and re-run the train; it is never hand-resolving on `main`, bare-pushing a train
|
|
44
|
+
branch, acking to save time, or bypassing a check.
|
|
45
|
+
- **Version probes anchor on the latest Release tag, not a branch.** `next-version.mjs` and the fold derive
|
|
46
|
+
the next version from tags; a branch manifest or a stale local tag is not a version source. (This is why a
|
|
47
|
+
stray tag must be deleted locally too — a surviving local tag silently mints the *next* version, Step 3.)
|
|
48
|
+
|
|
49
|
+
## Step 0 — confirm + probe
|
|
50
|
+
|
|
51
|
+
Confirm the human holding train authority for THIS repo authorized a prod release this turn. Probe:
|
|
52
|
+
```bash
|
|
53
|
+
mmi-cli oracle org access role {owner}/{repo} --json # Hub-verified: { role, train }
|
|
54
|
+
```
|
|
55
|
+
`train: false` — or any error (fail closed) → stop: a product repo's train belongs to that repo's
|
|
56
|
+
project-admin or the master; the Hub train is master-only. **Three exceptions to fail-closed here, and all
|
|
57
|
+
mean "unverified", never an authority verdict:**
|
|
58
|
+
|
|
59
|
+
- A `repository … does not exist or is not visible to the org App` error (HTTP 404) means the probe target
|
|
60
|
+
is not a resolvable repo — a package name, a typo, a repo never created (#4778). It is UNKNOWN, not a
|
|
61
|
+
denial, and retrying will never change it: fix the `{owner}/{repo}` argument, then re-probe. Before the
|
|
62
|
+
fix this string came back `role: master, train: true, verified: true` for anything owner/name-shaped.
|
|
63
|
+
|
|
64
|
+
- An `unknown command 'oracle'` error is a STALE CLI — the house-prefixed namespaces ship in newer
|
|
65
|
+
trains, so an old installed CLI can't even parse the probe. Jump to Step 0a, heal the CLI, then re-run
|
|
66
|
+
this probe; never conclude the command form is wrong from a pre-heal failure (#3150). The mirror-image
|
|
67
|
+
failure is a Wave-3 flat alias on a CURRENT CLI: the un-housed `org …`, `secrets …`, and
|
|
68
|
+
`release …` forms were removed in #4316 and fail closed with a house-prefix error — rewrite to the
|
|
69
|
+
house form (`mmi-cli oracle org …`, `mmi-cli vault secrets …`, `mmi-cli devops release …`); that is
|
|
70
|
+
never a stale CLI and never an authority verdict.
|
|
71
|
+
- A **TIMEOUT or network error** (`operation aborted due to timeout`, DNS/socket failures) is **not** a
|
|
72
|
+
`train: false` denial (#3321). Read the probe's own `verified` field: `verified: false` means the verdict
|
|
73
|
+
could not be established, so the `train: false` beside it carries NO authority meaning. The client
|
|
74
|
+
already retries transient failures internally, so a *persistent* timeout points at the local network
|
|
75
|
+
rather than the Hub — resolve the Hub API host first (fails in seconds and rules out the whole
|
|
76
|
+
local-network class), then re-run the probe. Conclude "no train authority" ONLY from a `verified: true`
|
|
77
|
+
response. Aborting a legitimate release on an unverified probe is the failure this carve-out exists to
|
|
78
|
+
prevent.
|
|
79
|
+
|
|
80
|
+
Then
|
|
81
|
+
preconditions: clean tree; full-track repos run from `rc` (or from `development` with `--dev`), while
|
|
82
|
+
direct-track repos run from `development`. Ordinary `/release` takes its bump intent from the bare
|
|
83
|
+
argument (`/release minor|major|patch` → `MMI_BUMP_INTENT`, exported for the whole run); unstated, the
|
|
84
|
+
train defaults to `patch` (#4929). An invalid intent fails `--apply` closed at intent resolution
|
|
85
|
+
(`MMI_BUMP_INTENT must be one of major|minor|patch`), before any merge, tag, or fold.
|
|
86
|
+
|
|
87
|
+
**Run from the checkout that already has that branch — never a fresh isolated worktree (#2770).** This is
|
|
88
|
+
a shared-branch train operation (merge/tag/push against origin's protected `main`/`rc`), not an isolated
|
|
89
|
+
feature-branch edit. A background-job harness that isolates every task into a fresh worktree by default
|
|
90
|
+
breaks here: the new worktree's branch is never literally named `development`/`rc`, and `git checkout
|
|
91
|
+
development` inside it fails outright when `development` is already checked out in the primary checkout (git
|
|
92
|
+
worktrees cannot have the same branch checked out twice). If you are in such a worktree, exit it first and
|
|
93
|
+
run the release from the primary checkout.
|
|
94
|
+
The clean-tree check rejects UNTRACKED scratch too, not just modified tracked files. When `--apply` or
|
|
95
|
+
`--resume` stops with `working tree must be clean before …`, run `git status --porcelain` on the paths it
|
|
96
|
+
named and read BOTH status columns before touching anything (#1472, #4004):
|
|
97
|
+
|
|
98
|
+
- `??` — untracked scratch. Gitignore it, or move it to a gitignored path like `tmp/`, then retry.
|
|
99
|
+
- exactly ` M` (blank staged column) — the only state that can be churn rather than work. Test it with
|
|
100
|
+
`git diff HEAD --numstat -- <paths>`: non-empty is a real edit to commit or stash; empty means the
|
|
101
|
+
worktree normalizes straight back to HEAD (an LF↔CRLF rewrite, say), and `git checkout -- <paths>`
|
|
102
|
+
then clears the refusal without discarding anything.
|
|
103
|
+
- anything else — `M `, `MM`, `A`, `D`, `R`, `T`, or any `U` conflict — is real work or an exceptional
|
|
104
|
+
index state. Commit, stash or resolve it; do not try to classify it.
|
|
105
|
+
|
|
106
|
+
Do not substitute plain `git diff --numstat` for the `HEAD` form. It compares the worktree against the
|
|
107
|
+
INDEX, so a change that is merely staged prints nothing at all, and reading that emptiness as
|
|
108
|
+
"line-ending churn" throws the change away. The remedy is the index form of `git checkout` for the same
|
|
109
|
+
reason: `git checkout HEAD -- <paths>` would overwrite a staged edit that was misread as churn.
|
|
110
|
+
|
|
111
|
+
Full-track repos:
|
|
112
|
+
```bash
|
|
113
|
+
git fetch origin
|
|
114
|
+
git rev-list --count origin/main..origin/rc
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Direct-track repos (e.g. MMI-Hub):
|
|
118
|
+
```bash
|
|
119
|
+
git fetch origin
|
|
120
|
+
git rev-list --count origin/main..origin/development
|
|
121
|
+
```
|
|
122
|
+
`0` → stop ("nothing to release").
|
|
123
|
+
|
|
124
|
+
## Step 0a — stale CLI preflight (#1410)
|
|
125
|
+
|
|
126
|
+
Before local gates or `release --apply`, ensure the repo-local / PATH `mmi-cli` matches the released train
|
|
127
|
+
path — a stale checkout (e.g. 2.32.0 while 2.32.4 is released) fails release gates with opaque errors.
|
|
128
|
+
Self-heal in place without repo writes (heals run by default since #3975; the flag suppresses only
|
|
129
|
+
mutations of the working tree you are about to release from):
|
|
130
|
+
```bash
|
|
131
|
+
mmi-cli doctor --no-repo-writes
|
|
132
|
+
```
|
|
133
|
+
**Read the checks, not the exit code.** `doctor` exits non-zero on any failed check it could have healed —
|
|
134
|
+
not just a version gap — so a red exit here does not by itself mean the train is blocked (#2962). The
|
|
135
|
+
inverse is also true: a run can exit **0** while printing ✗ lines (report-only rows, #3485). Neither
|
|
136
|
+
direction is a gate. Read the rows.
|
|
137
|
+
|
|
138
|
+
- **Blocking:** a red `mmi-cli` / `Claude plugin` version line, or a reported `minClientVersion` gap.
|
|
139
|
+
`--no-repo-writes` does not block the env heals — the step above self-updates the npm global CLI
|
|
140
|
+
(#3272) and reinstalls the plugin (#3282), suppressing only repo-file writes — so the re-run should clear
|
|
141
|
+
it. If a gap persists, fix it — `mmi-cli doctor`, or `npm install -g @mutmutco/cli` — before
|
|
142
|
+
continuing. Do **not** proceed to Step 0b while the installed CLI is behind the Hub's `minClientVersion`.
|
|
143
|
+
- **Not blocking:** every other red check is hygiene, not a train gate. Stale branches and scratch heal with
|
|
144
|
+
a plain `mmi-cli doctor` (after the release); `plugin cache` needs a different verb,
|
|
145
|
+
`mmi-cli plugin prune --apply` — saying doctor cleared it was wrong. Both still exit 1 until you run the
|
|
146
|
+
right verb. Read them, then proceed.
|
|
147
|
+
|
|
148
|
+
Treating doctor's exit code as the gate halts a healthy train on cosmetic drift — or, worse, teaches an
|
|
149
|
+
operator to ignore doctor's exit code entirely.
|
|
150
|
+
|
|
151
|
+
**A local `npm ci` failure during the fold on a lockfile CI already accepted is an npm-major mismatch, not a
|
|
152
|
+
bad lockfile (#4578).** Before touching the lockfile, compare toolchains:
|
|
153
|
+
```bash
|
|
154
|
+
npm -v
|
|
155
|
+
gh api repos/{owner}/{repo}/actions/runs --jq '.workflow_runs[0].id' # or read the gate log's
|
|
156
|
+
# `node -v && npm -v` toolchain-preflight line (#3446) for the npm major CI ran with
|
|
157
|
+
```
|
|
158
|
+
A different npm MAJOR (e.g. local 12 vs. CI's 11) resolves the same lockfile differently and throws `Missing:
|
|
159
|
+
<pkg> from lock file` on a lockfile that is not actually broken. Align the local npm major
|
|
160
|
+
(`npm install -g npm@<CI major>`) and re-run `npm ci` before rewriting or regenerating the lockfile.
|
|
161
|
+
|
|
162
|
+
**A `npm ci was killed (signal SIGTERM)` failure during the fold is a cold npm cache, not a lockfile or
|
|
163
|
+
toolchain problem (#4841).** The signature is the kill, not error text: no `Missing:` line, no lockfile
|
|
164
|
+
complaint at all — the fold's child-process budget expired while a cold cache installed the full tree.
|
|
165
|
+
The npm-major note above is inert here (majors match, the lockfile is fine), and following it tempts the
|
|
166
|
+
one thing it warns against — regenerating the lockfile for a problem that has nothing to do with it.
|
|
167
|
+
Warm the cache with a plain `npm ci` in the release checkout, then rerun the identical train command; the
|
|
168
|
+
rollback the failed fold already ran makes the rerun clean. Never regenerate or rewrite the lockfile in
|
|
169
|
+
response to a SIGTERM kill.
|
|
170
|
+
|
|
171
|
+
**Then check the version yourself — do not rely on doctor having printed a row (#3674).** "Read the rows"
|
|
172
|
+
resolves to "proceed" when the row is *absent*, and doctor's version row is conditional. On 2026-07-27 this
|
|
173
|
+
step reported healthy with no version line while the installed CLI was 3.70.0 against a released 3.71.0;
|
|
174
|
+
`release --apply` refused one command later. Two lines, no dependency on another command's reporting
|
|
175
|
+
completeness:
|
|
176
|
+
```bash
|
|
177
|
+
mmi-cli --version
|
|
178
|
+
npm view @mutmutco/cli version # public package; a bare view is trustworthy here
|
|
179
|
+
```
|
|
180
|
+
Mismatch → `npm install -g @mutmutco/cli@<released>` before Step 0b, **regardless of what doctor's rows
|
|
181
|
+
said**. This is the check whose absence Step 0a's own title promises to cover, so it is not optional.
|
|
182
|
+
|
|
183
|
+
## Step 0b — registry + main secret-name preflight
|
|
184
|
+
|
|
185
|
+
Resolve the project META first; its `deployModel` decides the deploy path. `tenant-container` repos use the
|
|
186
|
+
central tenant deployer and therefore need DEPLOY# coords. `hub-serverless` (MMI-Hub), `serverless`,
|
|
187
|
+
`registry-publish`, and `solo-container` repos deploy from their own branch/release-triggered or model
|
|
188
|
+
workflow, so do **not** dispatch `tenant-deploy.yml` for them.
|
|
189
|
+
Verify META + required SSM secret names before touching `main`:
|
|
190
|
+
```bash
|
|
191
|
+
mmi-cli oracle org project get {owner}/{repo}
|
|
192
|
+
mmi-cli vault secrets preflight --stage main --repo {owner}/{repo}
|
|
193
|
+
```
|
|
194
|
+
An enumerated missing META row or missing secret name → stop and repair the registry/secrets first. An
|
|
195
|
+
HTTP 5xx, timeout, DNS, socket, or other transport failure is **unverified**, not evidence that a name is
|
|
196
|
+
missing: retry the read/preflight and repair connectivity if it persists. Never provision or rename a
|
|
197
|
+
secret from a transport-error response.
|
|
198
|
+
|
|
199
|
+
## Step 0c — hotfix-coverage guard (fail closed, #839, #958)
|
|
200
|
+
|
|
201
|
+
Full-track repos only. Direct-track repos skip this specific guard — not because they are exposed to no
|
|
202
|
+
risk, but because the risk this guard checks structurally cannot arise there: a direct-track `/hotfix`
|
|
203
|
+
cherry-picks straight from `development` onto `main`, so every hotfix trailer's source sha is *by
|
|
204
|
+
construction* an ancestor of `origin/development` — a trailer-ancestry check would be permanently green and
|
|
205
|
+
add no signal (#2786 — a literal port was considered and rejected as a false-confidence guard).
|
|
206
|
+
|
|
207
|
+
Hotfixes are cherry-picked from `development` to `main` with **no back-merge** (see `/hotfix`), so a
|
|
208
|
+
candidate cut *before* a fix landed on `development` would silently revert that hotfix in prod. The guard
|
|
209
|
+
proves every main-only commit is in the candidate before the `rc → main` merge.
|
|
210
|
+
|
|
211
|
+
**Direct-track repos are not risk-free, though — they carry a different, real failure mode:** a hotfix
|
|
212
|
+
freezes a snapshot of the files it cherry-picks (e.g. docs, a test assertion) while `development` keeps
|
|
213
|
+
evolving those same files afterward. Step 1's `development → main` merge preflight (untolerated-path
|
|
214
|
+
conflict check) is what actually catches this — a genuine content conflict on the **next** release is
|
|
215
|
+
expected when a hotfix's cherry-picked commits touched files `development` went on to rewrite
|
|
216
|
+
incompatibly. That is routine, working-as-designed friction, not a fluke or a misconfiguration (incident:
|
|
217
|
+
hotfix v3.16.1 #2765 cherry-picked #2762/#2768's doc sweep onto `main`; `development` then rewrote the same
|
|
218
|
+
files further via #2767/#2771; `/release` correctly stopped on 4 conflicted files, resolved by confirming
|
|
219
|
+
`development`'s side already represented the correct, current content and landing alignment PR #2784 — see
|
|
220
|
+
#2786). When Step 1 stops this way: run `git merge-tree` (or read the conflict content directly) to confirm
|
|
221
|
+
which side carries the correct current content, then land a true-merge alignment PR (never hand-resolve on
|
|
222
|
+
`main`) before rerunning release.
|
|
223
|
+
|
|
224
|
+
It runs **automatically inside `mmi-cli devops release --apply`** (Step 1+ below) — built into the CLI so it
|
|
225
|
+
works in every product repo with no repo-local script. You do not invoke it separately.
|
|
226
|
+
|
|
227
|
+
Per main-only commit it accepts: the `(cherry picked from commit <sha>)` trailer with that dev SHA an
|
|
228
|
+
ancestor of `origin/rc` (immune to conflict-resolved ports); a matching `git patch-id` on the rc side
|
|
229
|
+
(trailer-less picks); or a distribution-manifest-only bump (exempt — rc carries its own). Anything else
|
|
230
|
+
**fails the release closed** → **stop**: the right fix is a re-cut `/rcand` from `development`. Only when
|
|
231
|
+
the authorized human has manually verified the content is in the candidate, rerun with
|
|
232
|
+
`mmi-cli devops release --apply --ack <sha>[,<sha>…]` — the ack is recorded in the verdict. Never ack to save time.
|
|
233
|
+
|
|
234
|
+
## Step 0d — no docs-keeping on the release train (Hub#4164)
|
|
235
|
+
|
|
236
|
+
Scheduled docs-janitor / wiki-keeper / living-docs freshness passes are **retired** (#4117/#4119).
|
|
237
|
+
The release train does **not** open docs-only PRs, wait on README/architecture auto-merges, or
|
|
238
|
+
treat prose refresh as a gate.
|
|
239
|
+
|
|
240
|
+
Current state is compute-at-read + the estate repo-index (`mmi-cli oracle repo-index search`,
|
|
241
|
+
`mmi-cli oracle org project`, board/schedules verbs). Durable WHY stays in `docs/decisions/`.
|
|
242
|
+
If shipping code made a hand-written surface wrong, fix it on `development` as an ordinary PR
|
|
243
|
+
**outside** the train — never as a release Step.
|
|
244
|
+
|
|
245
|
+
## Step 1 — merge to main (never force)
|
|
246
|
+
|
|
247
|
+
Full-track repos:
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
git checkout main
|
|
251
|
+
git pull --ff-only origin main
|
|
252
|
+
git merge rc --no-edit
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Direct-track repos (e.g. MMI-Hub):
|
|
256
|
+
```bash
|
|
257
|
+
git checkout main
|
|
258
|
+
git pull --ff-only origin main
|
|
259
|
+
git merge development --no-edit
|
|
260
|
+
```
|
|
261
|
+
Conflict → abort + stop (the train is misaligned — investigate; don't hand-resolve on `main`). On a
|
|
262
|
+
direct-track repo, a conflict here right after a hotfix is expected friction, not a fluke — see Step 0c for
|
|
263
|
+
why (a hotfix freezes a snapshot that `development` can keep rewriting).
|
|
264
|
+
Alignment PRs are the exception to the org's squash default: land them with a true merge —
|
|
265
|
+
`mmi-cli devops pr merge <n> --auto --merge` (not squash; a squash discards the merge parentage, so the
|
|
266
|
+
misalignment guard re-flags the same divergence on the next run). `--auto` clears the checks the PR
|
|
267
|
+
triggers, which block an immediate merge right after a release.
|
|
268
|
+
|
|
269
|
+
**Exception — version-manifest and `.gitignore` paths.** `mmi-cli devops release --apply` tolerates
|
|
270
|
+
conflicts confined to the version-fold paths (Step 1b) and `.gitignore` (#1037 — a
|
|
271
|
+
repo bootstrapped before the managed-gitignore era still carries the legacy file on `main`; the candidate
|
|
272
|
+
carries the Hub-managed copy with project-local entries preserved). The org spine is no longer delivered per
|
|
273
|
+
repo (hub-v3 WS4), so a spine-path conflict is treated like any other untolerated path. A hotfix bumps the manifests +
|
|
274
|
+
committed CLI bundle on `main` only, so the next merge re-conflicts there even when the train is healthy —
|
|
275
|
+
and the fold rewrites those exact paths right after the merge. For all tolerated paths the CLI takes the
|
|
276
|
+
incoming side deterministically and continues. Any other conflicted path → abort + stop as above.
|
|
277
|
+
|
|
278
|
+
## Step 1b — version fold (automatic, inside `mmi-cli devops release --apply`)
|
|
279
|
+
|
|
280
|
+
Every release folds the version bump into the release itself (#976): after the merge onto local `main` and
|
|
281
|
+
before the tag, the CLI bumps the version manifests to the release version and commits — **unconditionally,
|
|
282
|
+
changed or not** — then the tag-first push (Step 3) earns that fresh commit its required checks. There is
|
|
283
|
+
no separate bump PR, no `development` prep cycle, and `development` never sits ahead of the published
|
|
284
|
+
version (the back-merge in Step 5 carries the bump back).
|
|
285
|
+
|
|
286
|
+
What the fold bumps, by repo:
|
|
287
|
+
- **Hub (`hub-serverless`):** the full locked distribution set via `scripts/release-distribution.mjs
|
|
288
|
+
prepare` — spine dogfood (`scripts/spine-dogfood.mjs`: verify docs/surfaces + the managed `.gitignore` block),
|
|
289
|
+
every registry-declared version holder, adapter payload synchronization, build output, and the public
|
|
290
|
+
artifact bill of materials — then verifies the set (`verify --skip-npm-view`). Publication and staging
|
|
291
|
+
both derive from `surfaces.json`; there is no second package list. Claude, Codex, Kimi, Cursor, and
|
|
292
|
+
Kilo are active.
|
|
293
|
+
- **App-style repos with a root `package.json`** (most products): the manifest + lockfile version via
|
|
294
|
+
`npm version --no-git-tag-version`, kept in lockstep with the release tag.
|
|
295
|
+
- **Repos with neither:** nothing to fold — the tag is the version.
|
|
296
|
+
|
|
297
|
+
Nothing to do by hand; the `--apply` result reports the fold outcome (`versionFold`).
|
|
298
|
+
|
|
299
|
+
## Step 2 — tag the release
|
|
300
|
+
|
|
301
|
+
Full-track repos drop the `-rc.N` suffix from the open cycle. Direct-track repos use the next cycle directly
|
|
302
|
+
because they have no rc tag — `cycle` mode reads `MMI_BUMP_INTENT` (the `/release` argument; unset →
|
|
303
|
+
`patch`, #4929). The Step 0 export is what covers the apply run — the inline prefix below only resolves
|
|
304
|
+
the tag for display; `mmi-cli devops release --apply` re-derives intent itself and fail-closes before any
|
|
305
|
+
merge, tag, or fold when the value is invalid:
|
|
306
|
+
```bash
|
|
307
|
+
TAG=$(node scripts/next-version.mjs release) # full-track repos -> vX.Y.0
|
|
308
|
+
TAG=$(MMI_BUMP_INTENT=minor node scripts/next-version.mjs cycle) # direct-track repos -> vX.Y.0
|
|
309
|
+
git tag "$TAG"
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## Step 3 — push tag, wait for the REQUIRED checks, then push main (the gate)
|
|
313
|
+
|
|
314
|
+
`mmi-cli devops release --apply`'s exit code is **not** the release verdict: exit `2` can mean the release
|
|
315
|
+
shipped successfully while protected-branch alignment remains pending, and exit `1` can mean a
|
|
316
|
+
post-release follow-up failed after promotion — **or that the follow-up is merely unresolved** (#4939):
|
|
317
|
+
a verdict of `releaseStatus=succeeded` with `followUpStatus=pending` (the release-triggered runs — e.g.
|
|
318
|
+
the Hub's `deploy.yml` + `publish.yml` — had only just auto-fired) also exits nonzero, and a
|
|
319
|
+
background-task harness surfacing exit codes misreads it as "release failed" when the release actually
|
|
320
|
+
shipped. Classify from the verdict fields, never the exit code alone: `succeeded` + `pending` means
|
|
321
|
+
shipped with the follow-up unresolved — watch the enumerated `workflowRuns` to conclusion; only a
|
|
322
|
+
resolved failure there is a failed follow-up. Read the live release verdict and verify these four
|
|
323
|
+
facts instead: the `main..development` count, the tag on `origin`, `gh release view`, and the runs on
|
|
324
|
+
the release SHA.
|
|
325
|
+
|
|
326
|
+
Required status checks are **per-repo branch protection, not a fixed list** — MMI-Hub's `main` requires
|
|
327
|
+
`cli` · `infra` · `docs`, but a product repo may require different contexts or none at all (#1045). The
|
|
328
|
+
release SHA is always fresh (the Step 1b fold commits on local `main`), so when checks ARE required,
|
|
329
|
+
pushing the branch *first* is structurally rejected until CI catches up. Push the **tag first** (it lands
|
|
330
|
+
the SHA and triggers the repo's CI — in MMI-Hub `gate.yml` runs on `v*` tags — without touching the
|
|
331
|
+
protected ref), then probe what `main` actually requires and wait only for those contexts:
|
|
332
|
+
|
|
333
|
+
```bash
|
|
334
|
+
git push origin "vX.Y.0" # lands the SHA + triggers the repo's CI
|
|
335
|
+
SHA=$(git rev-parse main)
|
|
336
|
+
# discover the REQUIRED contexts on main (classic protection + rulesets; 404 = none from that source):
|
|
337
|
+
gh api repos/{owner}/{repo}/branches/main/protection/required_status_checks --jq '[.contexts[]]'
|
|
338
|
+
gh api repos/{owner}/{repo}/rules/branches/main \
|
|
339
|
+
--jq '[.[]|select(.type=="required_status_checks")|.parameters.required_status_checks[].context]'
|
|
340
|
+
# ZERO required contexts -> push main immediately (the GitHub push gate is the backstop).
|
|
341
|
+
# Otherwise poll until every required context is "success" on $SHA — never a hard-coded list, and bound
|
|
342
|
+
# the wait (~10 min): on timeout, stop with a clear failure naming the pending/failed contexts.
|
|
343
|
+
gh api repos/{owner}/{repo}/commits/$SHA/check-runs \
|
|
344
|
+
--jq '[.check_runs[]|{name:.name,conclusion:.conclusion}]'
|
|
345
|
+
git push origin main
|
|
346
|
+
```
|
|
347
|
+
(`mmi-cli devops release --apply` performs this discovery + bounded wait itself.) A required context can only ever
|
|
348
|
+
resolve on a tag SHA if its workflow runs on `push: tags` (or is otherwise SHA-addressable) — a PR/issue-event
|
|
349
|
+
job (e.g. a stale hand-added `add-to-project` / `mark-merged-pr-done` board-automation check) structurally
|
|
350
|
+
never produces a check-run there. The train recognizes that fixed pair by name and, after a short grace
|
|
351
|
+
window, treats it as satisfied-for-the-tag instead of burning the full ~10-minute budget on a check-run that
|
|
352
|
+
can never appear (#2404). Any OTHER required context still waits the full budget and, on timeout, names
|
|
353
|
+
exactly which contexts never materialized vs. which were merely pending. If your repo's ruleset requires a
|
|
354
|
+
PR-only job outside that pair, fix the ruleset (drop it, or replace with a real gate) — don't rely on the
|
|
355
|
+
grace window for anything not on the allowlist.
|
|
356
|
+
Rejected → stop (nothing released).
|
|
357
|
+
|
|
358
|
+
**Halted on a FAILED required check (not merely pending)?** That is a **stray unreleased tag** (#2734): the
|
|
359
|
+
tag is pushed but `main` was not, no GitHub Release exists, and nothing deployed/published off it — so it can
|
|
360
|
+
never go forward (a failed check will not pass on that SHA). This is the **opposite** of a *resumable* partial
|
|
361
|
+
state (checks passing/pending, only the branch push / Release / deploy left, where the recovery is to finish
|
|
362
|
+
forward and you must NOT delete the tag). Here: fix the cause on `development` via a CI-gated PR, then **delete
|
|
363
|
+
the stray tag** — `git push origin --delete vX.Y.0` **and** `git tag -d vX.Y.0` (delete the local tag too; a
|
|
364
|
+
surviving local tag makes `next-version cycle` silently mint the *next* version) — and re-run `mmi-cli devops release
|
|
365
|
+
--apply` (the fold re-tags the same version on the fixed HEAD). **Tag deletion is deletion-class → it needs the
|
|
366
|
+
authorized human's explicit per-turn go.** Exception: if a publish/deploy DID run off the stray tag, do **not**
|
|
367
|
+
delete — mint the next version with `MMI_RELEASE_VERSION` instead. The CLI already classifies this
|
|
368
|
+
(`isStrayUnreleasedTag`, `cli/src/train-apply.ts`); this note surfaces the doctrine so a halted operator does
|
|
369
|
+
not re-derive it from source.
|
|
370
|
+
|
|
371
|
+
## Step 4 — GitHub Release + start prod deploy (non-blocking)
|
|
372
|
+
|
|
373
|
+
For `tenant-container` repos, publish the GitHub Release and dispatch the central tenant deploy.
|
|
374
|
+
```bash
|
|
375
|
+
gh release create "vX.Y.0" --target main --generate-notes --latest
|
|
376
|
+
gh workflow run tenant-deploy.yml --repo mutmutco/MMI-Hub \
|
|
377
|
+
-f slug={slug} -f repo={owner}/{repo} -f ref=main -f stage=main
|
|
378
|
+
gh run watch "$(gh run list --workflow tenant-deploy.yml --limit 1 --json databaseId -q '.[0].databaseId')" \
|
|
379
|
+
--exit-status # the central prod-deploy run — run this in the BACKGROUND (Bash run_in_background)
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
For `hub-serverless` (MMI-Hub), publish the GitHub Release but do **not** dispatch `tenant-deploy.yml`:
|
|
383
|
+
the release event auto-fires `deploy.yml` for prod and `publish.yml` for the
|
|
384
|
+
plugin/CLI package. A missing `DEPLOY#main` registry row for MMI-Hub is expected, not a tenant stack repair
|
|
385
|
+
task. Watch/report the release-triggered `deploy.yml` and `publish.yml` runs instead.
|
|
386
|
+
|
|
387
|
+
For other direct-track repos, the train dispatches nothing centrally: a `registry-publish` repo's release
|
|
388
|
+
event fires its own `publish.yml` (npm / plugin marketplace); a `solo-container` repo deploys via its own
|
|
389
|
+
workflow. Publish the GitHub Release, then watch/report that repo's own release-triggered run.
|
|
390
|
+
|
|
391
|
+
`mmi-cli devops release --apply --json` returns the relevant run id/url data with `deployStatus`; `--watch` blocks
|
|
392
|
+
on the run(s) and resolves `deployStatus` to `success`/`failure`. For tenant-container repos, that is the
|
|
393
|
+
dispatched `tenant-deploy.yml` run. For Hub serverless, that is the auto-fired release `deploy.yml` and
|
|
394
|
+
`publish.yml` workflow pair — watched on `mutmutco/MMI-Hub`. For `registry-publish`, that is the target
|
|
395
|
+
repo's own release-triggered `publish.yml` run — watched on that repo, never dispatched, and never the
|
|
396
|
+
central `tenant-publish.yml` (#2428: dispatching both is a guaranteed npm E409 double-publish, since the
|
|
397
|
+
repo's own release-triggered publish always wins the version race).
|
|
398
|
+
`promoted: true` stays set even on a failed deploy — promotion and deploy are separate outcomes.
|
|
399
|
+
|
|
400
|
+
**`deployStatus: 'pending'` is NEVER a terminal, reportable state (#3322).** A release that returns
|
|
401
|
+
`pending` has an UNRESOLVED deploy, not a clean one. The report now prints `deploy: UNVERIFIED` instead of
|
|
402
|
+
omitting the deploy line entirely — which is exactly how a failed deploy once hid behind a "successful"
|
|
403
|
+
release (Jerv-PowerTools v1.22.0: the release-triggered `publish.yml` went green, a separate `push: main`
|
|
404
|
+
SAM deploy FAILED, and the train returned `deployStatus: pending`, so the run read as clean). Resolve it,
|
|
405
|
+
or report it as unverified-RED. Never as success.
|
|
406
|
+
|
|
407
|
+
**Read the `--apply` result's `workflowRuns` array — the train already enumerates every run on the release
|
|
408
|
+
SHA (#3699).** That enumeration includes `push: main` infra deploys (`deploy.yml`, SAM) the deploy model does
|
|
409
|
+
not name, and `deployStatus` is held at `pending` until they resolve. Any non-success entry — including
|
|
410
|
+
`pending` — keeps the release unverified: watch those listed runs to conclusion:
|
|
411
|
+
```bash
|
|
412
|
+
gh run watch <run-id> --repo {owner}/{repo} --exit-status
|
|
413
|
+
```
|
|
414
|
+
The one remaining caveat: `--watch` blocks only on deploy-model-named runs, not extra push-triggered
|
|
415
|
+
deploys — those you conclude from the `workflowRuns` list yourself. Any `failure` there is a failed release
|
|
416
|
+
deploy even when the train reported success — flag it loudly and fix it before calling the release healthy.
|
|
417
|
+
|
|
418
|
+
**Hub releases announce to Slack (#883).** Hub scope is **only** `mutmutco/MMI-Hub`: the commits/PRs on
|
|
419
|
+
`origin/main..origin/development`, this repo's `deploy.yml` + `publish.yml`, and Hub tooling (`mmi-cli`,
|
|
420
|
+
skills, plugin, registry, central workflows). Do **not** read another repo's board (`mmi-cli oracle board` / `/mmi`
|
|
421
|
+
on a product), watch or dispatch `ds-propagate.yml`, run `design-system` / `doctor` design-system heals for
|
|
422
|
+
consumers, or treat a product's deploy state as part of this release — those belong to that product's own
|
|
423
|
+
`/release` or `/rcand`, never a Hub train.
|
|
424
|
+
|
|
425
|
+
Before running `--apply` for MMI-Hub, resolve the real tag first:
|
|
426
|
+
`TAG=$(MMI_BUMP_INTENT=minor node scripts/next-version.mjs cycle)` — always print `$TAG` (e.g.
|
|
427
|
+
`v2.43.0`) in summaries, Slack, chat, and the final report; never a placeholder like `vX.Y.0` or
|
|
428
|
+
`v0.x.0`.
|
|
429
|
+
|
|
430
|
+
Write a curated summary — 3-6 very short plain lines, one change per line, dev-readable, no PR-dump —
|
|
431
|
+
to a fresh temp file (`f=$(mktemp tmp/release-summary.XXXXXX)`, so a stale prior summary is never reused).
|
|
432
|
+
Source from **Hub PR titles only** (`origin/main..origin/development` on `mutmutco/MMI-Hub`), but **rewrite**
|
|
433
|
+
each line in neutral Hub-subsystem terms (CLI, skills, plugin, workflows, registry, deploy hub) — **never**
|
|
434
|
+
a product or brand name (FoFu, Katip, etc.) anywhere in the summary file, Slack post, chat, or release
|
|
435
|
+
report. Product names are allowed only when releasing **that product's repo**. Then pass the file through:
|
|
436
|
+
`mmi-cli devops release --apply --announce-summary-file "$f"`. After the GitHub Release publishes, the CLI posts
|
|
437
|
+
the summary to the org alerts channel as the MMI-Future Slack app (token + channel from SSM at run time).
|
|
438
|
+
For a new MMI-Hub `--apply`, the CLI refuses before promotion when the file is missing, unreadable, or does
|
|
439
|
+
not contain 3–6 non-empty lines; generated-note fallback is not an agent release path. `--resume` never
|
|
440
|
+
requires or republishes a summary, and non-Hub repos skip the announcement automatically. After a valid
|
|
441
|
+
summary is accepted, Slack delivery remains best-effort: a transport failure is reported in the result and
|
|
442
|
+
never rolls back an otherwise completed release.
|
|
443
|
+
|
|
444
|
+
**Don't block on the deploy.** Start the watch as a background task and proceed to Steps 4b–5 (docs, project
|
|
445
|
+
info, branch alignment) while prod deploys. The verdict is collected in Step 6 — verification is not
|
|
446
|
+
skipped, only un-blocked. Deploy failure → report plainly, then **retry the existing promoted ref by deploy
|
|
447
|
+
model** once the runtime is repaired — never a re-tag or republish (`main` is already at the release, which is
|
|
448
|
+
correct). Read the model from the registry (`mmi-cli oracle org project get {owner}/{repo}` → `deployModel`):
|
|
449
|
+
- **tenant-container** — the central tenant deployer owns the redeploy: `mmi-cli devops runtime tenant redeploy
|
|
450
|
+
{owner}/{repo} main --watch`.
|
|
451
|
+
- **repository-owned serverless / registry-publish** (e.g. Jerv-PowerTools' `Deploy Jerv Memory`) — there is
|
|
452
|
+
no `runtime tenant redeploy` for these; re-run the repo's OWN declared `workflow_dispatch` deploy workflow at
|
|
453
|
+
the promoted-equivalent ref: `gh workflow run <deploy-workflow> --repo {owner}/{repo} --ref <ref>`.
|
|
454
|
+
|
|
455
|
+
Name the sanctioned dispatch mechanism you used in the train report.
|
|
456
|
+
|
|
457
|
+
## Step 4b — re-sync project info (no in-repo docs pass)
|
|
458
|
+
|
|
459
|
+
In-repo README/architecture freshness is **not** a train step (Step 0d retired, Hub#4164).
|
|
460
|
+
`mmi-cli devops release --apply` re-syncs the GitHub **Project** short description + thin README from the
|
|
461
|
+
released commit and the registry member list through the command ladder. For a manual repair or preview use:
|
|
462
|
+
```bash
|
|
463
|
+
mmi-cli oracle org project sync-info --apply # omit --apply for the read-only plan
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
## Step 5 — roll development forward
|
|
467
|
+
|
|
468
|
+
- Keep branches aligned: `mmi-cli devops release --apply` back-merges the released `main` (incl. the version
|
|
469
|
+
fold) into `development` and reports it as `devRollForward`. When `development` has no required checks it
|
|
470
|
+
pushes directly (`status: pushed`). When `development` *requires* checks (e.g. MMI-Hub needs
|
|
471
|
+
`cli`/`infra`/`docs`), the fresh merge commit carries no passing checks, so a direct push is structurally
|
|
472
|
+
rejected — the train instead **opens an alignment PR** `main → development` (`status: pr-pending`) and the
|
|
473
|
+
release report prints the exact land command. The release itself has already shipped; **land the alignment
|
|
474
|
+
PR with a true merge** — `mmi-cli devops pr merge <number> --auto --merge` (never squash — a squash drops the merge
|
|
475
|
+
parentage and the misalignment guard re-flags the divergence). `--auto` is what makes it land right after a
|
|
476
|
+
release: the alignment PR's own `cli`/`infra`/`docs` checks are still running, so a plain immediate merge is
|
|
477
|
+
policy-blocked — `--auto` merges once they pass. Never force.
|
|
478
|
+
- Full-track repos: `mmi-cli devops release --apply` already aligned `rc` to the released `main` (#1036 — the
|
|
479
|
+
push runs inside the authority-gated train; the result reports it as `rcAlignment`). No manual `rc`
|
|
480
|
+
push — if the result reports a failed alignment, investigate and rerun via the train, never bare-push.
|
|
481
|
+
- **Local branches never left stale (#2582).** A release advances `main`/`development`/`rc` on **origin**;
|
|
482
|
+
`mmi-cli devops release --apply` also fast-forwards the LOCAL train branches to match, so the checkout you
|
|
483
|
+
released from does not lag what you just pushed. The train restores + fast-forwards the start branch
|
|
484
|
+
(`checkout`, #2340) and fast-forwards the others (`localSync`) — non-destructively: only a true
|
|
485
|
+
fast-forward moves a branch; a diverged local branch is reported and left untouched (never a force).
|
|
486
|
+
The one branch it **cannot** sync at release time is a check-gated `development` whose roll-forward is a
|
|
487
|
+
`pr-pending` alignment PR (Hub) — that PR merges on origin *after* the release returns. That case is
|
|
488
|
+
healed automatically by the **next session**: `mmi-cli plugin session-start` (invoked by the resume
|
|
489
|
+
channel — #3630 took SessionStart off the hook surface) runs the
|
|
490
|
+
same fast-forward first thing, so a stale local `development`/`main`/`rc` self-heals to origin before you
|
|
491
|
+
work — no manual `git pull`. Nothing here ever blocks or fails the release.
|
|
492
|
+
- **Read the `checkout:` clause — it does not always say "returned" (#4006).** The report always prints
|
|
493
|
+
one, and restoration is deliberately skipped rather than forced when the repo is not in a fit state.
|
|
494
|
+
A release leaves the repo on `main` until the train moves it back, so a non-`returned` outcome means
|
|
495
|
+
you are still there and must return by hand once the cause is cleared:
|
|
496
|
+
- `checkout restoration skipped: working tree changed after release` — the tree went dirty DURING the
|
|
497
|
+
train, so the train declined to move you. Diagnose those paths with the Step 0 clean-tree rules,
|
|
498
|
+
then `git checkout <start branch>` yourself. The release itself already shipped; this is a
|
|
499
|
+
deliberate, harmless decline, not a failed release.
|
|
500
|
+
- `checkout restoration failed while returning to …` / `origin/<branch> fast-forward failed` — the
|
|
501
|
+
checkout or the pull errored. Read the appended git message; the release is unaffected.
|
|
502
|
+
|
|
503
|
+
`--resume` prints no `checkout:` clause at all, so after a resumed release check `git branch
|
|
504
|
+
--show-current` rather than assuming you were moved back.
|
|
505
|
+
|
|
506
|
+
## Step 6 — collect deploy verdict + report
|
|
507
|
+
|
|
508
|
+
The `release --apply` exit code is **not** the release verdict. A nonzero exit with
|
|
509
|
+
`releaseStatus=succeeded` and `followUpStatus=pending` is the unresolved-follow-up case from Step 3 —
|
|
510
|
+
the release shipped; the runs enumerated in `workflowRuns` are the outstanding verdict, so watch them
|
|
511
|
+
to conclusion here rather than re-running or alarming. Report the live release verdict from
|
|
512
|
+
the four checks that matter: the `main..development` count, the tag on `origin`, `gh release view`,
|
|
513
|
+
and the runs on the release SHA; an alignment PR is normal follow-up work when those checks confirm
|
|
514
|
+
the release shipped.
|
|
515
|
+
|
|
516
|
+
Collect the backgrounded prod-deploy watch from Step 4 (it has typically finished by now). Confirm prod is
|
|
517
|
+
healthy (the central deploy workflow smoke step / a health check); **red** → report the failure prominently and flag
|
|
518
|
+
that the release shipped on a failed deploy (re-run just the deploy — `main` is already correct).
|
|
519
|
+
|
|
520
|
+
**This distribution verifier is Hub-only.** It assumes the Hub release fold has already committed its
|
|
521
|
+
generated artifacts. Non-Hub `registry-publish` repositories must not run `release-distribution.mjs
|
|
522
|
+
verify`: use their own release-workflow evidence and the publish-visibility contract below.
|
|
523
|
+
|
|
524
|
+
Hub releases always carry a distribution bump (the Step 1b fold), so the **publish workflow**
|
|
525
|
+
(`publish.yml`) ships every registry-declared public npm artifact on the GitHub Release from Step 4 —
|
|
526
|
+
don't publish by hand.
|
|
527
|
+
Watch it, then confirm npm caught up:
|
|
528
|
+
```bash
|
|
529
|
+
gh run watch "$(gh run list --workflow publish.yml --event release --limit 1 --json databaseId -q '.[0].databaseId')" --exit-status
|
|
530
|
+
node scripts/release-distribution.mjs verify "vX.Y.0" # asserts registry versions, BOM identities, and published npm artifacts
|
|
531
|
+
```
|
|
532
|
+
Run that verify from a checkout **at the tag** (`main` right after the release merge, `git checkout
|
|
533
|
+
"vX.Y.0"`, or a worktree at the tag) — verify checks the working tree and refuses any other commit once
|
|
534
|
+
the tag exists, so a stale `development`/`rc` checkout can't masquerade as a broken release.
|
|
535
|
+
Release-blocking for Hub tooling changes: every public artifact must match the registry version and BOM
|
|
536
|
+
identity. Manual fallback if CI can't publish: `node scripts/release-distribution.mjs publish "vX.Y.0"`
|
|
537
|
+
from a machine with npm auth.
|
|
538
|
+
|
|
539
|
+
**Private packages (#2405) — never trust a bare `npm view` to mean "not published".** An unauthenticated
|
|
540
|
+
`npm view <pkg>` returns a hard 404 on any package published with `publishConfig.access=restricted`
|
|
541
|
+
(private) — indistinguishable from "not published" — because npm masks private packages from anonymous
|
|
542
|
+
reads. Before verifying ANY npm publish (Hub's own, a `registry-publish` product repo's own `publish.yml`,
|
|
543
|
+
or a `publishRequired` product repo's companion `tenant-publish.yml`), check that surface's
|
|
544
|
+
`publishVisibility` in the target repo's `surfaces.json`
|
|
545
|
+
(`node -e "console.log(require('./surfaces.json').surfaces.find(s=>s.id==='<id>').publishVisibility)"`, or
|
|
546
|
+
the repo-agnostic reader `scripts/release-distribution.mjs` exports as `npmVerifyModeFor`):
|
|
547
|
+
- `public` → an unauthenticated `npm view <pkg>@<version> version` is a trustworthy verify signal.
|
|
548
|
+
- `private` → do **not** run a bare `npm view`. Verify from an authenticated source instead: the
|
|
549
|
+
release-triggered run's conclusion (`gh run watch … --exit-status` — the repo's own `publish.yml` for
|
|
550
|
+
`registry-publish`, #2428; the central `tenant-publish.yml` for a `publishRequired` companion publish) and
|
|
551
|
+
its logged `+ pkg@ver` / `"already on npm — nothing to do"` / `"already on npm ... treating as idempotent
|
|
552
|
+
success"` output (`gh run view <id> --log`), or `npm view` with a registry auth token in env. A 404 here is
|
|
553
|
+
"unverifiable from here", never "not published" — report it as such, don't block or alarm on it.
|
|
554
|
+
- `n/a` → nothing publishes to npm for that surface; skip.
|
|
555
|
+
Hub's registry-declared npm surfaces are public today, so `release-distribution.mjs verify` uses a bare
|
|
556
|
+
`npm view` for them — this changes only if a package's declared visibility changes.
|
|
557
|
+
|
|
558
|
+
**tenant-publish.yml E409 belt-and-braces (#2428).** The central `tenant-publish.yml` lane (the
|
|
559
|
+
`publishRequired` companion publish only — never dispatched for `registry-publish`, see above) already
|
|
560
|
+
treats a lost `npm publish` race as idempotent inside the run (a re-check `npm view` after a failed publish;
|
|
561
|
+
see the workflow file). `mmi-cli devops release --apply --watch` mirrors that at the CLI layer: a watched
|
|
562
|
+
`tenant-publish.yml` run that concludes `failure` is re-checked against that SAME run's own log (bounded
|
|
563
|
+
retries, visibility-agnostic — never a bare `npm view`) for the idempotent-success marker before the CLI
|
|
564
|
+
reports the publish as failed. A run whose log never shows the version landed stays a loud failure.
|
|
565
|
+
|
|
566
|
+
Report: Release `$TAG` (the resolved tag from Step 2 — never a placeholder) + the GitHub Release URL · prod
|
|
567
|
+
deploy run + URL + **green/red** · branch-alignment note · npm publish run + CLI version (Hub releases).
|
|
568
|
+
|
|
569
|
+
## Notes
|
|
570
|
+
|
|
571
|
+
- `/hotfix` stays the cherry-pick promotion lane (a hotfix always skips rc — it cherry-picks
|
|
572
|
+
`development → main` directly), but a `/release patch` from development is a legitimate ordinary
|
|
573
|
+
release (#4929). Never force-push `main`.
|
|
574
|
+
- **`MMI_BUMP_INTENT=major|minor|patch` comes from the bare `/release` argument; unstated it defaults to
|
|
575
|
+
`patch`** (#4929). Export it for the whole run, not a one-off prefix on `next-version.mjs cycle`:
|
|
576
|
+
`mmi-cli devops release --apply` (and `rcand --apply`) resolve intent themselves **before any merge,
|
|
577
|
+
tag, or fold** — direct-track included — defaulting to `patch` when the env is absent and failing closed
|
|
578
|
+
there with `MMI_BUMP_INTENT must be one of major|minor|patch` on an invalid value, so the tree stays
|
|
579
|
+
clean on the start branch. `MMI_RELEASE_VERSION`
|
|
580
|
+
below is a different knob: it overrides the computed version; it does not replace the intent export
|
|
581
|
+
unless you set that exact-target env instead.
|
|
582
|
+
- **`--dev` (full-track only):** releases `development → main` skipping rc, with the same fold/tag/Release/
|
|
583
|
+
deploy machinery plus the post-release rc retirement and rc alignment. Refuses (fail closed) when
|
|
584
|
+
`origin/rc` carries commits not in `development`; on direct-track repos it's a no-op (they already
|
|
585
|
+
release from `development`).
|
|
586
|
+
- **MAJOR / exact-target release:** to ship a version the tag math can't derive (a MAJOR like `2.0.0`, or
|
|
587
|
+
skipping a version already on npm), product repos export `MMI_RELEASE_VERSION=X.Y.Z` for **both** `/rcand`
|
|
588
|
+
and `/release`; MMI-Hub exports it for `/release` only. Steps 1b/2 then fold and tag exactly that
|
|
589
|
+
version. Unset, the train uses the declared `MMI_BUMP_INTENT` increment, defaulting to `patch` when
|
|
590
|
+
neither the argument nor the env declares one (#4929).
|
|
591
|
+
|
|
592
|
+
## Retro — one check before you finish
|
|
593
|
+
Before your final report, answer one question honestly: did **this skill's own instructions** misfire
|
|
594
|
+
this run — ambiguous wording, a misleading message, or an environment failure it should have warned
|
|
595
|
+
about? (Process only — never the user's code or task; e.g. a misleading authority or gate message, or an
|
|
596
|
+
ambiguous version-fold or back-merge step.) If yes, file **one** lesson and move on; a clean run is silent
|
|
597
|
+
(hard cap: one per run). It lands on the Hub board (deduped) and is fixed only via a reviewed PR — never
|
|
598
|
+
edit the skill live; the retro is advisory, so if the call fails, note it and continue:
|
|
599
|
+
`mmi-cli learning skill-lesson --skill release --title "<what misfired>" --body "<what; evidence; proposed amendment>"`
|