Make doc generation reproducible and Linux-only; unify "API diff" terminology - #4200
Conversation
…ism bugs
Context: documentation/dev/release-notes-and-changelogs.md
The doc-generation pipeline has three "paths": API changelogs (api-diff),
release notes, and mdoc XML API docs. Two of them produced different output
depending on the host or the warmth of the local caches, which is why the
bot's regenerated docs kept "wobbling" between runs and why the output was
hard to reproduce locally. This reworks the pipeline so all three paths are
deterministic, share one set of entry points, and run entirely on Linux
(local Docker == CI).
~~ Path 1 (API changelogs): host/cache-independent ~~
api-diff-tools.cake resolved a package's own previous version with a
filesystem glob over the warm package cache. The set of matches — and thus
the chosen baseline — depended on what happened to be unpacked locally, so a
cold machine and a warm machine produced different changelogs. Removed the
self-dependency glob; the baseline is now derived deterministically.
Proven: cold vs warm runs are byte-identical across all 1205 output files.
~~ Path 3 (mdoc API docs): document ref/ assemblies, not lib/, and run on Mono ~~
mdoc was fed the lib/ assemblies (the implementation surface). For SkiaSharp
that includes ABI-compat members marked [Obsolete(..., error: true)] — e.g.
the by-value SetMatrix(SKMatrix) that sits next to SetMatrix(in SKMatrix).
mdoc's member comparer ignores the by-ref marker, treats the pair as equal,
and its sort is unstable, so the two members swapped places on every run.
Because mdoc runs in merge mode (the previous output is part of the next
run's input), this showed up as a 2-cycle oscillation with no fixed point.
The C# compiler strips error-obsolete members from ref/ assemblies, so the
ref/ surface is the canonical public API and is stable. docs.cake now
prefers ref/ and falls back to lib/. Only SkiaSharp.nupkg ships ref/; every
other package is lib-only and is unaffected.
Proven: three consecutive runs produce the identical output hash (92f6c0ec);
the obsolete by-value SetMatrix(SKMatrix) no longer appears in the docs.
mdoc.exe is also now invoked via Mono so Path 3 runs on Linux. The managed
GTK# reference assemblies it needs come from NuGet (fed to mdoc as --lib by
the cake comparer), so no system GTK# install is required.
~~ Co-locate the three engines + single source of truth ~~
The api-diff and release-notes engines lived under the release-notes skill
and scripts/infra/shared; mdoc lived in scripts/infra/docs. They are all the
same concern, so api-diff.cake, generate-release-notes.py, and
api-diff-tools.cake move into scripts/infra/docs alongside docs.cake (git
renames; #load/ROOT paths updated to match). The skill is now just SKILL.md
plus a thin generate.sh that delegates here.
local/CI/Docker now all call the same three entry points:
scripts/infra/docs/generate-{changelogs,release-notes,api-docs}.sh
They are git-free (REPO_ROOT derived from the script dir) so they work for a
worktree mounted inside Docker. A reproducible image lives in
scripts/infra/docs/docker (dotnet10 + mono + python3 + gh).
~~ Generated content intentionally excluded ~~
The changelog / release-note / API-doc *content* is deliberately not in this
PR. With the determinism fixes in place, CI regenerates it from scratch on
merge, which both keeps this PR reviewable and exercises the pipeline
end-to-end. The companion mono/SkiaSharp-API-docs PR flips the mdoc stub job
to Linux to match.
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 -- 4200PowerShell / Windows: iex "& { $(irm https://raw.githubusercontent.com/mono/SkiaSharp/main/scripts/get-skiasharp-pr.ps1) } 4200"Step 2 — Add the local NuGet source dotnet nuget add source ~/.skiasharp/hives/pr-4200/packages --name skiasharp-pr-4200More options
Or download manually from Azure Pipelines — look for the Remove the source when you're done: dotnet nuget remove source skiasharp-pr-4200 |
The documentation pipeline has three distinct outputs, but two of them
shared confusingly close names: "release notes" and "API changelogs".
In conversation the word "changelog" kept colliding with "release notes",
and it also clashes with GitHub's own "Full Changelog" compare links that
appear *inside* the generated release-notes pages. Settle on three
unambiguous terms — release notes, API docs, API diffs — and rename our
own "changelog" vocabulary to "API diff" everywhere it refers to the
api-diff engine.
Scope of the rename:
* Files: generate-changelogs.sh -> generate-api-diffs.sh;
release-notes-and-changelogs.md -> release-notes-and-api-diffs.md
(the behavior spec). All cross-references updated.
* Cake identifiers in api-diff.cake: changelogVersion -> apiDiffVersion,
CopyChangelogs -> CopyApiDiffs, changelogPath -> apiDiffPath,
changelogOutputPath -> apiDiffOutputPath; log dir
output/logs/changelogs -> output/logs/api-diffs.
* Prose/docs/workflow: AGENTS.md, dev docs, docs-overview, run.sh help
and subcommand (changelogs -> api-diffs), generate.sh, the
update-release-notes workflow markdown, and the Python engine's
"API-changelog engine" -> "API-diff engine" banner.
* Recompiled update-release-notes.lock.yml via `gh aw compile` so the
embedded prompt body and workflow display name match the edited
markdown (lock is generated; never hand-edited).
Deliberate retentions (NOT misses):
* GitHub's literal "[Full Changelog](compare/...)" link text is GitHub's
release convention, not our term — preserved in the Python engine and
SKILL.md examples, and in the generated docfx/releases pages.
* SKILL.md keeps "changelog" listed as a legacy trigger synonym so the
skill still fires when a user types the old word.
* Generated documentation/docfx/releases/**.md is untouched (CI
regenerates it) and unrelated skills (skia-analyst,
native-dependency-update, security-audit, release-publish) keep their
own third-party "changelog" vocabulary.
* gh-aw's protected_files list contains the literal filename
"CHANGELOG.md" — boilerplate, left as-is.
Validation: `python3 -m py_compile` on the engine passes; `dotnet cake
--target=docs-api-diff --dryrun` compiles every #load'd script and
resolves the target; the recompiled lock contains no stray "changelog"
beyond the protected_files boilerplate.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
After the changelog→API-diff rename, reviewed every changed doc-gen file
for terminology consistency, comment quality, and — the main goal —
comments that describe the FINAL state rather than the chain of edits we
made to get there. Most "no longer exists/generated" comments are correct
runtime descriptions (stale-moniker/stub pruning) and were left alone;
the fixes below target the handful that narrated the migration itself or
slipped during the bulk rename.
* generate-api-diffs.sh: the rename turned the `run.sh api-diffs`
subcommand into prose "api diffs" (a space). Restore the hyphenated
command token so the doc matches the actual subcommand.
* Windows→Linux narrative (generate-api-docs.sh, Dockerfile, docs.cake):
replaced "runs the docs step on windows-latest today" / "now invokes
mdoc under mono" / "no longer needs a Windows runner" with the final
state — mdoc.exe runs under mono on non-Windows hosts, so Path 3 runs
on Linux (auto-api-docs-writer is Linux + mono). The old wording was
soon-stale and contradicted docs-overview.md, which already documents
that workflow as Linux + mono.
* Dropped temporal markers that no longer add anything: "used to live
here" (docs.cake header), "ships ref/ today" (docs.cake), "no longer
bundled here" (writing-docs.md), "no longer Windows-only"
(docs-overview.md).
* run.sh: the `help` sed range (2,34) stopped at the "Environment:"
header, hiding COLD / IMAGE / ALLOW_NO_TOKEN. Extended to 2,37 so the
documented env vars actually print.
Validation: `bash -n` on the edited scripts; `run.sh help` now lists the
Environment vars; `dotnet cake --target=update-docs --dryrun` compiles
docs.cake; the changelog-residual set is unchanged (only the intentional
GitHub "Full Changelog" retentions and the SKILL.md legacy trigger).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
📖 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. |
The update-release-notes pipeline (API diffs + release notes) always regenerates from main: both the prepare job and the agent pre-step reset to origin/main before running the generators. That's correct for push/schedule/tag runs — release notes target main, and a push to release/* is a content *source*, not the PR base — but it means a manual dispatch can only ever exercise main's scripts, so there was no way to validate a feature branch's doc-gen pipeline on CI before merging it. Add a workflow_dispatch `source_branch` input (default: main) and thread it through both "Start from a clean … tree" steps via SOURCE_BRANCH. The default keeps every existing trigger byte-for-byte identical; only a manual run can point it at a feature branch to generate from that branch's tree (its scripts + content). This mirrors the docs repo's auto-api-docs-writer `skiasharp_branch` input. Threading it through the agent pre-step too keeps the Prepare patch applying cleanly, since both jobs then check out the same source. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
GetPlatformDirectories is the shared helper that both the API-diff engine and the mdoc API-doc generator use to pick which assembly inside a NuGet package represents its public surface. When a package shipped a cross-platform (BCL) build it returned the *first* such folder the filesystem happened to enumerate, which in practice was netstandard2.0 — so the docs and diffs described the netstandard surface. That surface is the lowest common denominator: it hides the members SkiaSharp only exposes under `#if NET` (Span overloads and friends), and netstandard is not what real apps target anymore. Rank the cross-platform candidates instead of taking the first match: prefer the newest plain .NET build (e.g. net10.0), then the highest netstandard, then portable, and only fall back to per-platform heads when no cross-platform build exists. SkiaSharp's TFMCurrent is net10.0, so the core packages are now documented and diffed from net10.0 — the most complete, real-world surface. Selecting by explicit rank rather than enumeration order also removes a latent nondeterminism: the chosen TFM no longer depends on the host's directory ordering. The regex `^net\d+\.\d+$` matches modern .NET only — the required dot excludes net4x desktop and the anchors exclude platform heads like net10.0-ios, which continue to flow through the per-platform branch unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The platform build tooling compiles non-API helper classes directly into
SkiaSharp's public "Views" assemblies, and they leak into the generated
api-diff markdown as enormous "New Type" blocks of nothing but resource-id
constants:
- the Android "aapt" resource designer <RootNamespace>.Resource and its
nested R.* tables (Resource.Drawable, Resource.Id, Resource.Styleable, ...)
in the Android builds of SkiaSharp.Views.Android / .Forms / .Maui.*;
- the Uno/WinUI XAML GlobalStaticResources class in the UWP/WinUI builds of
SkiaSharp.Views.UWP / .Windows / .Uno.
A single Forms designer is 2600+ lines, none of it real public API, and the
exact contents vary with the platform tooling and the TFM that happens to be
selected on each side of the diff. A real-package diff of SkiaSharp.Views.Forms
2.80.0 -> 2.88.0 carries 12 designer entries (Resource plus 11 nested
Resource.* tables) that swamp the six genuine type changes.
Exclude them at the source via Mono.ApiTools.NuGetDiff's IgnoreMemberRegex,
which feeds the formatted-markdown diff's IgnoreAdded/IgnoreNew/IgnoreRemoved
sets so a match drops the whole type block (including nested members) at
generation time. This is cleaner than post-processing the markdown, and unlike
a ".Resource" suffix heuristic it also catches the dot-nested R.* tables and
GlobalStaticResources.
The two patterns are anchored under "SkiaSharp.Views." and require
Resource/GlobalStaticResources to be a whole trailing name segment, so real API
lookalikes outside that namespace are untouched: SkiaSharp.Resources.* and
SkiaSharp.GR*TextureResourceInfo still diff normally. Verified on real packages:
the 12 designer entries disappear while all six genuine SkiaSharp.Views.Forms
type changes (Extensions, SKCanvasViewRenderer(Base), SKGLViewRenderer(Base),
SKPaintSurfaceEventArgs) are preserved.
Only the api-diff path is affected; the mdoc engine (scripts/infra/docs/docs.cake)
already filters the designer itself and consumes this comparer solely for its
SearchPaths.
Co-authored-by: Matthew Leibowitz <mattleibow@live.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The previous determinism pass (f4dde60) removed the self-dependency glob from CreateNuGetDiffAsync because it was a genuine source of host/cache nondeterminism: it added EVERY cached version of SkiaSharp, HarfBuzzSharp and SkiaSharp.Views.Maui.Core to comparer.SearchPaths, and since Mono.Cecil binds by simple assembly name the winning build depended on filesystem-enumeration order and cache warmth. But removing it outright regressed correctness. ApiInfo needs to resolve the referenced SkiaSharp assembly to see the accessibility of the types a public type inherits/implements. SkObject implements the two internal infrastructure interfaces ISKReferenceCounted and ISKSkipObjectRegistration (binding/SkiaSharp/SKObject.cs:450,472). When SkiaSharp.dll is on the search paths, ApiInfo sees they are `internal`, drops them, and surfaces the public base System.IDisposable instead. With no SkiaSharp.dll to resolve, those internal names LEAK into the public markdown diff as bogus base-interface entries. A full historical run leaked them into 8 files across the lib-only packages whose types derive from SKObject — e.g. 3.116.0/SkiaSharp.Resources/SkiaSharp.Resources.md listed "ISKReferenceCounted, ISKSkipObjectRegistration" where origin/main correctly shows "System.IDisposable"; the Vortice GR* types and the Skottie/SceneGraph trees were affected too. Restore resolution without reintroducing the nondeterminism: stage exactly ONE build per self-dependency, chosen as the latest STABLE release on nuget.org via NuGetVersions.GetLatestAsync. That is the same answer on every host and every cache state, and because these infra types are long-lived and stable a single recent build resolves them identically for every historical diff. Proven in the Linux Docker harness: * leak gone — 0 occurrences of either interface across all 1205 output files (was 8); 3.116.0/SkiaSharp.Resources is byte-identical to origin/main again, and GRVorticeD3D* / Maui.Controls now show the correct System.IDisposable / ISKGLView surface; * deterministic — two consecutive full runs are byte-identical (manifest hash c2089aa0, delta hash a1362be8). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Context: independent review of #4200 found a wording contradiction. The §5.2 "Deterministic resolution" narrative and §7.2 invariant 9 still claimed *every* assembly reference is satisfied by a version "pinned" in `scripts/VERSIONS.txt` via `AddDep`/`AddPackageDir`. That stopped being true once SkiaSharp's own inter-package self-references moved to `AddSelfDep` (commit ddc2798), which resolves the latest stable on nuget.org rather than a VERSIONS.txt pin. The mechanism is still host- and cache-independent — the property invariant 9 actually guarantees — but it is not "pinned", so the spec contradicted the code an independent reviewer was auditing it against. Reconcile the prose with the implementation: describe third-party deps as pinned in VERSIONS.txt and SkiaSharp's own references as resolved to the latest stable via `AddSelfDep`, and reword invariant 9 from "pinned dependency" to "real, host-independent dependency" covering both paths. Also fold in why self-deps must resolve at all (SKObject's internal ISKReferenceCounted / ISKSkipObjectRegistration interfaces leak into the public diff otherwise), which the §5.2 text previously left implicit. Documentation-only; no generated output changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Context: PR #4200 Spec: documentation/dev/release-notes-and-api-diffs.md §5.2, invariant 9 The historical API-diff generator resolves a public type's inherited interfaces from whatever SkiaSharp/HarfBuzzSharp build is on Mono.Cecil's search path (it binds by simple assembly name). If our own inter-package references aren't resolved, SKObject's `internal` ISKReferenceCounted and ISKSkipObjectRegistration leak into the public diff as bogus base interfaces. The previous fix (AddSelfDep) put a single global build — the latest stable version on nuget.org — on the path for every diff. That removed the leak but mixed eras: a 1.x diff was described using today's 3.x assembly and could show inherited members the historical type never had. It also can't be made correct by reusing the package's own version, because the self-dependency family is not one version line — e.g. SkiaSharp.HarfBuzz 2.88.7 was built against HarfBuzzSharp 7.3.0.1, not 2.88.7 — so the package version tells you nothing about its deps. Stage self-dependencies per-diff instead, at the exact version pinned in the package-under-diff's own .nuspec (the version it was actually built against), then unstage them before the next line so nothing leaks across versions. This is contemporaneous, deterministic, and host/cache- independent (nuspec content is frozen), and it generalises to every self-dependency uniformly via the one source that records their versions. * StageSelfDepsFromNuspecAsync / UnstageSearchPaths / IsSelfDependency / GetSelfDepLibDirectory in api-diff-tools.cake; AddSelfDep removed. * Wired into both diff call sites in api-diff.cake (current CI gate and the authoritative docs-api-diff-past loop) with try/finally unstage. * NativeAssets.* dependencies are excluded — they ship only native binaries (no managed types to resolve), are the largest packages to fetch, and a missing preview native build would otherwise abort the whole run. This alone cut a full run's download from ~2.3GB to ~265MB. * A self-dependency version that can't be fetched is logged and skipped, not fatal. Validated by a full authoritative run (00:18:06): 0 errors, 0 internal- interface leaks across all 60 version folders (1.x through 4.x), a 1.55.0 SkiaSharp.Views diff that correctly resolves SKObject to the era's public System.IDisposable, and byte-identical output across two independent runs (idempotent). docs.cake (mdoc) is unaffected — it self-stages from locally-built frameworks. Co-authored-by: Matthew Leibowitz <mattleibow@live.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Regenerate the entire committed documentation/docfx/releases/ api-diff tree from scratch using the current generation scripts (nuspec-driven per-diff self-dependency staging + the Resource/GlobalStaticResources member exclusions) so the committed tree is a faithful, reproducible output of today's scripts rather than the stale output last written by #4184 (old, pre-self-dep-fix scripts). The committed tree had drifted from what the scripts now produce: every subsequent run legitimately rewrote ~24 files (self-dep version pinning, contemporaneous inherited-type resolution, and the new 4.150.0 dev line), which made it impossible to tell an intended change from baseline drift. Committing a clean full regeneration establishes the authoritative baseline so future runs (local or CI) produce only the diff for whatever actually changed. Generated by: scripts/infra/docs/generate-api-diffs.sh (docs-api-diff-past), run in the docs Docker image, exit 0, no errors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Run the full Prepare pipeline exactly as the "Update Release Notes & API Diffs" workflow does — `.agents/skills/release-notes/scripts/generate.sh` (Path 1 Cake api diffs, then Path 2 Python release notes --all) — in the docs Docker image. Path 1 reproduced the api-diff tree committed in the previous commit with zero diff (deterministic), confirming the api-diff baseline is reproducible; this commit adds the Path 2 release-notes output that the api-diff-only commit was missing. New release-notes pages for the lines that had none committed yet (4.147.0, 4.148.0, 4.150.0 + 4.150.0-unreleased, HarfBuzzSharp 14.2.0 and 8.3.1.6) plus the regenerated TOC.yml / index.md that link them. Together with the api-diff baseline this is the complete, reproducible output of today's generation scripts. Generated by: .agents/skills/release-notes/scripts/generate.sh (docs-api-diff-past + generate-release-notes.py --all), docs Docker image. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…I recreate Remove the api-diff trees and release-notes pages for three lines so that merging this branch lets the "Update Release Notes & API Diffs" workflow regenerate exactly these — and only these — proving the pipeline recreates deleted output deterministically and that a steady-state run is a no-op. Deleted (51 files), api diff dir + notes page for each: - documentation/docfx/releases/3.119.3/ + 3.119.3.md - documentation/docfx/releases/4.148.0/ + 4.148.0.md - documentation/docfx/releases/harfbuzzsharp/14.2.0/ + 14.2.0.md The top-level TOC.yml and index.md are deliberately left intact: Path 2 regenerates them every run, so they already match what CI will produce (zero diff there). The recreate PR should therefore contain only these 51 files reappearing, making the round-trip easy to verify. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove the generated-but-unpolished release-notes prose pages for the 4.x line (4.147.0, 4.150.0, 4.150.0-unreleased) so the "Update Release Notes & API Diffs" workflow regenerates them and the AI polish step rewrites the prose. The raw generator output is a placeholder; the human-readable notes come from the polish pass that only runs in CI, so committing the unpolished pages here would just be overwritten. Only the top-level prose pages are removed. The deterministic api-diff trees (4.147.0/ and 4.150.0/) are kept — they need no polishing and already match what CI produces. 4.148.0's prose page was already removed in the previous commit as part of the recreate-validation set. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…sh via CI Same rationale as the 4.x prose pages: the raw generated 8.3.1.6 notes are a placeholder that the CI AI-polish pass rewrites, so remove the page and let the "Update Release Notes & API Diffs" workflow regenerate and polish it. Only the prose page is removed; no api-diff tree exists for this version to keep. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Re-running the full Prepare pipeline (.agents/skills/release-notes/scripts/ generate.sh: Cake docs-api-diff-past + generate-release-notes.py --all) in the docs Docker image recreated every file previously deleted on this branch. A byte-for-byte hash comparison against the prior baseline showed 1204/1211 files identical; only the 7 raw release-notes prose pages differ run-to-run, confirming the api-diff generation is fully deterministic. This commit re-adds the regenerated api-diff trees (3.119.3, 4.148.0, HarfBuzzSharp 14.2.0) and the raw notes so the branch holds the complete, current generator output before we re-stage the intended deletions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…polish Remove the 7 raw release-notes prose pages flagged by the generator in output/files-to-polish.txt. These are the only files that differ between two identical generate.sh runs, because the raw generator output is a placeholder that the CI AI-polish pass rewrites. Committing the unpolished text here would just be overwritten, so let the "Update Release Notes & API Diffs" workflow regenerate and polish them instead. Removed: - 3.119.3.md, 4.147.0.md, 4.148.0.md, 4.150.0.md, 4.150.0-unreleased.md - harfbuzzsharp/8.3.1.6.md, harfbuzzsharp/14.2.0.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…CI recreate Remove the api-diff trees for 4.148.0 and HarfBuzzSharp 14.2.0 so that merging this branch lets the "Update Release Notes & API Diffs" workflow regenerate exactly these — and only these — proving the pipeline recreates deleted api-diff output deterministically (the byte-identical re-run already demonstrated this locally for the full tree). Deleted (26 files): - documentation/docfx/releases/4.148.0/ (24) - documentation/docfx/releases/harfbuzzsharp/14.2.0/ (2) The 3.119.3 api-diff tree and the top-level TOC.yml / index.md are left intact; CI regenerates the nav at zero diff, so the recreate PR should show only these api-diff files reappearing alongside the regenerated, AI-polished release notes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drop the two API-diff "local-iteration" knobs from the doc-generation
engine: --nugetDiffMinVersion (NUGET_DIFF_MIN_VERSION) and
--useOutputNugets (NUGET_DIFF_USE_OUTPUT_NUGETS).
Neither was part of the documented contract. The behavior spec
(documentation/dev/release-notes-and-api-diffs.md §5) describes the
api-diff engine as always rebuilding "the complete set authoritatively"
— which is exactly the unfloored full run CI already used. The flags
only ever existed as undocumented code knobs:
* nugetDiffMinVersion was wired (IsLineBelowMin + a per-line skip, a
partial-run guard on the index/co-release writes, and floor checks
in ClearOwnedApiDiffFolders) but only NARROWED which lines a local
run regenerated; it never changed committed output.
* useOutputNugets was fully vestigial — defined and documented in
shared.cake and generate-api-diffs.sh, but never read by any .cake
code path. The "diff freshly built unpublished packages" behavior
lives in the separate docs-api-diff ("current") target (spec §5.3),
which gates on FileExists(output/nugets/...) and never consulted
this flag.
Removing them leaves docs-api-diff-past doing the single authoritative
full walk, matching the spec and CI. No documentation references either
flag, so nothing else needs updating. Can be reintroduced later if a
real local-iteration speed knob is needed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The two developer docs in `documentation/dev/` had grown into long
unbroken paragraphs and oversized bullet blocks that were hard to scan,
and neither had a table of contents. This restructures both for
readability **without changing any information** — no facts, paths, rules,
or `§N.M` cross-references were added, removed, or reworded in meaning.
Constraint: the spec's numbered headings (`## N.` / `### N.M`) are
referenced ~120 times from code comments and skills
(`api-diff.cake`, `api-diff-tools.cake`, `generate-release-notes.py`,
the release-notes skill scripts), so renumbering would break those
references. Verified the numbered-heading set is byte-identical to the
pre-edit baseline, all new subheads are unnumbered `####`, and every
code `§N.M` reference still resolves.
docs-overview.md (the entry-point map):
* add a "Start here — what are you trying to do?" chooser table and a
clickable Contents TOC
* convert the CI key-points and local/Docker bullet blocks to tables
release-notes-and-api-diffs.md (the 820-line behavior spec):
* add a Contents TOC (plain `§`-text, non-clickable, to avoid fragile
auto-anchors on backtick/em-dash headings)
* break the worst walls of text into unnumbered `####` subheads, lists,
and tables — §1.5 (two families), §3.5/§3.6 (ownership, co-release
map), §4.4 (API-diff link rule), §5.2 (deterministic reference
resolution), §5.4 (package cache), §7.2 invariant 9
Reviewed by GPT-5.5 for information loss; the three clauses it flagged in
docs-overview.md (a dropped "editing API docs" item, an overstated deps
hint, a "lives in" → "runs in" wording shift) were restored.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
The backport to To backport manually, run these commands in your terminal: # Fetch latest updates from GitHub
git fetch
# Create a new working tree
git worktree add .worktrees/backport-release/4.148.x release/4.148.x
# Navigate to the new working tree
cd .worktrees/backport-release/4.148.x
# Create a new branch
git switch --create backport/pr-4200-to-release/4.148.x
# Cherry-pick the merged commit of this pull request and resolve the conflicts
git cherry-pick -x --mainline 1 eb251ab24c96a32418b39db3c327b836d5c268f3
# Push it to GitHub
git push --set-upstream origin backport/pr-4200-to-release/4.148.x
# Go back to the original working tree
cd ../..
# Delete the working tree
git worktree remove .worktrees/backport-release/4.148.xThen, create a pull request where the |
[CI] Run API-docs stub regeneration on Linux via Mono (#147) Two related changes to the auto-api-docs-writer workflow: flip the regenerate-stubs job to Linux, and stop the writer from running on pull requests. ~~ Move regenerate-stubs from Windows to Linux ~~ This job was the last non-Linux job across the SkiaSharp + SkiaSharp-API-docs doc pipeline; with it flipped, the entire two-repo pipeline runs on Linux, matching the local Docker image and the SkiaSharp-side CI. The job only used Windows because mdoc.exe is a .NET Framework tool, but that requirement no longer holds: * mdoc.exe runs fine under Mono, and SkiaSharp's docs.cake already invokes it via `mono`. * The managed GTK# reference assemblies mdoc needs come from NuGet via the cake comparer (passed as --lib paths), so no system GTK# install is required — the Windows GTK# 2 MSI download/install was redundant. So Mono (mono-complete via apt) is the only added dependency. The job now calls the shared scripts/infra/docs/generate-api-docs.sh entry point instead of `dotnet cake --target=update-docs`, uses global.json for the SDK and Linux-style nuget cache paths, and drops the now-redundant `dotnet tool restore` and docs-download-output steps. Proven locally end-to-end on Linux (Docker, dotnet10 + mono-complete) against this repo's stub tree: it correctly pruned the orphaned net6-only type files (ActionHelper, FloatFloatActionHelper) and added the newer [Nullable] attributes, and two consecutive runs produced byte-identical output (hash ec15190a) — so the job is both correct and idempotent on Linux. Prerequisite: the companion mono/SkiaSharp#4200, which adds scripts/infra/docs/generate-api-docs.sh, has merged to main, so the checked-out SkiaSharp tree now has the script. ~~ Drop the pull_request trigger ~~ The writer is a full agentic workflow: it regenerates stubs, fills placeholders with AI, and opens a PR via gh-aw safe-outputs. When a PR edits this workflow, the writer fires on that PR and then fails, because safe-outputs refuses to create a PR that touches protected workflow files (protect_top_level_dot_folders: true) — surfacing as a red safe_outputs check on this very PR. Removing the pull_request trigger stops the writer running on PRs; push:[main] still validates workflow edits after they land, and workflow_dispatch + schedule are unaffected. Recompiled .lock.yml via `gh aw compile` for both commits. Supersedes #144, which gh-aw's recreate_ref:true auto-closed when a run was dispatched on its branch; moved to the dedicated ci/linux-mdoc-regenerate-stubs branch so the agentic placeholder-fill PRs reusing dev/linux-mdoc-regenerate-stubs (e.g. #145) can't collide with it. Co-authored-by: Matthew Leibowitz <mattleibow@live.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
The repo generates its release documentation through three independent paths, and two of them produced different output depending on the build host or local NuGet-cache warmth. That is why the doc-bot's regenerated output kept "wobbling" between runs and was painful to reproduce locally. This PR makes all three paths deterministic, gives them one shared set of entry points that behave identically locally, in CI, and in Docker (Linux), unifies the confusing "changelog" → "API diff" terminology, and commits a freshly regenerated baseline to replace the drifted one from #4184.
Background: the three doc paths
Everything lives under
scripts/infra/docs/and is regenerated by theupdate-release-notesCI workflow. The three paths share one versioning model (versions.json):api-diff.cake+api-diff-tools.cakeSkiaSharp.md,*.breaking.md), by diffing each published NuGet against its baselinedocumentation/docfx/releases/<line>/<pkg>/…generate-release-notes.py<line>.mdrelease-note pages — a structured draft the CI AI-polish pass rewritesdocumentation/docfx/releases/<line>.mddocs.cake(mdoc)The problems this PR fixes:
scripts/infra/shared, so a local run never matched CI and was not reproducible.Path 1 — API diffs: now deterministic and correct
Self-dependency resolution.
api-diff-tools.cakeresolved a package's own previous version with a filesystem glob over the warm package cache, so the chosen baseline depended on what happened to be unpacked locally — a cold machine and a warm machine produced different diffs. Simply deleting the glob fixed determinism but regressed correctness: ApiInfo must resolveSkiaSharp.dllto see that the two infrastructure interfacesSKObjectimplements (ISKReferenceCounted,ISKSkipObjectRegistration) areinternaland surface the public baseSystem.IDisposableinstead. With noSkiaSharp.dllon the search paths those internal names leaked into the public diff (8 files, e.g.SkiaSharp.Resources, the VorticeGR*types, Skottie/SceneGraph). The fix (AddSelfDep) stages exactly one build per self-dependency, chosen deterministically from each diff's own nuspec dependency closure — the same answer on every host and cache state, stable enough that one recent build resolves these long-lived infra types identically for every historical diff.Exclude auto-generated resource designers. The platform build tooling compiles non-API helpers into the public "Views" assemblies — the Android aapt
Resource/R.*tables and the Uno/WinUIGlobalStaticResources— and they leaked into the diff as enormous "New Type" blocks of nothing but resource-id constants (a single Forms designer is 2600+ lines). They are excluded at the source viaMono.ApiTools.NuGetDiff.IgnoreMemberRegex, anchored underSkiaSharp.Views.so real API lookalikes (SkiaSharp.Resources.*,SkiaSharp.GR*TextureResourceInfo) still diff normally.Document the newest plain .NET TFM, not netstandard.
GetPlatformDirectoriesreturned the first cross-platform folder the filesystem enumerated (in practicenetstandard2.0), hiding the#if NETmembers (Span overloads) and describing a surface no real app targets. It now ranks candidates — newest plain .NET (e.g.net10.0) → highest netstandard → portable → per-platform heads — so SkiaSharp is documented and diffed fromnet10.0. (This selection is shared with Path 3.) Ranking also removes the enumeration-order nondeterminism.Proven: consecutive full runs are byte-identical; 0 internal-interface leaks (was 8);
3.116.0/SkiaSharp.Resourcesis byte-identical to the committed baseline again.Path 2 — release notes: logic unchanged, now co-located
Path 2's engine logic is unchanged in this PR. It moves into
scripts/infra/docs/with the other two and keeps its structure-then-polish split: the Python script emits a structured draft and a CI AI pass polishes the prose. The raw draft is therefore intentionally non-deterministic (the AI rewrites it), which is why this PR does not commit raw note prose — see What changed vsmain.Path 3 — mdoc API docs:
ref/overlib/, on Mono/Linuxmdoc was fed the
lib/assemblies (the implementation surface). For SkiaSharp that includes ABI-compat members marked[Obsolete(..., error: true)]— e.g. the by-valueSetMatrix(SKMatrix)next toSetMatrix(in SKMatrix). mdoc's member comparer ignores the by-ref marker, treats the pair as equal, and its sort is unstable, so the two members swapped places every run. Because mdoc runs in merge mode (the previous output is part of the next run's input), this showed up as a 2-cycle oscillation with no fixed point.The C# compiler strips error-obsolete members from
ref/assemblies, so theref/surface is the canonical public API and is stable.docs.cakenow prefersref/and falls back tolib/. OnlySkiaSharp.nupkgshipsref/; every other package is lib-only and unaffected.Proven: three consecutive runs produce the identical output hash; the obsolete by-value
SetMatrix(SKMatrix)no longer appears in the docs. mdoc.exe is invoked via Mono so Path 3 runs on Linux. The managed GTK# reference assemblies it needs come from NuGet (fed to mdoc as--libby the cake comparer), so no system GTK# install is required.Cross-cutting changes
One home, one set of entry points. The API-diff and release-notes engines lived under the release-notes skill and
scripts/infra/shared; mdoc lived inscripts/infra/docs. They are all the same concern, soapi-diff.cake,generate-release-notes.py, andapi-diff-tools.cakemove intoscripts/infra/docsalongsidedocs.cake(git renames). The skill is now justSKILL.mdplus a thingenerate.shthat delegates here. local / CI / Docker now all call the same three entry points:They are git-free (REPO_ROOT derived from the script dir) so they work for a worktree mounted inside Docker.
Linux-only via Docker. A reproducible image lives in
scripts/infra/docs/docker(dotnet10 + mono + python3 + gh), so a local run in the container matches CI exactly.CI: validate a feature branch before merge. The
update-release-notesworkflow always regenerated frommain. A newworkflow_dispatchsource_branchinput (default:main) threads through both "start from a clean tree" steps so a manual run can exercise a feature branch's doc-gen pipeline on CI before merging. The default keeps every existing trigger byte-for-byte identical; this mirrors the docs repo'sskiasharp_branchinput.Terminology: "changelog" → "API diff". The repo used "changelog" for the machine-generated API-diff trees, which collided with "release notes" and with GitHub's own "Full Changelog" compare links. This unifies the vocabulary onto API diff: renamed
generate-changelogs.sh→generate-api-diffs.shand the dev specrelease-notes-and-changelogs.md→release-notes-and-api-diffs.md; renamed Cake identifiers, theoutput/logs/api-diffslog path, and prose across docs / skill / workflow; recompiled theupdate-release-noteslock viagh aw compile. Intentionally kept: GitHub's literal[Full Changelog](compare/…)links (their own convention), therelease-notesskill's legacy "changelog" trigger synonym, and unrelated third-party "changelog" vocabulary in other skills.Developer docs. The
documentation/dev/guides that describe this system are added and restructured for readability: a newdocs-overview.mdmap (what each engine, skill, and workflow does and where its output lives) and a re-laid-outrelease-notes-and-api-diffs.mdspec — both gain a table of contents, and the spec's worst walls of text are broken into subheads, lists, and tables. The relayout is information-preserving: the spec's numbered headings (referenced ~120× from code comments and skills) are byte-identical to before, so no§N.Mcross-reference breaks.What changed vs
main76 files changed (+1498 / −1467). The script / workflow / doc changes above are the engine of the PR; the rest is the regeneratedreleases/baseline replacing the drifted #4184 output. Measured againstmain, thereleases/tree changes are:TOC.ymlandindex.md.4.148.0→4.150.0. git renders most of this as renames of4.148.0/<pkg>/…to4.150.0/<pkg>/…(a few files differ enough to show as add/delete). So4.150.0/is new here — it is not a pre-existing tree being "kept".4.148.0/andharfbuzzsharp/14.2.0/API-diff trees. CI must regenerate exactly these after merge.mainis3.119.3.md; the newer note pages (4.147.0.md,4.148.0.md,4.150.0.md,4.150.0-unreleased.md, harfbuzz8.3.1.6.md/14.2.0.md) never existed onmain, so they simply do not appear yet.2.88.0/SkiaSharp.Views.Forms/SkiaSharp.Views.Forms.breaking.md— a leak the resource-exclusion fix means should no longer be generated.Determinism evidence. A full regeneration in the Linux Docker harness reproduced the committed API-diff baseline byte-for-byte — 1204 of 1211
releases/files identical, the 7 differing files being exactly the raw release-note prose pages the AI-polish pass owns. So Path 1 output is fully reproducible andmainstarts from a correct, stable tree.Companion PR
mono/SkiaSharp-API-docs#147 flips the mdoc stub job to Linux to match. This PR must merge first — it adds
scripts/infra/docs/generate-api-docs.sh, which that workflow calls.Expected recreate PR after merge (hard record)
For an audit trail: once this PR merges, the
update-release-notesworkflow's recreate PR must touch exactly these 33 files underdocumentation/docfx/releases/and nothing else (TOC.yml/index.mdstay at zero diff).26 API-diff files — regenerated byte-for-byte identical (the deleted
4.148.0/+harfbuzzsharp/14.2.0/trees, proven deterministic):7 release-note pages — generated and AI-polished (content will differ from the raw generator output; that is expected):
Red flags (would mean the pipeline is not yet deterministic): any file other than these 33 appearing; any of the 26 API-diff files coming back with a content diff; or
TOC.yml/index.mdshowing changes.