Migrate a repository
Move an existing library to the current standard in one pull request, in an order that never breaks a release - agent steps, settings, the maintainer's npm step, the first release and cleanup, with every known edge case.
A migration is one pull request plus a few settings. An agent does almost all of it. The maintainer does the npm steps in step 3 below and approves the first publish. Follow the order below; most failed migrations come from a step done too early or too late.
Before you start
Run the audit and read every FAIL before you change anything:
pnpm audit:repos OWNER/REPOThen answer these questions from the repository and npm. The answers change the steps below.
| Question | How to check | Why it matters |
|---|---|---|
| Which workflow publishes today? | The workflow files that run npm publish, and the trusted publisher in each package's settings on npmjs.com | The trusted publisher must name release.yml before the first new release. An NPM_TOKEN secret must be deleted. |
Is the version on main already on npm? | npm view @lupinum/PACKAGE@VERSION version for every package | release.yml offers to publish every version on main that npm does not have, right after the merge. |
| Is the library in a prerelease? | npm view @lupinum/PACKAGE dist-tags, .changeset/pre.json | The prerelease line must continue, not jump to a stable or an older version. |
Does Changesets exist, and which format is pre.json? | A changesets array in pre.json is the Changesets 2 format | Changesets 3 ignores that list and repeats every released change in the next changelog. |
Are the packages in packages/* or the root? | pnpm-workspace.yaml | release.yml and the changeset lint only look there, and release.yml cannot be changed. |
| How do the packages depend on each other? | dependencies and peerDependencies that name a package of the same repository | A fixed range such as workspace:^0.1.0 changes when versions bump, so the Version packages pull request fails its frozen install. Use workspace:^. |
| Does the library need a docs site? | Its size and who reads it (Write library docs) | Without a site, delete the docs parts of the starter instead of copying them. |
1. The migration pull request (agent)
Generate the matching starter into a temporary directory with the library's real name, package names, repository and domain, and copy from it. Do not edit the old workflows into shape.
Copy the files the standard owns (FILE-10):
release.yml,preview.yml,scripts/release.mjs,scripts/lint-changesets.mjs,scripts/audit-deps.mjs,scripts/agent-docs.mjs(only with a docs site),.changeset/config.json,.github/renovate.json,.claude/CLAUDE.md,.github/ISSUE_TEMPLATE/,.github/pull_request_template.mdand the quarantine block ofpnpm-workspace.yaml. Also take the starter's ESLint config unless yours already lints the TypeScript sources.- Keep action pins that are newer than the starter's; the audit ignores pins.
- If a formatter checks
scripts/, exclude the copied scripts from it. ci.ymldoes not have to match: it must pass FILE-02. Copy the starter's, or keep yours, for example onecijob that runspnpm verify, or extra jobs that the finalcijob needs.
- Delete what the standard replaces: other release, publish, preview,
CodeQL and policy workflows,
MAINTAINING.md,dependabot.yml, release ledgers, certification and verifier scripts, skill folders that repeat the docs, and the package scripts only they used. Aim for about 20 package scripts and 1,500 lines inscripts/(budget). - Use the shared layout (Overview):
CONTRIBUTING.md,SECURITY.mdandrenovate.jsonmove to.github/,CLAUDE.mdto.claude/CLAUDE.md(it imports../AGENTS.md),DECISIONS.mdtointernals/decisions.md. Merge other notes such asARCHITECTURE.mdintoAGENTS.md, the docs orinternals/, and delete configuration that no tool reads. - Set up versioning. Without Changesets, add
.changeset/from the starter; the existingCHANGELOG.mdstays and new sections go on top. Eachpackage.jsonversion onmainmust equal the newest version on npm, or the merge offers that version for publication. - Keep the prerelease line. For a library in a prerelease, run
pnpm changeset pre enter TAGwith its current tag (for examplebeta). Ifpre.jsonlists released changesets in achangesetsarray, move those files into.changeset/pre/and reducepre.jsontomodeandtag. - Ship the docs for agents (DOC-03, only with a docs site): add the
./agent-docsexport to every public package, the Agent setup section to every package README, andnode scripts/agent-docs.mjsafter the docs build inpnpm build. A build that uses unbuild or the Nuxt Module Builder warns thatdist/agent/AGENTS.mdis missing, because the docs are written after it; copy thebuild:donehook from thenuxt-modulestarter'sbuild.config.ts. A packed-package test must pack the output ofpnpm build, not rebuild without the docs. - Link the Discord at
https://discord.lupinum.com, as in the starter: the badge in the README header, the README's support section, and one line at the end of the docs Get started page. Replace older invite links. - Keep generated changelogs out of the formatter. If the formatter checks
Markdown, ignore
**/CHANGELOG.md; the version job runs no formatter. - Write
AGENTS.md(commands, hard rules, library principles) andinternals/decisions.md:D1for the adoption, and one entry per kept exception that names the check as(ID: key), for example(FILE-08: lean). The audit prints the key in brackets. Rewrite older bare(ID)exceptions: they work only for an item with a single check. A migration that changes no published code needs no changeset. One that removes an export or afilesentry needs one, although the changeset lint does not ask for it. - Check, in this order:
node scripts/audit.mjs --local .until no item fails,pnpm verify, andpnpm changeset versionin a scratch copy. The scratch run must produce the expected next version, and its changelog section must list only unreleased changes.
2. Settings (agent with gh, before the merge)
Apply everything in GitHub settings
except the required check: auto-merge off, Actions token read-only and allowed
to create pull requests, Dependabot security updates off, the npm
environment without administrator bypass and for main only, the tag
ruleset, and CodeQL default setup. Read each setting back.
The tag ruleset covers all tags (~ALL). Replace an older tag ruleset that
lists prefixes such as refs/tags/v*.
Change the required check of the main ruleset to ci last, right before the
merge: the old checks no longer report, and the new ci does not exist on
main until the merge.
3. npm (maintainer)
Do this right before the merge, also for a package whose first version was published by hand: npm drops a new trusted-publisher connection that is not used for a publish within about two days.
For every package, on npmjs.com under Settings:
- Trusted publishing: repository
OWNER/REPO, workflowrelease.yml, environmentnpm, allowed to publish directly. Remove the old record forpublish.ymlonce the first release throughrelease.ymlworked. - Publishing access: require two-factor authentication and disallow tokens.
Record the date as NPM-01 evidence in fleet/libraries.json. Without this
step the first publish fails with E403; nothing is published, and a re-run
after the fix succeeds.
4. Merge and the first release
- Merge the migration pull request.
- Cancel release or publish runs of the old workflows that still wait for an approval: Actions → the run → Cancel. A waiting run holds the publish slot and blocks the new one.
- With changesets pending,
release.ymlopens the Version packages pull request. Itscirun can wait for approval ("action_required"), because a workflow pushed the branch. Approve that run under Actions; it only runs tests and is not the npm approval. - Read the whole Version packages diff: versions, the changelog sections and
.changeset/. Merge it. - Approve the deployment to
npm. Check the result:npm view @lupinum/PACKAGE dist-tags, provenance on the new version, the Git tag and the GitHub release. The first publish through a new trusted publisher can take half an hour to show on npm ("Your package is being processed");npm viewanswersE404until then. Wait; do not publish again.
5. Clean up
- Delete Actions secrets the old setup used:
NPM_TOKENfirst, then deployment tokens such asVERCEL_TOKEN. The standard needs none. - Delete merged and abandoned branches; close pull requests that only touch deleted files.
- Check that Renovate's Dependency Dashboard issue exists (GH-08).
- Add the repository to
fleet/libraries.jsonif it is new there, and runpnpm audit:repos OWNER/REPOuntil it answers "Follows the standard: yes". Advice that is left is fine; fix it when it is cheap.
Edge cases
| Situation | Do this |
|---|---|
A public package lives outside packages/* (for example layer/) | Move it to packages/. release.yml must match the starter (FILE-10), so it cannot learn another path. |
| Packages version independently | Leave them out of a fixed group; release.yml then tags name@X.Y.Z per package. The tag ruleset on all tags covers them. |
The version on main was bumped but never published | Either publish it through the new flow on purpose, or set main back to the published version in the migration pull request. |
| The library is in a prerelease but never used Changesets | pnpm changeset pre enter TAG with the current tag continues the line: 1.0.0-beta.4 becomes 1.0.0-beta.5 for any bump. Check it in the scratch copy. |
Changesets 2 (@changesets/cli below 3) | Update to the starter's version in the migration pull request, then move released prerelease changesets into .changeset/pre/ (step 5). |
changelog: false in .changeset/config.json, or changelogen | Use the starter's config.json; remove changelogen. Keep the existing CHANGELOG.md. |
A prepack, prepublishOnly or postpack script builds, checks or blocks publishing | Remove it. release.yml builds before it packs, pnpm pack would run the script again, and the publish job uses --ignore-scripts. |
The old publish workflow runs on workflow_run or on a push | Delete it in the migration pull request, and after the merge cancel any run it still started. |
| Scheduled workflows (dependency canaries, drift checks, nightly CI) | Delete them, or keep one with a reason in internals/decisions.md, for example (FILE-01: upstream-drift.yml). Renovate and the weekly routine replace them. |
latest points at an old version and next at the prerelease | Expected until the first stable release. The audit warns; nothing to do. |
| The newest published version has no provenance and is not the first version | NPM-02 fails until the next release through release.yml. Release a patch, or record an exception in internals/decisions.md until then. |
| The docs build needs a secret or a token | Move it to the Vercel project's environment variables. CI and Actions need none. |
pnpm audit fails on advisories in the docs site or dev tools | Replace it with node scripts/audit-deps.mjs from the starter: only advisories in the published packages' production dependencies block. See Dependencies. |
docs:build calls pnpm build | Remove that call. In the starter, build runs docs:build; the old direction builds forever. A Nuxt module whose docs read the root .nuxt/tsconfig.json keeps nuxt-module-build prepare in docs:build. |
| Tests need the built package | Build before the tests in test, for example nuxt-module-build build && vitest run. |
| A committed file contains the package version (a report, a snapshot) | Remove the version from it. The Version packages pull request changes the version, so its ci fails on the stale file. |
| The library leaves its prerelease | pnpm changeset pre exit in a pull request, then merge it. release.yml opens the Version packages pull request with the stable versions. |
A custom release.yml guard or step | Drop it. Behavior that a library needs goes into scripts/release.mjs with an exception (FILE-10: release.mjs). |
| Code review bots comment on the migration pull request | Every review thread must be resolved before the merge. Answer each one with the fix or the reason, then resolve it. |