Version annotations
Added in 2.0 appears beside an object’s source link, in the row under its heading. It answers
the question every reader arrives with: is this in the release I have installed? You only need to
supply a list of past git refs from your own repository. Where the sourceLink preset says which
forge you are on, the version links to that release’s page.
This page describes how to label the pages of one release. If you want each release to have its own set of pages at its own URL instead, see Versioned docs.
starlightPydocs({ packages: [ { name: 'mypkg', search: ['../src'], versions: { // Oldest first. The working tree is the // newest version, so it is not listed. refs: [ { ref: 'v1.0.0', label: '1.0' }, { ref: 'v1.1.0', label: '1.1' }, { ref: 'v2.0.0', label: '2.0' }, ], }, }, ],});Which objects get a label
Section titled “Which objects get a label”With this configuration, a class introduced in 2.0 reads Added in 2.0. A method that has existed
since 1.1 reads Added in 1.1. The text is your label, exactly as you wrote it. Two kinds of
objects get no label:
- Objects in the oldest listed ref. “Added in 1.0” on two-thirds of a package would
be noise. It would also be wrong for any object older than 1.0. Listing
1.0as the oldest ref makes it the baseline, not a label. - Objects in none of the listed refs. These objects exist only in the current source, which has no version number to show. Add a ref for the latest release, and everything added since then stands out by having none. Add a ref for a release you are about to cut, and its new objects get a label.
Removed objects get no label either. They are not in the current model, so nothing can look them up.
Inherited and re-exported members are labelled by where they are defined. For example,
mypkg.Report, re-exported from mypkg.report, carries the history of mypkg.report.Report.
How the labels are computed
Section titled “How the labels are computed”Griffe’s own griffe check command prints prose, not data. So starlight-pydocs compares dumps
instead:
- Each
refis resolved to a commit withgit rev-parse --verify <ref>^{commit}. - That commit is checked out into the cache directory with
git worktree add --detach <cacheDir>/starlight-pydocs/worktrees/<sha>. This shares the object database with your repository instead of cloning it. - The package’s
searchpaths are rebased onto the worktree. The samegriffe dump -f -d <style>command runs there, with the same extensions and docstring options as the current source. This keeps the two dumps comparable. - Every object path in each dump is collected. Then starlight-pydocs walks the refs from oldest to newest: the first ref that contains a path is the release that introduced it.
The result, a map from object path to version label, is written beside the package’s dump. It is read back when pages render. Only the label text reaches the browser.
Listing the releases automatically
Section titled “Listing the releases automatically”refs is a plain array, so build it from your tags instead of editing the config on every
release:
import { execFileSync } from 'node:child_process';import { fileURLToPath } from 'node:url';
// The repository root, one level above this Astro project.const repo = fileURLToPath(new URL('..', import.meta.url));const opts = { cwd: repo, encoding: 'utf8' };
// Every release tag, oldest first. `v:refname` sorts 1.10 after// 1.9, which an alphabetical sort does not.const args = ['tag', '--list', 'v*', '--sort=v:refname'];const refs = execFileSync('git', args, opts) .split('\n') // Releases only: drops `v2.0.0.dev0` and other prereleases. .filter((tag) => /^v\d+\.\d+(\.\d+)?$/.test(tag)) .map((tag) => ({ ref: tag, label: tag.slice(1) }));
starlightPydocs({ packages: [ { name: 'mypkg', search: ['../src'], // A checkout with no tags gives no labels, rather than a // build that stops on a ref it cannot resolve. ...(refs.length > 0 && { versions: { refs } }), }, ],});The oldest tag stays the baseline and gets no label, so the list grows one at the new end each time you release.
What your build needs
Section titled “What your build needs”A git checkout with history
Section titled “A git checkout with history”starlight-pydocs extracts the sources from your own repository. A shallow clone with no tags
cannot resolve v1.0.0. In GitHub Actions, set fetch-depth: 0:
- uses: actions/checkout@v7 with: fetch-depth: 0Without this, the build stops and names the missing ref: 'mypkg' lists the version ref 'v1.0.0', which /repo does not have.
Search paths inside that repository
Section titled “Search paths inside that repository”starlight-pydocs materialises the refs as a worktree of the repository that the first search
path belongs to, and rebases every search path onto it. A path outside that repository has no
equivalent in the worktree, and the build reports this.
Extraction rather than a pinned dump
Section titled “Extraction rather than a pinned dump”You cannot combine versions with source: { file } or source: { url }. A pinned dump is one
release with no history behind it. Validation rejects this combination; it does not fail silently.
To document an old release as pages of its own, add a second packages entry instead. See
Versioned docs.
Refs that contain the package
Section titled “Refs that contain the package”starlight-pydocs extracts each listed ref with the same package name and search layout. A release from before the package existed at that path fails extraction. Start your list at the first release you can extract.
Caching
Section titled “Caching”A commit cannot change, so starlight-pydocs caches a ref’s dump permanently at
<cacheDir>/starlight-pydocs/versions/<name>-<sha>-<options>/dump.json. The first build checks out
one worktree per ref and runs griffe once per ref. Every later build reads the cached dumps and
does no git work. Worktrees are keyed by sha and reused, so each one costs only one checkout.
The options half of the key covers the docstring style, the docstring options, the extensions,
forceInspection, and the repository-relative search paths. If you change any of these,
starlight-pydocs re-extracts the refs. Otherwise the comparison would compare dumps made in two
different ways.
cacheDir defaults to node_modules/.astro. Point it at a directory your CI caches, so the git
work happens once across builds instead of once per build.
In GitHub Actions
Section titled “In GitHub Actions”Cache the ref dumps the same way you cache pnpm’s store or uv’s: with actions/cache, keyed on
the tag list. A build then extracts only the releases the cache does not already have.
Put the cache directory outside node_modules, so installing dependencies cannot remove it:
starlightPydocs({ packages: [ { name: 'mypkg', search: ['../src'], versions: { refs } }, ], cacheDir: '.cache',});steps: - uses: actions/checkout@v7 with: # Version refs are resolved in your own repository. fetch-depth: 0
- id: refs run: | key=$(git tag --list 'v*' | sha256sum | cut -c1-16) echo "key=$key" >> "$GITHUB_OUTPUT"
- uses: actions/cache@v6 with: # Only the dumps. Worktrees are rebuilt on demand and are large. path: .cache/starlight-pydocs/versions key: pydocs-versions-${{ steps.refs.outputs.key }} restore-keys: pydocs-versions-A cache entry cannot be overwritten, so the key has to change when the tag list does. restore-keys
is what makes that cheap: the new key misses, the prefix match restores the previous build’s dumps,
griffe runs for the new tag only, and the job saves the whole set under the new key.
Use the cache rather than an artifact. Artifacts are for files a build publishes; they are not restored into the next run on their own.
The dumps are megabytes each, so a package with many releases makes a large cache. GitHub’s limits apply: 10 GB per repository, and an entry that goes unread for 7 days is evicted, which costs you one slow build.
Deprecations are separate
Section titled “Deprecations are separate”The Deprecated badge and its notice come from the docstring: a Deprecated: block, a
deprecated section, or griffe’s is_deprecated. They need no git history. Deprecated since 0.3
is what your docstring says. Added in 1.1 is what the refs say. The two are independent, and
both can appear together. Only Deprecated is a badge; the version is plain text in the row
beneath the heading.
In the Content Layer loader
Section titled “In the Content Layer loader”The loader takes the same
versions option. It puts the label on each entry as addedIn. To build a “what is new in this
release” page, filter with getCollection:
---import { getCollection } from 'astro:content';
const added = (await getCollection('api')).filter((entry) => entry.data.addedIn === '2.0');---