Files
msd-core/scripts/changeset
Tom Boucher 19edab21da fix(#1006): rc CHANGELOG preview crash on malformed changeset fragment + validate fragment content at the gate (#1007)
* fix(#1006): harden render --preview against fragment parse failures

`render --preview` wrote `report.preview` unconditionally. When a `.changeset`
fragment fails to parse, `cmdRender` early-returns with `{exitCode:1, report:
{failures}}` and NO `preview` key, so `process.stdout.write(undefined)` threw
ERR_INVALID_ARG_TYPE and the rc release job's "Preview CHANGELOG" step died with
a cryptic TypeError that masked the real cause.

Guard the preview write on `typeof report.preview === 'string'` (ADR-227: shape,
not just type); when absent, fall through to the existing failure reporter that
names the offending fragment and exits non-zero — identical to a non-preview
render. Also backfills the stray placeholder `pr: 0` -> `pr: 939` in
.changeset/936-convergence-inline-plan-phase.md that triggered the live failure.

Regression test (red-then-green verified) added at the render --preview seam.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#1006): validate changeset fragment content at the Changeset Required gate

The `Changeset Required` gate (scripts/changeset/lint.cjs) only checked that a
`.changeset/*.md` fragment EXISTS in the PR diff; it never validated the
fragment's contents. So a malformed fragment (e.g. an un-backfilled `pr: 0`
placeholder) silently merged to `next` and only detonated later in the rc
release job. This is the upstream prevention for #1006 — the crash hardening
turns the failure into a clear message, this stops the bad fragment ever
reaching the release path.

evaluateLint now accepts `fragmentFailures` and fails with the typed reason
`fail_invalid_fragment` (naming each offending file) before the existence/
opt-out checks — a malformed fragment beats `no-changelog`, since it will break
the render regardless. main() reads + parseFragment()s every changed fragment:
a deleted fragment (not on disk) is skipped, a present-but-unreadable one fails
closed. Tests assert on the typed LINT_REASON enum (no raw-text matching), a
precedence case over the opt-out label, and an end-to-end suite that drives the
real main() against a temp git repo (malformed -> fail, valid -> pass, deleted
-> skipped) so the wiring is regression-proof.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(#1006): assert the typed --json report in the preview regression test

Code review flagged the preview parse-failure regression test for positive
raw-text matching on CLI output (`combined.includes('bad-fragment.md')` /
`'invalid_pr'`), which this repo's testing standards forbid. Keep the non-json
`runRenderRaw` call for the negative crash proof (the ERR_INVALID_ARG_TYPE
crash lives only on the non-json stdout.write path), and add a `--json`
invocation that asserts the offending fragment + typed `invalid_pr` reason via
the structured `report.failures[]` surface instead of rendered prose.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:55:31 -04:00
..

changeset/ — release-notes tooling

This directory holds the scripts that turn per-PR fragments in .changeset/ and git history into the project's CHANGELOG.md and GitHub release notes.

The entry point is cli.cjs. It exposes three subcommands:

Subcommand Purpose
render Render a single version's changelog section from consolidated data.
github-release-notes Build GitHub release-notes body for a ref range.
extract Pull existing CHANGELOG.md entries that fall in a version range.

The rest of this document specifies the extract contract, because it is the surface most likely to be called by external tooling (CI workflows, npm scripts, release automation) that needs a stable exit-code and output guarantee to code against.


cli.cjs extract

Extract the changelog entries for every release in a version range, reading from an existing CHANGELOG.md. The range is --from exclusive, --to inclusive.

node scripts/changeset/cli.cjs extract --from VERSION --to VERSION \
  [--changelog FILE] [--repo <dir>] [--json]

Flags

Flag Required Description
--from VERSION Yes Lower bound, exclusive — entries equal to --from are not returned.
--to VERSION Yes Upper bound, inclusive — entries equal to --to are returned.
--changelog FILE No Path to the changelog to read. Defaults to <repo>/CHANGELOG.md.
--repo <dir> No Repo root used to locate CHANGELOG.md when --changelog is omitted. Defaults to the current working directory.
--json No Emit the structured report as JSON instead of rendered markdown.

Version validation

Both --from and --to must be stable triplet semver — MAJOR.MINOR.PATCH, digits only.

  • A leading v is accepted and stripped: v1.42.0 is treated as 1.42.0.
  • Pre-release and build suffixes are rejected: 1.42.0-rc.1, 1.42.0+build, and partial versions like 1.42.x all fail validation and exit 1.

Strict validation is deliberate. Coercing a malformed bound such as 1.42.x to 1.42.0 would silently change which releases the range selects, so a malformed bound is rejected early with a structured error rather than guessed at.

Changelog entries that are themselves pre-release or non-semver (and the Unreleased section) are skipped during matching; a notice for each skipped entry is written to stderr.

Exit codes

extract resolves to one of three exit codes. The output shape depends on whether --json is passed.

Exit Meaning Default stdout --json stdout
0 One or more releases fall in the range. Rendered markdown for the matched releases. { "releases": [ ... ], "from": "...", "to": "..." }
1 Bad input: --from/--to is not stable semver, a required flag is missing, or the changelog file was not found. Nothing (a missing-flag error and usage go to stderr). { "error": "<message>", "releases": [] }
2 Bounds are valid but no release falls in the range. A no releases found in range notice on stderr. { "releases": [], "from": "...", "to": "..." }

Notes for callers:

  • Treat exit 2 as "empty range", not "failure". For a well-formed invocation it means the request was understood and simply matched nothing — do not surface it as an error. (At the argument-parsing layer, malformed argv such as an unknown flag also exits 2; pass well-formed arguments and this overlap does not arise.)
  • In default (text) mode, a failure is signalled by the exit code alone — exit 1 from invalid semver or a missing changelog writes nothing to stdout. Machine consumers should pass --json to receive the error field.

Output shape

With --json, the report is pretty-printed JSON. The releases array contains one object per matched release (version, date, and parsed sections); from and to echo the normalized bounds. On exit 1, releases is empty and an error string describes the failure.

Without --json, exit 0 prints the matched releases as markdown, ready to paste into release notes:

## [1.42.0] - 2026-01-15

### Added

- New `--json` flag on the extract command (#3796)

### Fixed

- Trailing-slash handling in config paths (#3651)

Examples

Extract everything released after 1.41.0 up to and including 1.42.0:

node scripts/changeset/cli.cjs extract --from 1.41.0 --to 1.42.0

The same range as structured JSON, reading an explicit changelog file:

node scripts/changeset/cli.cjs extract \
  --from v1.41.0 --to v1.42.0 \
  --changelog ./CHANGELOG.md --json

Handle the three outcomes in a shell consumer:

if out=$(node scripts/changeset/cli.cjs extract --from "$FROM" --to "$TO" --json); then
  echo "$out"            # exit 0 — releases found
else
  case $? in
    2) echo "no releases in range — nothing to publish" ;;  # not an error
    *) echo "extract failed: $out" >&2; exit 1 ;;            # exit 1 — bad input
  esac
fi