# Prepare Release

> Prepare a MegaLinter release — update CHANGELOG (prune empty sections, collapse linter versions, backfill PR numbers), run the release build, push commit and tag, and guide GitHub release creation.

- Skill: `oxsecurity/prepare-release` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add oxsecurity/prepare-release`
- Raw SKILL.md: https://api.skillmd.com/api/skills/oxsecurity/prepare-release/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: oxsecurity (https://skillmd.com/u/oxsecurity)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/oxsecurity/prepare-release

---


Prepare a MegaLinter release.

## Step 1 — Resolve the version

Parse `$ARGUMENTS`. Validate against `^v[0-9]+\.[0-9]+\.[0-9]+$`. If absent or invalid:

```
AskUserQuestion: "What semver version should this release have? (format: vX.Y.Z)"
```

Call the resolved value `RELEASE_VERSION` (e.g. `v9.6.0`).

## Step 1b — Major release only: bump the default major version

Skip this step unless `RELEASE_VERSION` is a **new major** (`vN.0.0` with `N` greater than the major of the latest existing tag). For a major release, the moving major tag (`vN`) and every "current version" reference must be bumped from the previous major (`vP`) to `vN` **before** the release build. This was done for v9 in PRs #6144/#6197 and for v10 in the v10.0.0 release commit.

The authoritative checklist is the **`MAJOR-RELEASE-IMPACTED` markers** left in the sources:

```bash
grep -rn "MAJOR-RELEASE-IMPACTED" . --include='*.py' --include='*.js' --include='*.yml' --include='*.md'
```

Fix every marked spot, then sweep for leftovers (`grep -rn 'megalinter[@:]vP\b\|tag: vP\b\|DEFAULT_RELEASE'` excluding `docs/`, `CHANGELOG.md`, `node_modules`, `.claude/worktrees`, `megalinter-reports`, `linter-helps.json`). Known spots (as of v10):

- **Core defaults**: `megalinter/constants.py` (`DEFAULT_RELEASE`), `mega-linter-runner/lib/config.js` (`DEFAULT_RELEASE`).
- **Runner upgrade rules**: add a new "VP to VN migration rules" block at the end of `getUpgradeItems()` in `mega-linter-runner/lib/upgrade.js`, mirroring the previous major's 8 rules (flavors `@vP`/`@vP.x`, docker flavors `:vP`/`:vP.x`, main action `@vP`/`@vP.x`, main image `:vP`/`:vP.x`). The bare `oxsecurity/...` regexes also match `ghcr.io/`-prefixed references and keep the prefix. Verify with `npx mocha test/megalinter-upgrade.test.js` (each rule has `test`/`testRes` self-checks).
- **Release workflows**: `deploy-RELEASE.yml`, `deploy-RELEASE-flavors.yml`, `deploy-RELEASE-linters.yml` (moving `:vP` image tags, `type=raw,value=vP`, including the commented Docker-Hub mirror blocks), `mirror-docker-image.yml` (the `contains(...,'vP')` guard). Do **not** touch `actions/github-script@... # v9`-style pin comments — that's the github-script action's own version.
- **Docs source of truth**: `README.md` — all `@vP` / `:vP` / `tag: vP` current-version references. Keep **historical** version mentions as-is (e.g. the Docker-Hub registry-freeze note's "since v9.5.0" / "frozen at v9.4.0"), but reword sentences that would become nonsensical with the new tag. All `docs/install-*.md` and `docs/quick-start.md` are **split from README by the doc build** — do not edit them, they regenerate. `docs/reporters/AzureCommentReporter.md` is standalone: edit it directly.
- **Other standalone files**: `SECURITY.md` (supported versions), `TEMPLATES/mega-linter.yml` (`uses: oxsecurity/megalinter@vP`), `.automation/test/yaml_schema/good/action_good_*.yml` fixtures, `skills/megalinter-setup/SKILL.md` example tags, `mega-linter-runner/lib/options.js` help-text examples.
- **Config schema**: `megalinter/descriptors/schemas/megalinter-configuration.jsonschema.json` — the `MEGALINTER_VERSION` property's `description` example and `examples` array. Then regenerate the runner copy: `python .automation/generate_runner_vars.py` (also re-run automatically by the build).
- **Runner tests**: tests must reference `DEFAULT_RELEASE` imported from `../lib/config.js`, never a hardcoded major (fix any that regressed). Run `cd mega-linter-runner && npx mocha "test/**/*.test.js"` — the Docker-based `(Module) run on own code base` test may fail locally on Windows (MSYS mount mangling), rely on CI for it.

**Auto-handled by the release build — never edit manually**: root `action.yml` and `flavors/*/action.yml` (stamped with the exact `RELEASE_TAG` by `build.py` when `--release` is used), `docs/` pages split from README, `mega-linter-runner/lib/megalinter-vars.json`.

When touching a new file that carries the moving major version, add a `MAJOR-RELEASE-IMPACTED` comment marker next to it so the next major release finds it.

Finally, add a CHANGELOG entry for the switch itself (mirroring past majors), e.g. under `mega-linter-runner`: `--upgrade` now migrates vP references to vN, and `--install` defaults to vN.

## Step 2 — Rewrite CHANGELOG.md

The mechanical transformation is done by the bundled helper script
`.claude/skills/prepare-release/prepare_changelog.py` (stdlib only, no venv needed). It:

- converts the `## [Unreleased]` block into a dated `## [RELEASE_VERSION] - YYYY-MM-DD` entry;
- prunes empty sections from the release entry;
- collapses the linter-version bumps to **one line per linter**, alphabetically sorted, using the **chronological first-seen `from` → last-seen `to`** range with no date (this is intentional: it stays correct across renumberings, e.g. cfn-lint `3.14 → 1.52.0`, where a naive semver-min/max would print a backwards range);
- strips `<!-- linter-versions-end -->` from the release entry;
- prepends a fresh empty `## [Unreleased]` block that holds the **sole** marker and keeps the repo's `(N)` placeholder on the linter-versions header.

PR-number backfill is the one **judgment** part, so the script splits it into `analyze` (lists the lines that need a PR) and `apply` (consumes your decisions).

### 2a — List the lines that need a PR number

```bash
python .claude/skills/prepare-release/prepare_changelog.py analyze
```

This prints a JSON array of candidates `{id, section, text}` — every content line (`- …`) in a non-version section that does **not** already carry a MegaLinter reference. A line counts as already-referenced only if it has `(#N)`, a bare `#N` after a space/paren, or an `oxsecurity/megalinter` URL. A line that merely links an external repo/issue (e.g. the linter's own GitHub page) is **not** considered referenced and will appear as a candidate.

### 2b — Decide the PR(s) for each candidate

Build the commit map for the release window:

```bash
PREV_TAG="$(git tag --sort=-creatordate | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1)"
git log "$PREV_TAG"..HEAD --oneline --no-merges
```

For each candidate, extract distinctive keywords (linter name, descriptor ID, quoted identifier) and find the PR:

- `grep -i "KEYWORD"` against the commit map (squash-merge subjects end in `(#NNNN)`);
- for terms not in commit subjects, `gh pr list --state merged --search "KEYWORD in:title" --json number,title --limit 5`.

Write the **confident** matches to a JSON file, keyed by candidate `id`, value = PR number(s) (comma-separated for multiple):

```json
{ "0": "8216", "1": "8216", "5": "7907", "12": "8133,8134" }
```

Leave a candidate **out** of the JSON when no PR is found, the match is ambiguous, or the matching commit has no PR number. Unmatched lines are simply left as-is — do **not** report them or ask the user about them.

### 2c — Apply the transformation

```bash
python .claude/skills/prepare-release/prepare_changelog.py apply \
  --version RELEASE_VERSION --date "$(date +%F)" --prs /path/to/prs.json
```

This rewrites `CHANGELOG.md` in place. (For a dry run, add `--out /some/tmp/path` to write elsewhere and leave `CHANGELOG.md` untouched.)

### 2d — Editorial pass for end users

Reread the release entry against `.claude/rules/changelog.md`: the CHANGELOG is release notes for **end users**, not maintainers. In user-facing sections, rewrite entries that lead with implementation details so they lead with the user benefit or required action, and move internal/technical entries (refactors, test suite, repo CI, build tooling) under the `Dev`/`CI` sections — technical detail is fine there. Keep before/after tables for performance and size improvements (user-visible measures in the user-facing sections, CI job timings in the `CI` section). Check readability: key words/expressions in **bold** for scanning, and long entries split into a short lead line + sub-bullets rather than long sentences. The rewrites will appear in the Step 3 diff review.

## Step 3 — Confirm the CHANGELOG

Show the diff:

```bash
git diff CHANGELOG.md
```

Ask:

> **AskUserQuestion**: "Is the updated CHANGELOG correct? Proceed to create the release?"
>
> Options: **Yes, proceed** / **No, let me edit it first**

If **No**: stop. The user edits `CHANGELOG.md` manually, then re-invokes `/prepare-release RELEASE_VERSION`.

## Step 4 — Confirm deploy-BETA-linters was run

Ask:

> **AskUserQuestion**: "Did you manually run the **deploy-BETA-linters** GitHub Actions workflow and confirm it completed successfully? The release workflow reuses the BETA linter images — the release will be incomplete if those images have not been built."
>
> Options: **Yes, it completed successfully** / **No / not sure**

If **No / not sure**: stop. Guide the user:

1. Trigger: `gh workflow run deploy-BETA-linters.yml` (or GitHub UI → Actions → **Deploy BETA linters** → Run workflow).
2. Wait for it to finish green.
3. Re-invoke `/prepare-release RELEASE_VERSION`.

## Step 5 — Run the release build (manual, in a separate terminal)

**Do not run this build yourself.** It regenerates all documentation and Dockerfiles and can take several minutes — run it in a dedicated terminal so its output stays visible and interruptible.

Tell the user to open another command line at the repo root and run:

```bash
make megalinter-release RELEASE_VERSION=RELEASE_VERSION
```

The Makefile activates the venv automatically. This runs:
- `build.sh --doc --version RELEASE_VERSION` — regenerates docs/Dockerfiles stamped with the release version.
- `build.sh --release RELEASE_VERSION` — stages all changed files, creates commit `"Release MegaLinter RELEASE_VERSION"`, and creates a git tag `RELEASE_VERSION`.

Do not run `make megalinter-build-with-doc` separately.

Then ask:

> **AskUserQuestion**: "Has `make megalinter-release RELEASE_VERSION=RELEASE_VERSION` finished successfully in your other terminal?"
>
> Options: **Yes, it completed** / **No, it failed**

If **No, it failed**: ask the user to paste the error output, then help diagnose and stop until it succeeds. Once it succeeds, continue to Step 6.

## Step 6 — Confirm before pushing

Show what will be pushed:

```bash
git log -1 --oneline
git tag --sort=-creatordate | head -1
```

Ask:

> **AskUserQuestion**: "Release commit and tag are ready. Push to origin now?"
>
> Options: **Yes, push** / **No, I'll push manually**

If **Yes**:

```bash
BRANCH="$(git branch --show-current)"
git push origin "$BRANCH"
git push origin RELEASE_VERSION
```

> **Main-push exception**: pushing to `main` is the one authorized exception to the global "never push to main" rule in CLAUDE.md. MegaLinter releases are always cut from `main`, and the release commit must land there. This skill explicitly owns this exception.

## Step 7 — Guide GitHub release creation

Walk the user through the GitHub UI. Match the structure of the previous releases (look at `gh release view <PREV_TAG> --repo oxsecurity/megalinter --json body` if unsure):

1. Open: `https://github.com/oxsecurity/megalinter/releases/new?tag=RELEASE_VERSION`
2. Set the release **title** to `RELEASE_VERSION`.
3. Tick **"Set as the latest release"**.
4. Click **"Generate release notes"**. GitHub fills the body with `## What's Changed` (an auto list of every merged PR), `## New Contributors`, and a `**Full Changelog**` compare link. **Keep all of this** — then make the manual edits below.

### Manual edits after "Generate release notes"

**a. Curated summary at the top.** Directly under the `## What's Changed` heading (above the auto-generated PR bullet list), paste the **release entry** the skill just wrote to `CHANGELOG.md` (the `## [RELEASE_VERSION] - DATE` block — its section bullets and the collapsed linter-versions list), dropping the `## [RELEASE_VERSION]` header line itself. This puts the human-readable highlights above the raw PR dump.

**b. Announcement call-to-action line.** If an announcement issue exists for this release, add it as the first line under `## What's Changed`, matching previous releases:

```markdown
[**Take 2 mn to read MegaLinter RELEASE_VERSION announcements**](https://github.com/oxsecurity/megalinter/issues/<ANNOUNCEMENT_ISSUE>)
```

**c. OX Security banner + GitHub-star call to action.** Just **above** the `**Full Changelog**:` line at the very bottom, add the OX Security banner (present on every release) followed by a star CTA:

```markdown
[![MegaLinter is provided by OX Security](https://raw.githubusercontent.com/oxsecurity/megalinter/main/docs/assets/images/ox-banner.png)](https://www.ox.security/?ref=megalinter)

⭐ If MegaLinter is useful to you, please [give it a star on GitHub](https://github.com/oxsecurity/megalinter/stargazers) — it helps the project a lot!
```

**d. (Optional) Social share.** Some past releases add a `[**Share the news on LinkedIn :)**](<post-url>)` line near the bottom. Add one only if the user has a post URL to link.

5. Review the rendered preview, then click **"Publish release"**.

## Step 8 — Remind about pending workflows

Tell the user:

- The push and tag trigger **deploy-RELEASE**, **deploy-RELEASE-linters**, and **deploy-RELEASE-flavors** workflows.
- Go to **GitHub → Actions** and approve any runs awaiting manual approval.
- Monitor those three workflows until they all complete green before announcing the release publicly.

