Skip to content

Commit 64c39d7

Browse files
vdusekclaude
andcommitted
docs: add documentation versioning support
Add Docusaurus versioning with version dropdown, version snapshots for 3.3, 2.7, and 1.7, and automated versioning in the release workflow. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5a37023 commit 64c39d7

File tree

202 files changed

+99902
-3
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

202 files changed

+99902
-3
lines changed

.github/workflows/manual_release_stable.yaml

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,17 +102,79 @@ jobs:
102102
- name: Publish package to PyPI
103103
uses: pypa/gh-action-pypi-publish@release/v1
104104

105+
version_docs:
106+
name: Version docs
107+
needs: [release_prepare, changelog_update, pypi_publish]
108+
runs-on: ubuntu-latest
109+
permissions:
110+
contents: write
111+
env:
112+
NODE_VERSION: 22
113+
PYTHON_VERSION: 3.14
114+
115+
steps:
116+
- name: Checkout repository
117+
uses: actions/checkout@v6
118+
with:
119+
token: ${{ secrets.APIFY_SERVICE_ACCOUNT_GITHUB_TOKEN }}
120+
121+
- name: Set up Node
122+
uses: actions/setup-node@v6
123+
with:
124+
node-version: ${{ env.NODE_VERSION }}
125+
126+
- name: Set up Python
127+
uses: actions/setup-python@v6
128+
with:
129+
python-version: ${{ env.PYTHON_VERSION }}
130+
131+
- name: Set up uv package manager
132+
uses: astral-sh/setup-uv@v7
133+
with:
134+
python-version: ${{ env.PYTHON_VERSION }}
135+
136+
- name: Install Python dependencies
137+
run: uv run poe install-dev
138+
139+
- name: Install website dependencies
140+
run: |
141+
cd website
142+
corepack enable
143+
yarn install
144+
145+
- name: Snapshot the current version
146+
run: |
147+
cd website
148+
VERSION="$(python -c "import tomllib, pathlib; print(tomllib.loads(pathlib.Path('../pyproject.toml').read_text())['project']['version'])")"
149+
MAJOR_MINOR="$(echo "$VERSION" | cut -d. -f1-2)"
150+
export MAJOR_MINOR
151+
# Remove existing version if present (patch releases override)
152+
rm -rf "versioned_docs/version-${MAJOR_MINOR}"
153+
rm -rf "versioned_sidebars/version-${MAJOR_MINOR}-sidebars.json"
154+
jq 'map(select(. != env.MAJOR_MINOR))' versions.json > tmp.json && mv tmp.json versions.json
155+
# Build API reference and create version snapshots
156+
bash build_api_reference.sh
157+
npx docusaurus docs:version "$MAJOR_MINOR"
158+
npx docusaurus api:version "$MAJOR_MINOR"
159+
160+
- name: Commit and push versioned docs
161+
uses: EndBug/add-and-commit@v9
162+
with:
163+
add: "website/versioned_docs website/versioned_sidebars website/versions.json"
164+
message: "docs: version ${{ needs.release_prepare.outputs.version_number }} docs [skip ci]"
165+
default_author: github_actions
166+
105167
doc_release:
106168
name: Doc release
107-
needs: [changelog_update, pypi_publish]
169+
needs: [changelog_update, pypi_publish, version_docs]
108170
permissions:
109171
contents: write
110172
pages: write
111173
id-token: write
112174
uses: ./.github/workflows/_release_docs.yaml
113175
with:
114-
# Use the ref from the changelog update to include the updated changelog.
115-
ref: ${{ needs.changelog_update.outputs.changelog_commitish }}
176+
# Use the default branch ref to include both changelog and versioned docs.
177+
ref: ${{ github.ref }}
116178
secrets: inherit
117179

118180
trigger_docker_build:

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ allow-direct-references = true
9999
[tool.ruff]
100100
line-length = 120
101101
include = ["src/**/*.py", "tests/**/*.py", "docs/**/*.py", "website/**/*.py"]
102+
exclude = [
103+
"website/versioned_docs/**",
104+
]
102105

103106
[tool.ruff.lint]
104107
select = ["ALL"]
@@ -201,6 +204,7 @@ python-version = "3.10"
201204

202205
[tool.ty.src]
203206
include = ["src", "tests", "scripts", "docs", "website"]
207+
exclude = ["website/versioned_docs"]
204208

205209
[[tool.ty.overrides]]
206210
include = [

website/build_api_reference.sh

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,21 @@
11
#!/bin/bash
22

3+
# On macOS, sed requires a space between -i and '' to specify no backup should be done
4+
# On Linux, sed requires no space between -i and '' to specify no backup should be done
5+
sed_no_backup() {
6+
if [[ $(uname) = "Darwin" ]]; then
7+
sed -i '' "$@"
8+
else
9+
sed -i'' "$@"
10+
fi
11+
}
12+
13+
# Create docspec dump of this package's source code through pydoc-markdown
14+
pydoc-markdown --quiet --dump > docspec-dump.jsonl
15+
sed_no_backup "s#${PWD}/..#REPO_ROOT_PLACEHOLDER#g" docspec-dump.jsonl
16+
317
# Generate import shortcuts from the modules
418
python generate_module_shortcuts.py
19+
20+
# Transform the docspec dumps into Typedoc-compatible docs tree
21+
node transformDocs.js

website/docusaurus.config.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const { join, resolve } = require('node:path');
33
const { config } = require('@apify/docs-theme');
44

55
const { externalLinkProcessor } = require('./tools/utils/externalLink');
6+
const versions = require('./versions.json');
67

78
const GROUP_ORDER = [
89
'Actor',
@@ -88,6 +89,17 @@ module.exports = {
8889
label: 'GitHub',
8990
position: 'left',
9091
},
92+
{
93+
type: 'docsVersionDropdown',
94+
position: 'left',
95+
className: 'navbar__item',
96+
'data-api-links': JSON.stringify([
97+
'reference/next',
98+
...versions.map((version, i) => (i === 0 ? 'reference' : `reference/${version}`)),
99+
]),
100+
dropdownItemsBefore: [],
101+
dropdownItemsAfter: [],
102+
},
91103
],
92104
},
93105
},
@@ -121,6 +133,7 @@ module.exports = {
121133
typedocOptions: {
122134
excludeExternals: false,
123135
},
136+
pathToCurrentVersionTypedocJSON: `${__dirname}/api-typedoc-generated.json`,
124137
sortSidebar: groupSort,
125138
routeBasePath: 'reference',
126139
python: true,
@@ -280,13 +293,20 @@ module.exports = {
280293
includeGeneratedIndex: false,
281294
includePages: true,
282295
relativePaths: false,
296+
excludeRoutes: [
297+
'/sdk/python/reference/[0-9]*/**',
298+
'/sdk/python/reference/[0-9]*',
299+
'/sdk/python/reference/next/**',
300+
'/sdk/python/reference/next',
301+
],
283302
},
284303
},
285304
],
286305
...config.plugins,
287306
],
288307
themeConfig: {
289308
...config.themeConfig,
309+
versions,
290310
tableOfContents: {
291311
...config.themeConfig.tableOfContents,
292312
maxHeadingLevel: 5,

website/pydoc-markdown.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
loaders:
2+
- type: python
3+
search_path: [../src]
4+
processors:
5+
- type: filter
6+
skip_empty_modules: true
7+
- type: crossref
8+
renderer:
9+
type: docusaurus
10+
docs_base_path: docs
11+
relative_output_path: reference
12+
relative_sidebar_path: sidebar.json
13+
sidebar_top_level_label: null
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import React from 'react';
2+
import { useVersions, useActiveDocContext, useDocsVersionCandidates } from '@docusaurus/plugin-content-docs/client';
3+
import { useDocsPreferredVersion } from '@docusaurus/theme-common';
4+
import { translate } from '@docusaurus/Translate';
5+
import { useLocation } from '@docusaurus/router';
6+
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
7+
import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
8+
import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem';
9+
10+
const getVersionMainDoc = (version) => version.docs.find((doc) => doc.id === version.mainDocId);
11+
12+
function getApiLinks(props, pathname, baseUrl) {
13+
if (!pathname.startsWith(`${baseUrl}reference`)) {
14+
return [];
15+
}
16+
17+
try {
18+
return JSON.parse(props['data-api-links']);
19+
} catch {
20+
return [];
21+
}
22+
}
23+
24+
/* eslint-disable react/prop-types */
25+
export default function DocsVersionDropdownNavbarItem({
26+
mobile,
27+
docsPluginId,
28+
dropdownActiveClassDisabled,
29+
dropdownItemsBefore,
30+
dropdownItemsAfter,
31+
...props
32+
}) {
33+
const { search, hash, pathname } = useLocation();
34+
const { siteConfig } = useDocusaurusContext();
35+
const baseUrl = siteConfig.baseUrl.endsWith('/') ? siteConfig.baseUrl : `${siteConfig.baseUrl}/`;
36+
const apiLinks = getApiLinks(props, pathname, baseUrl);
37+
38+
const activeDocContext = useActiveDocContext(docsPluginId);
39+
const versions = useVersions(docsPluginId);
40+
const { savePreferredVersionName } = useDocsPreferredVersion(docsPluginId);
41+
const versionLinks = versions.map((version, idx) => {
42+
// We try to link to the same doc, in another version
43+
// When not possible, fallback to the "main doc" of the version
44+
const versionDoc = activeDocContext.alternateDocVersions[version.name] ?? getVersionMainDoc(version);
45+
return {
46+
label: version.label,
47+
// preserve ?search#hash suffix on version switches
48+
to: `${apiLinks[idx] ?? versionDoc.path}${search}${hash}`,
49+
isActive: () => version === activeDocContext.activeVersion,
50+
onClick: () => savePreferredVersionName(version.name),
51+
};
52+
});
53+
const items = [...dropdownItemsBefore, ...versionLinks, ...dropdownItemsAfter];
54+
const dropdownVersion = useDocsVersionCandidates(docsPluginId)[0];
55+
// Mobile dropdown is handled a bit differently
56+
const dropdownLabel =
57+
mobile && items.length > 1
58+
? translate({
59+
id: 'theme.navbar.mobileVersionsDropdown.label',
60+
message: 'Versions',
61+
description: 'The label for the navbar versions dropdown on mobile view',
62+
})
63+
: dropdownVersion.label;
64+
let dropdownTo = mobile && items.length > 1 ? undefined : getVersionMainDoc(dropdownVersion).path;
65+
66+
if (dropdownTo && pathname.startsWith(`${baseUrl}reference`)) {
67+
dropdownTo = versionLinks.find((v) => v.label === dropdownVersion.label)?.to;
68+
}
69+
70+
// We don't want to render a version dropdown with 0 or 1 item. If we build
71+
// the site with a single docs version (onlyIncludeVersions: ['1.0.0']),
72+
// We'd rather render a button instead of a dropdown
73+
if (items.length <= 1) {
74+
return (
75+
<DefaultNavbarItem
76+
{...props}
77+
mobile={mobile}
78+
label={dropdownLabel}
79+
to={dropdownTo}
80+
isActive={dropdownActiveClassDisabled ? () => false : undefined}
81+
/>
82+
);
83+
}
84+
return (
85+
<DropdownNavbarItem
86+
{...props}
87+
mobile={mobile}
88+
label={dropdownLabel}
89+
to={dropdownTo}
90+
items={items}
91+
isActive={dropdownActiveClassDisabled ? () => false : undefined}
92+
/>
93+
);
94+
}

0 commit comments

Comments
 (0)