Skip to content

[docs] Unify changelog + release-notes generation: stable rollups and local skia links#4177

Merged
mattleibow merged 2 commits into
mainfrom
mattleibow/api-diff-stable-rollup
Jun 17, 2026
Merged

[docs] Unify changelog + release-notes generation: stable rollups and local skia links#4177
mattleibow merged 2 commits into
mainfrom
mattleibow/api-diff-stable-rollup

Conversation

@mattleibow

@mattleibow mattleibow commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Two related fixes to the shared versions.json-driven docs pipeline (release notes + API changelogs). Combined here since we're iterating on both together.


1. API changelogs: one folder per release line, not per preview (docs.cake)

The unified-versioning refactor (#4171) started passing --nugetDiffPrerelease=true to docs-api-diff-past so the 4.x line — which ships only as previews/rcs until it stabilises — would get changelogs. But the loop still emitted a folder for every enumerated version, so every intermediate preview produced its own directory. The last API Diff run filled bot/api-diff (#4173) with 4.147.0-preview.{1,2,3}, 4.148.0-rc.1.2 and 4.150.0-preview.1.1 dirs across every tracked package (~2,300 added files). Historically changelogs/ held stable versions only.

Fix: collapse the feed to one entry per release line (numeric version core, prerelease label stripped: 4.148.0-rc.1.24.148.0; a genuine 4-part stable like 1.49.2.1 keeps its fourth digit) and emit a single rollup changelog per line, diffed against the line's representative package — newest stable if it shipped, otherwise newest prerelease. This mirrors the release-notes pages.

A line is emitted when it shipped stable, or when it's an active dev line (preview-only, not superseded, newer than the last stable — e.g. 4.148, 4.150). Abandoned preview-only lines (superseded in versions.json, e.g. 4.147) and old never-shipped previews are skipped; their changes are absorbed into the successor's cumulative diff. Baselines unchanged: compare_to overrides win, otherwise the previous emitted line.

RunBreakingAndFullDiff (published overload) now takes the changelog folder name separately from the diffed package version, since they differ while a line is in preview.

Effect for SkiaSharp:

Added 3.119.4 (shipped stable, previously missing), 4.148.0, 4.150.0
Removed superseded preview-only 3.0.0, 3.119.3 (predated versions.json)
Unchanged every other stable line; genuine 4-part stables (1.53.1.1, 1.68.2.1) preserved

Once merged, the API Diff workflow regenerates #4173 with the collapsed set. Validated: Cake dryrun compiles clean; emit rule simulated against the live feed produces exactly the table above (53 cores, zero preview-named dirs).


2. Release notes: drop per-PR effort metric, resolve skia links locally (generate-release-notes.py)

The --all run was timing out: the dropped "effort" metric and the companion-skia-PR link resolution each issued one network fetch per PR (162 serial round-trips).

  • Removed the per-PR effort metric entirely (unused in output).
  • Made skia-link resolution pure-local: detect submodule bumps via git rev-parse <commit>:externals/skia vs parent, batch-fetch only the bumped SHAs (blobless + shallow) into externals/skia, and read each skia commit subject to parse the companion mono/skia#N PR. No per-PR network calls.

Result: companion links for 22/23 skia-bumping PRs in 4.148 (the one miss is an anomalous backwards gitlink with no PR), and a cold full --all run drops to ~35s.


Supersedes #4176 (release-notes change folded into this PR).

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

The unified-versioning refactor (#4171) started passing
--nugetDiffPrerelease=true to docs-api-diff-past so the 4.x line — which
ships only as previews/rcs until it stabilises — would get changelogs.
But the loop still emitted a folder for *every* enumerated version, so
every intermediate preview now produced its own directory: the API Diff
run filled bot/api-diff (PR #4173) with 4.147.0-preview.{1,2,3},
4.148.0-rc.1.2 and 4.150.0-preview.1.1 dirs across every tracked package
(~2,300 added files). Historically changelogs/ held stable versions only.

Collapse the feed to one entry per release line (the numeric version core
with the prerelease label stripped: 4.148.0-rc.1.2 -> 4.148.0; a genuine
4-part stable like 1.49.2.1 keeps its fourth digit) and emit a single
rollup changelog per line, diffed against the line's representative
package — the newest stable if it shipped, otherwise the newest
prerelease. This mirrors the release-notes pages, which are stable-named
rollups of the previews in between.

A line is emitted when it shipped stable, or when it is an active dev line
(preview-only, not superseded, and newer than the last stable, e.g.
4.148/4.150). Abandoned preview-only lines (superseded in versions.json,
e.g. 4.147) and old never-shipped previews are skipped; their changes are
absorbed into the successor's cumulative diff. Baselines are unchanged:
versions.json compare_to overrides win, otherwise diff against the
previous emitted line.

For SkiaSharp this regenerates to the stable set plus the two active
lines: it adds 3.119.4 (the shipped stable, previously missing) and the
4.148.0/4.150.0 rollups, and prunes the superseded preview-only 3.0.0 and
3.119.3 directories that predated versions.json.

RunBreakingAndFullDiff (published overload) now takes the changelog folder
name separately from the diffed package version, since they differ while a
line is still in preview.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

📦 Try the packages from this PR

Warning

Do not run these scripts without first reviewing the code in this PR.

Step 1 — Download the packages

bash / macOS / Linux:

curl -fsSL https://raw.githubusercontent.com/mono/SkiaSharp/main/scripts/get-skiasharp-pr.sh | bash -s -- 4177

PowerShell / Windows:

iex "& { $(irm https://raw.githubusercontent.com/mono/SkiaSharp/main/scripts/get-skiasharp-pr.ps1) } 4177"

Step 2 — Add the local NuGet source

dotnet nuget add source ~/.skiasharp/hives/pr-4177/packages --name skiasharp-pr-4177
More options
Option Description
--successful-only / -SuccessfulOnly Only use successful builds
--force / -Force Overwrite previously downloaded packages
--list / -List List available artifacts without downloading
--build-id ID / -BuildId ID Download from a specific build

Or download manually from Azure Pipelines — look for the nuget artifact on the build for this PR.

Remove the source when you're done:

dotnet nuget remove source skiasharp-pr-4177

The "Update Release Notes" agentic workflow
(.github/workflows/update-release-notes.md, timeout-minutes: 15) timed out on
its first post-merge run. The cause was the per-PR "effort" badge: it ran one
network `git fetch refs/pull/N/head` per PR to recover pre-squash commit
detail. For the 4.148 release that is 162 serial fetches, each paying full
proxied-TLS setup (~4s) behind the workflow's Squid firewall — ~11 min plus an
unshallow, blowing the 15-min cap. On timeout `safe_outputs` is skipped, so no
PR is ever opened and the cold author-cache never warms: the cold start cannot
self-heal.

The effort value (commit count + working days) earned none of that cost: it is
not rendered in the notes and the model never surfaced it. Everything actually
shown — title, author, number, body, companion skia link — already comes from a
single local `git log`.

Changes:

  * Remove the effort computation and its rendering entirely
    (_get_pr_effort_from_git, compute_pr_effort, _fetch_skia_pr_effort,
    add_pr_effort). This alone takes a cold full `--all` from a 15-min timeout
    to ~35s.
  * Keep the companion `skia: mono/skia#N` hint, parsed locally from the PR
    body, and add the `mono/skia#N` shorthand that milestone bumps use.
  * Resolve the companion link for submodule-bump PRs that don't spell it out,
    purely locally. The bump and its target skia SHA are read from the
    superproject tree (`git rev-parse <commit>:externals/skia` vs its parent);
    the bumped skia commit's own subject ((#N) / "Merge pull request #N") is then
    read from the submodule. The only network is one batched, blobless, shallow
    `git fetch` of just the bumped SHAs; already-present commits are skipped, so
    `--all` fetches each skia commit at most once (a handful per run, not 162).

For 4.148 this lifts companion-link coverage of skia-bumping PRs from 18 to 22
of 23. The single remaining gap is an anomalous backwards gitlink (PR #4081
resets the submodule to mono/skia's scaffold root, which has no PR); its real
companion, skia #242, is already linked via the forward bump. The other
historical misses are skia commits pushed directly to mono/skia with no PR at
all, so there is nothing to link.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mattleibow mattleibow changed the title [docs] Emit one API changelog per release line, not per preview [docs] Unify changelog + release-notes generation: stable rollups and local skia links Jun 17, 2026
@mattleibow
mattleibow merged commit 4b8e43f into main Jun 17, 2026
3 of 4 checks passed
@mattleibow
mattleibow deleted the mattleibow/api-diff-stable-rollup branch June 17, 2026 00:09
@github-actions

Copy link
Copy Markdown
Contributor

📖 Documentation Preview

The documentation for this PR has been deployed and is available at:

🔗 View Staging Site
🔗 View Staging Docs
🔗 View Staging Gallery (Blazor)
🔗 View Staging Gallery (Uno Platform)
🔗 View Staging SkiaFiddle

This preview will be updated automatically when you push new commits to this PR.


This comment is automatically updated by the documentation staging workflow.

mattleibow added a commit that referenced this pull request Jun 17, 2026
A version's "released" and "unreleased" states are orthogonal: a version
can be on NuGet as a shipped prerelease (e.g. 4.150.0-preview.1) AND have
newer, not-yet-released commits on its head branch (main). The original
two-file model gave each of those its own page — a full-rollup released
`{version}.md` and a small-delta `{version}-unreleased.md` — and they
coexisted while the version was in flight.

A five-PR refactor stack (#4174, #4177, #4179) collapsed this into a
single page per version:

  * #4174 added `--all` + `cleanup_stale_unreleased`, which deleted
    `{v}-unreleased.md` whenever `{v}.md` existed — killing the
    coexisting in-flight delta page.
  * #4179 (R2) forced `main`/`.x` heads to honor `versions.json`
    `compare_to`, turning the small "what may ship next" delta into a
    full rollup.
  * #4179 (R1) remapped prerelease branches to `-unreleased` and
    deferred them, collapsing released + unreleased onto one file.

Restore the original delta semantics while keeping the genuinely-good
additions from the stack (versions.json supersession + compare_to on the
RELEASED rollup pages, preview-milestone rollup, contributor credit,
local skia links, `--all` idempotency):

  * `_page_filename`: versioned branch (incl. -rc/-preview) -> released
    `{version}.md`; `main`/`.x` head -> `{version}-unreleased.md`.
  * `determine_diff_range` (main + servicing `.x`): drop the versions.json
    `compare_to` override on heads so the unreleased page is again the
    small delta from the last release to head, not a rollup. `compare_to`
    still drives the released rollup pages produced by versioned branches.
  * `cmd_all`: process main + servicing + ALL canonical versioned branches;
    no deferral (distinct filenames never collide).
  * `cleanup_stale_unreleased`: delete `{v}-unreleased.md` only once the
    line advances to a strictly-higher version in the same minor — keeps
    the in-flight released+unreleased pair, still prunes obsolete deltas
    (e.g. 3.119.4-unreleased once 3.119.5 appears).
  * `_write_page`: an unreleased head with an empty delta emits no page
    (and prunes any stale one) — nothing to ship next.

Net effect on the 4.x line: 4.148.0.md / 4.150.0.md are full released
rollups (with preview milestones + supersede banners), 4.150.0-unreleased.md
is the small `release/4.150.0-preview.1..main` delta, and 4.148.0 has no
unreleased page because release/4.148.x has zero commits past the rc.
Verified by running the script for real (`--all`): it emitted exactly
those pages and listed only the changed ones under "Files to polish".

Keep the page model where it belongs — in the SCRIPT. The script owns ALL
file creation, naming, diff ranges, supersession, and cleanup; the AI/skill
only polishes the prose of the files the script lists. The detailed model
now lives in the script's module + function docstrings, and SKILL.md is
trimmed to a clear "division of responsibility" instead of duplicating
(and drifting from) the naming rules.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
mattleibow added a commit that referenced this pull request Jun 17, 2026
A version's "released" and "unreleased" states are orthogonal: a version
can be on NuGet as a shipped prerelease (e.g. 4.150.0-preview.1) AND have
newer, not-yet-released commits on its head branch (main). The original
two-file model gave each of those its own page — a full-rollup released
`{version}.md` and a small-delta `{version}-unreleased.md` — and they
coexisted while the version was in flight.

A five-PR refactor stack (#4174, #4177, #4179) collapsed this into a
single page per version:

  * #4174 added `--all` + `cleanup_stale_unreleased`, which deleted
    `{v}-unreleased.md` whenever `{v}.md` existed — killing the
    coexisting in-flight delta page.
  * #4179 (R2) forced `main`/`.x` heads to honor `versions.json`
    `compare_to`, turning the small "what may ship next" delta into a
    full rollup.
  * #4179 (R1) remapped prerelease branches to `-unreleased` and
    deferred them, collapsing released + unreleased onto one file.

Restore the original delta semantics while keeping the genuinely-good
additions from the stack (versions.json supersession + compare_to on the
RELEASED rollup pages, preview-milestone rollup, contributor credit,
local skia links, `--all` idempotency):

  * `_page_filename`: versioned branch (incl. -rc/-preview) -> released
    `{version}.md`; `main`/`.x` head -> `{version}-unreleased.md`.
  * `determine_diff_range` (main + servicing `.x`): drop the versions.json
    `compare_to` override on heads so the unreleased page is again the
    small delta from the last release to head, not a rollup. `compare_to`
    still drives the released rollup pages produced by versioned branches.
  * `cmd_all`: process main + servicing + ALL canonical versioned branches;
    no deferral (distinct filenames never collide).
  * `cleanup_stale_unreleased`: delete `{v}-unreleased.md` only once the
    line advances to a strictly-higher version in the same minor — keeps
    the in-flight released+unreleased pair, still prunes obsolete deltas
    (e.g. 3.119.4-unreleased once 3.119.5 appears).
  * `_write_page`: an unreleased head with an empty delta emits no page
    (and prunes any stale one) — nothing to ship next.

Net effect on the 4.x line: 4.148.0.md / 4.150.0.md are full released
rollups (with preview milestones + supersede banners), 4.150.0-unreleased.md
is the small `release/4.150.0-preview.1..main` delta, and 4.148.0 has no
unreleased page because release/4.148.x has zero commits past the rc.
Verified by running the script for real (`--all`): it emitted exactly
those pages and listed only the changed ones under "Files to polish".

Keep the page model where it belongs — in the SCRIPT. The script owns ALL
file creation, naming, diff ranges, supersession, and cleanup; the AI/skill
only polishes the prose of the files the script lists. The detailed model
now lives in the script's module + function docstrings, and SKILL.md is
trimmed to a clear "division of responsibility" instead of duplicating
(and drifting from) the naming rules.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
mattleibow added a commit that referenced this pull request Jun 17, 2026
A version's "released" and "unreleased" states are orthogonal: a version
can be on NuGet as a shipped prerelease (e.g. 4.150.0-preview.1) AND have
newer, not-yet-released commits on its head branch (main). The original
two-file model gave each of those its own page — a full-rollup released
`{version}.md` and a small-delta `{version}-unreleased.md` — and they
coexisted while the version was in flight.

A five-PR refactor stack (#4174, #4177, #4179) collapsed this into a
single page per version:

  * #4174 added `--all` + `cleanup_stale_unreleased`, which deleted
    `{v}-unreleased.md` whenever `{v}.md` existed — killing the
    coexisting in-flight delta page.
  * #4179 (R2) forced `main`/`.x` heads to honor `versions.json`
    `compare_to`, turning the small "what may ship next" delta into a
    full rollup.
  * #4179 (R1) remapped prerelease branches to `-unreleased` and
    deferred them, collapsing released + unreleased onto one file.

Restore the original delta semantics while keeping the genuinely-good
additions from the stack (versions.json supersession + compare_to on the
RELEASED rollup pages, preview-milestone rollup, contributor credit,
local skia links, `--all` idempotency):

  * `_page_filename`: versioned branch (incl. -rc/-preview) -> released
    `{version}.md`; `main`/`.x` head -> `{version}-unreleased.md`.
  * `determine_diff_range` (main + servicing `.x`): drop the versions.json
    `compare_to` override on heads so the unreleased page is again the
    small delta from the last release to head, not a rollup. `compare_to`
    still drives the released rollup pages produced by versioned branches.
  * `cmd_all`: process main + servicing + ALL canonical versioned branches;
    no deferral (distinct filenames never collide).
  * `cleanup_stale_unreleased`: delete `{v}-unreleased.md` only once the
    line advances to a strictly-higher version in the same minor — keeps
    the in-flight released+unreleased pair, still prunes obsolete deltas
    (e.g. 3.119.4-unreleased once 3.119.5 appears).
  * `_write_page`: an unreleased head with an empty delta emits no page
    (and prunes any stale one) — nothing to ship next.

Net effect on the 4.x line: 4.148.0.md / 4.150.0.md are full released
rollups (with preview milestones + supersede banners), 4.150.0-unreleased.md
is the small `release/4.150.0-preview.1..main` delta, and 4.148.0 has no
unreleased page because release/4.148.x has zero commits past the rc.
Verified by running the script for real (`--all`): it emitted exactly
those pages and listed only the changed ones under "Files to polish".

Keep the page model where it belongs — in the SCRIPT. The script owns ALL
file creation, naming, diff ranges, supersession, and cleanup; the AI/skill
only polishes the prose of the files the script lists. The detailed model
now lives in the script's module + function docstrings, and SKILL.md is
trimmed to a clear "division of responsibility" instead of duplicating
(and drifting from) the naming rules.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
mattleibow added a commit that referenced this pull request Jun 17, 2026
A version's "released" and "unreleased" states are orthogonal: a version
can be on NuGet as a shipped prerelease (e.g. 4.150.0-preview.1) AND have
newer, not-yet-released commits on its head branch (main). The original
two-file model gave each of those its own page — a full-rollup released
`{version}.md` and a small-delta `{version}-unreleased.md` — and they
coexisted while the version was in flight.

A five-PR refactor stack (#4174, #4177, #4179) collapsed this into a
single page per version:

  * #4174 added `--all` + `cleanup_stale_unreleased`, which deleted
    `{v}-unreleased.md` whenever `{v}.md` existed — killing the
    coexisting in-flight delta page.
  * #4179 (R2) forced `main`/`.x` heads to honor `versions.json`
    `compare_to`, turning the small "what may ship next" delta into a
    full rollup.
  * #4179 (R1) remapped prerelease branches to `-unreleased` and
    deferred them, collapsing released + unreleased onto one file.

Restore the original delta semantics while keeping the genuinely-good
additions from the stack (versions.json supersession + compare_to on the
RELEASED rollup pages, preview-milestone rollup, contributor credit,
local skia links, `--all` idempotency):

  * `_page_filename`: versioned branch (incl. -rc/-preview) -> released
    `{version}.md`; `main`/`.x` head -> `{version}-unreleased.md`.
  * `determine_diff_range` (main + servicing `.x`): drop the versions.json
    `compare_to` override on heads so the unreleased page is again the
    small delta from the last release to head, not a rollup. `compare_to`
    still drives the released rollup pages produced by versioned branches.
  * `cmd_all`: process main + servicing + ALL canonical versioned branches;
    no deferral (distinct filenames never collide).
  * `cleanup_stale_unreleased`: delete `{v}-unreleased.md` only once the
    line advances to a strictly-higher version in the same minor — keeps
    the in-flight released+unreleased pair, still prunes obsolete deltas
    (e.g. 3.119.4-unreleased once 3.119.5 appears).
  * `_write_page`: an unreleased head with an empty delta emits no page
    (and prunes any stale one) — nothing to ship next.

Bucket PRs per preview so the AI can summarize each milestone directly.
A released rollup spans several prerelease tags; a single flat PR list
left the AI guessing which work shipped in which preview. The script now
partitions the PRs into per-preview buckets via git ancestry — each PR
under the EARLIEST milestone tag whose range contains its commit, i.e.
the preview it first shipped in (`bucket_prs_by_milestone`). The buckets
exhaustively cover the range (every PR lands in exactly one), so they ARE
the full list — no separate flat list to drift. The AI renders one
`## Preview N` section per bucket and merges them for the Highlights.
Verified: 4.148.0.md partitions its 162 PRs as RC1 35 + P3 37 + P2 27 +
P1 63 = 162 (no leftover); 4.150.0.md as Preview 1 = 2; the
4.150.0-unreleased.md delta has no previews and stays a flat 7-PR list.

Keep the page model where it belongs — in the SCRIPT. The script owns ALL
file creation, naming, diff ranges, supersession, cleanup, and PR
bucketing; the AI/skill only polishes prose and, on any anomaly (missing
or unexpected page, bad data), STOPS and reports rather than editing the
script or working around it. The model lives in the script's module +
function docstrings; SKILL.md is trimmed to a "division of responsibility"
plus how to consume the pre-bucketed PR data, instead of duplicating (and
drifting from) the naming rules.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant