.agents/skills/release-notes/SKILL.md
You are writing the human prose for one release-notes page. You do not build the
page. A script (release-notes-render.py) owns every heading, table, banner, @handle,
❤️, and PR link. Your entire job is to fill a small set of prose slots, and the
renderer assembles the page from those plus the facts in data.json.
This split exists on purpose: the parts that used to break — dropped headings, bare handles, missing contributors, malformed links — are now impossible because you never type them. Spend your effort on the one thing only a human-quality writer can do: turn a raw activity log into a changelog a NuGet consumer wants to read.
Would a consumer notice this change without looking inside our repo?
If yes, write about it. If no (CI tweaks, internal refactors, doc/workflow
plumbing, test infra), leave it out — the renderer already collapses that noise.
data.json tags every PR product / mixed / internal; treat internal as
invisible unless it changed shipped behaviour, and for mixed (build config in
native/, or a docs API-docs bump) judge from the title.
Producing release notes is three steps. Two are scripts you run; the middle one is the writing this skill is about.
prepare.sh → (you write prose.json per page) → render.sh
(network) (this skill) (offline)
.agents/skills/release-notes/scripts/prepare.sh — regenerates the API diffs
(Cake), the per-page _sources/<version>.data.json facts, and _sources/index.json,
and writes the list of pages needing prose to output/files-to-polish.txt. When a
page's facts changed, Prepare DELETES that page's prose.json so there is nothing
stale to keep — every page on the list starts from a blank prose slate.data.json and write its prose.json from scratch
(below). Each page in the list has no prose.json — do not go looking for an old
one to "check if it still matches"; the facts moved (new/removed PRs, re-tags), so you
author fresh. Cover every product PR you'd expect a consumer to notice — a page that
silently drops a real change is the failure this design prevents..agents/skills/release-notes/scripts/render.sh — renders every page from
data.json + prose.json and rebuilds TOC.yml + index.md. It fails loudly if
any page on the list still lacks a prose.json (you missed one) or if prose is invalid.Both scripts take the same three flags — --force, --min-version, --max-version
— and nothing else. Choose them from what was asked:
--min-version 4.151.0 --max-version 4.151.0--force (rebuilds even
cached api diffs / unchanged pages)Everything is incremental: an unforced run skips work whose output already exists (a shipped version's api diff never changes), so a routine run is cheap — there is no "notes-only" mode to reach for.
Running locally (needs dotnet, python3, git, gh):
# one version, end to end
.agents/skills/release-notes/scripts/prepare.sh --min-version 4.151.0 --max-version 4.151.0
# … you write documentation/docfx/releases/_sources/4.151.0.prose.json …
.agents/skills/release-notes/scripts/render.sh --min-version 4.151.0 --max-version 4.151.0
# everything
.agents/skills/release-notes/scripts/prepare.sh
.agents/skills/release-notes/scripts/render.sh
In CI a separate prepare job runs step 1, and you (the agent) do steps 2 and 3 —
write each page's prose, then run release-notes-render.py --all to finalize (the workflow's
tool allowlist permits python3 for exactly this).
You are given a list of pages to write (in CI, output/files-to-polish.txt; one
documentation/docfx/releases/<version>.md path per line). The list may be
empty — that just means no page needs new prose this run, but you must still run
the final render (render.sh, or release-notes-render.py --all in CI) to materialize the
deterministic pages and rebuild the TOC/index; don't exit early. Every input for a
page lives in a _sources/ folder beside it — for a page releases/<version>.md
the inputs are releases/_sources/<version>.data.json,
releases/_sources/<version>.prose.json (what you write), and an optional
releases/_sources/<version>.notes.md. HarfBuzzSharp is not a separate page — it
ships inside each SkiaSharp release, so it renders as a ## HarfBuzzSharp X.Y.Z
section on the SkiaSharp page (see harfbuzz_summary below). For each page:
_sources/<version>.data.json. It has:
prs (title, author, community, tag), previews (each with its PR list),
contributors (the authoritative roster), breaking_candidates, tallies,
and the banner/link facts.*.breaking.md API diff and any _sources/<version>.notes.md sidecar. These
are your material for the breaking slot — the API diff gives signature
removals, the notes sidecar gives behavioural breaks (same signature, new
runtime behaviour) that no diff can detect.documentation/docfx/releases/_sources/<version>.prose.json
(schema: scripts/infra/docs/release-notes-schema/prose.schema.json).python3 scripts/infra/docs/release-notes-render.py _sources/<version>.data.json _sources/<version>.prose.json <version>.md
(use the full documentation/docfx/releases/ paths). If it prints
PROSE VALIDATION FAILED, read the errors, fix that slot, and re-run. A clean
render — the .md written — is the bar.You never hand-edit the .md, TOC.yml, or index.md, and you never create,
rename, or delete pages — release-notes-render.py --all (which render.sh runs) owns page
creation and pruning. The per-page render above is just to validate your
prose as you go; render.sh does the authoritative final pass — it re-renders
every page and rebuilds TOC.yml + index.md from the committed JSON. Commit the
_sources/<version>.prose.json and the rendered .md together (the
_sources/<version>.data.json is already produced by the Prepare phase).
Each slot below lists its purpose, the cap the renderer enforces, and one good +
one bad example. Caps are hard: the renderer rejects an over-long highlight, a
missing contributor, or an unknown category. Stay well under and you never see an
error. Where a slot is nullable or optional, the note says so — reach for null
rather than padding.
theme — 2-6 wordsWhat this release is about, shown bold in the banner. No punctuation.
First stable v4 releaseVersion 4.148.0 (that's the title, not a theme) · Lots of fixes and new APIs (vague)highlights_headline — one sentence, ≤20 wordsThe single most important thing about the release. Not a list. Decide it from
the product-tagged PRs, the Skia milestone bump, and whether there are breaking
changes — the one thing a consumer would care about most, in a sentence. You are
not summarising every PR here.
SkiaSharp 4.148.0 is the first stable v4 release, built on Skia m148.This release adds WebP, SKStream.GetData, singleton lifecycle, pixel fixes, WinUI fixes, and more. (enumeration)highlights_body — optional, ≤60 words, or nullName the biggest themes to draw the reader in. Prose is best, but a short feature
list is fine for a big release — just keep the whole Highlights block (headline +
body) under ~100 words so it stays a lead-in, not the changelog. No PR links, no
@handles. If the headline already says enough, use null.
It adds variable fonts and animated WebP, and reworks the singleton lifecycle. This is a breaking release — check the changes below before upgrading.Includes #4125, #3771, #3772, #4080, #4068 and fixes from @ramezgerges. (links + handles, and it's just PR numbers, not themes)breaking — array, one entry per change a consumer must act onMerge from two sources: signature removals in the *.breaking.md diff, and
behavioural breaks described in breaking_candidates / the notes sidecar. Empty
array is fine and renders "None in this release." Give each a title, a body
that says what changed and what to do, and the prs it came from. Only write
what you can substantiate: a breaking_candidate carries a hint and sometimes
prs, but when its companion file isn't on disk and it lists no concrete change,
fall back to the PR titles in prs you can actually read — never invent a removal
you can't point at.
{"title": "SKPaint no longer exposes legacy text state", "body": "The paint text/font members obsoleted in v3 are now compile errors — move typeface and text size onto SKFont.", "prs": [4068, 4114]}{"title": "Refactoring", "body": "Various changes."} (no action, not consumer-facing)categories — array of {heading, bullets}The body of the page. heading must be exactly one of these six (the renderer
rejects anything else — this is the closed list, in the order they render):
| Heading | What belongs here |
|---|---|
Engine | The Skia milestone bump and upstream engine syncs; bundled-engine changes a consumer would feel. |
API Surface | New or changed public APIs — added types, methods, overloads, options. |
Bug Fixes | Corrected behaviour, crashes, wrong output — even when platform-specific. |
Lifecycle & Internals | Disposal, finalizers, initialization, singleton/handle lifecycle — consumer-visible runtime behaviour, not build plumbing. |
Platform | Platform-support changes: a target added or dropped, new native assets, TFM realignment. |
Security | Bundled native-dependency refreshes and security fixes. |
You choose which of the six to include — a section appears only when it has a real
product-facing bullet, and you may use as few as one. Prefer fewer, denser
sections over many one-bullet ones. Curate, don't enumerate: each bullet
MERGES related PRs into one product theme (aim 3-5 bullets per section on a big
release; 1-2 is perfectly fine on a servicing release — never merge distinct areas
just to hit a count), with a lead (bold summary) + detail (what it means for
the consumer) + the prs. The renderer adds the PR links and the ❤️ community
credit — never write those yourself. A change with a migration usually belongs in
breaking; don't also give it its own thin category section unless it has
independent product value. Placement rule of thumb: ordinary fixes go under Bug
Fixes even when platform-specific; use Platform only for platform-support
additions or removals.
{"heading": "Bug Fixes", "bullets": [{"lead": "Pixel access corrected", "detail": "GetPixelSpan now uses RowBytes for stride and the right axis for offsets.", "prs": [4148, 4128]}]} (two PRs → one theme){"heading": "Bugfixes", …} (not one of the six) · one bullet per PR restating its title · a section that lists 20 internal PRs.contributor_summaries — one line per roster logindata.contributors is authoritative — every login there needs an entry (the
renderer fails otherwise) and no one else gets one. Summarise that person's work
in prose; the renderer adds their @handle and PR links. This is the one place
internal work is worth naming — a contributor's sample or CI work still deserves
credit even though it never became a category bullet.
"ramezgerges": "Singleton lifecycle rework, the SKPath finalizer fix, and Uno sample updates""ramezgerges": "#4080, #4068, #3796" (that's data, not a summary)preview_summaries — one line per preview keydata.previews lists each preview/RC with the PRs that first shipped in it. Give
each key a 1-2 sentence summary of what that milestone delivered. When a preview
only carried internal work, describe the milestone itself (e.g. "opened the line"
or "cut the release candidate") rather than forcing a product story.
"4.148.0-p2": "Preview 2 added animated WebP encoding and the SKPath finalizer fix."harfbuzz_summary — one short paragraph, or nullHarfBuzzSharp ships inside each SkiaSharp release, so its notes are a
## HarfBuzzSharp X.Y.Z section on this page, not a separate page. data.harfbuzz
gives the version and prs — the PRs in this release that touched the HarfBuzz
binding (a subset of the page's PRs, so you have already written about most of them
above). Summarise the HarfBuzz-facing story in 1-2 sentences; the renderer adds the
heading, the ❤️ credit and the PR links.
data.harfbuzz.prs is non-empty. When it is empty the renderer
writes "No HarfBuzzSharp binding changes shipped…" itself — set harfbuzz_summary
to null. When data.harfbuzz is absent (e.g. an unreleased head), omit it."Adds variable-font shaping and an HBColor value type, and refreshes the bundled HarfBuzz to 8.3.0."There is no separate template, grouping guide, or checklist to reconcile — the renderer is the checklist, and this file is the only instructions. If a rule isn't here, it's because the renderer already guarantees it. Write the prose; let the script build the page.