Skip to main content

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:

bash
pnpm audit:repos OWNER/REPO

Then answer these questions from the repository and npm. The answers change the steps below.

QuestionHow to checkWhy it matters
Which workflow publishes today?The workflow files that run npm publish, and the trusted publisher in each package's settings on npmjs.comThe 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 packagerelease.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.jsonThe 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 formatChangesets 3 ignores that list and repeats every released change in the next changelog.
Are the packages in packages/* or the root?pnpm-workspace.yamlrelease.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 repositoryA 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.

  1. 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.md and the quarantine block of pnpm-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.yml does not have to match: it must pass FILE-02. Copy the starter's, or keep yours, for example one ci job that runs pnpm verify, or extra jobs that the final ci job needs.
  2. 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 in scripts/ (budget).
  3. Use the shared layout (Overview): CONTRIBUTING.md, SECURITY.md and renovate.json move to .github/, CLAUDE.md to .claude/CLAUDE.md (it imports ../AGENTS.md), DECISIONS.md to internals/decisions.md. Merge other notes such as ARCHITECTURE.md into AGENTS.md, the docs or internals/, and delete configuration that no tool reads.
  4. Set up versioning. Without Changesets, add .changeset/ from the starter; the existing CHANGELOG.md stays and new sections go on top. Each package.json version on main must equal the newest version on npm, or the merge offers that version for publication.
  5. Keep the prerelease line. For a library in a prerelease, run pnpm changeset pre enter TAG with its current tag (for example beta). If pre.json lists released changesets in a changesets array, move those files into .changeset/pre/ and reduce pre.json to mode and tag.
  6. Ship the docs for agents (DOC-03, only with a docs site): add the ./agent-docs export to every public package, the Agent setup section to every package README, and node scripts/agent-docs.mjs after the docs build in pnpm build. A build that uses unbuild or the Nuxt Module Builder warns that dist/agent/AGENTS.md is missing, because the docs are written after it; copy the build:done hook from the nuxt-module starter's build.config.ts. A packed-package test must pack the output of pnpm build, not rebuild without the docs.
  7. 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.
  8. Keep generated changelogs out of the formatter. If the formatter checks Markdown, ignore **/CHANGELOG.md; the version job runs no formatter.
  9. Write AGENTS.md (commands, hard rules, library principles) and internals/decisions.md: D1 for 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 a files entry needs one, although the changeset lint does not ask for it.
  10. Check, in this order: node scripts/audit.mjs --local . until no item fails, pnpm verify, and pnpm changeset version in 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, workflow release.yml, environment npm, allowed to publish directly. Remove the old record for publish.yml once the first release through release.yml worked.
  • 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

  1. Merge the migration pull request.
  2. 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.
  3. With changesets pending, release.yml opens the Version packages pull request. Its ci run 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.
  4. Read the whole Version packages diff: versions, the changelog sections and .changeset/. Merge it.
  5. 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 view answers E404 until then. Wait; do not publish again.

5. Clean up

  • Delete Actions secrets the old setup used: NPM_TOKEN first, then deployment tokens such as VERCEL_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.json if it is new there, and run pnpm audit:repos OWNER/REPO until it answers "Follows the standard: yes". Advice that is left is fine; fix it when it is cheap.

Edge cases

SituationDo 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 independentlyLeave 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 publishedEither 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 Changesetspnpm 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 changelogenUse the starter's config.json; remove changelogen. Keep the existing CHANGELOG.md.
A prepack, prepublishOnly or postpack script builds, checks or blocks publishingRemove 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 pushDelete 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 prereleaseExpected until the first stable release. The audit warns; nothing to do.
The newest published version has no provenance and is not the first versionNPM-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 tokenMove it to the Vercel project's environment variables. CI and Actions need none.
pnpm audit fails on advisories in the docs site or dev toolsReplace 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 buildRemove 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 packageBuild 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 prereleasepnpm 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 stepDrop 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 requestEvery review thread must be resolved before the merge. Answer each one with the fix or the reason, then resolve it.