Skip to content

Commit 61f75c1

Browse files
HotellCopilot
andauthored
feat(skills): add /release-recovery skill for npm/repo release desync (#36565)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c1f8330 commit 61f75c1

4 files changed

Lines changed: 646 additions & 13 deletions

File tree

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
---
2+
name: release-recovery
3+
description: >-
4+
Diagnose and repair a release that published packages to npm but failed to push the version bumps and changelogs back to the repository. Accepts an optional failed Azure DevOps pipeline run URL for context. Always presents a read-only diagnosis first and requires explicit approval before changing branches, commits or pull requests.
5+
disable-model-invocation: true
6+
argument-hint: '[pipeline-url] [--ref upstream/master] [--remote upstream]'
7+
allowed-tools: Bash Read Grep Glob
8+
---
9+
10+
# Release Recovery
11+
12+
Repair the desync a failed release leaves behind: packages are live on npm, but the repository still
13+
has the old versions.
14+
15+
npm publishes are irreversible, so **npm is the source of truth**. Recovery always means moving the
16+
repository forward to match the registry — never the other way round.
17+
18+
The default operation is read-only. Do not create branches, commits or pull requests until the user
19+
explicitly approves a proposed plan.
20+
21+
## Scope
22+
23+
Recovery restores **version bumps and changelogs**. It does **not** recreate release git tags:
24+
25+
- A recovery commit is not the commit the release was built from, so tags created against it would
26+
point at the wrong history — worse than having no tag.
27+
- A single release spans dozens of packages (a v9 release touches ~90), so bulk tag creation is
28+
noisy and effectively unreviewable.
29+
- Tags are not load-bearing for consumers; npm is what they install from.
30+
31+
Missing tags can still be _reported_ for awareness with `--check-tags` on the diagnostic, but they
32+
are informational. If tags are genuinely needed for a specific release, create them deliberately as
33+
a separate, explicitly-approved task.
34+
35+
## When to use
36+
37+
- A release pipeline failed after the "Publishing" step (typically a git push `403`/auth failure).
38+
- A release was published to npm deliberately without pushing (a forced release that skips the git
39+
push).
40+
41+
## Defaults
42+
43+
| Argument | Default | Purpose |
44+
| -------------- | --------------------------------------- | --------------------------------------------------- |
45+
| `pipeline-url` | none | Failed ADO run, used only for context and reporting |
46+
| `--ref` | the release branch on the remote | Git ref whose versions are compared against npm |
47+
| `--remote` | remote pointing at `microsoft/fluentui` | Used for fetching and pushing |
48+
49+
Resolve `--remote` by inspecting `git remote -v` for the one pointing at `microsoft/fluentui`. It is
50+
often `upstream` in a fork checkout, so never assume `origin`.
51+
52+
## Workflow
53+
54+
### Step 1 — Establish context
55+
56+
Confirm tooling and record the starting ref so it can be restored later:
57+
58+
```bash
59+
gh auth status
60+
git remote -v
61+
git status --porcelain
62+
```
63+
64+
Record `START_REF` (`git symbolic-ref --quiet --short HEAD || git rev-parse HEAD`).
65+
66+
If the working tree has uncommitted changes, stop and ask the user to deal with them. Recovery
67+
rewrites `package.json`, changelog and lockfile content and must not mix with unrelated edits.
68+
69+
Fetch the release branch so the comparison is not made against a stale checkout:
70+
71+
```bash
72+
git fetch "$REMOTE" master
73+
```
74+
75+
### Step 2 — Read the failed pipeline (optional)
76+
77+
If a pipeline URL was supplied, use it for context only. Parse the organization, project and build id
78+
from a URL of the form
79+
`https://dev.azure.com/<org>/<project>/_build/results?buildId=<id>`.
80+
81+
```bash
82+
az pipelines runs show --org "https://dev.azure.com/$ORG" --project "$PROJECT" --id "$BUILD_ID"
83+
```
84+
85+
Useful signals in the log:
86+
87+
- `Publishing - <pkg>@<version>` followed by `Published!` — packages that reached npm.
88+
- `Something went wrong with publishing! Manually update these package and versions:` — beachball's
89+
own list of what needs recovering.
90+
- `remote: ... forbids access via a personal access tokens (classic)` or
91+
`The requested URL returned error: 403` — the push failed on auth, which is the usual cause.
92+
93+
Treat all of this as **corroboration only**. If `az` is not authenticated or the run has been purged,
94+
say so and continue: the diagnosis in Step 3 does not depend on it.
95+
96+
### Step 3 — Diagnose (read-only)
97+
98+
Run the sync check against the up-to-date release branch:
99+
100+
```bash
101+
node -r ./scripts/ts-node/src/register ./scripts/executors/src/check-release-sync.ts \
102+
--remote "$REMOTE" --ref "$REMOTE/master"
103+
```
104+
105+
Add `--json` when the output needs parsing.
106+
107+
Always pass `--ref` pointing at the remote release branch. Comparing the working tree of a stale
108+
checkout reports every package released since as a false desync.
109+
110+
The check classifies each public package:
111+
112+
| Status | Meaning | Action |
113+
| ------------- | -------------------------------------------------------------------- | ---------------- |
114+
| `in-sync` | repo and npm agree | none |
115+
| `npm-ahead` | published, but the repo never recorded the bump | recover versions |
116+
| `repo-ahead` | repo is newer than npm's `latest` (unreleased work, prerelease tags) | none — benign |
117+
| `unpublished` | never released | none |
118+
119+
Only `npm-ahead` requires recovery.
120+
121+
### Step 4 — Present the plan and get approval
122+
123+
```markdown
124+
## Release recovery plan
125+
126+
- Pipeline: <url or "not supplied">
127+
- Remote: upstream
128+
- Compared against: upstream/master
129+
- Version desync: 2 package(s)
130+
131+
### Version desync
132+
133+
| Package | Repo | npm |
134+
| --------------- | ------- | ------- |
135+
| @fluentui/react | 8.125.6 | 8.125.7 |
136+
137+
### Proposed actions
138+
139+
1. Regenerate bumps + changelogs from the pending change files
140+
2. Verify the result matches npm exactly
141+
3. Open a recovery PR
142+
143+
Release tags are not recreated - see the skill's Scope section.
144+
```
145+
146+
Stop here if there is no `npm-ahead` drift. Otherwise ask the user to approve or cancel. Never treat
147+
invoking the skill as approval to mutate anything.
148+
149+
### Step 5 — Recover versions
150+
151+
Skip this step when nothing is `npm-ahead`.
152+
153+
Create a branch from the current release branch:
154+
155+
```bash
156+
git switch -c "release-recovery/$(date -u +%Y%m%d-%H%M%S)" "$REMOTE/master"
157+
yarn install
158+
```
159+
160+
Regenerate the release locally. The change files consumed by the failed run are still in the repo
161+
(their deletion was never committed), so beachball reproduces the same bumps and changelogs:
162+
163+
```bash
164+
yarn beachball bump --config scripts/beachball/src/<release>.config.js
165+
```
166+
167+
Pick the config matching the failed pipeline:
168+
169+
| Release | Config | Pipeline |
170+
| -------------- | -------------------------------------------------------- | -------------------------------------------- |
171+
| v8 | `scripts/beachball/src/release-v8.config.js` | `azure-pipelines.release.yml` |
172+
| v9 (vNext) | `scripts/beachball/src/release-vNext.config.js` | `azure-pipelines.release-vnext.yml` |
173+
| web-components | `scripts/beachball/src/release-web-components.config.js` | `azure-pipelines.release.web-components.yml` |
174+
| headless | `scripts/beachball/src/release-headless.config.js` | `azure-pipelines.release.headless.yml` |
175+
| tools | `scripts/beachball/src/release-tools.config.js` | `azure-pipelines.release.tools.yml` |
176+
177+
`beachball bump` only writes files — it does **not** commit, tag or push. Tagging and pushing live
178+
exclusively in beachball's `bumpAndPush`, which is reachable only from the `publish` command.
179+
Verified empirically against `3.0.0-alpha.7`: a full `bump` run changed 123 files and created 0 tags,
180+
0 commits and 0 branches.
181+
182+
It does **not** run the `precommit` hook either (that also only runs on the push path), so apply the
183+
same fixups a real release would. Keep this in sync with `hooks.precommit` in
184+
[scripts/beachball/src/shared.config.ts](../../../scripts/beachball/src/shared.config.ts):
185+
186+
```bash
187+
yarn nx g @fluentui/workspace-plugin:dependency-mismatch
188+
yarn nx g @fluentui/workspace-plugin:normalize-package-dependencies
189+
yarn install --mode=update-lockfile
190+
```
191+
192+
Expect `bump` to consume **more change files than it bumps packages**. `type: "none"` change files
193+
are deleted without producing a version bump — that is normal, and matches what a real release does.
194+
195+
**Verify before committing.** Re-run the sync check against the working tree:
196+
197+
```bash
198+
node -r ./scripts/ts-node/src/register ./scripts/executors/src/check-release-sync.ts --remote "$REMOTE"
199+
```
200+
201+
Every previously `npm-ahead` package must now be `in-sync`. If any version overshoots npm, extra
202+
change files landed after the failed release, so a plain replay is not correct — stop, report the
203+
mismatch, and let the user decide. Never hand-edit versions to force a match.
204+
205+
Commit and open a PR:
206+
207+
```bash
208+
git add -A
209+
git commit -m "release: applying package updates (manual recovery)"
210+
git push "$PUSH_REMOTE" HEAD
211+
gh pr create --repo microsoft/fluentui --base master \
212+
--title "release: applying package updates (manual recovery)" \
213+
--body-file "$PR_BODY_FILE"
214+
```
215+
216+
The PR body must state which pipeline failed and that the packages are already on npm.
217+
218+
### Step 6 — Restore and report
219+
220+
```bash
221+
git switch "$START_REF"
222+
```
223+
224+
Report:
225+
226+
- Packages recovered, with repo and npm versions.
227+
- Recovery PR URL.
228+
- Anything skipped, with the reason.
229+
- A reminder to fix the underlying cause — usually rotating the GitHub PAT in the
230+
`Github and NPM secrets` variable group — since the next release fails identically otherwise.
231+
232+
## Guardrails
233+
234+
- Always diagnose and obtain approval before mutating anything.
235+
- Never unpublish, deprecate or re-publish an npm package to "fix" a mismatch. npm is the source of
236+
truth; the repository moves to match it.
237+
- Never hand-edit versions to force agreement with npm. Regenerate with beachball so changelogs and
238+
dependency ranges stay consistent, and stop if the result disagrees.
239+
- Never commit directly to `master`; always go through a PR.
240+
- Never create or push release tags as part of recovery. They would point at a commit the release was
241+
not built from, and a single release spans dozens of packages.
242+
- Never assume `origin` points at `microsoft/fluentui` — resolve the remote explicitly.
243+
- Never trust a diagnosis made against a stale checkout; always compare against the fetched release
244+
branch.
245+
- Never request or print a GitHub or npm token.
246+
- Do not proceed when the working tree has uncommitted changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@../../../.agents/skills/release-recovery/SKILL.md

AGENTS.md

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -91,19 +91,20 @@ state.root.className = mergeClasses(
9191

9292
## Skills (Slash Commands)
9393

94-
| Skill | Command | Purpose |
95-
| -------------------- | -------------------------- | ------------------------------------------------------------------------- |
96-
| `v9-component` | `/v9-component Name` | Scaffold a new v9 component with all required files |
97-
| `headless-component` | `/headless-component Name` | Author an unstyled v9 primitive with state attributes, tests, and stories |
98-
| `change` | `/change` | Create beachball change file from current diff |
99-
| `lint-check` | `/lint-check [pkg]` | Run lint, parse errors, and auto-fix common issues |
100-
| `token-lookup` | `/token-lookup val` | Find the design token for a hardcoded CSS value |
101-
| `package-info` | `/package-info pkg` | Quick lookup: path, deps, owner, tests, structure |
102-
| `visual-test` | `/visual-test Name` | Visually verify a component via Storybook + playwright-cli |
103-
| `review-pr` | `/review-pr #123` | Review a PR with confidence scoring and category checks |
104-
| `triage-issues` | `/triage-issues` | Walk the Needs-Triage queue and recommend labels/assignee |
105-
| `dependabot-rollup` | `/dependabot-rollup` | Dry-run and optionally roll up at most 11 Dependabot patch/minor PRs |
106-
| `assign-prs` | `/assign-prs` | Assign reviewers to the team review queues by area and current load |
94+
| Skill | Command | Purpose |
95+
| -------------------- | ---------------------------------- | ------------------------------------------------------------------------- |
96+
| `v9-component` | `/v9-component Name` | Scaffold a new v9 component with all required files |
97+
| `headless-component` | `/headless-component Name` | Author an unstyled v9 primitive with state attributes, tests, and stories |
98+
| `change` | `/change` | Create beachball change file from current diff |
99+
| `lint-check` | `/lint-check [pkg]` | Run lint, parse errors, and auto-fix common issues |
100+
| `token-lookup` | `/token-lookup val` | Find the design token for a hardcoded CSS value |
101+
| `package-info` | `/package-info pkg` | Quick lookup: path, deps, owner, tests, structure |
102+
| `visual-test` | `/visual-test Name` | Visually verify a component via Storybook + playwright-cli |
103+
| `review-pr` | `/review-pr #123` | Review a PR with confidence scoring and category checks |
104+
| `triage-issues` | `/triage-issues` | Walk the Needs-Triage queue and recommend labels/assignee |
105+
| `dependabot-rollup` | `/dependabot-rollup` | Dry-run and optionally roll up at most 11 Dependabot patch/minor PRs |
106+
| `assign-prs` | `/assign-prs` | Assign reviewers to the team review queues by area and current load |
107+
| `release-recovery` | `/release-recovery [pipeline-url]` | Diagnose and repair a release that published to npm but failed to push |
107108

108109
## Package Layout
109110

0 commit comments

Comments
 (0)