Skip to content
mdlake

Upgrade and migrate

Move a site to a newer builder plugin with upgrade-ruleset and npx mdlake migrate. Covers the behind notice, each step, the migrations, the 0.20.0 folder order change for docs sites, the migrate flags and the CI check.

On this page

A site records the ruleset version it started from, in .mdlake/ruleset.json. The ruleset version is the same as the plugin version. When a newer plugin is installed, the site is behind it. The upgrade-ruleset skill brings the site forward as a reviewable change. Most of the work is mechanical, because the engine is the vendored @mdlake/site-kit and not code in your site.

What you do

  1. Update the plugin.
  2. Run /mdlake-builder:upgrade-ruleset on a branch.
  3. Read what changed and what was kept.
  4. Run npm run check and commit.

Step 1: update the plugin

Installed plugins only update when their version changes. From inside Claude Code:

/plugin marketplace update mdlake
/reload-plugins

mdlake is the marketplace name from the install command in Install the builder. A marketplace added from a local path is read in place, so run git pull in that repository first.

How you find out a site is behind

Claude Code prints one line at the start of a session in a site folder when the site’s ruleset is behind the installed plugin:

This site uses ruleset 0.7.1; the plugin is 0.8.1 (2 versions behind). Run /mdlake-builder:upgrade-ruleset.

Nothing is printed when the site is current. npm run check prints the same line as a warning, and so does check-lake.

For CI, fail the build when a site is too far behind:

PowerShell
npm run check:ruleset -- --max-behind 1
Exit codeMeaning
0Current, or behind by no more than the limit.
1More than N minor versions behind. Patch releases never count.
2The plugin was not found, so the check could not run.

In CI the plugin is not installed. Point MDLAKE_PLUGIN_ROOT at a checkout of builder/plugin. Without a plugin the check says so and exits 2, so CI never passes unchecked. --verbose lists every plugin copy it found. Claude Code’s own records can be stale, so the highest version wins, and MDLAKE_PLUGIN_ROOT overrides all of them.

Step 2: run the upgrade

Create a branch, then run the skill.

/mdlake-builder:upgrade-ruleset

It works in this order and commits after each step.

  1. Read the starting point. rulesetVersion in .mdlake/ruleset.json. If the file is missing, the site is treated as unversioned. The skill reads ruleset/CHANGELOG.md in the plugin. The entries newer than the site’s version are the migration notes.
  2. Swap the tarballs. vendor-packages.mjs <site> replaces the files in vendor/ and points package.json at them. Then npm install. For a site with a Node backend it also rewrites api/package.json to the vendored tarballs (file:../vendor/...) and the api entry of package-lock.json. Commit both. The skill notes the old and the new versions of @mdlake/lake-loader and @mdlake/site-kit.
  3. Migrate. npx mdlake migrate --check shows the plan. Then npx mdlake migrate runs it. See below.
  4. Record the versions. scaffold-ruleset.mjs <site> rewrites .mdlake/ruleset.json and adds the .mdlake/usage-* lines to .gitignore when they are missing. It never writes design.md. It refuses when design.md is a draft or missing. To mark an existing one approved, run check-design.mjs <site> --approve. The skill also replaces the ruleset section of the site’s CLAUDE.md with the current one and keeps the site’s own rules.
  5. Run the checks. npm run check. Most upgrades end here.
  6. Folder order. For a docs site coming from before 0.20.0, the skill lists the sections that change position and stops. See Upgrading to 0.20.0.
  7. Button overrides. If site.config.ts registers hero or cta only to change button colour or font, the skill offers to replace them with theme tokens. See Buttons.
  8. Handle what is left. Kept (modified) engine files and custom overrides that fail astro check against the new kit. For each, the skill shows the difference to the kit’s component and offers to move it to a site.config.ts override, port it or drop it. It stops for approval before changing a locked file.
  9. Report. What changed, what was deleted, what was kept and why, and what needs manual follow-up.

Note

The upgrade never overwrites site-owned files, adopted files or locked files. Files that the migration cannot write are reported with the change they need. See Ownership zones.

npx mdlake migrate

mdlake migrate runs the data-driven migrations between the site’s rulesetVersion and the kit’s version. A migration is a codemod on config files, such as the new astro.config.mjs, content.config.ts and package.json scripts. Run it in the site folder.

OptionMeaning
--checkLists what would change and writes nothing.
--from-templateFor a site scaffolded before 0.9.0. Removes copied engine files that are unmodified.
--trust-generatedRecords generated files as they are, without comparing them with plan.md. See below.
--buttonsTurns button-colour overrides into theme tokens. See Buttons.
--dir <site>The site folder. The default is the current folder.

It prints notes grouped by kind, then a summary line:

KindMeaning
removedAn unmodified copy was replaced by the kit.
changedA codemod edited the file.
addedA file or record was added.
overrideA modified file is now registered as an override in site.config.ts.
refusedThe file is adopted or locked and was not written. The note says what would have changed.
manualA person (or Claude) must look at it.
keptLeft alone.

When it has run, the last line says Next: npm install, npm run check.

The migrations

VersionIdWhat it does
0.9.0engine-to-site-kitRemoves the copied engine (components, lib, layout, pages, scripts, config glue) in favour of @mdlake/site-kit. Keeps and registers what was modified. Runs only with --from-template.
0.14.0managed-lakeAdds remote to lake.lock from the clone that LAKE_PATH points at, keeps a refresh-content preview in dev, and ignores .mdlake/lake.
0.16.0site-ownershipRecords the ownership zones in provenance.json. Adds the ignore lines, the type reference in src/env.d.ts and the check:ownership script.
0.17.0site-hostingAdds check:hosting to the check script. The site stays static.
0.19.1check-preparePuts mdlake prepare && in front of the check script, only when the script is made of the kit’s own steps (npm run check:ruleset, check:ownership, check:hosting, check:api, check:types, build, check:bindings, check:a11y, joined by &&, starting with check:ruleset). A script you changed is left alone, and the migration prints the line to use.
0.19.1default-ctaFor a site with a plan.md brief whose primaryCta has a target, adds site.cta to the defineLake call of lake.config.ts, when that call is what plan-to-config wrote. An adopted, locked or hand-edited lake.config.ts is left alone and the line to add is printed. It also says how many end-of-page buttons change their label. See Site types.

Only the migrations between the site’s version and the kit’s version run. Releases 0.19.2 and 0.20.0 have no migration. Their changes reach an existing site through upgrade-ruleset and the things you do by hand (below). If the site is already at the kit’s version, none runs and the command says Site at X: no migrations up to kit X.

Upgrading to 0.19.2

Release 0.19.2 has no new migration. Three things can need your attention:

  1. lake.lock needs the full commit hash. A lock with an abbreviated commit now fails with “Expected the full 40-character git commit hash”. Run node "<plugin>/scripts/write-lake-lock.mjs" <site> --commit <hash>, which writes the full hash (the managed clone in .mdlake/lake must have the commit), or run refresh-content.
  2. Hosting files are regenerated. Run npx mdlake hosting init --ci ... again. A server site gets the managed certificate resources and infra/modules/hostnameSni.bicep. A static or split site gets the customDomainType parameter. Existing infra/parameters/*.bicepparam files need no change, because the default is auto. Add param customDomainType = 'apex' only for a domain under a two-part suffix such as acme.co.uk. See Custom domains.
  3. A Node backend depends on the vendored tarballs. upgrade-ruleset step 2 handles it; commit api/package.json and the lockfile.

Upgrading to 0.20.0

Release 0.20.0 has no migration. It changes how documentation sites order their sidebar, and it needs nothing for other sites.

What changes. Before 0.20.0 a section in the sidebar took the lowest order of any page inside it. From 0.20.0 every folder has its own number, in a _folder.yaml inside it, and pages and sections are sorted together by their own numbers. A section without a _folder.yaml has no number and sorts after the numbered items of its folder, by name. A docs site that relied on the old behaviour therefore shows its sections in a different order, with no error. The rule is in Site types and the file in Lake format.

Collections with sort: order do not change. Neither do sites without a docs page.

What to do for a docs site.

  1. Run upgrade-ruleset. Step 2 swaps the vendored packages. A site that is not a docs site is done.
  2. At step 6 the skill runs ordering-changes.mjs. You can also run it yourself, in the site folder:
PowerShell
node "<plugin>/scripts/ordering-changes.mjs" .

It reads the lake the site uses (--lake <folder> to name another clone), finds the docs folders from src/pages.config.ts and lake.config.ts (--root <lake folder> names one, and can be repeated), and builds the sidebar both ways. It prints every folder whose items come out in another sequence, and the _folder.yaml that keeps the old place. --json prints the same as data. It writes nothing. For example:

Docs/ changes order:
  before: Guides > Reference > Introduction
  after:  Introduction > Guides > Reference
  keeps the old place: Docs/Guides/_folder.yaml  order: 1
  keeps the old place: Docs/Reference/_folder.yaml  order: 3
The lake is not changed here. Editors keep the old order by dragging each folder once in the CMS; or add the files above to the lake.

No output about a folder means nothing moved.

  1. Decide with the editors. The lake belongs to them, and the skill never writes to it. There are two ways to keep the old order:
    • In the CMS, an editor drags each listed folder to its old place once (Reorder documents and folders). The CMS writes the numbers into the lake, 10 apart, and publishes them as a Reorder commit.
    • For a lake kept by hand, add each listed _folder.yaml with the order shown.
  2. Refresh the site’s lock so the build sees the new commit (Refresh content).

Until then the site builds with the new order, the sections after the numbered pages. That is not a failure, so npm run check stays green.

Two smaller points:

  • A CMS older than the one that sends folder numbers shows every folder as unnumbered in a preview build. Published builds read _folder.yaml from the lake and are not affected.
  • Hand-written code that calls buildDocsTree or implements Provider passes folders: lake.folders. Without it, sections sort by name.

A _folder.yaml that is not valid YAML, has an order that is not a number, or sits at the root of the lake is a lake issue, and npm run check stops on it.

A site older than 0.9.0

Sites from before 0.9.0 hold a copy of the engine in src/lib, src/components, src/pages and scripts/. Use:

PowerShell
npx mdlake migrate --from-template

It compares every engine file with the hashes of every known template version, normalising line endings and the byte order mark.

  • Unmodified files are deleted, because the kit provides them.
  • Modified components, layout and pages are kept and registered in site.config.ts as overrides, and reported for review. A modified astro.config.mjs, content.config.ts or an old check script is kept and reported as manual.
  • Locked files in provenance.json are never touched.

Do not edit engine files by hand to “fix” a migration. astro check shows where an override no longer fits the kit.

The ownership migration

The 0.16.0 migration overwrites nothing. It records which files the kit wrote.

  • Kit files (the tarballs, .mdlake/ruleset.json, lake.lock, builder.json) are recorded as they are.
  • Generated copy whose outputHash is valid is recorded as generated.
  • lake.config.ts and src/pages.config.ts are recorded as generated when they match what plan.md would render.
  • Generated files that differ, or that cannot be compared because plan.md is missing, are reported. Each report names npx mdlake adopt <file> as the way to keep it. Do not run adopt without asking the developer.

npx mdlake migrate --trust-generated records them as they are instead. It works on its own, whether or not a migration is pending, because it is a statement about files and not about versions. It prints what it recorded, or that there was nothing. Files that are hand-edited or adopted are not touched. On a provenance.json from before 0.16 that the ownership migration has not seen yet, it builds the zones first.

upgrade-ruleset settles unrecorded generated files with you before it writes the new ruleset version (step 4 above): adopt a file, trust all of them, or regenerate. It never picks for you. npx mdlake check-ownership should print no “is not recorded” warning when you are done.

Finish by building the site, running npx mdlake snapshot and committing site-snapshot.json.

Two details of the ownership records are worth knowing. A kit or generated file with an entry in provenance.json but no hash counts as unrecorded: npx mdlake ownership lists it and check-ownership warns about it. And a plugin script that writes a file, such as vendor-packages.mjs or write-lake-lock.mjs, leaves an old provenance.json (from before 0.16) exactly as it is until the migration has run. See Ownership zones.

Buttons

Buttons use the tokens --color-action, --color-action-ink, --color-action-hover and --font-action. Links keep --color-accent. A site that wanted buttons in another colour used to override the hero and cta modules. migrate --buttons replaces such an override with tokens, when the override only changed colour or font.

  1. Build the current look: npx astro build --outDir ../before-buttons.
  2. Show the plan: npx mdlake migrate --buttons --check.
  3. Apply it on approval: npx mdlake migrate --buttons.
  4. Build the new look: npx astro build --outDir ../after-buttons.
  5. Compare: npx mdlake compare-buttons ../before-buttons ../after-buttons.

compare-buttons compares computed colours and fonts at rest and on hover, in a browser. Keep the change only when it prints “Buttons look the same”. Otherwise run git checkout . and keep the overrides. Overrides that change anything beyond button colour and font stay, and the command says why.

What changed in recent versions

VersionChange
0.20.0Folders have their own order in _folder.yaml. A docs sidebar sorts pages and sections together, and a section no longer takes its position from the pages inside it.
0.19.0The docs site type, order frontmatter, code highlighting with a copy button, callouts and static search.
0.18.0A separate backend (Node or .NET) for a split site, linked to Static Web Apps.
0.17.0Server code, the forms module, hosting tiers, pipelines and Bicep.
0.16.0Ownership zones, typed content access and the route snapshot.
0.15.0One layout and spacing system, case-study layout, end-of-page block, Lucide icons.
0.14.0The kit manages the lake clone. lake.lock records the remote. CI can build the site.
0.13.0Sidecars carry the public asset url. PUBLIC_ASSET_BASE_URL became optional.
0.12.0A built site serves all assets from its own domain.
0.11.0AI usage reporting from Claude Code sessions.
0.10.0The CMS read API and the lake MCP server.
0.9.0The engine moved into the vendored @mdlake/site-kit.

The full list, with migration notes for each version, is ruleset/CHANGELOG.md in the plugin.

What can go wrong

SymptomCause and fix
npm install cannot find ../../packages/lake-loader or site-kitThe site still points at the monorepo path. Run vendor-packages.mjs from the plugin folder and install again. claude plugin list shows the folder as “Read from”.
scaffold-ruleset.mjs refusesdesign.md is a draft or missing. Approve it with check-design.mjs <site> --approve.
migrate lists refused notesThe files are adopted or locked. Apply the shown change by hand, or let the file stay.
npm run check fails after the upgradeRead the first failing step. astro check errors in an override mean the override no longer fits the kit’s component.
A detail page’s call-to-action button shows the target document’s title instead of the brief’s labelThe site has no site.cta in lake.config.ts, and the page has no end.cta. Run npx mdlake migrate: the default-cta migration adds site.cta from the brief. Or set end: { cta: { label, href } } on the page. See Site types. After the migration the button reads the brief’s label (for example “Get in touch”) where it read the page title (“Contact”) before.

What’s next