Unify API changelog + release-notes versioning via shared versions.json#4171
Conversation
Introduce scripts/versions.json as a shared config for version comparison overrides and supersession tracking. Both the API changelog (Cake) and release notes (Python) systems now read this config. Changes: - scripts/versions.json: Override-only config declaring superseded versions (3.0→3.116, 3.118→3.119, 3.119.3→3.119.4, 4.147→4.148) and compare_to baselines (e.g. 4.148.0 compares to 3.119.4, not 4.147.0) - docs.cake (docs-api-diff-past): Add IncludePrerelease filter, load versions.json, skip superseded versions, use compare_to overrides - generate-release-notes.py: Add --all mode (iterates all branches), load versions.json for compare_to/superseded_by, add skip-unchanged logic (compare PR count + diff range before writing) - api-diff.yml: Add push triggers (main, release/**, v* tags), single bot/api-diff PR branch, past/preview modes, default prerelease=true - update-release-notes.md: Single bot/release-notes PR branch, use --all mode, skip AI polishing when nothing changed The result: max 2 automated PRs (down from 5), consistent comparison logic, and no unnecessary AI polishing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…f workflow Refines the unified versions.json-based comparison system for API changelogs and release notes: - Superseded versions are now GENERATED (they keep their own page/changelog); the supersede marker only excludes them from being a comparison *baseline*. Removed the wrong generation-skip in the Python --all mode and the Cake docs-api-diff-past target; both now walk back past superseded versions when picking a baseline (e.g. 4.148 compares to 3.119.4, skipping 4.147). - _is_valid_stable_base() now also rejects config-superseded versions for consistency with versions.json. - New `changelog` skill (.agents/skills/changelog/) with a runnable generate-changelogs.sh that wraps the Cake targets (past + preview modes) so changelogs can be regenerated locally exactly as CI does. - Slimmed api-diff.yml to call the script; generation logic now lives in the skill, the workflow only handles the PR. - Documented versions.json + --all mode in the release-notes SKILL.md. - Added extensive what/why comments across the script, Cake target, and workflow. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Removes duplicated/renumbered steps between the workflows and their skills: - release-notes/SKILL.md gains an "Automated workflow mode" section that is the canonical, numbered procedure (prep main → --all → polish → one PR). - update-release-notes.md is reduced to a pointer at that section plus the CI-specific overrides (bot/release-notes branch, base main, no-op rule). No more restated/renumbered steps. - changelog/SKILL.md is declared the source of truth; api-diff.yml header is trimmed to a pointer plus its workflow-specific choices instead of re-documenting modes/baselines. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The release-notes skill is AI-facing instructions for polishing pages from the data the script bakes into each file. versions.json baselines/supersession are script internals the polishing AI never needs: - Removed the "Configuration: scripts/versions.json" section from release-notes/SKILL.md. - Condensed the supersede-detection mechanics (base selection, main-version heuristic) to just what the AI renders: read the data-block markers, keep the script-generated headers verbatim. versions.json stays documented in its own $comment fields and in the generation-focused changelog skill. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Corrects the earlier over-correction that pushed a workflow-shaped procedure into the skill. Clean separation of concerns: - release-notes/SKILL.md: removed the "Automated workflow mode" section and the stray workflow cross-reference. The skill now only describes the work — run the script (incl. --all) and polish the pages. - update-release-notes.md: owns prep (clean main checkout/fetch) and automation (single bot/release-notes PR), and delegates the work to the skill without restating any polish steps. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Addresses genuine confusion points the executing model flagged:
Skill (release-notes/SKILL.md):
- Fix data-block key bug: superseded marker is `superseded:` not
`superseded_by:` (matches what the script writes into the file).
- Resolve frontmatter contradiction ("manual only" vs body "used by workflow").
- Step 1: add caveat to skip to --all when running unattended (no user to ask).
- Step 2: state the script runs from the repository root.
- Parallelization: clarify sub-agent fan-out is interactive-only; run
sequentially when unattended.
Workflow (update-release-notes.md):
- Clarify the PR is created by the create-pull-request safe-output from edited
files (no manual commit/push) on the single bot/release-notes branch.
- Make the no-op path explicit (no edits => existing PR left untouched).
- Note the --all script fetches release/* branches itself, so prep only needs
to land on main.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extract the duplicated diff mechanics and versions.json logic out of the two API-changelog targets into file-scope helpers, and make both targets honour supersession consistently. - Add shared helpers near CopyChangelogs: LoadVersionsConfig, IsVersionSuperseded, FindCompareToBaseline, and a two-overload RunBreakingAndFullDiff (local .nupkg vs published version) that performs the breaking-only + full diff passes and copies into changelogs/. - docs-api-diff-past now calls the shared helpers (removes its local IsSuperseded/FindCompareToOverride copies and the inline diff dance). - docs-api-diff (current build) now loads versions.json and selects its baseline via compare_to override, else the newest published version that is not superseded and not the build's own version. This fixes a latent bug where it could diff against a superseded preview (e.g. 4.147) picked by GetLatestAsync, and makes the two targets treat supersession the same. Behaviour for the committed historical changelogs (docs-api-diff-past) is unchanged; the only behaviour change is the current-mode baseline, whose output is an uncommitted CI artifact. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Regenerate the lock file so it matches the current .md frontmatter (single concurrency group, 15-minute timeout, refreshed prompt hashes). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reorder the script so the Task definitions read top-to-bottom (how the docs build fits together) with all helper/utility functions grouped below them, just before RunTarget. Pure reordering — no behavioural or content changes (Cake/Roslyn hoists the methods, so call-before-declare is fine). Verified with --dryrun on the affected targets. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two related fixes found by review and confirmed by running the script: 1. resolve_superseded_by now treats a versions.json entry as authoritative. Previously a successor entry carrying only compare_to/supersedes (no status key) — e.g. 4.148.0 — fell through to auto-detection and, because the 4.x line has no stable tag yet, was wrongly labelled 'superseded by 4.150.0'. Now only an explicit status: superseded marks a version superseded, matching Cake's IsVersionSuperseded. 2. _is_content_unchanged now also compares status + superseded/supersedes markers. Marking a preview line superseded does not change its own diff range or PR count (supersession only affects it as a baseline for later versions), so the old check skipped the rewrite and left the page without its 'Superseded by' banner. cmd_all now computes supersession before the skip-check so the banner change forces a rewrite. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
API changelog generation is purely mechanical (Mono.ApiTools.NuGetDiff →
markdown) with no AI polishing, so it does not need a skill. Remove the
.agents/skills/changelog skill and its generate-changelogs.sh wrapper, and have
the api-diff workflow run the Cake target directly:
dotnet cake --target=docs-api-diff-past --nugetDiffPrerelease=true
This is the exact command a human runs locally, so there is no skill/script/
workflow drift to keep in sync. Baseline and superseded-version handling still
come from scripts/versions.json via the Cake target.
Automated 'preview' mode is dropped — previewing an unreleased branch's diff is
now a documented manual procedure (git-fetch + VERSIONS.txt overlay + the
existing docs-download-output / docs-api-diff targets) in
documentation/dev/writing-docs.md. The Cake targets themselves are unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
📦 Try the packages from this PRWarning 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 -- 4171PowerShell / Windows: iex "& { $(irm https://raw.githubusercontent.com/mono/SkiaSharp/main/scripts/get-skiasharp-pr.ps1) } 4171"Step 2 — Add the local NuGet source dotnet nuget add source ~/.skiasharp/hives/pr-4171/packages --name skiasharp-pr-4171More options
Or download manually from Azure Pipelines — look for the Remove the source when you're done: dotnet nuget remove source skiasharp-pr-4171 |
The Prepare Build stage's 'Validate cache config coverage' check (repo-deps.py validate) failed because the new scripts/versions.json was not assigned to any job or exclude list. It drives the changelog baselines generated by the managed/package/api_diff job, so add it to that job's include list — changing it correctly invalidates only the api_diff cache. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Unifies version/baseline selection for documentation automation by introducing a shared scripts/versions.json override config that both the Cake API-diff pipeline and the release-notes generator use to handle superseded/abandoned versions consistently.
Changes:
- Added
scripts/versions.jsonas the shared source of truth forcompare_tooverrides and supersession tracking. - Refactored Cake API-diff targets to use shared helpers and to consistently skip superseded versions as baselines.
- Updated the release-notes generator and docs/workflows to align automation and guidance with the unified versioning model.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/versions.json | Introduces shared version comparison/supersession overrides consumed by both systems. |
| scripts/infra/docs/docs.cake | Refactors API-diff targets and adds shared helpers to load/apply versions.json rules. |
| scripts/infra/caching/repo-deps.json | Adds scripts/versions.json to the api_diff cache include set. |
| documentation/dev/writing-docs.md | Documents local regeneration and manual preview flow for API changelogs. |
| .github/workflows/update-release-notes.md | Updates agentic workflow guidance and concurrency/timeouts for release-notes automation. |
| .github/workflows/update-release-notes.lock.yml | Recompiled lock file to match updated workflow frontmatter/config. |
| .github/workflows/api-diff.yml | Simplifies workflow to run docs-api-diff-past directly and changes triggers/concurrency/PR behavior. |
| .agents/skills/release-notes/SKILL.md | Updates skill guidance for unattended --all usage and supersession messaging expectations. |
| .agents/skills/release-notes/scripts/generate-release-notes.py | Adds versions.json support, improves supersession resolution, and introduces idempotent --all. |
|
📖 Documentation Preview The documentation for this PR has been deployed and is available at: 🔗 View Staging Site This preview will be updated automatically when you push new commits to this PR. This comment is automatically updated by the documentation staging workflow. |
- api-diff.yml: PRERELEASE could not be disabled via workflow_dispatch — the `&& inputs.prerelease || 'true'` ternary collapsed false back to 'true'. Rewrite so only an explicit dispatch with prerelease=false yields 'false'. - api-diff.yml: 'Check for changes' tested mere file existence, but changelogs/README.md is committed so it was always true (publish job ran every run). Use 'git status --porcelain changelogs/' to detect real changes. - docs.cake: LoadVersionsConfig used a hard (JArray) cast that throws when 'versions' is present but not an array; use 'as JArray' for the intended safe fallback. - generate-release-notes.py: the main branch's compare_to override only resolved via release/* branch prefix; add exact-branch and v<compare_to> tag fallbacks to match the versioned-branch path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| git commit -m "[docs] Update API changelogs" \ | ||
| -m "Regenerated by the api-diff workflow using Mono.ApiTools.NuGetDiff. | ||
| Compares all published NuGet.org versions (including prereleases). | ||
| Respects scripts/versions.json for comparison overrides." |
There was a problem hiding this comment.
Not a bug here — these lines live inside a YAML run: | block scalar. YAML strips the block's common leading indentation (10 spaces) before the shell ever sees the string, so the continuation lines arrive with zero leading whitespace. Verified by parsing the workflow: the commit body lines resolve to 'Compares all published NuGet.org versions (including prereleases).' and 'Respects scripts/versions.json for comparison overrides.' — no leading spaces. So the commit message body is not indented. Leaving as-is.
| gh pr create --base main --head "$PR_BRANCH" \ | ||
| --title "[docs] API diff for v${VERSION}" \ | ||
| --title "[docs] API changelogs" \ | ||
| --label "documentation" \ | ||
| --body "Automated API diff comparing v${VERSION} against the latest published NuGet.org release. Generated by the \`api-diff\` workflow using \`Mono.ApiTools.NuGetDiff\`. | ||
| --body "Automated API changelogs comparing all published NuGet.org versions. | ||
| Generated by the \`api-diff\` workflow using \`Mono.ApiTools.NuGetDiff\`. | ||
|
|
||
| Respects \`scripts/versions.json\` for comparison overrides and supersession tracking. | ||
|
|
||
| --- | ||
| - **Version:** ${VERSION} | ||
| - **Workflow run:** ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" |
There was a problem hiding this comment.
Same as above — false positive. The --body string is inside a run: | block scalar, so YAML strips the common 10-space indentation before the shell sees it. Parsing the workflow confirms the body lines resolve with no leading spaces (e.g. 'Generated by the ...', '---', '- **Workflow run:** ...'), so nothing becomes a Markdown code block. The PR body renders correctly; no change needed.
Follow-up to the GPT-5.5 + Opus 4.8 deep review. Addresses correctness and Cake/Python consistency issues beyond the original 4 Copilot comments. generate-release-notes.py: - release_branch_sort_key: rewrite to a robust 6-tuple (major, minor, patch, subpatch, stage, prenum). The old regex returned (0,0,0,0,0) for -rc.N, undotted -previewN, and 4-segment versions, so real target branches like release/4.148.0-rc.1 sorted as garbage. Stages: alpha<beta<preview<rc<stable; unknown labels rank just below stable. - cmd_all: render each output page from exactly ONE canonical branch per version (highest sort key), so stable pages are no longer clobbered by a later-alphabetised preview branch (e.g. 3.119.2.md vs 3.119.2-preview.*). - determine_diff_range: anchor compare_to matching on version_from_branch(b) == compare_to instead of a startswith prefix, so "3.116.1" never also matches release/3.116.10. - resolve_superseded_by: make versions.json fully authoritative (no auto-detection), matching Cake's IsVersionSuperseded exactly. - _regen_unreleased: add _is_content_unchanged guard before both writes so reruns are idempotent and don't churn unreleased pages; include supersedes metadata. api-diff.yml: gh pr list --jq '.[0].number // empty' to avoid a literal null. docs.cake: clarify the IsVersionSuperseded major.minor.patch matching comment. writing-docs.md: document that the two systems intentionally differ in granularity (per-package vs cumulative) while sharing versions.json for supersession and compare_to. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Deep review (GPT-5.5 + Opus 4.8) — findings & fixesBoth reviewers independently converged on the same root area. After verifying every claim against the real git branches, fixed in Major
Minor
Verification (>5 iterations): py_compile + YAML parse + Cake dryrun all pass; sort-key unit tests pass; baseline resolution confirmed for Also documented in |
- scripts/versions.json: remove the `$schema` pointing at the draft 2020-12 meta-schema. It made the data file look like a JSON Schema to editors, which would try to validate it as one. There is no real schema for this file, so drop the key (the loaders only read the `versions` array). - docs.cake: the `ref path` verbose log in the refPath branch printed `libPath` instead of `refPath`, making verbose logs misleading. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Third-round audit fixes for the unified versions.json docs system: - Supersession is now config-only end-to-end. Removed dead auto-detection (_all_known_base_versions, _main_upcoming_version, detect_superseded_by, extract_base_version); resolve_superseded_by/detect_supersedes read versions.json directly, matching Cake's IsVersionSuperseded. - Subpatch (4-segment) baseline support in find_previous_stable_base and determine_diff_range (release/X.Y.Z.W). - Extracted _resolve_compare_to helper (was ~22 lines duplicated across the main and versioned diff paths). - Unified the three duplicated page-write blocks into _write_page, with _compute_page_status / _page_filename / _canonical_branches_by_version helpers; cmd_branch now remaps a versioned branch to its canonical branch so a preview ref can't clobber a stable page. - api-diff changelogs are now authoritative on both sides: docs-api-diff-past clears per-package changelog subdirs before regenerating (prunes stale *.breaking.md / removed packages; preserves README.md), and the workflow clears + restages so deletions propagate. - versions.json: dropped the dead supersedes arrays (auto-derived now). - Doc/comment drift: module + compute_pr_effort docstrings, and the writing-docs.md baseline-divergence caveat. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Third-round audit — all findings resolvedDid a whole-file coherence pass (not just the diff) across the four touched files plus Genuine bugs fixed
Coherence / dead-code cleanup
Verification
The one remaining item ( |
…ors correctly (#4174) release-notes: fix --all timeout, migrate pages, and credit contributors correctly (#4174) Context: builds on #4171 (08ac410), which landed the unified docs-automation system but left the update-release-notes workflow timing out and crediting contributors from guessed handles. The workflow's first `--all` run after #4171 timed out after 15 minutes. This PR makes the run cheap, migrates every page onto the skip-able metadata format, and replaces email-guessed contributor handles with authoritative GitHub logins. ~~ Fast-path: compute PR effort only after the skip check ~~ `_write_page` fetched per-PR effort (commit count + working days, ~1s/PR via `refs/pull/N/head`) for every page before `_is_content_unchanged`. The 4.148 page alone (162 PRs) took ~150s; across 66 pages an all-unchanged run blew past the 15-min agent cap. - Split the effort loop out of `get_prs_from_diff` (now a cheap git-log parse) into a new `add_pr_effort()`, called from `_write_page` only after the unchanged check passes. - Measured: 4.148 ~170s -> ~0.12s when skipped; a migrated `--all` now runs in ~30s instead of timing out. ~~ One-time migration: regenerate + re-polish every page ~~ Only 7 of 65 pages carried the `RAW PR DATA` block the skip check relies on; the other 58 legacy pages could never be skipped. Regenerated every page so all now carry the block, then re-polished each from its own raw PR data per TEMPLATE.md (Highlights, grouped New Features with Skia milestone bumps first, Bug Fixes, Platform Support, Community Contributors, Links). A second review pass removed hallucinated content, fabricated PRs, and bot credits from prose. ~~ Credit contributors with authoritative GitHub logins ~~ The generator previously derived handles from the commit-email local-part (`email.split("@")[0]`). For GitHub noreply emails that works, but for everyone else it produced dead links (`@jerome.laban`, `@sebastien.pouliot`) or, worse, valid-but-wrong mentions (`@jon`, `@martin`) that ping unrelated users. - `_login_from_email` now returns the noreply-embedded login or None — no more guessing. - Unresolved authors are looked up via the GitHub GraphQL API in batches of 50 and cached in scripts/infra/docs/pr-authors.json (921 entries, 0 unresolved) so CI stays warm and offline; truly unresolvable authors fall back to a plain name with no @ and no link. - Noreply-first ordering preserves repo conventions (@Copilot, the *[bot] suffixes) that a GraphQL-for-all lookup would have mis-credited. - All existing pages migrated in place: only broken handle tokens and their profile URLs were rewritten (longest-token-first, boundary-safe), leaving the reviewed prose otherwise untouched. ~~ Relocate docs-gen data into scripts/infra/docs/ ~~ versions.json (comparison/supersession config) and pr-authors.json (login cache) are data files for the docs-generation systems — the release-notes generator and the api-diff Cake infra in scripts/infra/docs/docs.cake. Moved both next to that code and updated the two code paths plus the references in writing-docs.md, api-diff.yml, and SKILL.md. ~~ Seed a regeneration test ~~ Intentionally drop the generated 4.148.0/4.150.0 pages, their TOC/index entries, and the 134 matching cache entries (921 -> 787). The post-merge `--all` run should recreate both pages from release/4.148.0-rc.1 and release/4.150.0-preview.1, re-resolve their authors (cache now cold), repopulate the cache, and re-link both versions — exercising the create/update path end to end on a real run. Co-authored-by: Matthew Leibowitz <mattleibow@live.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… local skia links (#4177) [docs] Unify changelog + release-notes generation: stable rollups and local skia links (#4177) Context: #4171 (shared versions.json), #4173 (bot/api-diff PR), #4176 (folded in here) Two fixes to the shared versions.json-driven docs pipeline, shipped together because they are the two halves of the same "one rollup per release line" model. ~~ API changelogs: one folder per release line, not per preview ~~ The unify-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 at all. But the loop still emitted a folder for *every* enumerated version, so each 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. Collapse the feed to one entry per release line (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. * 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. 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. Once merged, the API Diff workflow regenerates #4173 with the collapsed set. ~~ Release notes: drop per-PR effort metric, resolve skia links locally ~~ The --all run was timing out: the now-removed "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 (it never appeared in output). * Made skia-link resolution pure-local: detect submodule bumps via git rev-parse <commit>:externals/skia against the 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. Result: companion links resolved 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. Co-authored-by: Matthew Leibowitz <mattleibow@live.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lup (R1/R2/R3) The versions.json supersession migration (#4171/#4174) correctly made "which version supersedes which" deterministic via versions.json, but its move from per-push branch runs to a bulk `--all` pass silently regressed three documented behaviours. Pages were migrated in #4174 without the script emitting the data those pages used to contain, so the features were dropped on the next regeneration instead of failing loudly. Three regressions, each restored from a deterministic source (not heuristics): R1 — in-flight naming. 4.148 and 4.150 were written as terminal `{version}.md` and the cleanup pass then deleted the real `{version}-unreleased.md`. Root cause: `--all` picks the rc.1/preview.1 branch as canonical for the version. Fixed with a terminal-vs-in-flight rule in `_page_filename` (suffix-less `release/X.Y.Z` branch exists, or `status: superseded` in versions.json => `{version}.md`; otherwise `{version}-unreleased.md`) plus an ownership filter in `cmd_all` so the line head (main / `release/X.Y.x`) owns the `-unreleased` page and the matching rc/preview branch is deferred instead of colliding. R2 — in-flight `.x` rollup was a servicing-delta instead of the full cumulative rollup. `determine_diff_range` now honors the versions.json `compare_to` first (4.148.0 -> 3.119.4) and only does a servicing delta once the `.0` has actually shipped a stable tag. R3 — the trailing `## Preview N (date)` sections were lost. The original 3.119.2.md (#3763) and TEMPLATE.md have them and SKILL rules 9/10 mandate them, but #4174 deleted them on regen because the script never emitted preview data. `collect_preview_milestones` now enumerates them from the published prerelease git tags within the page's diff range (deduped to the latest build per milestone, dated, with chained compare links). A page rolls up a skipped predecessor minor's previews too, so the 4.148 page lists the 4.147 previews that match its rolled-up PRs. Two separate deterministic sources, no magic: versions.json answers "which version supersedes which"; published tags answer "which previews shipped and when". Documented in code comments (R1/R2/R3) and a new SKILL.md "Page naming & preview rollup" section so this can't silently regress on the next migration. Validated with an offline `--all` dry-run: correct `-unreleased` filenames, 4.148 rolls up 4.147 p1/p2/p3 + rc1, no bare 4.148.0.md/4.150.0.md, 3.119.x unchanged, and the 3.119.2 compare links reproduce the original page exactly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lup (R1/R2/R3) (#4179) release-notes: restore in-flight -unreleased naming + per-preview rollup (R1/R2/R3) (#4179) The versions.json supersession migration (#4171/#4174) correctly made "which version supersedes which" deterministic via versions.json, but its move from per-push branch runs to a bulk `--all` pass silently regressed three documented behaviours. #4174 migrated the pages without the script emitting the data those pages used to contain, so the features were dropped on the next regeneration instead of failing loudly. Three regressions, each restored from a deterministic source (not heuristics): R1 — in-flight naming. 4.148 and 4.150 were written as terminal `{version}.md` and the cleanup pass then deleted the real `{version}-unreleased.md`. Root cause: `--all` picks the rc.1/preview.1 branch as canonical for the version. Fixed with a terminal-vs-in-flight rule in `_page_filename` (a suffix-less `release/X.Y.Z` branch exists, or `status: superseded` in versions.json => `{version}.md`; otherwise `{version}-unreleased.md`) plus an ownership filter in `cmd_all` so the line head (main / `release/X.Y.x`) owns the `-unreleased` page and the matching rc/preview branch is deferred instead of colliding. R2 — in-flight `.x` rollup was a servicing delta instead of the full cumulative rollup. `determine_diff_range` now honors the versions.json `compare_to` first (4.148.0 -> 3.119.4) and only does a servicing delta once the `.0` has actually shipped a stable tag. R3 — the trailing `## Preview N (date)` sections were lost. The original 3.119.2.md (#3763) and TEMPLATE.md have them and SKILL rules 9/10 mandate them, but #4174 deleted them on regen because the script never emitted preview data. `collect_preview_milestones` now enumerates them from the published prerelease git tags within the page's diff range (deduped to the latest build per milestone, dated, with chained compare links). A page rolls up a skipped predecessor minor's previews too, so the 4.148 page lists the 4.147 previews that match its rolled-up PRs. Two separate deterministic sources, no magic: versions.json answers "which version supersedes which"; published tags answer "which previews shipped and when". Documented in code comments (R1/R2/R3) and a new SKILL.md "Page naming & preview rollup" section so this can't silently regress on the next migration. Validated with an offline `--all` dry-run: correct `-unreleased` filenames, 4.148 rolls up 4.147 p1/p2/p3 + rc1, no bare 4.148.0.md/4.150.0.md, 3.119.x unchanged, and the 3.119.2 compare links reproduce the original page exactly. Co-authored-by: Matthew Leibowitz <mattleibow@live.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
What & why
Unifies the two documentation-automation systems — API changelogs (Cake targets) and website release notes (Python script) — onto a single shared override config,
scripts/versions.json, and removes the bespoke per-system "magic" that made superseded/abandoned releases hard to handle.The driving problem: some versions are previewed then abandoned (e.g.
4.147.*never went stable, rolled up into4.148). Both systems need to (a) still generate output for the superseded version, but (b) skip it when picking a baseline for later versions — so4.148compares back to3.119.4, not to the dead4.147. Previously each system auto-detected this differently and got it wrong.Key changes
scripts/versions.json— single override config read by both Cake and the Python script. Entries declarestatus: superseded/superseded_by,compare_to, andsupersedes. Each version otherwise just compares to its predecessor.scripts/infra/docs/docs.cake) — extracted shared helpers (LoadVersionsConfig,IsVersionSuperseded,FindCompareToBaseline,RunBreakingAndFullDiff), unified supersession handling across thedocs-api-diff(current) anddocs-api-diff-past(historical) targets, and fixed a latent baseline bug in current mode. Helpers moved below the targets for readability.changelogskill — API diffing is purely mechanical (Mono.ApiTools.NuGetDiff→ markdown, no AI polishing), so it doesn't need a skill or a shell wrapper..github/workflows/api-diff.ymlnow runsdotnet cake --target=docs-api-diff-pastdirectly — the exact command a human runs locally, so there's no skill/script/workflow drift. Automated "preview" mode dropped; the manual preview procedure is documented indocumentation/dev/writing-docs.md.generate-release-notes.py):resolve_superseded_bynow treats anyversions.jsonentry as authoritative (a successor carrying onlycompare_tono longer falls through to auto-detection and gets mislabelled)._is_content_unchangednow also compares supersession metadata, so toggling supersession forces the page banner to refresh even when the diff range is unchanged.update-release-notesagentic workflow lock.Verification
Ran both systems over the full version set end-to-end:
docs-api-diff-past--allrelease/2.xmaintenance branch skipped)Both produce identical baselines:
4.147.03.119.44.148.03.119.44.150.04.148.0compare_tooverride4.148.0-rc.1.2/SkiaSharp.breaking.mdconfirmsAssembly Version Changed: 4.148.0.0 vs 3.119.0.0.No generated changelogs/release-notes are included in this PR — those are regenerated by the
api-diffworkflow post-merge. This PR is infrastructure only.Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com