Versioning and releases
Merlon Docs uses Semantic Versioning 2.0.0 and Changesets for predictable releases. Each npm package and each Agent Skill versions independently — a changeset only bumps the items it lists.
| Item | Identifier | Published to |
|---|---|---|
| CLI | @merlon-docs/merlon-docs-cli |
npm + GitHub Release |
| Core | @merlon-docs/merlon-docs-core |
npm + GitHub Release |
| Presets | @merlon-docs/merlon-docs-presets |
npm + GitHub Release |
| Agent Skill | @merlon-docs/skill-<id> |
GitHub Release only (private carrier under packages/skill-<id>/) |
The CLI’s newest final release is the repository’s Latest release on GitHub. The release job creates every other final release with --latest=false, so a release that doesn’t bump the CLI leaves the badge where it is, and it marks each prerelease as a prerelease.
Independent versioning is configured in .changeset/config.json (fixed is empty). Dependents of a bumped workspace package still get a patch internal dependency update (updateInternalDependencies).
Branches
Section titled “Branches”| Branch | Role | Who merges into it |
|---|---|---|
develop |
Integration trunk. Work lands here once it passes the gate | claude/** and land/** pushes, topic PRs and Dependabot PRs |
main |
Protected release branch. npx skills add installs from it |
Release PRs from develop, and hotfix/* branches |
hotfix/* |
An urgent fix cut from main that can’t wait for the next release |
Merged to main; the sync job carries it to develop |
Landing on develop
Section titled “Landing on develop”Pushing to a branch named claude/** or land/** runs the land workflow. It merges the current develop into the pushed commit and runs the same gate as CI’s Check and Changeset jobs on the result. When the gate passes, it pushes that tested merge to develop. When the branch changes anything under formal/ or the formal-check scripts, the gate also runs pnpm formal:check, as CI’s Formal models job does. There is no PR to open, review or merge.
- Evidence lives with the work. Put what was measured or verified in the commit message, or in a research record for larger results. The run summary lists the commits each landing brought in.
- A failed gate lands nothing. Fix the branch and push again, and do the same when it conflicts with
develop. Only one run waits behind the running one, so a newer landing or sync cancels the waiting run. When that happens, or whendevelopmoved during the run, re-run all jobs of the workflow, or push a new commit to the branch. Afterdevelopmoves, re-running only the failed job reuses the olddevelopand fails again. Pushing a branch whose tip hasn’t changed doesn’t start a run. - Landings run one at a time, and each one pushes only the tree that passed its gate. A clean sync from
maingoes through the same gate. Other paths movedevelopwithout the gate. A merged PR, from Dependabot or from the sync whenmainconflicts, does unless a branch rule makes PRs intodevelopup to date, and a maintainer can push directly unless a rule blocks it. When a PR’s base changes frommaintodevelop, CI runs again againstdevelop. Formal models shows which guards close each of those paths. - The gate doesn’t wait for scans. Gitleaks scans every
claude/**andland/**push, in parallel with the land run, and every pull request that isn’t a draft. A pull request whose head commit already passed its push scan, as a landing’s commit does on the release PR, passes without scanning it again. The landing is pushed with the workflow’s ownGITHUB_TOKEN, and GitHub doesn’t start workflows for such pushes. The scan’s push trigger lists onlyclaude/**andland/**, so the release commit’s push tomainand a push to ahotfix/*branch aren’t scanned until they reach a pull request. TheScancheck thatmain’s branch rule requires runs on the pull request. CodeQL and Zizmor first see landed work on the release PR. A finding from any of the three is fixed forward ondevelop. - Stakeholder review happens on topic PRs. A push to
claude/**orland/**lands without review, so put a change that someone should review on a topic branch instead, as Reviewing a change says.
Dependabot still opens PRs against develop, because its changes come from outside the project.
Reviewing a change
Section titled “Reviewing a change”Give a reviewer one concern at a time. Push the change to a topic branch, any name outside claude/** and land/**, open a draft PR into develop, mark it ready when the work is done, and squash-merge it after review. The reviewer sees only that change: its commits, its Files changed tab and its discussion.
The release PR from develop to main then bundles changes that were already reviewed. Review it on its Files changed tab. Its Commits tab lists the commits since the last release, because the sync resets develop to main after each release.
Pull requests
Section titled “Pull requests”Open a pull request as a draft, and mark it ready once, when its work is done. CI, CodeQL, Zizmor and the gitleaks scan skip a draft, so a push to a draft runs only the Release source check. Marking the PR ready runs them, and each push after that runs them again. Editing a ready PR’s title or body doesn’t rerun CI’s jobs when its latest run on that head passed. When a squash merge puts a PR’s tested tree on main, CI’s run on that push skips its jobs too. The release commit has no PR, so CI tests it. GitHub won’t merge a draft, so the skipped runs can’t let one merge. Moving a ready PR back to draft and marking it ready again repeats the full run, so leave a PR in draft until it is ready. Dependabot opens its PRs as ready, and CI runs on them at once.
CI doesn’t assume that a contributor ran the git hooks. The pre-push hook runs the land gate’s checks before a push, which catches a failure before CI spends a run on it.
main stays the repository’s default branch because skills.sh installs from it, so GitHub proposes main as the base of a new PR. The Release source check fails any PR into main whose head is not develop or hotfix/*, and blocks the merge where main’s branch rule requires it. It runs from its own workflow, which runs only on PRs into main, including a PR whose base changes to main.
Release schedule (release PR from develop to main)
Section titled “Release schedule (release PR from develop to main)”There is no calendar cadence and no Version Packages PR. A release is a PR from develop to main, followed by one CI job:
- Changesets reach
developwith the landings they describe (When to add a changeset). The release workflow versions and publishes only after a release PR reachesmain. - When
developis tested and ready, confirm that the pending.changeset/*.mdfiles list only the packages and skills that should bump. Then open a PR fromdeveloptomainand squash it, the only merge method the repository allows.mainthen holds one commit per release, and the sync resetsdevelopto it, as step 4 says, so each release PR lists only the commits since the last release. Review it on its Files changed tab, as Reviewing a change says. - The merge runs the release workflow (
pnpm release:main), which needs theRELEASE_GITHUB_TOKENsecret described in Publishing. The Release plan job posts the pending changesets (and any missing GitHub Releases) to the run summary. Read that summary on the latest run to review the bumps and release notes, then approve thereleaseenvironment there. The version job resets to the currentorigin/maintip (so post-approval work includes all merged changesets), then sequentially: applies changesets → syncs skillmetadata.version→ runspnpm skill:validate→ builds → runspnpm typecheckandpnpm test→ commitschore: release→ pushes tomain→ runs the dependency auditpnpm audit --audit-level=high→ publishes public packages to npm → pushes tags → creates GitHub Releases, the skill’s included. - Every push to
mainruns the sync workflow. After a squashed release,developusually holds exactlymain’s files, and the sync resetsdeveloptomain. That drops the old history fromdevelop, so the next release PR lists only new commits. It pushes with a lease on thedevelopcommit it compared, so a landing in between makes the reset fail instead of disappearing; run Sync develop again. Whendevelophas commitsmainlacks, the sync mergesmaininto it instead, and the rest of this step applies. A clean merge is pushed to theland/sync-mainbranch, and the sync dispatches the land workflow on it. The merge goes todeveloponly after it passes the gate. If the gate fails,developdoesn’t getmain’s commits until someone fixes the failure. They can mergemaininto aland/**branch other thanland/sync-mainand fix it there, or land the fix ondevelopand run Sync develop again. The next sync overwritesland/sync-main, so a fix pushed there is lost. A conflict opens a PR frommaintodevelopinstead. Resolve it the same way, by mergingmaininto aland/**branch other thanland/sync-main. The PR itself can’t take the fix, because its head is the protectedmain. Oncedevelopcontainsmain, the PR closes as merged. - Check that each bumped item, skill carriers included, has a GitHub Release and that npm shows the new version of each public package. Then confirm that the sync reset
developtomain, or, when it merged instead, that the land workflow’s run onland/sync-mainput the release commit ondevelop. A landing queued after it can cancel it while it waits, and it fails whendevelopmoves during the run. In either case, re-run all of its jobs or run Sync develop again. - When a workflow change reached
developafter the previous sync, check that the sync’s push toland/sync-mainwent through. The sync pushes withGITHUB_TOKEN, which can’t be given theworkflowspermission, and its force-push includes every workflow changedevelopgained since that sync. If the push was refused, resetland/sync-maintodevelopwith your own access and run Sync develop again. Its push then includes only whatmainadds.
Latest main wins: concurrency does not cancel an in-flight publish (cancel-in-progress: false). The Release plan job cancels other Release runs on main that are still waiting (env approval) or queued. If main moves during the short version window, pnpm release:main aborts the push with a superseded message (no force-push).
On a run without pending changesets, the Release plan detects missing name@version git tags and GitHub Releases, and the release job heals them without bumping versions again (tag + gh release create … --target at the commit that last changed that package’s package.json). Heal restores tags and GitHub Releases, not npm packages. The audit runs after the push to main. When it fails on a new advisory, main holds versions that npm never received. Dry run and manual fallback says how to publish them.
Skills are under skills/, the directory that npx skills add installs from. Each skill versions through a private carrier package in packages/skill-<id>/, which holds its package.json and CHANGELOG. Keep both files out of skills/, because every install would copy them into agent context. pnpm release:main syncs the carrier version into skills/<id>/SKILL.md metadata.version, so never edit that field by hand.
Pre-1.0 policy (0.x.y)
Section titled “Pre-1.0 policy (0.x.y)”Packages and skills are pre-1.0 while on 0.x.y. Until an item reaches 1.0.0, that item has no API stability guarantee. On 0.x.y, a breaking change is a minor bump. A major changeset takes an item to 1.0.0, and from then on a breaking change needs a major. A 1.0.0-draft.N prerelease has no stability guarantee either.
| Bump | When |
|---|---|
| patch | Bug fixes, internal refactors with no intended API change |
| minor | New capabilities, or a breaking change on 0.x.y |
| major | The move to 1.0.0, then any breaking change from 1.0.0 onward |
Prereleases
Section titled “Prereleases”While .changeset/pre.json says "mode": "pre", the repository is in Changesets prerelease mode. Each release then versions the items it bumps as prereleases of the line’s tag, such as 1.0.0-draft.N for the draft line, publishes them under that npm dist-tag, and marks their GitHub Releases as prereleases. changeset version moves each changeset it consumes into .changeset/pre/; leave the files there, because the final release reads them to write each item’s CHANGELOG entry. The changeset status check counts a moved changeset as consumed. pnpm release:build refuses a prerelease version.
pnpm changeset pre exit ends a line by setting "mode": "exit". The next release then versions each item the line drafted at its final version, publishes it under latest, and deletes .changeset/pre/ and pre.json. The draft line ended this way at 1.0.0, so every package and skill carrier releases as 1.0.0. pnpm changeset pre enter <tag> starts a new prerelease line.
Durable docs vs pending changesets
Section titled “Durable docs vs pending changesets”Pending .changeset/*.md files are temporary. Point consumers at package CHANGELOGs under packages/*/ or at GitHub Releases, never at pending changesets. pnpm docs:check fails on a link from a Markdown file under docs/ to anything under .changeset/ other than README.md and config.json. Its first step, pnpm docs:lint:changeset-links, runs that check.
A notice to consumers about removed or breaking behavior goes in the changeset. The release turns it into a package CHANGELOG entry.
When to add a changeset
Section titled “When to add a changeset”Run pnpm changeset when a change touches a published item:
- any file under
packages/merlon-docs-core/→@merlon-docs/merlon-docs-core - any file under
packages/merlon-docs-cli/→@merlon-docs/merlon-docs-cli - any file under
packages/merlon-docs-presets/→@merlon-docs/merlon-docs-presets - any file under
skills/<id>/orpackages/skill-<id>/→@merlon-docs/skill-<id>
Every file in those paths counts, tests and compiled READMEs included, so a shard edit that changes packages/merlon-docs-cli/README.md or packages/merlon-docs-core/README.md needs a changeset for that package. The skill carriers count although they are private, because .changeset/config.json sets privatePackages.version. A change that edits only devDependencies in a package’s package.json is exempt. packages/merlon-docs-site/ is never published, and Changesets ignores it.
The land gate and CI on pull requests both run pnpm changeset:status. For a package path, the status check fails unless the branch adds or edits a changeset. For skills/<id>/ it fails only when no changeset is pending, so add one for the skill even when develop already has others. The status check passes any branch that consumed a changeset, and then it doesn’t check the branch’s own package changes either. A branch consumed one when its diff from the base deletes a changeset, as release and sync diffs do, or when its commits since the base include a release commit that deleted one. That release commit counts even when the changeset was never on the base, which happens to a sync when the hotfix sync before it didn’t land.
Dependabot
Section titled “Dependabot”| Dependabot PR type | Changeset / merge gate |
|---|---|
Runtime deps in packages/*/package.json |
Human approval + patch changeset (added by bot) |
devDependencies only, or GitHub Actions |
No changeset |
The Dependabot changeset workflow writes the patch changeset for you. When a Dependabot PR changes dependencies, peerDependencies or optionalDependencies in a packages/*/package.json, it commits .changeset/dependabot-pr-<n>.md to the PR branch, and CI re-runs on that commit. The workflow lives in .github/workflows/dependabot-changeset.yml and runs scripts/dependabot-changeset.mjs. It runs on pull_request_target from the base branch and reads the PR’s manifests through the API as data; it never checks out or runs the PR’s code.
The push uses the DEPENDABOT_CHANGESET_TOKEN secret that Dependabot changeset token sets up, so that the commit starts CI.
Once the workflow has committed to a PR, Dependabot stops rebasing it on its own. Comment @dependabot rebase to refresh it; the rebase drops the changeset commit and the workflow adds it again.