documentation/dev/writing-docs.md
New here? Read docs-overview.md first — it maps the whole documentation system (the four artifacts, the engines, the skills, and the cross-repo automation). This page is the hands-on operator guide for generating and editing API docs and api diffs.
This guide covers generating and updating SkiaSharp API documentation.
SkiaSharp provides two types of documentation: concept docs and API docs.
The conceptual docs live on the main branch under documentation/conceptual/ and are published to:
See site.md for how to build, preview, and customize the docs site.
The API docs are XML reference documentation generated from SkiaSharp assemblies using mdoc. They are published to https://docs.microsoft.com/dotnet/api/skiasharp.
Because the docs are large and contain examples, images, and other assets, they are hosted in a separate repository:
This repository is pulled into the main SkiaSharp repo as a Git submodule at docs/. The XML files live under docs/SkiaSharpAPI/.
Two workflows automate the full docs lifecycle. They live in different repos — the stub-generation/AI-writing workflow is in the docs repo (it writes into that repo), and the submodule-sync workflow is here:
A single agentic workflow handles the entire pipeline — regenerating XML stubs from CI NuGet packages AND filling "To be added." placeholders with AI-written documentation:
auto-api-docs-writer.md (agentic, in the docs repo mono/SkiaSharp-API-docs — it checks SkiaSharp out to borrow the engines, then writes the XML into the docs repo)dotnet cake --target=update-docs to regenerate XML stubsapi-docs skill, finds all "To be added." placeholders, reads C# source code, writes proper documentationmono/SkiaSharp-API-docsautomation/write-api-docs → main in mono/SkiaSharp-API-docsRunner note:
mdoc.exeis a .NET Framework executable, butdocs.cakeruns it under mono, so the stub-regeneration job runs on a Linux runner withmono-completeinstalled (see docs-overview.md → Path 3).
After docs changes are merged to mono/SkiaSharp-API-docs main (whether from the AI pipeline, manual edits, or any other source), the submodule pointer in this repo needs to be updated:
auto-docs-submodule-sync.yml (in this repo)docs/ submodule SHA with the latest mono/SkiaSharp-API-docs main SHA. If behind, creates a PR to bump the submodule.automation/update-docs-submodule → main in mono/SkiaSharp| Time (UTC) | Workflow | Action |
|---|---|---|
| 8:00 AM | auto-api-docs-writer | Regenerates stubs + AI fills placeholders → PR to docs repo |
| 10:00 AM | auto-docs-submodule-sync | Bumps docs/ submodule → PR to SkiaSharp |
Each step produces a PR that requires human review before merging.
You can also update the submodule manually:
cd docs
git checkout main
git pull
cd ..
git add docs
git commit -m "Update docs submodule"
dotnet tool restore
cd docs
git checkout -b my-docs-update
cd ..
dotnet cake --target=docs-download-output
This downloads NuGet packages from the SkiaSharp-CI feed. By default it fetches from the main branch. To use a different branch:
dotnet cake --target=docs-download-output --gitBranch=my-feature-branch
dotnet cake --target=update-docs
This generates the XML API docs in the docs/ directory. (The API diffs
are produced separately by the docs-api-diff* targets — see below.)
After generating, you can open the XML files in any text editor and add or update documentation. Any members that do not have content will have the placeholder text "To be added."
You can also use the api-docs Copilot skill to write and review XML documentation. Ask Copilot to "fill in missing docs", "document SKCanvas", or "review documentation quality" and it will follow the .NET API documentation guidelines automatically.
As you edit, you can apply formatting and see what docs are still missing by running:
dotnet cake --target=docs-format-docs
This will report a summary of documentation coverage (types and members with missing docs).
Once you are happy with your changes, push them to your fork of mono/SkiaSharp-API-docs and open a PR.
For detailed XML documentation patterns and review criteria, see:
.agents/skills/api-docs/references/patterns.md — XML syntax and examples.agents/skills/api-docs/references/checklist.md — Review severity criteria| Target | Description |
|---|---|
docs-download-output | Downloads _nugets and _nugetspreview packages from the SkiaSharp-CI Azure Artifacts feed |
docs-api-diff | Compares current (unpublished CI) NuGets against latest published versions; writes transient markdown diffs to output/api-diff/ only (a CI gate — never the committed tree) |
docs-api-diff-past | Regenerates the committed historical API diffs under documentation/docfx/releases/ across all published versions |
docs-update-frameworks | Extracts assemblies, builds frameworks.xml with monikers, runs mdoc update to generate XML API docs |
docs-format-docs | Cleans XML output, removes duplicates, syncs extension method docs, reports coverage |
update-docs | Runs docs-update-frameworks → docs-format-docs in sequence (the current API diff is a standalone CI gate, not bundled here) |
The two
docs-api-diff*targets live alongside the other doc engines atscripts/infra/docs/api-diff.cake; the wrappers inbuild.cakejust forward to it. The behavior is specified inrelease-notes-and-api-diffs.md— read it before changing either engine.
The per-assembly API diffs are written into the in-site releases tree
(line-first, package-namespaced): documentation/docfx/releases/<line>/{PackageId}/{Assembly}.md
for SkiaSharp-versioned packages, and the parallel
documentation/docfx/releases/harfbuzzsharp/<hb-line>/{PackageId}/{Assembly}.md
tree for HarfBuzzSharp-versioned packages (see
release-notes-and-api-diffs.md §3). They are
generated by Mono.ApiTools.NuGetDiff (the docs-api-diff-past Cake target) and
kept up to date automatically as part of the Prepare phase of the unified
update-release-notes pipeline
(on push to main and on a daily cron — main walks every release/* ref and
reads v* tags itself, so release branches and tags need no trigger of their own).
That one workflow's
Prepare phase regenerates both the API diffs and the release-notes raw data,
then the AI Polish phase rewrites prose, and it opens a single rolling PR with
both the api diffs and the notes — there is no separate api-diff workflow.
To regenerate the full committed set locally exactly the way CI does:
dotnet tool restore
dotnet cake --target=docs-api-diff-past --nugetDiffPrerelease=true
git diff documentation/docfx/releases/
docs-api-diff-past diffs published NuGet.org versions: every version is
compared against its predecessor, with baselines and superseded-version skips
driven by scripts/infra/docs/versions.json (same config the
release-notes script uses). Superseded versions still get their own api diff;
they are only removed from the pool of baselines, so e.g. 4.148.0 walks past
the abandoned 4.147.* previews and lands on 3.119.4.
Previewing the API diff of a branch/tag that is not yet on NuGet.org is a manual, multi-step operation (it is intentionally not part of the automated workflow). It diffs CI packages for the ref against their NuGet.org baseline:
Overlay the ref's nuget versions onto scripts/VERSIONS.txt so the download
and diff targets resolve the right package versions (skip this if previewing
main, which is already the working tree):
git fetch --depth=1 origin release/4.150.0-preview.1
git show FETCH_HEAD:scripts/VERSIONS.txt | grep -E '[[:space:]]nuget[[:space:]]'
# replace the "# nuget versions" block in scripts/VERSIONS.txt with those lines
Download the CI packages for the ref and run the diff against the feed:
dotnet cake --target=docs-download-output --gitBranch=release/4.150.0-preview.1
dotnet cake --target=docs-api-diff --gitBranch=release/4.150.0-preview.1 --nugetDiffPrerelease=true
(Use --gitSha=<40-char-sha> instead of --gitBranch for a tag or commit.)
The preview output lands in output/api-diff/ for inspection — it is not
meant to be committed.
The API diffs (Cake) and the website release notes
(generate-release-notes.py)
are separate systems that deliberately share only one thing:
scripts/infra/docs/versions.json. That file is the single
source of truth for two decisions, and both systems honour it identically:
status: superseded entry
is treated as superseded (Cake's IsVersionSuperseded, Python's
resolve_superseded_by). Neither side auto-detects it; to skip a version
everywhere, add it to versions.json.compare_to baselines — when present, both sides diff against the same
baseline version (e.g. 4.148.0 → 3.119.4).For any version not carrying a compare_to override, each system picks the
default baseline (the previous version) on its own, and the two can differ
slightly: the Python release-notes generator additionally skips any candidate
baseline that has no stable git tag, whereas Cake walks purely by version order.
This only matters for unlisted, preview-only versions; add a compare_to entry
to versions.json to pin both systems to the same baseline.
Where they intentionally differ is granularity, and this is by design — do not "fix" them into agreement:
4.147.0-preview.* against the one
before it). The audience is "what changed in this exact package".So a given version can show a finer-grained baseline under
documentation/docfx/releases/<line>/ than on its release-notes page, even
though both agree on supersession and any explicit compare_to override.