Skip to content

Unify API changelog + release-notes versioning via shared versions.json#4171

Merged
mattleibow merged 17 commits into
mainfrom
mattleibow/changelog-generation-plan
Jun 16, 2026
Merged

Unify API changelog + release-notes versioning via shared versions.json#4171
mattleibow merged 17 commits into
mainfrom
mattleibow/changelog-generation-plan

Conversation

@mattleibow

Copy link
Copy Markdown
Contributor

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 into 4.148). Both systems need to (a) still generate output for the superseded version, but (b) skip it when picking a baseline for later versions — so 4.148 compares back to 3.119.4, not to the dead 4.147. Previously each system auto-detected this differently and got it wrong.

Key changes

  • Shared scripts/versions.json — single override config read by both Cake and the Python script. Entries declare status: superseded / superseded_by, compare_to, and supersedes. Each version otherwise just compares to its predecessor.
  • Cake API-diff targets refactored (scripts/infra/docs/docs.cake) — extracted shared helpers (LoadVersionsConfig, IsVersionSuperseded, FindCompareToBaseline, RunBreakingAndFullDiff), unified supersession handling across the docs-api-diff (current) and docs-api-diff-past (historical) targets, and fixed a latent baseline bug in current mode. Helpers moved below the targets for readability.
  • Removed the changelog skill — 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.yml now runs dotnet cake --target=docs-api-diff-past directly — 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 in documentation/dev/writing-docs.md.
  • Release-notes script fixes (generate-release-notes.py):
    • resolve_superseded_by now treats any versions.json entry as authoritative (a successor carrying only compare_to no longer falls through to auto-detection and gets mislabelled).
    • _is_content_unchanged now also compares supersession metadata, so toggling supersession forces the page banner to refresh even when the diff range is unchanged.
  • Recompiled the update-release-notes agentic workflow lock.

Verification

Ran both systems over the full version set end-to-end:

System Result
Cake docs-api-diff-past EXIT_OK · 25/25 packages · 2107 diffs · 0 errors
Python --all EXIT_OK · only benign warning (release/2.x maintenance branch skipped)

Both produce identical baselines:

Version Baseline Note
4.147.0 3.119.4 superseded, still generated
4.148.0 3.119.4 skips superseded 4.147.*
4.150.0 4.148.0 compare_to override

4.148.0-rc.1.2/SkiaSharp.breaking.md confirms Assembly 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-diff workflow post-merge. This PR is infrastructure only.

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

mattleibow and others added 11 commits June 15, 2026 20:46
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>
@github-actions

github-actions Bot commented Jun 15, 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 -- 4171

PowerShell / 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-4171
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-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>
@mattleibow
mattleibow requested a review from Copilot June 15, 2026 23:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.json as the shared source of truth for compare_to overrides 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.

Comment thread .github/workflows/api-diff.yml
Comment thread .github/workflows/api-diff.yml
Comment thread scripts/infra/docs/docs.cake Outdated
Comment thread .agents/skills/release-notes/scripts/generate-release-notes.py Outdated
@github-actions

github-actions Bot commented Jun 16, 2026

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.

- 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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Comment thread scripts/versions.json Outdated
Comment on lines +162 to +165
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."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 173 to 182
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}"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread scripts/infra/docs/docs.cake
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>
@mattleibow

Copy link
Copy Markdown
Contributor Author

Deep review (GPT-5.5 + Opus 4.8) — findings & fixes

Both reviewers independently converged on the same root area. After verifying every claim against the real git branches, fixed in e1aa3e6:

Major

  • release_branch_sort_key was broken for real target branches. The old regex release/(\d+)\.(\d+)\.(x|\d+(?:-preview\.\d+)?)$ returned (0,0,0,0,0) for -rc.N, undotted -previewN, and 4-segment versions — so release/4.148.0-rc.1 sorted as garbage. Rewrote as a robust 6-tuple (major, minor, patch, subpatch, stage, prenum) with stage order alpha<beta<preview<rc<stable.
  • cmd_all could clobber stable pages. git branch -r is alphabetical, so release/3.119.2 and its release/3.119.2-preview.* all rendered to 3.119.2.md and the last one processed won. Now each page is produced by exactly one canonical branch per version (highest sort key).
  • Config-authoritative supersession. resolve_superseded_by no longer auto-detects; versions.json is the single source of truth, matching Cake's IsVersionSuperseded exactly.

Minor

  • determine_diff_range: anchor compare_to matching on version_from_branch(b) == compare_to so 3.116.1 never also matches release/3.116.10.
  • _regen_unreleased: added the _is_content_unchanged guard before both writes (idempotent reruns) + supersedes metadata.
  • api-diff.yml: gh pr list --jq '.[0].number // empty' to avoid a literal null.
  • docs.cake: clarified the supersession-matching comment.

Verification (>5 iterations): py_compile + YAML parse + Cake dryrun all pass; sort-key unit tests pass; baseline resolution confirmed for 4.148.0-rc.1→3.119.4, 4.150.0-preview.1→4.148.0-rc.1, 3.119.4→3.119.2, 3.119.2→3.119.1 (matches Cake); single-branch run twice is idempotent (clean tree).

Also documented in writing-docs.md that the two systems intentionally differ in granularity (per-package vs cumulative) while sharing versions.json — so it isn't "fixed" into breakage.

mattleibow and others added 2 commits June 16, 2026 11:10
- 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>
@mattleibow

Copy link
Copy Markdown
Contributor Author

Third-round audit — all findings resolved

Did a whole-file coherence pass (not just the diff) across the four touched files plus versions.json. Twelve findings surfaced; all are now addressed in c29bda1.

Genuine bugs fixed

  • Supersession was half config / half auto-detection. Ripped out the dead auto-detection path (_all_known_base_versions, _main_upcoming_version, detect_superseded_by, extract_base_version) so Python now reads supersession only from versions.json — identical to Cake's IsVersionSuperseded. This was the root of the "magic" the design set out to remove.
  • Subpatch (4-segment) baselines (release/X.Y.Z.W) weren't handled in find_previous_stable_base / determine_diff_range. Added Case 0 + a subpatch-aware regex.
  • api-diff didn't prune. CopyChangelogs only overlaid, so a stale *.breaking.md (or a package removed from TRACKED_NUGETS) lingered forever. Fixed at the source: docs-api-diff-past now clears the per-package changelog subdirs before regenerating (preserving README.md), and the workflow clears + git add -A so deletions propagate. A local dotnet cake run is now authoritative too.

Coherence / dead-code cleanup

  • Extracted _resolve_compare_to (~22 lines were duplicated across two diff paths).
  • Unified three duplicated page-write blocks into one _write_page + small helpers; cmd_branch now remaps a versioned branch to its canonical branch so a preview ref can't clobber a stable page.
  • Removed the now-dead supersedes arrays from versions.json (auto-derived).
  • Doc/comment drift: module + compute_pr_effort docstrings (/merge/head), and a writing-docs.md caveat that the default baseline for unlisted preview-only versions can differ slightly between the two systems (Python skips baselines without a stable tag; Cake walks purely by version order — pin both with compare_to).

Verification

  • py_compile clean; Cake --dryrun compiles; api-diff.yml YAML parses.
  • Supersession harness passes (4.147.0→4.148.0, 3.119.4→None, detect_supersedes for 4.148/3.119.4/4.150).
  • Live diff ranges unchanged: 3.119.4→3.119.2 (compare_to), 3.119.2→3.119.1 (default), 4.147.0→3.119.4 (skips superseded), main(4.150.0)→4.148.0-rc.1.
  • Working tree is infra/script/doc only — no generated changelog/release-notes output committed (regenerated post-merge by the workflow).

The one remaining item (all-clobbers-legacy) is the one-time --all migration that rewrites the 59 legacy pages lacking the metadata block — no code change, idempotent thereafter, as discussed.

@mattleibow
mattleibow merged commit 08ac410 into main Jun 16, 2026
5 of 6 checks passed
@mattleibow
mattleibow deleted the mattleibow/changelog-generation-plan branch June 16, 2026 17:25
mattleibow added a commit that referenced this pull request Jun 16, 2026
…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>
mattleibow added a commit that referenced this pull request Jun 17, 2026
… 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>
mattleibow added a commit that referenced this pull request Jun 17, 2026
…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>
mattleibow added a commit that referenced this pull request Jun 17, 2026
…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>
@mattleibow mattleibow added this to the 4.150.0-preview.2 milestone Jun 29, 2026
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.

2 participants