changelog
Generate or update the changelog entry for the current branch under
changelog/YYYYMMDD-HHMMSS-<slug>.md
: derive its metadata from git and the diff,
write the frontmatter and a grouped, categorised body, run the deterministic
enrichment scripts, then validate the result.
This skill is the single source of truth for what a valid changelog entry is
— the frontmatter schema, the field-ownership boundaries, idempotent
update-vs-create, and the validation gate. The same contract is enforced
downstream by a consumer repo's CI and relied on by a release-orchestrator that
finalises the post-merge fields, so the authoring rules live here once.
It is invoked two ways:
- Standalone () — author, refresh, or repair this branch's entry
and leave it uncommitted in the working tree for review. No commit, push,
or PR.
- Inside a ship flow (e.g. a ) — the changelog step that runs
before push; the ship flow commits the entry, pushes, and opens the PR.
Configuration
Config lives in
beside this file; the bundled
scripts read it automatically. Edit your copied
to match the
consuming repo (a neutral
ships as a
template).
and
are
required — they have no
default, so a missing
or either key absent makes the scripts fail
loudly rather than silently inherit ACME's identity. The rest are structural and
keep generic, overridable defaults:
| Key | Meaning | Default |
|---|
| Team-key prefixes used to recognise issue IDs in the branch and body. The issue-ID regex is built from these. | required |
| Linear workspace slug used to build issue links (https://linear.app/<slug>/issue/<id>
). | required |
| The trunk the branch diff is taken against (). Overridable per-run via the env var. | |
| Directory the dated entries live in (scanned by the enrichment + validation scripts). | |
| Monorepo dir prefixes mapping → package when deriving . | ["apps", "packages", "services"]
|
| Package name for changed paths matching no prefix. | |
| Whether to emit the field at all. Leave for single-package repos (the field is write-only and redundant there — entries stay clean); set in genuine monorepos. flips it on when it detects a workspace config. | |
All bundled scripts use only Node built-ins — no
, no build step.
They operate on the
consumer repo's root directory (run them
from the repo root).
Running it
Step 1 — Detect an existing entry (idempotency)
Grep
for a file whose frontmatter contains
branch: "<current-branch>"
.
If exactly one matches, you are in
update mode: preserve its
and
filename, rewrite the rest. Otherwise you are in
create mode.
Step 2 — Analyse the branch
git log origin/<base>..HEAD --pretty=full
— full commit list including bodies
and trailers.
git diff origin/<base>...HEAD --name-only
— changed files, for grouping the
body by package.
is
's
(default
). Fetch it first
(
) so the diff is accurate — skip the fetch if the
caller already did it (e.g. a ship flow fetches in its preflight step).
Step 3 — Derive metadata
| Field | How to derive |
|---|
| Match the issue-ID regex (built from ) against the branch name (upper-cased) and against commit subjects/bodies. Deduplicate. |
| . |
| Parse Co-authored-by: Name <email>
trailers across all branch commits. Store the email or form. Empty array if none. |
| Infer from commit subjects and diff: , , , , , . If ambiguous, ask the user to confirm. |
| Infer from trailers, in conventional-commit subjects, or removal of public surfaces. If unclear, ask the user. Default . |
| One-sentence user-facing summary distinct from . Optional — leave blank if the change has no public-facing impact (chore, internal refactor). |
Field ownership — what this skill authors vs. what it must leave alone is the
crux of the contract; see
references/changelog-contract.md
for the full rules. In short:
- Authored here: , , , , ,
, , and — only when is on —
(written by the enrichment script in Step 5, not
hand-edited). Single-package repos leave (the
default) and omit the field entirely.
- is sacred — set once on create (UTC time of first run); on
update, preserve it verbatim.
- Never authored here: (, , ,
) and the post-merge fields / / / . A release
step finalises them from canonical GitHub PR data after merge — included, resolved
from the merged PR by its (never written by the ship flow). Emit them as
blank placeholders on create; leave existing values untouched on update.
The skill
emits the derived array as a handoff — a ship flow reuses
it for the PR body and any Linear writeback (e.g. via a
skill).
Step 4 — Generate the body
Group bullets by package, categorised under
/
/
.
Only include headings that have entries. For multi-package changes use
subheaders.
If
, the body MUST start with a
section describing
the change and the migration path.
Write the
,
, and body prose in the consuming repo's documented
prose language. Across this estate that is
British English (
,
,
/
) — prose only, never identifiers, dependency names, or upstream API
field names.
Step 5 — Write or update the file
Filename: changelog/YYYYMMDD-HHMMSS-<slug>.md
, where the timestamp is
(UTC time of first run) and the slug derives from
(lowercase,
non-alphanumerics →
, collapse repeats, ~60-char cap on a word boundary).
Always quote timestamp strings in YAML (
created_at: "2026-04-26T13:24:00Z"
).
Unquoted ISO timestamps parse as Date objects and gain
millis on the
enrichment round-trip; quoting keeps them lossless.
On update: preserve
and the filename; rewrite
,
,
,
,
,
, and the body;
leave
/
/
/
/
alone (the
release/enrich step fills them post-merge,
branch-resolved).
Use the frontmatter field order shown in
references/changelog-contract.md
.
Only when
is on, emit
as a placeholder — the
script fills it in place. When it is off (the single-package default), omit the
field;
set-affected-packages.mjs
is a no-op.
Then run the two deterministic enrichment scripts from the consumer repo root
(both idempotent; they match the entry by its
frontmatter and leave the
post-merge fields blank):
bash
node skills/changelog/scripts/set-affected-packages.mjs # writes affected_packages from the branch diff
node skills/changelog/scripts/add-links.mjs # rewrites bare issue IDs in the current branch's entry to Linear URLs
Adjust the path prefix if you installed the skill to a different location.
Both enrichment scripts also accept
(alias
) — a read-only
preview that reports what would change and writes nothing, exiting
when the
entry is already up to date and
when a rewrite is needed (prettier-
style, so CI can gate on it):
bash
node skills/changelog/scripts/set-affected-packages.mjs --check # current branch's entry only
node skills/changelog/scripts/add-links.mjs --check # ALL entries in the changelog dir
Both enrichers are
branch-scoped by default (A-603):
with no
arguments rewrites only the entry/entries whose
frontmatter matches the
current git branch, so authoring a new entry never churns unrelated, already-merged
ones. Two modes still scan the
whole directory:
(a deliberate
full-directory rewrite) and
/
(the completeness gate, which can
exit
on a historical entry). Use
to confirm the directory is fully
enriched; use the default for the per-PR pass on one branch's entry. (When git is
unavailable the default falls back to the full sweep.)
Step 6 — Validate against the contract
This is the gate:
bash
node scripts/preflight-changelog-ci.mjs # optional: checks Node vs engines/.nvmrc, then pnpm install --frozen-lockfile
node scripts/validate-changelog.mjs # validates frontmatter schema, filename format, field types, ISO timestamps, Breaking section, issue IDs
preflight-changelog-ci.mjs
is optional and pnpm-specific — skip it if the
consumer repo doesn't use pnpm. On failure, stop and fix the entry before
continuing — do not hand a malformed entry to the ship flow.
Standalone vs inside a ship flow
- Standalone () runs Steps 1–6 and then reports, leaving the
entry uncommitted in the working tree for the user to review and commit. It
never pushes or opens a PR.
- Inside a ship flow the same steps run before push; the ship flow then
commits the entry (), pushes, and opens or updates the
PR. It leaves blank — the release/enrich step fills it post-merge,
branch-resolved from the merged PR.
Implementation
All the scripts the changelog lifecycle needs live under
in this bundle and run on plain Node (no npm dependencies, no build step). They
cover the
whole lifecycle the bundle owns — authoring (run by this skill) and
finalisation (wired into the consumer's
/ CI / release
orchestrator). Each takes
(usage, exit 0) and
(an offline
smoke test of its pure logic); the file-writing scripts also take
/
(report, write nothing).
Authoring — run by this skill (the flow):
scripts/set-affected-packages.mjs
— writes from the branch diff (monorepo consumers only; a no-op when is off).
- — rewrites bare issue IDs in the body to Linear URLs.
scripts/preflight-changelog-ci.mjs
— optional Node/lockfile CI-parity check (pnpm).
scripts/validate-changelog.mjs
— validates the entry against the contract.
Finalisation and the CI gate — run by the consumer, not by this skill. These
ship in the bundle too, and an adopter wiring up the orchestrator/CI gate needs
them. They are referenced from the consumer's
scripts and
workflows rather than invoked during authoring:
scripts/finalise-changelog.mjs
— release-time enrichment + version-stamping for npm targets, run by the release orchestrator right after release-please release-pr
(the consumer exposes it as the script). For each un-finalised entry it resolves the merged PR via /, fills the post-merge fields ( / / / / , the last including the merge-excluded count from the PR commits API), stamps with the just-bumped version, and links bare Linear IDs. It composes (the PR-metadata fill), (the merge-excluded commit count) and (the version stamp).
scripts/enrich-changelog.mjs
— post-merge enrichment for deploy targets (octavo, shared-workflows), which are never checked out during the release flow and so can't finalise inline. Run by the release orchestrator's daily cron on the checked-out target (the consumer exposes it as the script). It reads one merged PR's data from an env-var interface ( / / / / / / / ), finds the entry by its , and fills the same post-merge field group as finalise (minus , which a deploy target's own tag flow owns, and minus , which the cron doesn't resolve). A thin wrapper over ; fill-once and idempotent, so the cron can re-run safely. exits 1 when an entry still needs enriching; previews.
scripts/check-changelog-completeness.mjs
— the CI completeness gate, run by the consumer's validation workflow: a release-triggering (//breaking) PR title must carry a dated entry, or the build fails.
scripts/backfill-commits.mjs
— a one-off backfill of across the existing backlog (for adopting the count after the fact). Resolves each entry's merged PR via , splices in only the line (no re-serialise), and is idempotent; previews. Not part of authoring or the release flow.
They share helpers under
(
,
,
,
,
,
,
). So while this skill
itself stops at authoring + validation and leaves the post-merge fields blank,
the
finalisation and completeness scripts that fill them are part of this
bundle — the consumer's
/ CI / release orchestrator run them, not
the
flow.
Note for adopters: unit tests for these scripts are maintained in the
repo (not bundled into the skill). See the skill's README.