# Release

> Release markuplint — branch promotion, version bump (lerna fixed mode), tag push, publish workflow monitoring, npm-state verification, GitHub Release notes, and X post drafting. Use when asked to release, publish, or cut a new version.

- Skill: `markuplint/release` (Agent Skill)
- Install (CLI): `npx skillmds@latest add markuplint/release`
- Raw SKILL.md: https://api.skillmd.com/api/skills/markuplint/release/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Productivity
- Author: markuplint (https://skillmd.com/u/markuplint)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/markuplint/release

---


# Preconditions

- **Fixed versioning**: all packages share one version (`lerna.json` `version`).
- **The release branch is not the development branch.** See [Branch model](#branch-model) — getting this wrong is the single most common mistake in this repository.
- Pushing a `v*` tag triggers `.github/workflows/publish.yml`, which runs `lerna publish from-git` with a dist-tag derived from the version string (`-alpha.*` → `alpha`, `-beta.*` → `beta`, `-rc.*` → `rc`, no prerelease suffix → `latest`) via npm OIDC Trusted Publishing.
- **Publishing cannot be undone.** Confirm with the user at each gate.
- The VS Code extension ships separately (`yarn vscode:package` to verify the VSIX build locally, then `yarn vscode:release`) and is out of scope here.

# Branch model

| Release type | Cut from | How work gets there |
| --- | --- | --- |
| **Stable** (`X.Y.Z`) | `main` | Merge `dev` (or the prerelease branch) into `main` first, then run `yarn release` on `main` |
| **Prerelease** (`-rc.N` / `-beta.N` / `-alpha.N`) | A dedicated branch, e.g. `v5-rc`, `v5-alpha` | Branch off `dev`, run `yarn release:rc` there, then merge that branch back into `dev` |
| Development | `dev` (v5 line), `v4` (v4 maintenance) | — never released from directly |

Verify the claim rather than trusting this table if anything looks off:

```bash
git log --oneline main --grep="chore(release)"   # stable releases live on main
git log --oneline v5-rc --grep="chore(release)"  # rc.0–rc.4 live on v5-rc
```

Three consequences that matter:

- **`main` looking "stale" is normal.** It carries the last released version until the next release. Merging into `main` *is* the release act, not a chore someone forgot.
- **Never run `yarn release` on `dev`.** `.husky/pre-commit` rejects every commit on `dev`, and Lerna only passes `--no-verify` when `commitHooks` is `false` (it defaults to `true`), so the version commit is refused. `main` and the prerelease branches are not guarded.
- **Prereleases are rare** — the v5 cycle is the only one so far. Keep using a dedicated branch for them; the v6 line will need the same.

## What the `main` merge switches over

For a stable release, merging into `main` flips four user-facing surfaces at once. There is no way to stage them separately, so treat the merge as the point of no return:

| Surface | Wiring |
| --- | --- |
| npm packages | the `v*` tag push fires `publish.yml` |
| markuplint.dev (production docs) | `website/config.js` — no `NEXT_VERSION` ⇒ production URL, editUrl `main` |
| JSON Schema | `config.schema.json` and every rule `schema.json` `$ref` a `raw.githubusercontent.com/.../main/...` URL |
| VS Code completion | `vscode/package.json` — `markuplint.defaultConfig` `$ref` and `jsonValidation[0].url` point at that same schema |

Website changes therefore reach production in the same step. Confirm with the user that the website is in a shippable state before merging.

# Steps

## 1. Determine the release type and target branch

Ask the user for the release type if it is not already clear, then pick the branch from [Branch model](#branch-model). Everything below writes `<release-branch>` for that branch.

`git status` must be clean. Anything dirty: report and wait for instructions.

```bash
git fetch origin
git checkout <release-branch>
git pull origin <release-branch>
```

For a **stable** release, merge the source branch in first and let the user review the diff before continuing:

```bash
git merge origin/dev          # or the prerelease branch, e.g. origin/v5-rc
```

For a **prerelease**, create the branch from `dev` if it does not exist yet (`git checkout -b v6-rc origin/dev`).

## 2. Unmerged PRs

```bash
gh pr list --base dev --state open
```

Present anything that looks release-relevant; confirm whether to continue. (PRs target `dev`, not the release branch.)

## 3. Pre-checks

`yarn lint-check`, `yarn build`, `yarn test` must all pass **in this session, on the release branch after the merge**. This is the only real gate:

- `.github/workflows/test.yml` also triggers on `push` to `dev` (added alongside this skill's rewrite), so `gh run list --branch dev --workflow=test.yml` is real evidence for `dev`. It is not evidence for `main` or a prerelease branch — those still need this session's own `yarn lint-check && yarn build && yarn test`.
- Yarn 4 does not run arbitrary pre/post lifecycle hooks, so the root `prerelease` script (build + test) is NOT executed by any `yarn release*` variant.
- `publish.yml` does not run tests either — it installs, builds, and publishes.

If the release touches any rule the benchmark covers, also run `yarn bench:xref --audit` (see the `bench-xref` skill).

## 4. Present release contents

```bash
git describe --tags --abbrev=0
git log --oneline <last-tag>..HEAD
```

Show the current version (`lerna.json`). `yarn release` derives the next version from conventional commits automatically, so do **NOT** ask the user to choose a release type — present the diff only as a "what's going in" check.

## 5. Version bump (user-executed)

`lerna version` is an interactive command (selection/confirmation prompts) that cannot be driven through the `!` prefix — the prompt renders but accepts no input. Ask the user to:

1. Exit the Claude Code session (`exit`)
2. Confirm they are on `<release-branch>`, then run the release command directly in the terminal and answer the prompts
3. Return to this conversation with `claude --continue`

```
yarn release          # graduate (stable) — run on main
```

For a prerelease, on the dedicated branch:

```
yarn release:rc       # RC prerelease
yarn release:beta     # beta prerelease
yarn release:alpha    # alpha prerelease
```

Notes:

- Graduating from a prerelease: `--conventional-graduate` targets every package carrying a prerelease id, and `semver.inc` returns the same base version for major, minor and patch alike — `5.0.0-rc.4` becomes `5.0.0` even when the range contains a `feat!`.
- All variants use `--no-push`: the version commit and tag stay local until step 6.

## 6. Push the version commit and tag

Verify the tag exists locally, then push:

```bash
git tag --points-at HEAD
git push origin <release-branch> --follow-tags
git ls-remote --tags origin
```

## 7. Merge back

- **Stable**: merge `main` back into `dev` so the version commit is not lost (`git checkout dev && git merge origin/main`, then push via a PR if `dev` is protected).
- **Prerelease**: merge the prerelease branch into `dev`.

Do this before step 8 so a failure in verification does not leave the branches diverged.

## 8. Watch the publish workflow

The `v*` tag push fires `publish.yml`:

```bash
gh run watch --exit-status
```

On failure, show the log URL and go to step 10.

## 9. Verify npm state (the actual success gate)

Workflow success only means the publish process exited 0. Verify the registry:

```bash
npm view markuplint version
npm view markuplint dist-tags
npm view @markuplint/rules dist-tags
```

- Version must match step 5; dist-tag must match the release type (`latest` / `rc` / `beta` / `alpha`).
- Fixed mode can still partially publish. Spot-check core packages; if ANY mismatch is found, enumerate every public package (`npx lerna list --json`) and check each.
- Confirm provenance attestations are present (`npm view markuplint --json` → `dist.attestations`).

**Do not report the release as done before this step passes.**

## 10. Failure handling

- **Workflow failed before anything published**: fix the cause, `gh run rerun`.
- **Partial publish**: published versions are immutable. Ask the user before retrying — options are `gh run rerun` (re-attempts the same tag) or a local `npx lerna publish from-package` (publishes only versions missing from the registry; requires local npm auth, which OIDC does not provide).
- **Wrong version published**: unpublish is generally impossible. Propose `npm deprecate <package>@<version> "<reason>"` plus a corrected follow-up release — only with the user's explicit approval.

## 11. GitHub Release notes

Create a GitHub Release for the tag. All content in **English**.

```bash
gh release create v<VERSION> --title "v<VERSION>" --notes "$(cat <<'EOF'
<release notes body>
EOF
)"
```

Format:

```markdown
## Highlights

- One-line summary of each major change (3-5 bullet points)

---

## Features

### @markuplint/<package>@<version>

- **Feature title** ([#PR](https://github.com/markuplint/markuplint/pull/N), [#Issue](https://github.com/markuplint/markuplint/issues/N))
  - Additional detail or context

---

## Bug Fixes

### @markuplint/<package>@<version>

- Fix description ([#PR](url))

---

## Other Changes

- `@markuplint/<package>`: Brief description of non-feature, non-fix changes

---

## Updated Packages

| Package | Version |
|---------|---------|
| markuplint | x.y.z |
| @markuplint/<package> | x.y.z |

---

**Full Changelog**: https://github.com/markuplint/markuplint/compare/vPREVIOUS...vCURRENT
```

Formatting rules:

- Group changes by package, with `### @markuplint/<package>@<version>` subheadings
- Bold the main description of each feature or fix
- Include PR and issue links where available; use external spec issue links (whatwg, w3c) when relevant
- Separate sections with `---` horizontal rules
- `Highlights` summarizes the most impactful changes (not every change)
- `Other Changes` uses a flat bullet list (no subheadings)
- `Updated Packages` lists only packages with version bumps in this release
- Omit sections that have no entries

## 12. X (Twitter) post

Generate an X post message and present it to the user for copying.

```
v<VERSION> released🎉 New features: <brief summary>. Bug fix for <brief summary>.
https://github.com/markuplint/markuplint/releases/tag/v<VERSION>
```

Rules:

- **280 characters or fewer** — count carefully; URLs always count as 23 characters (t.co)
- Summary is a single paragraph with no line breaks; URL goes on the next line
- Start with `v<VERSION> released🎉` (no space before 🎉)
- Omit the "Bug fix" sentence if there are no bug fixes; omit "New features" if there are none
- No hashtags or mentions
- End with the GitHub Release URL

