Skip to content

Commit 1d79b03

Browse files
vdusekclaude
andauthored
docs: add documentation versioning support (#834)
## Summary - Add Docusaurus versioning support with version dropdown in the navbar. - Add manual doc snapshots for versions 3.3, 2.7, and 1.7 (with API reference). - Automate doc version creation on stable releases in the release workflow (minor/major create new versions, patches override the latest). Mirrors the approach from apify-client-python PRs --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5a37023 commit 1d79b03

File tree

199 files changed

+99501
-9
lines changed

Some content is hidden

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

199 files changed

+99501
-9
lines changed

.github/workflows/manual_release_stable.yaml

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,17 +102,83 @@ 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+
outputs:
110+
version_docs_commitish: ${{ steps.commit_versioned_docs.outputs.commit_long_sha }}
111+
permissions:
112+
contents: write
113+
env:
114+
NODE_VERSION: 22
115+
PYTHON_VERSION: 3.14
116+
117+
steps:
118+
- name: Checkout repository
119+
uses: actions/checkout@v6
120+
with:
121+
token: ${{ secrets.APIFY_SERVICE_ACCOUNT_GITHUB_TOKEN }}
122+
ref: ${{ needs.changelog_update.outputs.changelog_commitish }}
123+
124+
- name: Set up Node
125+
uses: actions/setup-node@v6
126+
with:
127+
node-version: ${{ env.NODE_VERSION }}
128+
129+
- name: Set up Python
130+
uses: actions/setup-python@v6
131+
with:
132+
python-version: ${{ env.PYTHON_VERSION }}
133+
134+
- name: Set up uv package manager
135+
uses: astral-sh/setup-uv@v7
136+
with:
137+
python-version: ${{ env.PYTHON_VERSION }}
138+
139+
- name: Install Python dependencies
140+
run: uv run poe install-dev
141+
142+
- name: Install website dependencies
143+
run: |
144+
cd website
145+
corepack enable
146+
yarn install
147+
148+
- name: Snapshot the current version
149+
run: |
150+
cd website
151+
VERSION="$(python -c "import tomllib, pathlib; print(tomllib.loads(pathlib.Path('../pyproject.toml').read_text())['project']['version'])")"
152+
MAJOR_MINOR="$(echo "$VERSION" | cut -d. -f1-2)"
153+
export MAJOR_MINOR
154+
# Remove existing version if present (patch releases override)
155+
rm -rf "versioned_docs/version-${MAJOR_MINOR}"
156+
rm -rf "versioned_sidebars/version-${MAJOR_MINOR}-sidebars.json"
157+
jq 'map(select(. != env.MAJOR_MINOR))' versions.json > tmp.json && mv tmp.json versions.json
158+
# Build API reference and create version snapshots
159+
bash build_api_reference.sh
160+
npx docusaurus docs:version "$MAJOR_MINOR"
161+
npx docusaurus api:version "$MAJOR_MINOR"
162+
163+
- name: Commit and push versioned docs
164+
id: commit_versioned_docs
165+
uses: EndBug/add-and-commit@v9
166+
with:
167+
add: "website/versioned_docs website/versioned_sidebars website/versions.json"
168+
message: "docs: version ${{ needs.release_prepare.outputs.version_number }} docs [skip ci]"
169+
default_author: github_actions
170+
105171
doc_release:
106172
name: Doc release
107-
needs: [changelog_update, pypi_publish]
173+
needs: [changelog_update, pypi_publish, version_docs]
108174
permissions:
109175
contents: write
110176
pages: write
111177
id-token: write
112178
uses: ./.github/workflows/_release_docs.yaml
113179
with:
114-
# Use the ref from the changelog update to include the updated changelog.
115-
ref: ${{ needs.changelog_update.outputs.changelog_commitish }}
180+
# Use the version_docs commit to include both changelog and versioned docs.
181+
ref: ${{ needs.version_docs.outputs.version_docs_commitish }}
116182
secrets: inherit
117183

118184
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/docusaurus.config.js

Lines changed: 32 additions & 6 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',
@@ -18,9 +19,14 @@ const GROUP_ORDER = [
1819
];
1920

2021
const groupSort = (g1, g2) => {
21-
if (GROUP_ORDER.includes(g1) && GROUP_ORDER.includes(g2)) {
22-
return GROUP_ORDER.indexOf(g1) - GROUP_ORDER.indexOf(g2);
23-
}
22+
const i1 = GROUP_ORDER.indexOf(g1);
23+
const i2 = GROUP_ORDER.indexOf(g2);
24+
// Both known – sort by defined order
25+
if (i1 !== -1 && i2 !== -1) return i1 - i2;
26+
// Unknown groups go after known ones
27+
if (i1 !== -1) return -1;
28+
if (i2 !== -1) return 1;
29+
// Both unknown – alphabetical
2430
return g1.localeCompare(g2);
2531
};
2632

@@ -66,19 +72,21 @@ module.exports = {
6672
title: 'SDK for Python',
6773
items: [
6874
{
69-
to: 'docs/overview',
75+
type: 'doc',
76+
docId: 'introduction/introduction',
7077
label: 'Docs',
7178
position: 'left',
7279
activeBaseRegex: '/docs(?!/changelog)',
7380
},
7481
{
75-
to: '/reference',
82+
type: 'custom-versioned-reference',
7683
label: 'Reference',
7784
position: 'left',
7885
activeBaseRegex: '/reference',
7986
},
8087
{
81-
to: 'docs/changelog',
88+
type: 'doc',
89+
docId: 'changelog',
8290
label: 'Changelog',
8391
position: 'left',
8492
activeBaseRegex: '/docs/changelog',
@@ -88,6 +96,17 @@ module.exports = {
8896
label: 'GitHub',
8997
position: 'left',
9098
},
99+
{
100+
type: 'docsVersionDropdown',
101+
position: 'left',
102+
className: 'navbar__item',
103+
'data-api-links': JSON.stringify([
104+
'reference/next',
105+
...versions.map((version, i) => (i === 0 ? 'reference' : `reference/${version}`)),
106+
]),
107+
dropdownItemsBefore: [],
108+
dropdownItemsAfter: [],
109+
},
91110
],
92111
},
93112
},
@@ -280,13 +299,20 @@ module.exports = {
280299
includeGeneratedIndex: false,
281300
includePages: true,
282301
relativePaths: false,
302+
excludeRoutes: [
303+
'/sdk/python/reference/[0-9]*/**',
304+
'/sdk/python/reference/[0-9]*',
305+
'/sdk/python/reference/next/**',
306+
'/sdk/python/reference/next',
307+
],
283308
},
284309
},
285310
],
286311
...config.plugins,
287312
],
288313
themeConfig: {
289314
...config.themeConfig,
315+
versions,
290316
tableOfContents: {
291317
...config.themeConfig.tableOfContents,
292318
maxHeadingLevel: 5,
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import OriginalComponentTypes from '@theme-original/NavbarItem/ComponentTypes';
2+
import VersionedReferenceNavbarItem from './VersionedReferenceNavbarItem';
3+
4+
export default {
5+
...OriginalComponentTypes,
6+
'custom-versioned-reference': VersionedReferenceNavbarItem,
7+
};
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import React, { useMemo } 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+
/* eslint-disable react/prop-types */
13+
export default function DocsVersionDropdownNavbarItem({
14+
mobile,
15+
docsPluginId,
16+
dropdownActiveClassDisabled,
17+
dropdownItemsBefore,
18+
dropdownItemsAfter,
19+
...props
20+
}) {
21+
const { search, hash, pathname } = useLocation();
22+
const { siteConfig } = useDocusaurusContext();
23+
const baseUrl = siteConfig.baseUrl.endsWith('/') ? siteConfig.baseUrl : `${siteConfig.baseUrl}/`;
24+
const apiLinks = useMemo(() => {
25+
if (!pathname.startsWith(`${baseUrl}reference`)) {
26+
return [];
27+
}
28+
try {
29+
return JSON.parse(props['data-api-links']);
30+
} catch {
31+
return [];
32+
}
33+
}, [props['data-api-links'], pathname, baseUrl]);
34+
35+
const activeDocContext = useActiveDocContext(docsPluginId);
36+
const versions = useVersions(docsPluginId);
37+
const { savePreferredVersionName } = useDocsPreferredVersion(docsPluginId);
38+
const versionLinks = versions.map((version, idx) => {
39+
// We try to link to the same doc, in another version
40+
// When not possible, fallback to the "main doc" of the version
41+
const versionDoc = activeDocContext.alternateDocVersions[version.name] ?? getVersionMainDoc(version);
42+
return {
43+
label: version.label,
44+
// preserve ?search#hash suffix on version switches
45+
to: `${apiLinks[idx] ?? versionDoc.path}${search}${hash}`,
46+
isActive: () => version === activeDocContext.activeVersion,
47+
onClick: () => savePreferredVersionName(version.name),
48+
};
49+
});
50+
const items = [...dropdownItemsBefore, ...versionLinks, ...dropdownItemsAfter];
51+
const dropdownVersion = useDocsVersionCandidates(docsPluginId)[0];
52+
// Mobile dropdown is handled a bit differently
53+
const dropdownLabel =
54+
mobile && items.length > 1
55+
? translate({
56+
id: 'theme.navbar.mobileVersionsDropdown.label',
57+
message: 'Versions',
58+
description: 'The label for the navbar versions dropdown on mobile view',
59+
})
60+
: dropdownVersion.label;
61+
let dropdownTo = mobile && items.length > 1 ? undefined : getVersionMainDoc(dropdownVersion).path;
62+
63+
if (dropdownTo && pathname.startsWith(`${baseUrl}reference`)) {
64+
dropdownTo = versionLinks.find((v) => v.label === dropdownVersion.label)?.to;
65+
}
66+
67+
// We don't want to render a version dropdown with 0 or 1 item. If we build
68+
// the site with a single docs version (onlyIncludeVersions: ['1.0.0']),
69+
// We'd rather render a button instead of a dropdown
70+
if (items.length <= 1) {
71+
return (
72+
<DefaultNavbarItem
73+
{...props}
74+
mobile={mobile}
75+
label={dropdownLabel}
76+
to={dropdownTo}
77+
isActive={dropdownActiveClassDisabled ? () => false : undefined}
78+
/>
79+
);
80+
}
81+
return (
82+
<DropdownNavbarItem
83+
{...props}
84+
mobile={mobile}
85+
label={dropdownLabel}
86+
to={dropdownTo}
87+
items={items}
88+
isActive={dropdownActiveClassDisabled ? () => false : undefined}
89+
/>
90+
);
91+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import React from 'react';
2+
import { useDocsVersionCandidates } from '@docusaurus/plugin-content-docs/client';
3+
import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
4+
5+
/* eslint-disable react/prop-types */
6+
export default function VersionedReferenceNavbarItem({ docsPluginId, ...props }) {
7+
const [version] = useDocsVersionCandidates(docsPluginId);
8+
9+
// Latest version → /reference, "current" (next) → /reference/next, others → /reference/{name}
10+
let to = '/reference';
11+
if (!version.isLast) {
12+
to = `/reference/${version.name === 'current' ? 'next' : version.name}`;
13+
}
14+
15+
return <DefaultNavbarItem {...props} to={to} />;
16+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
changelog.md
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
id: introduction
3+
title: Overview
4+
sidebar_label: Overview
5+
slug: /overview
6+
description: 'The official library for creating Apify Actors in Python, providing tools for web scraping, automation, and data storage integration.'
7+
---
8+
9+
The Apify SDK for Python is the official library for creating [Apify Actors](https://docs.apify.com/platform/actors) in Python.
10+
11+
```python
12+
from apify import Actor
13+
from bs4 import BeautifulSoup
14+
import requests
15+
16+
async def main():
17+
async with Actor:
18+
actor_input = await Actor.get_input()
19+
response = requests.get(actor_input['url'])
20+
soup = BeautifulSoup(response.content, 'html.parser')
21+
await Actor.push_data({ 'url': actor_input['url'], 'title': soup.title.string })
22+
```
23+
24+
## What are Actors?
25+
26+
Actors are serverless cloud programs that can do almost anything a human can do in a web browser. They can do anything from small tasks such as filling in forms or unsubscribing from online services, all the way up to scraping and processing vast numbers of web pages.
27+
28+
Actors can be run either locally, or on the [Apify platform](https://docs.apify.com/platform/), where you can run them at scale, monitor them, schedule them, and even publish and monetize them.
29+
30+
If you're new to Apify, learn [what is Apify](https://docs.apify.com/platform/about) in the Apify platform documentation.
31+
32+
## Quick start
33+
34+
To create and run Actors through Apify Console, see the [Console documentation](https://docs.apify.com/academy/getting-started/creating-actors#choose-your-template). For creating and running Python Actors locally, refer to the [quick start guide](./quick-start).
35+
36+
Explore the Guides section in the sidebar for a deeper understanding of the SDK's features and best practices.
37+
38+
## Installation
39+
40+
The Apify SDK for Python requires Python version 3.8 or above. It is typically installed when you create a new Actor project using the [Apify CLI](https://docs.apify.com/cli). To install it manually in an existing project, use:
41+
42+
```bash
43+
pip install apify
44+
```
45+
46+
:::note API client alternative
47+
48+
If you need to interact with the Apify API programmatically without creating Actors, use the [Apify API client for Python](https://docs.apify.com/api/client/python) instead.
49+
50+
:::

0 commit comments

Comments
 (0)