release-manager
voidzero-dev/vite-plus/.claude/skills/release-manager/SKILL.md
Run the standard vite-plus release process end-to-end as the release manager. Covers preparing the release PR, syncing the NAPI binding version, writing the categorized changelog, smoke-testing via a preview build, handling release-branch CI failures, merging, and post-release verification and announcements. Use when asked to cut, prepare, or manage a vite-plus release (e.g. "release v0.2.3", "act as release manager").
- Deletes or force-pushes
- Installs packages
- Commits and pushes
What's in it
- Vite+ Release Manager
- Usage
- Pipeline overview
- 1. Start the release
- 2. Sync release versions (required every release)
- NAPI binding
- Documentation and prompts
- 3. Write the release PR description
- Structure
- Categorization rules
- Bundled Versions table
- Style rules
- Validate before finishing
- 4. Preview build smoke test (before merging)
- Example (v0.2.2, PR #2016)
- Triaging failures across the catalog
- 5. Release-branch CI
- 6. Merge
- 7. Automated release pipeline (what happens after merge)
- 8. Post-release
- 9. Update this skill (post-release)
- Checklist
Tools it asks for
- Bash
- Read
- Edit
- Write
- Grep
- Glob
- WebFetch
---
name: release-manager
description: Run the standard vite-plus release process end-to-end as the release manager. Covers preparing the release PR, syncing the NAPI binding version, writing the categorized changelog, smoke-testing via a preview build, handling release-branch CI failures, merging, and post-release verification and announcements. Use when asked to cut, prepare, or manage a vite-plus release (e.g. "release v0.2.3", "act as release manager").
allowed-tools: Bash, Read, Edit, Write, Grep, Glob, WebFetch
---
# Vite+ Release Manager
Run a standard vite-plus release from version bump to published announcement. Any maintainer with repo write access can follow this; the only extra privilege needed is approval rights on the `release` GitHub environment (step 7).
## Usage
```
/release-manager # start a new release: ask for the target version, begin at step 1
/release-manager X.Y.Z # start a new release for that version
/release-manager <PR URL or #N> # take over an in-flight release
```
When given a release PR (URL or number), do not start from step 1. First audit the release's current state, then continue from the earliest unfinished step:
- Is the binding version synced? (step 2: `grep -c "'<prev>'" packages/cli/binding/index.cjs` on the release branch)
- Do the release version examples in the migration guide and the setup, migration, and upgrade prompts match `packages/cli/package.json`? (step 2)
- Is the PR description still the `prepare_release` boilerplate, or already a categorized changelog? (step 3)
- Is a preview build present and for the current head? (step 4)
- Does `main` have commits the release branch lacks? (`git log origin/release/vX.Y.Z..origin/main`, step 5)
- What is CI status? (`gh pr checks <PR#>`, step 5)
- Already merged? Check the Release workflow (`gh run list --workflow Release --repo voidzero-dev/vite-plus`) and whether the GitHub release body is still the generated stub, then continue at step 7 or 8.
Report the detected state before making changes, so the previous release manager's work is not redone or overwritten.
Before post-release work, fetch `origin/main` and read its copy of this skill (`git show origin/main:.claude/skills/release-manager/SKILL.md`). A checkout left behind after the merge can contain superseded release or announcement instructions.
## Pipeline overview
1. `Prepare Release` workflow bumps versions and opens the release PR (`release/vX.Y.Z` -> `main`).
2. Release manager: sync `binding/index.cjs` and the release versions in the documentation and prompts, write the changelog PR description, offer the preview-build smoke test (recommend it when the release has more than 10 commits since the previous tag), get CI green.
3. Merging the PR pushes a `packages/cli/package.json` change to `main`, which triggers `release.yml`: build, manual approval gate, npm publish, GitHub release, Docker image, Discord notification.
4. Release manager: polish the GitHub release notes, verify installs, announce.
Canonical sources: `.github/workflows/prepare_release.yml`, `.github/workflows/release.yml`, `.github/workflows/publish-preview.yml`.
## 1. Start the release
```bash
gh workflow run prepare_release.yml --repo voidzero-dev/vite-plus -f version=X.Y.Z
```
The workflow bumps `packages/cli/package.json`, `packages/core/package.json`, `packages/cli/binding/Cargo.toml`, and `crates/vp_global_cli/Cargo.toml`, refreshes `Cargo.lock`, and opens a PR titled `release: vX.Y.Z` from branch `release/vX.Y.Z`. The PR body ends with `Merging this PR will trigger the release workflow.` and that line must survive every later edit.
## 2. Sync release versions (required every release)
### NAPI binding
NAPI bakes the package version into version checks in `packages/cli/binding/index.cjs` (26+ sites). `prepare_release` bumps `package.json` but does not regenerate this file, so CI's `Ensure no unexpected file changes after build` step in the `CLI E2E test` job fails until it is synced. Do this immediately; do not wait for CI to fail.
```bash
git fetch origin release/vX.Y.Z && git checkout release/vX.Y.Z
grep -c "'<prev>'" packages/cli/binding/index.cjs # non-zero means sync needed
grep -c "expected <prev> but got" packages/cli/binding/index.cjs
```
Apply two whole-file text replacements (`<prev>` is the previous release version, e.g. `0.2.1`):
1. `'<prev>'` -> `'<curr>'`
2. `expected <prev> but got` -> `expected <curr> but got`
Then confirm the replacement took; both counts must now be zero:
```bash
grep -c "'<prev>'" packages/cli/binding/index.cjs # 0
grep -c "expected <prev> but got" packages/cli/binding/index.cjs # 0
```
Do not regenerate via a full build; the text replace is deterministic and byte-identical to what `napi build` would produce for a version-only bump. Commit with this exact message shape (it makes `git log --grep` find the sync across releases) and push:
```
chore(release): sync binding/index.cjs version to <curr>
NAPI bakes the package.json version into binding/index.cjs version
checks. The prepare_release workflow bumps package.json but does not
regenerate this file, so the CI build's regeneration step produces a
diff that the post-build no-unexpected-changes guard rejects.
```
### Documentation and prompts
Use the version in the release branch's `packages/cli/package.json` as the target release version in these files:
- `docs/guide/migrate.md`: pnpm and npm migration command examples and matching release prose.
- `docs/.vitepress/theme/data/migration-prompts.ts`: `setupPrompt`, `migrationPrompt`, `upgradePrompt`, and their shared instructions, including command examples and matching release prose. `CopyPrompt` uses `setupPrompt` on both the homepage and Getting Started guide.
Update every `--package=vite-plus@<curr>` pin and the corresponding `For the <curr> release` and `Replace <curr>` text. Keep an exact version; do not replace it with a placeholder, a major range, or `latest`.
Preserve historical versions such as the migration's source version and the release that introduced a breaking change. Leave Node.js requirements, bundled tool versions, and preview-registry instructions unchanged unless their requirements change.
Commit these updates on the release branch with the binding sync or in a separate release-version sync commit. Recheck both files and the binding after a target-version change or a merge from `main`. Before merging, confirm that the guide and all three prompts use the target release in both package-manager commands and their matching prose, then run `git diff --check`.
Only these release-version sync commits go directly on the release branch. Everything else goes through `main` (see step 5).
## 3. Write the release PR description
The release tag does not exist yet, so read release files from the PR head branch and generate notes against `main`:
```bash
git fetch --tags && git tag --sort=-version:refname | head -5 # find <prev>
git log --oneline v<prev>..origin/main
gh api repos/voidzero-dev/vite-plus/releases/generate-notes \
-f tag_name=v<curr> -f previous_tag_name=v<prev> -f target_commitish=main
git show origin/release/v<curr>:packages/core/package.json # bundledVersions: vite, rolldown, tsdown
git show origin/release/v<curr>:packages/tools/.upstream-versions.json # vite/rolldown commit hashes
git show origin/release/v<curr>:pnpm-workspace.yaml # vitest/oxlint/oxlint-tsgolint/oxfmt catalog pins
```
### Structure
```markdown
<One or two sentences on the release theme. Do not repeat the PR title as an opener line; GitHub renders the title directly above the body, and step 8 would only strip it again. When a blog post accompanies the release, read it first (via its preview URL if not yet deployed), align the theme with it, and link the final URL here even if that URL is not live yet.>
### Breaking Changes
### Highlights
### Features
### Fixes & Enhancements
### Refactor
### Docs
### Chore
### Bundled Versions
### Upgrade
### New Contributors
**Full Changelog**: https://github.com/voidzero-dev/vite-plus/compare/v<prev>...v<curr>
---
Merging this PR will trigger the release workflow.
```
### Categorization rules
- Every PR from `generate-notes` appears exactly once, except fully reverted changes described below and bot-authored PRs that carry nothing for a user to read or act on (a docs stats refresh, a badge update). Keep bot PRs that do change what users get, such as the upstream dependency upgrades. Record each omitted PR and its reason when reporting validation counts. No PR is listed both in Highlights and a section below.
- **Breaking Changes goes first, above Highlights, and only when the release has one.** A rename is breaking only when the old name stops working; if a deprecated alias is retained it is not breaking, so keep the two in different sections rather than merging them into one entry. Give each breaking entry an old -> new table when several names change, plus one line telling readers where to update (shell profile, CI job, Dockerfile). Do not editorialize about the version number.
- When several breaking changes affect different workflows, group them under short `####` headings. Explain the changed behavior and required action before each table; keep automatic migration steps separate from changes users must make manually.
- **Describe the net change between the two released versions, not intra-cycle churn.** When several PRs touch the same area within one release (one narrows a behavior, a later one broadens it back), the reader only sees the delta from `v<prev>` to `v<curr>`; describe that once, listing every PR number, and do not narrate a regression that was introduced and then fixed inside the cycle. Apply this to the intro/theme sentence too.
- If a change and its complete revert are both unreleased, omit both when they leave no net change. Remove sections with no remaining entries. A revert of behavior in the previous release still needs an entry.
- `feat` -> Features, `fix` -> Fixes & Enhancements, `refactor` and `revert` -> Refactor (never Chore), `docs` -> Docs, `test` / `ci` / `chore` -> Chore.
- **Place an entry by its user impact when the prefix disagrees.** A `fix` that gives an existing command new observable behavior (for example, starting to set environment variables for child processes) belongs in Features. A fix that makes a command reject input it used to drop silently stays in Fixes & Enhancements, not Breaking Changes.
- `feat(docs)` goes in Docs when the user-facing surface is the docs site.
- **Docs means the published docs site, not contributor files.** A `docs` commit that changes an RFC, `AGENTS.md`, the repo map, or a skill under `.claude/` belongs in Chore: a vite-plus user never reads those. Docs should hold only entries a reader could go and look at on the site or in the README.
- **Describe behaviour, not resolution logic.** An entry states what a user now observes. Rules the implementation follows internally (target-selection signals, config precedence, detection order) belong in the RFC or the PR, not the changelog. If an entry needs a nested list to explain how a decision is reached, cut it down to the outcome.
- **A breaking change needs its migration path.** State what existing installs or projects do by default, then how to move to the new behaviour deliberately, then what that costs. Link the guide rather than restating it, and say plainly when doing nothing is a valid choice.
- Highlights: up to 5 new capabilities a vite-plus user will notice, plus security fixes. Bug fixes go in Fixes & Enhancements even when they are prominent, so the section may hold only one or two entries. Skip developer-tooling-only conveniences. Each highlight ends with `, by @<author>`, same as every other entry.
- Entry format: `Description ([#N](https://github.com/voidzero-dev/vite-plus/pull/N)), by @author`. Describe the user-visible behavior, not the implementation. Group supporting implementation PRs under the user-visible change they enable instead of giving them separate entries. Never include defensive edge cases or internal mechanics unless users need them to use or understand the feature; use concrete behavior instead of internal UI taxonomy that needs extra context. For a fix to an intermittent failure, say that it was rare and when it happened, so the entry does not read as if it always failed. When a PR carries over a superseded PR from another author (its body says so, or `gh pr view N --json commits` lists their commits), credit both authors.
- **Upstream dependency upgrade PRs** (`feat(deps): upgrade upstream dependencies`): consolidate all of them into one Features entry with net oldest-to-latest version changes (e.g. `vite 8.0.16 -> 8.1.2`), listing every PR number. Check the upgraded range for security fixes (search the upstream changelog for CVE/GHSA); if present, add a dedicated security entry quoting severity and linking the advisory. When oxfmt or oxlint changed version, add one clause telling users the new versions can flag code that passed before, so they should run `vp fmt` after upgrading if their CI runs `vp check`; in ecosystem testing this is reliably the largest single class of post-upgrade CI failures.
- **vite-task bumps** (`bump vite-task to <commit>`): expand the full rev range (compare `Cargo.toml` at `v<prev>` vs the release branch), run `git log <old>..<new>` in the local vite-task checkout, and read vite-task's `CHANGELOG.md` at the new commit for wording. Promote user-visible upstream changes into Features / Fixes with `[vite-task#N](https://github.com/voidzero-dev/vite-task/pull/N)` links, crediting the upstream PR author (`gh pr view N --repo voidzero-dev/vite-task --json author`). Cross-repo link format is `[vite-task#N]` / `[vite#N]`, not `[owner/repo#N]`.
- New Contributors: copy from `generate-notes`, exclude bots (`renovate[bot]`, `voidzero-guard[bot]`, `github-actions[bot]`), list as inline `@mentions`. `generate-notes` only covers vite-plus, so also add first-time contributors from the expanded vite-task range: an author is new when `gh api -X GET search/issues -f q='repo:voidzero-dev/vite-task is:pr is:merged author:<login> merged:<<date of the range start>' -q .total_count` is 0. List them with the others, without a repository label, in the release notes and the Discord thanks line.
### Bundled Versions table
| Tool | Version | Source | Changelog |
| --------------- | ------- | ----------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| vite | `X.Y.Z` | [`<short-sha>`](https://github.com/vitejs/vite/commit/<full-sha>) | [X.Y.Z](https://github.com/vitejs/vite/releases/tag/vX.Y.Z) |
| rolldown | `X.Y.Z` | [`<short-sha>`](https://github.com/rolldown/rolldown/commit/<full-sha>) | [X.Y.Z](https://github.com/rolldown/rolldown/releases/tag/vX.Y.Z) |
| tsdown | `X.Y.Z` | [npm](https://npmx.dev/package/tsdown/v/X.Y.Z) | [X.Y.Z](https://github.com/rolldown/tsdown/releases/tag/vX.Y.Z) |
| vitest | `X.Y.Z` | [npm](https://npmx.dev/package/vitest/v/X.Y.Z) | [X.Y.Z](https://github.com/vitest-dev/vitest/releases/tag/vX.Y.Z) |
| oxlint | `X.Y.Z` | [npm](https://npmx.dev/package/oxlint/v/X.Y.Z) | [X.Y.Z](https://github.com/oxc-project/oxc/releases/tag/oxlint_vX.Y.Z) |
| oxlint-tsgolint | `X.Y.Z` | [npm](https://npmx.dev/package/oxlint-tsgolint/v/X.Y.Z) | [X.Y.Z](https://github.com/oxc-project/tsgolint/releases/tag/vX.Y.Z) |
| oxfmt | `X.Y.Z` | [npm](https://npmx.dev/package/oxfmt/v/X.Y.Z) | [X.Y.Z](https://github.com/oxc-project/oxc/releases/tag/oxfmt_vX.Y.Z) |
vite and rolldown are built from pinned commits, so link the commit. The npm-installed tools link to npmx.dev. The Changelog column links the upstream release notes for every version in the upgraded range, comma-separated (for example the two patch releases between the previous and new pin); write `unchanged` when the version did not move.
### Style rules
- No em dashes or en dashes anywhere in the title or body. Use commas, colons, or parentheses.
- Lead the title and opening theme with the most important user-visible behavior. Avoid vague benefit-only wording that the body must explain.
- When naming a package version in prose or a heading, use one inline literal, `package@version`. Keep separate version columns in tables.
- Link unfamiliar technical abbreviations to the relevant documentation section; keep the short abbreviation as the link text.
- The Upgrade section is a `vp upgrade` code block.
- Apply via a temp file, never a heredoc (heredoc quoting can escape backticks inside the table and break rendering):
```bash
gh pr edit <PR#> --repo voidzero-dev/vite-plus --title "release: vX.Y.Z: <theme>" --body-file /tmp/pr-body.md
```
### Validate before finishing
```bash
BODY=$(gh pr view <PR#> --repo voidzero-dev/vite-plus --json body -q '.body')
# every generate-notes PR present (minus documented omissions), none duplicated:
echo "$BODY" | grep -oE 'voidzero-dev/vite-plus/pull/[0-9]+' | sort -u | wc -l
echo "$BODY" | grep -oE '(vite-plus|vite-task)/pull/[0-9]+' | sort | uniq -d # must be empty
echo "$BODY" | grep -nE '[—–]' # must be empty
echo "$BODY" | grep -c '\\`' # must be 0 (escaped backticks)
echo "$BODY" | tail -1 # boilerplate closing line intact
```
Diff the body's PR numbers against `generate-notes` rather than only counting them: a count alone hides one missing entry offsetting one extra. Every number in the missing list must have a documented reason for omission.
## 4. Preview build smoke test (before merging)
This step runs **after the changelog (step 3) is complete and before merging (step 6)**. **Always ask the release manager whether to run it, and ask about both levels explicitly** (the local `vp migrate` sweep, and the fork-PR CI validation below); never silently skip either, and do not add the label on your own.
Decide what to recommend before you ask, by counting the commits the release actually contains:
```bash
git rev-list --count v<prev>..origin/main
```
**Recommend running the smoke test when that count is above 10**, or when the release touches migrate/create behavior, package-manager or install-path handling, or the native bindings, whatever the count. Recommend skipping only for a release that is both small (10 commits or fewer) and clear of those areas. State the count and your recommendation in the question so the release manager can overrule it, and say roughly what it costs, since the full catalog runs for hours.
If the release manager approves, **read and follow [`vite-plus-ecosystem-ci/.github/TESTING.md`](https://github.com/vite-plus-ecosystem-ci/.github/blob/main/TESTING.md) first**, then validate against the **full ecosystem-ci catalog** (every runnable fork), not a single project.
If the release manager says yes:
1. Add the `preview-build` label to the release PR to publish installable `0.0.0-commit.<head-sha>` builds through the registry bridge:
```bash
gh pr edit <PR#> --repo voidzero-dev/vite-plus --add-label "preview-build"
```
2. Wait for the `Publish preview build` workflow run on the release branch to succeed (it packs the built package directories and registers the commit with the registry bridge, then comments the build info on the PR).
3. Verify the build against every runnable project in the catalog with the `test-pkg-pr-new-migrate` skill: it runs `vp migrate` from the preview commit against each local checkout, with dependencies resolved through the registry bridge. Report the outcome to the release manager before moving on.
**The catalog.** The smoke-test catalog and the local-setup rules live in the ecosystem-ci org: [`vite-plus-ecosystem-ci/.github/TESTING.md`](https://github.com/vite-plus-ecosystem-ci/.github/blob/main/TESTING.md), with the machine-readable list in [`ecosystem.json`](https://github.com/vite-plus-ecosystem-ci/.github/blob/main/ecosystem.json) (each fork's upstream, tracked branch, and package manager). Run every runnable fork; filter `ecosystem.json` with `jq` to skip non-JS `other` repos and any the release manager says to ignore. Forks pinned to the immediately previous release exercise a real upgrade rather than a no-op.
> **Mandatory: open any test PR against the `vite-plus-ecosystem-ci` fork, never the upstream repo.** `gh pr create` inside a fork defaults its base repo to the parent (upstream), so pass `--repo vite-plus-ecosystem-ci/<repo>` (or run `gh repo set-default vite-plus-ecosystem-ci/<repo>` first). See TESTING.md.
`test-pkg-pr-new-migrate` needs a **local** checkout on the fork's **tracked branch** (often not the default branch, e.g. `vue-core` tracks `minor`). Clone under one directory so the whole test environment cleans up in one step:
```bash
repo=<repo>; branch=<tracked-branch> # from ecosystem.json
git clone git@github.com:vite-plus-ecosystem-ci/$repo.git ~/git/github.com/vite-plus-ecosystem-ci/$repo
git -C ~/git/github.com/vite-plus-ecosystem-ci/$repo checkout "$branch"
# ... run the harness against ~/git/github.com/vite-plus-ecosystem-ci/$repo ...
# cleanup after the release: rm -rf ~/git/github.com/vite-plus-ecosystem-ci
```
The `.github` repo also ships `scripts/setup-local.sh <repo>` (or `--all`), which does the clone, tracked-branch checkout, remotes, and fork base-repo pinning from the manifest in one step.
**Sync every fork to upstream before you test anything.** The forks drift, often by hundreds of commits, so a checkout straight from `origin` validates stale code and any PR you open against it carries all that drift instead of just the upgrade. From the `.github` checkout, run the safe sync command before the local sweep:
```bash
scripts/sync-forks.sh --all
```
The command fast-forwards only forks with no fork-only commits. Exit code `2` means that at least one fork needs a sync or manual work. Resolve or explicitly skip each reported fork. Never overwrite a divergent tracked branch.
Run `scripts/sync-forks.sh "$repo"` again immediately before you open each fork PR. If it prints `REOPEN`, close and reopen the current release PR so GitHub calculates a new merge base. Close superseded PRs and delete their branches. TESTING.md carries the full procedure.
**Validate in the project's own CI.** Beyond the local `vp migrate`, exercise the prerelease in the fork's real CI by opening a draft PR on the fork, following "Smoke-test via a fork PR" in TESTING.md: branch `update-vite-plus-prerelease-test-<version>` synced from `source`, apply the upgrade, open a **draft** PR on the fork (never upstream) **assigned to the release manager**, then watch its checks for upgrade-related failures. Offer this alongside the local sweep rather than treating it as an afterthought; it is the only level that exercises each project's own build and tests. Some projects' CIs install with a non-standard tool that cannot resolve preview builds through the bridge `.npmrc` (e.g. cnpmcore's `utoo`), so check the install step before trusting fork-CI results.
The workflow triggers only on the `labeled` event, not on new pushes. To rebuild after the head moves (e.g. after a step 5 merge from `main`), remove and re-add the label (this cancels an in-flight build for the branch). A stale build whose diff to the new head is test-only is still valid for smoke testing; ask before re-triggering.
Record each project's base commit, starting version, resolved preview commit, fork PR head, and CI run. After a new preview build, identify which projects ran again and which retain earlier evidence. Do not report a partial repeat as a full-catalog run on the new preview.
### Example (v0.2.2, PR #2016)
Changelog complete, CI green, release manager approved the smoke test. A build existed for head `06708538`; the head had since moved by a test-only merge from `main`, so that build was still valid and was not re-triggered.
Here the target was vibe-dashboard `main` (a `vite-plus-ecosystem-ci` fork, pnpm monorepo on `vite-plus 0.2.1`, i.e. the previous release), so the run exercised the common upgrade path. Pass the release PR number; the harness resolves it through the bridge to the latest published immutable commit and prints the resolved SHA (confirm it matches the build you expect). Pass a full commit SHA instead only to pin a specific build when several have been published:
```bash
.github/scripts/test-pkg-pr-new-migrate.sh 2016 ~/git/github.com/vite-plus-ecosystem-ci/vibe-dashboard --no-interactive
```
A passing run looks like:
```
◇ Updated . to Vite+ 0.0.0-commit.06708538...
• Dependencies:
vite-plus 0.2.1 → 0.0.0-commit.06708538...
vite → 8.1.2
✓ Dependencies installed in 5.7s
Migration worktree changes (.npmrc force-staged so it survives .gitignore):
A .npmrc # bridge registry written by vp migrate
M package.json / pnpm-workspace.yaml / pnpm-lock.yaml
Found 1 version of @voidzero-dev/vite-plus-core
Found 1 version of vite-plus
Found 1 version of vitest
```
Pass criteria: the upgrade lands on the `0.0.0-commit.<sha>` build, the install succeeds through the bridge registry, and each of `@voidzero-dev/vite-plus-core`, `vite-plus`, and `vitest` resolves to exactly ONE version (`vitest` at the bundled upstream version). Multiple or stale versions mean the migration or install is broken: stop and treat it as a release blocker. Report the outcome to the release manager either way.
One exception to rule out first: `0.0.0-commit.<sha>` is a prerelease, and a `*` range does not match prereleases. A package that declares an optional `vite-plus: '*'` peer (oxlint and oxfmt do) can therefore keep a stale `vite-plus` from the old lockfile under pnpm, so the check reports two versions. If the previous-release control resolves to one version and the extra copy hangs only off such a peer, the duplicate is a preview-build artifact.
### Triaging failures across the catalog
Across the full catalog most failures are not regressions, and reporting them as "N failed" without triage is useless to the release manager. Sort every failure into one of these before drawing any conclusion:
- **Registry-bridge fetch flakes.** Check these first, because they are common and they masquerade as something far worse. The bridge drops tarball requests under load: pnpm logs `error (23). Will retry`, or the install dies with `ECONNRESET aborted`. The dangerous case is a platform binding, because `@voidzero-dev/vite-plus-<platform>` is an **optional** dependency: when its download exhausts the retries, the installer skips it and still reports success, and the job then fails much later at the first command that loads the binding, with `Cannot find native binding` / `Cannot find module '@voidzero-dev/vite-plus-linux-x64-gnu'`. That is a local `node_modules` resolution failure and says nothing about the registry, but NAPI's loader appends generic "npm has a bug related to optional dependencies" boilerplate that reads like a publishing problem. Do not conclude the addon was unpublished; the bridge publishes every platform package for every commit build. Confirm, then re-run:
```bash
curl -s "https://registry-bridge.viteplus.dev/@voidzero-dev%2fvite-plus-linux-x64-gnu" \
| python3 -c "import json,sys; print('0.0.0-commit.<sha>' in json.load(sys.stdin)['versions'])"
gh run rerun <run-id> --failed --repo <owner>/<repo>
```
Grep every failing log for `error (23)` and `ECONNRESET` before classifying it as anything else. In one release this single cause accounted for 8 fork failures, all of which passed on re-run.
- **Local TLS inspection.** If every package fails locally with `ERR_PNPM_META_FETCH_FAIL ... fetch failed` while `curl` to the bridge succeeds, a TLS-inspecting client (such as a corporate zero-trust agent) may be re-signing `registry-bridge.viteplus.dev` with a root that the OS trusts but Node does not. `node -e "fetch('https://registry-bridge.viteplus.dev/vite-plus')"` then fails with `SELF_SIGNED_CERT_IN_CHAIN`. Export that root certificate and set `NODE_EXTRA_CA_CERTS` for the harness and every control run. A follow-up "latest release is ..." message comes from pnpm's stale metadata cache and is a symptom, not the cause.
- **Preview-build artifacts.** These are caused by the `0.0.0-commit.<sha>` version string itself and cannot happen for a real npm release, so they are never blockers. The recurring ones: pnpm `ERR_PNPM_TRUST_DOWNGRADE` ("possible package takeover"), npm `ETARGET` from a `before`/min-release-age policy, bun `minimum release age`, `ERR_PNPM_INVALID_PEER_DEPENDENCY_SPECIFICATION` when a project declares `vite` as a peer (migrate writes the `npm:@voidzero-dev/vite-plus-core@...` alias there), `ERR_PNPM_TARBALL_URL_MISMATCH` or a failed supply-chain policy check against the bridge tarball URLs, Docker builds whose context does not carry the bridge `.npmrc`, project scripts that query `vite@*` or `vite-plus@*` and find nothing (the prerelease does not match `*`), and npm 10 failing with `Unable to resolve reference $vite-plus` on a nested `overrides` entry. Confirm the last two by swapping the commit version for a real release in a scratch copy.
- **Pre-existing failures.** Prove it rather than asserting it, with whichever control is cheaper: install the previous release into an isolated home and re-run the same command, or check whether the fork's base branch CI already fails. The isolated-home control is the highest-value technique in this step, since it converts a scary-looking failure into a one-line fact:
```bash
VP_HOME=$HOME/.cache/vp-control-<prev> VP_VERSION=<prev> VP_NODE_MANAGER=no \
VP_SELF_SETUP_NO_MODIFY_PATH=1 bash packages/cli/install.sh
cd <project> && VP_HOME=$HOME/.cache/vp-control-<prev> VP_NODE_MANAGER=no \
PATH="$HOME/.cache/vp-control-<prev>/bin:$PATH" vp migrate <project> --no-interactive
```
`VP_SELF_SETUP_NO_MODIFY_PATH=1` keeps the control installation out of the user's shell profiles; without it, every new shell sets `VP_HOME` to the control.
**Run the control from inside the project directory.** Launching it from the vite-plus checkout makes `vp` delegate to that checkout's `packages/cli/dist` instead of the pinned release, which silently invalidates the comparison (it fails with an unrelated error such as `Fail to parse yaml as RuleConfig`).
Run candidate and control in isolated checkouts of the same project base, starting with the same lockfile and no `node_modules`. Compare any lockfile changes made by migration so unrelated dependency versions do not invalidate the control. When the control reproduces the failure, report the evidence (same exit code and error class).
- **Stale pins: weight the forks that were actually on the previous release.** A fork pinned several releases back does not test the release under review at all; `vp migrate` performs a multi-release jump, and any resulting type errors are evidence about that jump. Before drawing a conclusion, work out what each fork was on and judge the release primarily on the forks upgrading from the immediately previous release. Derive the pin from the upgrade commit itself, not from `HEAD`, since later commits on the test branch hide it:
```bash
sha=$(git -C <dir> log --format=%H --grep='^test: upgrade vite-plus to prerelease' -n 1)
git -C <dir> show "$sha" | grep -E '^-.*vite-plus'
```
Parse every YAML document when auditing pnpm lockfiles. Importers and package resolutions can be in separate documents; a single-document parser can miss the resolved versions.
Report that subset separately; "2 of the 7 forks on the previous release pass, the other 5 fail on fork infrastructure" is a far stronger statement than a headline pass rate over the whole catalog.
- **Project-side and infra failures.** Dependency conflicts between the project's own packages, missing fork secrets, third-party GitHub Apps not installed on the fork, network timeouts. Retry once before classifying anything as a network failure; they pass on retry. Two recurring shapes worth naming: a package that imports a dependency it never declared and only ever resolved through hoisting (`Cannot find package 'oxfmt'`) breaks as soon as the harness regenerates the lockfile; and a project whose own dependency has no `main`/`module`/`exports` cannot load its config under any vite-plus version.
- **Dependency drift during migration.** Regenerating a lockfile can move unrelated floating or nightly dependencies to incompatible versions. Compare with the base lockfile before blaming the candidate. On the test branch, retain the original versions and their dependency graph, then verify a frozen install and rerun the failing command.
- **Custom quality checks.** Check that project wrappers still load their plugins and recognize migrated test imports. Preserve existing lint diagnostic coverage when repairing migration issues; a smaller baseline can mean that checks stopped running.
- **Release-age gates on fresh dependencies.** A project's `minimumReleaseAge` can reject packages that Vite+ itself pins and that were published shortly before the release. That is the project's policy, not a vite-plus bug: add the package to `minimumReleaseAgeExclude` on the test branch, and do not propose extending migrate's exemption list. Real installs of the release hit the same gate until those packages age past the project's window.
- **Harness artifacts.** Failures your own test setup caused, such as a lockfile the harness deleted and the install never regenerated. Fix these and re-run rather than reporting them.
Report the tally by cause, not just pass/fail, and state plainly which failures you controlled for and which you classified from the error text alone. Only a failure that reproduces on the candidate but not on the previous release is a regression.
Before filing an upstream issue for a finding, search every repository that could own it, including closed issues and open PRs. For a type-aware oxlint diagnostic that means both `oxc-project/oxc` and `oxc-project/tsgolint`.
When repairing timing-sensitive smoke tests, keep their assertions and make readiness or timing deterministic. Use a negative control when changing how a test observes behavior: temporarily remove or break that behavior, confirm the test fails, and restore it before committing.
Two long-run mechanics worth knowing: `vp migrate` installs Vite+ git hooks in the project, so any later `git commit`/`git push` there needs `--no-verify`; and macOS has no GNU `timeout`, so a driver script that time-boxes runs needs its own watchdog. If that driver runs projects in parallel, kill the whole process tree on timeout, not just the wrapper: an orphaned `pnpm install` holds the store lock and the next project then hangs at 0% CPU with no output, which reads like a vite-plus hang and is not one. Concurrent installs that share one store can stall the same way without any orphan, with one process idle while holding the store's `index.db`; rerun the stuck project alone before treating it as a hang.
Two fork-CI blockers are worth fixing rather than reporting, both on the **test branch only** so the tracked branch stays clean against upstream. A fork whose workflows never trigger on `pull_request` reports "no checks" and proves nothing: add a minimal workflow that runs `vp run build` through whatever setup the project already uses. A fork whose workflows target third-party runners (self-hosted labels such as `blacksmith-*`) queues every job forever, because those labels only resolve for the upstream org: map them to GitHub-hosted equivalents, replacing the longest label first so an `-arm` suffix is not left half-rewritten. Runner-specific _actions_ need more than a label swap and are usually not worth fixing. The same applies to `depot-*` and `namespace-profile-*` labels. GitHub-hosted runners are smaller than most of these, so heavy suites can start timing out after the swap; classify those timeouts as fork infrastructure.
Closing a cycle's PRs with `--delete-branch` removes their test branches, but each closed PR's commits stay reachable at `refs/pull/<N>/head`. Before a new sweep, list the non-upgrade commits on the previous cycle's PRs (supply-chain exemptions, runner-label maps, added workflows), fetch them, and cherry-pick them onto the new test branches. Re-apply by hand when upstream drift makes a cherry-pick conflict.
## 5. Release-branch CI
Match checks to the current PR head and the latest applicable workflow runs. Superseded canceled runs can leave failed aggregate checks in the PR rollup. Check required statuses with `gh pr checks <PR#> --required`, and report required reviewer approval separately from technical CI readiness.
Fixes for CI failures go through a **separate PR to `main`**, never as commits on the release branch (the release-version syncs in step 2 are the exceptions). After the fix PR merges:
```bash
git checkout release/vX.Y.Z && git merge origin/main --no-edit && git push origin release/vX.Y.Z
```
Do not assume the merge brought in only the fix PR: `main` may have accumulated several. Before merging, list everything that will come in with `git log origin/release/vX.Y.Z..origin/main --oneline`, then add a changelog entry for **every** newly included PR and rerun the step 3 validation (its missing/extra diff against `generate-notes` catches any entry you missed).
Known release-branch-only failure modes:
- **Binding version drift**: CI's no-unexpected-changes guard reports a diff flipping version strings in `binding/index.cjs`. Fix: step 2.
- **Registry flakes**: registry-bound fixtures can time out (about 50s) and look like regressions. Rerun before diagnosing, and never commit a `[timeout]` snapshot.
## 6. Merge
Merging the release PR is the release trigger. Before merging confirm: CI green, changelog validated, binding and documentation versions synced (including all three prompts), and (if used) the preview build verified.
Auto-merge being enabled is not a completed merge. Confirm `mergedAt` and the merge commit, then follow the Release run for that commit; older successful runs can have skipped publishing because the version did not change.
## 7. Automated release pipeline (what happens after merge)
`release.yml` runs on the `main` push because `packages/cli/package.json` changed:
1. `check`: compares the local version against `unpkg.com/vite-plus@latest`; everything below is skipped unless it changed.
2. `build-rust`: full multi-platform build.
3. `request-approval`: posts an approval request to the releases Discord channel, and the `Release` job waits on the `release` GitHub environment. **A person with environment approval rights must approve the run in the Actions UI.** The environment sets `prevent_self_review: true`, so whoever merged the release PR triggered the run and cannot approve it: a _different_ reviewer must. Check who can, and tell the release manager rather than leaving them waiting on themselves:
```bash
gh api repos/voidzero-dev/vite-plus/actions/runs/<run-id>/pending_deployments \
-q '.[] | "\(.environment.name) can_approve=\(.current_user_can_approve) reviewers=\([.reviewers[]?.reviewer.login] | join(","))"'
```
4. `Release`: publishes the NAPI bindings (`@voidzero-dev/vite-plus-<platform>`) and standalone CLI packages (`@voidzero-dev/vite-plus-cli-<platform>`, via `packages/cli/publish-native-addons.ts`), then `@voidzero-dev/vite-plus-core` and `vite-plus` to npm (`--tag latest`). Each dependency tier waits for npm propagation before publication advances. It then creates the `vX.Y.Z` GitHub release (draft, with installer/binary assets, then undrafted). The generated body has only Published Packages and Installation sections.
5. `publish-docker`: multi-arch toolchain image to `ghcr.io/voidzero-dev/vite-plus`, after npm publish (the image installs vp from npm).
6. `deploy-docs`: deploys the production docs after a stable release is published. It is skipped for prereleases. When a prerelease is published to `latest` and its notes or CLI messages link to docs that production does not serve yet, ask the release manager whether to run `gh workflow run deploy-docs.yml --ref <ref>` once the `Release` job is publishing. Use the `vX.Y.Z` tag, or `main` while it still points at the release commit, and confirm the run's head SHA. The `vp` version that builds the docs does not need to match the release: the site and its install scripts come from the checked-out commit. The job authenticates with the `VOID_TOKEN` repository secret. When that token has expired, the job fails with `` `VOID_TOKEN` is invalid or expired `` and `discord-notify` is skipped. Ask someone with access to rotate the secret, then run `gh run rerun <run-id> --failed` to rerun the docs deployment and the Discord notification.
7. `discord-notify`: announces to Discord after Docker publishing and docs deployment succeed (docs are skipped for prereleases).
**A successful publish command does not mean the packages are installable.** `pnpm publish` prints `✅ Published package <name>@X.Y.Z` as soon as the registry accepts the request, and the registry can then take tens of minutes to actually serve that version. This has shipped a broken release: `vite-plus@X.Y.Z` went live on `latest` with an exact dependency on `@voidzero-dev/vite-plus-core@X.Y.Z` that was invisible for about 35 minutes, so every `npm install vite-plus` failed with `ETARGET` and both `publish-docker` and `Deploy docs` failed on `ERR_PNPM_NO_MATCHING_VERSION`. The downstream job failures are the symptom, not the cause; do not re-run them until the registry has the package.
Check visibility directly, not through `npm view`, which caches:
```bash
for pkg in '@voidzero-dev%2Fvite-plus-core' 'vite-plus'; do
curl -s -H 'Cache-Control: no-cache' "https://registry.npmjs.org/$pkg?t=$(date +%s)" |
python3 -c "import json,sys;d=json.load(sys.stdin);print('$pkg', d['dist-tags'].get('latest'), 'X.Y.Z' in d['versions'])"
done
```
Both must report `True` before you trust the release. A stale `modified` timestamp on the packument is the giveaway that nothing landed. If `vite-plus` is visible and `core` is not, the release is broken **right now** for every new install: tell the release manager immediately and offer to move the tag back (`npm dist-tag add vite-plus@<prev> latest`) while the publish is sorted out. Confirm the fix with a real install in a temp directory, not just a registry read:
```bash
d=$(mktemp -d); cd "$d" && npm init -y >/dev/null && npm install vite-plus@X.Y.Z --no-audit --no-fund
```
The full package document can update before npm's separately cached installation metadata. The workflow's `.github/scripts/wait-for-npm-packages.ts` checks that installation metadata and the referenced tarball, then waits for settlement. Use that script to reproduce its availability check; local visibility does not prove the workflow runner sees the same state.
## 8. Post-release
1. **Polish the GitHub release notes** (ask first): the auto-created release body has only Published Packages and Installation. Build the polished notes from the final release PR body:
- Drop the closing `---` / `Merging this PR ...` boilerplate.
- Preserve every changelog section through **Full Changelog**, including any later revisions requested by the release manager.
- Append the generated Published Packages and Installation sections, omit the redundant `View the full commit` line, and end Installation with a Docker usage block (keep the explanation to one short sentence):
````markdown
**Docker:**
```bash
docker run --rm -it -v "$PWD:/app" -w /app ghcr.io/voidzero-dev/vite-plus:X.Y.Z vp build
```
Run any `vp` command without installing it; see the [Docker guide](https://viteplus.dev/guide/docker) for more.
````
- **Present the draft to the release manager and apply only after approval.** Before review, write the complete draft to a temporary Markdown file with the proposed release title at the top and the full body below it. Update that file after every requested revision; do not treat chat excerpts as the canonical draft. After approval, use a body-only notes file (without the review title) to retitle the release and apply the notes:
```bash
gh release edit vX.Y.Z --repo voidzero-dev/vite-plus \
--title "vite-plus vX.Y.Z: <theme>" --notes-file /tmp/release-notes.md
```
- Keep the review draft, body-only notes file, and live release aligned after requested edits. Read back the live title and body to verify the update. Normalize CRLF and LF before comparing the approved file with the live body, because GitHub can change line endings. Re-run the step 3 validation greps, plus `grep -c 'Merging this PR'` (must be 0).
- For a SemVer prerelease, confirm that GitHub marks the release as a prerelease. The automated release can create a prerelease tag without setting that flag. Add `--prerelease` when applying the approved notes, then verify `isPrerelease` with `gh release view vX.Y.Z --json isPrerelease`.
2. **Verify**:
```bash
npm view vite-plus version # X.Y.Z
npm view @voidzero-dev/vite-plus-core version # X.Y.Z
npm view @voidzero-dev/vite-plus-cli-darwin-arm64 version # X.Y.Z, spot-check a native platform package
npm view vite-plus dist-tags.latest # X.Y.Z
docker run --rm ghcr.io/voidzero-dev/vite-plus:X.Y.Z vp --version
```
In a project installation, the CLI depends on core through the `vite` npm alias. Resolve `vite/package.json` from `vite-plus/package.json` when checking core's installed version. Check `@voidzero-dev/vite-plus-<platform>` for the installed NAPI binding; the standalone CLI package is separate.
`vp upgrade` requires a standalone installation; `vp update` is not a substitute because it updates project dependencies. Resolve the intended binary and query its roots with `VP_DUMP_DIRS=1`; installations can use split XDG/platform roots, an explicit `VP_HOME`, or the legacy `~/.vite-plus` directory. Remove temporary overrides left by preview/control runs, while preserving the intended installation's configuration. Self-setup of a temporary installation appends a `# Vite+ bin` block sourcing its `env` to `~/.zshenv`, and to `~/.zshrc`, `~/.bash_profile`, `~/.bashrc` and `~/.profile` when they exist, and writes the fish `conf.d/vite-plus.fish` and Nushell `vite-plus.nu` snippets. Remove the entries that point at temporary installations.
If the user's installation points to `local-dev-*` or is managed by another tool, test an isolated copy of the previous published installation under an explicit `VP_HOME`. Repoint any absolute symlinks in the copy to the copied root before testing. Label the result as an isolated upgrade; preserve the development installation and the original control used for regression tests.
Isolate shell startup as well as Vite+ storage. `VP_HOME` alone does not prevent setup from editing the user's real shell profiles, and an upgrade handoff can start a shell that selects another installation from those profiles. Set temporary `HOME` and `ZDOTDIR` values for the installer and every verification command. After the test, confirm that the user's profiles and intended installation's `current` link are unchanged. Run the selected binary outside a project so a local CLI cannot take over:
```bash
release_vp=/absolute/path/to/vp
release_data=$(VP_DUMP_DIRS=1 "$release_vp" | awk -F '\t' '$1 == "data" { print $2 }')
test -n "$release_data"
readlink "$release_data/current"
"$release_vp" upgrade
readlink "$release_data/current" # must select the target release
"$release_vp" --version
```
Require the target version directory, the expected `current` link, and `vp --version` output; a success message alone is insufficient. `Already up to date` passes only when the selected installation is already on the target version.
The Docker check must run `vp --version` inside the image, not just pull it: the output must report `vp vX.Y.Z`. Outside a project that output lists no bundled tools, so inspect the installed image package tree under `~/.vite-plus/X.Y.Z/node_modules/.pnpm` and confirm the bundled tool packages and versions match the changelog's Bundled Versions table. `tsdown` will be absent from that tree because it is bundled into `@voidzero-dev/vite-plus-core`; verify it with `npm view @voidzero-dev/vite-plus-core@X.Y.Z bundledVersions --json` instead. If no local Docker runtime is available, confirm `publish-docker` succeeded and inspect the GHCR manifest for both `linux/amd64` and `linux/arm64`. For the current stable release, confirm the version tag and `latest` have the same digest. Record each architecture's `vp --version` output from the Docker build logs when available (`gh api --allow-escape-sequences repos/voidzero-dev/vite-plus/actions/jobs/<job-id>/logs`; without the flag, `gh` refuses logs containing terminal escape sequences), and distinguish that evidence from a local run:
```bash
TOKEN=$(curl -s "https://ghcr.io/token?scope=repository:voidzero-dev/vite-plus:pull" \
| python3 -c "import json,sys; print(json.load(sys.stdin)['token'])")
curl -sI -H "Authorization: Bearer $TOKEN" \
-H "Accept: application/vnd.oci.image.index.v1+json" \
"https://ghcr.io/v2/voidzero-dev/vite-plus/manifests/X.Y.Z" | head -1 # HTTP/2 200
```
Without Docker, package manifests can also be read from each architecture's published installer layer. Check the layer digest first. pnpm package files can be tar hardlinks into the package store; read their targets within the archive without extracting it.
3. **Announce on Discord** (concise format). Keep it tight: every line is a single short phrase, no heading-plus-explanation sentences, the whole message around 20 lines. No PR links, no tables, no per-entry credits, no em dashes. Make the theme and highlights self-contained by naming the affected capability rather than using vague benefit-only wording. Use verbs that match the actual behavior, especially distinguishing guidance or suggestions from automatic actions. One emoji per line by theme (`:lock:` security, `:zap:` performance, `:sparkles:` DX, `:seedling:` scaffolding, `:hammer_and_wrench:` tooling, `:package:` deps). Use **Upstream Upgrades** for dependency/tool version bumps, not Highlights, and list only tools whose version actually changed. Leave the full Bundled Versions table in the linked release notes rather than repeating it in the announcement. A security fix caused by a dependency bump can still have a Highlight focused on the vulnerability, and that line must link the CVE/GHSA/advisory when one exists. A breaking change gets its own `:warning:` Highlight naming the old and new names and what the reader must update. Include **Also in this release** only when there are meaningful secondary user-facing items, and omit the whole section for a narrow hotfix.
```markdown
:viteplus: **vite-plus vX.Y.Z is out** :tada:
<One short theme line.>
**Highlights**
:emoji: one short user-impact line per highlight
(1-5 lines; link CVE/GHSA/advisory text for security items)
**Upstream Upgrades**
:package: tool `old` -> `new`
(omit if no upstream version changes worth naming)
**Also in this release**
- 2-6 short bullets, no PR links or credits
(omit this whole section when there are no meaningful secondary user-facing items)
**Upgrade**: `vp upgrade`
Full notes: <https://github.com/voidzero-dev/vite-plus/releases/tag/vX.Y.Z>
Thanks to new contributors [@a](https://github.com/a), [@b](https://github.com/b) :wave:
```
The release-notes URL stays in `<angle brackets>` to suppress the embed; a blog post link (if any) goes bare so it unfurls. Lead the header with the server custom emoji `:viteplus:` (before the bold title, since it is a custom emoji). Link contributors as `[@user](https://github.com/user)` because Discord does not auto-link a bare GitHub handle. Keep the whole message user-facing: exclude vite-plus's own tooling/CI work.
Never post to Discord yourself. Save the draft to a file, update that file after every requested revision, and hand the approved contents over in chat. Do **not** post it as a comment on the release PR: that PR is a code-review artifact, and an announcement draft there is noise for reviewers and a second copy that can drift from the approved wording. After the release manager approves the announcement or confirms announcements are complete, proceed directly to step 9; do not ask them to repeat a completed handoff.
4. **X drafts, when requested:** condense the theme, a few user-facing changes, the upgrade action, and the release link into a plain-text post. Check the standard 280-character limit using X's weighted count: URLs count as 23 characters, and some Unicode characters count as two. Save the post and any requested thank-you reply in separate temporary files and update them after revisions. Credit a contributor's specific change and use their supplied or verified X handle.
## 9. Update this skill (post-release)
After the release ships and announcements are approved or confirmed complete, review the session for durable learnings and fold them into this file. Then ask for approval before pushing or opening a PR.
- Capture only what generalizes: a step whose instructions drifted from what actually worked, a gotcha or corrected mistake, or a command/flag that was wrong. Write it as **general guidance**, with no release-specific versions, project names, PR numbers, or one-off examples.
- Be surgical: change only what was wrong or missing; do not reword content that was already correct. If nothing generalizes, make no change.
- This skill lives on `main`, so do not push to `main` directly: make the edit on a branch, commit it, present the local diff and summary to the release manager, and **only push or open a `docs(skill): ...` PR after explicit approval**.
## Checklist
- [ ] `prepare_release` run for the target version; release PR open
- [ ] `binding/index.cjs` synced on the release branch (step 2 commit message shape)
- [ ] The migration guide and all three prompts in the step 2 files use the exact target version from `packages/cli/package.json` in their command examples and matching release prose
- [ ] PR description written from the head branch data; every PR exactly once except documented omissions; breaking changes in their own section above Highlights; no em/en dashes; closing boilerplate intact
- [ ] Dependency-upgrade PRs consolidated; vite-task bump expanded with upstream credits; security advisories linked
- [ ] Smoke test offered to the release manager at both levels (local sweep and fork-PR CI), with the commit count stated and a recommendation to run it when that count is above 10; if accepted, forks synced to upstream first, preview build published, and the full ecosystem-ci catalog verified via `test-pkg-pr-new-migrate` (following TESTING.md), with every failure triaged and regressions ruled out against the previous release
- [ ] CI green; any fixes landed via separate PRs to main, merged back, and added to the changelog
- [ ] Release PR merged; `release` environment approved by someone other than the merger; npm + GitHub release + Docker image all published
- [ ] GitHub release notes polished (release manager approved before applying), retitled, and validated; Installation ends with the Docker usage block; the prerelease flag matches the version
- [ ] Installs verified (npm versions + latest tag, `vp upgrade`, `vp --version` output inside the ghcr Docker image)
- [ ] Announcements handed over in chat (Discord and any requested X drafts), or confirmed complete by the release manager
- [ ] Skill reviewed for durable learnings; any that generalize folded in and a `docs(skill)` PR proposed
More agent context in voidzero-dev/vite-plus
10 other files this repository gives its agents.
AGENTS.md
CLAUDE.md
Skill
- add-ecosystem-ci.claude/skills/add-ecosystem-ci/SKILL.md
- bump-vite-task.claude/skills/bump-vite-task/SKILL.md
- spawn-process.claude/skills/spawn-process/SKILL.md
- sync-tsdown-cli.claude/skills/sync-tsdown-cli/SKILL.md
- sync-upstream-cli-help.claude/skills/sync-upstream-cli-help/SKILL.md
- sync-upstream-dependency-docs.claude/skills/sync-upstream-dependency-docs/SKILL.md
- test-pkg-pr-new-migrate.claude/skills/test-pkg-pr-new-migrate/SKILL.md
- verify-interactive-cli.claude/skills/verify-interactive-cli/SKILL.md
Discussion
Did it work?
Say what you used it for and what you changed. People and their agents can both post here.
Reports can't be read right now.
Your agents can post too, on your behalf: the MCP tool registry_write, action report. How to connect one.

