Skip to content

Commit 49677ce

Browse files
authored
Merge pull request #15154 from nextcloud/backport/15140/stable34
[stable34] fix: show numbered version in manual titles and version picker
2 parents 40e3fa2 + dfc9a86 commit 49677ce

10 files changed

Lines changed: 286 additions & 33 deletions

File tree

.github/workflows/sphinxbuild.yml

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,40 @@ jobs:
5959
- name: Checkout repository
6060
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
6161

62+
- name: Get stable branches
63+
if: github.ref == 'refs/heads/master' || github.base_ref == 'master'
64+
id: stable_branches
65+
run: |
66+
branches=$(git ls-remote --heads origin "heads/stable[0-9][0-9]" \
67+
| awk '{gsub(/^refs\/heads\/stable/, "", $2); print $2}' \
68+
| sort -n -r | tr '\n' ' ')
69+
echo "branches=$branches" >> $GITHUB_OUTPUT
70+
71+
- name: Setup PHP for version validation
72+
if: github.ref == 'refs/heads/master' || github.base_ref == 'master'
73+
uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # v2.37.1
74+
75+
- name: Validate version constants in conf.py
76+
if: github.ref == 'refs/heads/master' || github.base_ref == 'master'
77+
env:
78+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
79+
run: |
80+
eval $(php build/detect-versions.php ${{ steps.stable_branches.outputs.branches }})
81+
82+
conf_stable=$(grep -m1 '^\s*version_stable\s*=' conf.py | grep -o '[0-9]\+')
83+
conf_start=$(grep -m1 '^\s*version_start\s*=' conf.py | grep -o '[0-9]\+')
84+
85+
err=0
86+
if [ "$highest_stable" != "$conf_stable" ]; then
87+
echo "::error::version_stable in conf.py ($conf_stable) != highest released stable ($highest_stable). Update conf.py."
88+
err=1
89+
fi
90+
if [ "$lowest_stable" != "$conf_start" ]; then
91+
echo "::error::version_start in conf.py ($conf_start) != lowest existing stable branch ($lowest_stable). Update conf.py."
92+
err=1
93+
fi
94+
exit $err
95+
6296
- name: Set up Python
6397
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
6498
with:
@@ -237,12 +271,17 @@ jobs:
237271

238272
- name: Compute PDF release version
239273
id: pdf_version
274+
shell: bash
240275
run: |
241-
branch="${GITHUB_REF#refs/heads/}"
242-
if [[ "$branch" == stable* ]]; then
243-
echo "release=${branch#stable}" >> $GITHUB_OUTPUT
276+
# For PRs use the target branch; for pushes use the current branch.
277+
# This handles both stable34 direct pushes and backport/*/stable34 PRs.
278+
branch="${GITHUB_BASE_REF:-${GITHUB_REF#refs/heads/}}"
279+
if [[ "$branch" =~ ^stable([0-9]+)$ ]]; then
280+
echo "release=${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT
244281
else
245-
echo "release=latest" >> $GITHUB_OUTPUT
282+
# master: derive the dev version from conf.py.
283+
version_stable=$(grep -m1 '^\s*version_stable\s*=' conf.py | grep -o '[0-9]\+')
284+
echo "release=$((version_stable + 1))" >> $GITHUB_OUTPUT
246285
fi
247286
248287
- name: Build pdf documentation

admin_manual/_templates/versions.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,20 @@
99
data-toggle="rst-current-version"
1010
aria-expanded="false"
1111
aria-controls="rst-other-versions-admin">
12-
☁️ {{ current_version }}
12+
☁️ {{ display_version }}
1313
<span class="fa fa-caret-down" aria-hidden="true"></span>
1414
</button>
1515
<div id="rst-other-versions-admin" class="rst-other-versions">
1616
<dl>
1717
<dt>☁️ {{ _('Versions') }}</dt>
18-
{% for slug, url in versions|reverse %}
18+
{% for slug, url, label in versions|reverse %}
1919
<dd style="width: 32%">
2020
<a href="{{ url }}"
2121
{% if current_version == slug %}
2222
style="color: var(--dark-link-color);"
2323
{% endif %}
2424
>
25-
{{ slug }}
25+
{{ label }}
2626
</a>
2727
</dd>
2828
{% endfor %}

admin_manual/conf.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
# -- Project information -----------------------------------------------------
2121
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
2222

23-
project = u'Nextcloud %s Administration Manual' % (version)
23+
project = u'Nextcloud %s Administration Manual' % (display_version)
24+
html_title = project
2425

2526
# -- General configuration ---------------------------------------------------
2627
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

build/build-index.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ function get_release_date(int $version): ?int {
124124
fwrite(STDERR, "➡️ Version $devVersion ($devStatus)\n");
125125

126126
// Collect released stable versions within support window
127+
$oneYearAgo = time() - (365 * 24 * 60 * 60);
127128
$stableVersions = [];
128129
foreach ($branches as $branch) {
129130
if (isset($released_branches[$branch]) && $released_branches[$branch] >= $oneYearAgo) {

build/detect-versions.php

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
<?php
2+
/**
3+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
4+
* SPDX-License-Identifier: AGPL-3.0-or-later
5+
*
6+
* Detect Nextcloud version metadata from a list of stable branch numbers.
7+
*
8+
* Shared helpers are used by build-index.php. When run as a CLI script,
9+
* outputs KEY=VALUE pairs (highest_stable, lowest_stable, dev_version)
10+
* suitable for appending to $GITHUB_OUTPUT or for eval in bash.
11+
*
12+
* Usage: php detect-versions.php <branch1> <branch2> ...
13+
* Example: php detect-versions.php 32 33 34
14+
*/
15+
16+
/**
17+
* Get the GitHub API headers with optional authentication.
18+
*/
19+
function get_github_headers(): string {
20+
$headers = 'User-Agent: Nextcloud Documentation Builder';
21+
if ($token = getenv('GITHUB_TOKEN')) {
22+
$headers .= "\r\nAuthorization: token $token";
23+
}
24+
return $headers;
25+
}
26+
27+
/**
28+
* Get the repository name for a given version.
29+
* Nextcloud moved to nextcloud-releases/server starting with version 32.
30+
*/
31+
function get_repo_for_version(int $version): string {
32+
return $version >= 32 ? 'nextcloud-releases/server' : 'nextcloud/server';
33+
}
34+
35+
/**
36+
* Parse the HTTP status code from the response headers populated by file_get_contents.
37+
*
38+
* @param array $headers The $http_response_header array
39+
*/
40+
function parse_http_status(array $headers): int {
41+
preg_match('/HTTP\/[\d.]+ (\d+)/', $headers[0] ?? '', $matches);
42+
return (int)($matches[1] ?? 0);
43+
}
44+
45+
/**
46+
* Fetch release info for a given version from the GitHub API.
47+
*
48+
* Returns an array ['date' => int] if the release exists (HTTP 200).
49+
* Returns null if the release does not exist (HTTP 404).
50+
* Exits with code 1 on any other HTTP status (rate limit, server error, etc.)
51+
* to prevent silently generating empty or incorrect output.
52+
*/
53+
function fetch_release_info(int $version): ?array {
54+
$repo = get_repo_for_version($version);
55+
$url = sprintf('https://api.github.com/repos/%s/releases/tags/v%d.0.0', $repo, $version);
56+
57+
$context = stream_context_create([
58+
'http' => [
59+
'header' => get_github_headers(),
60+
'timeout' => 10,
61+
'ignore_errors' => true
62+
]
63+
]);
64+
65+
$response = @file_get_contents($url, false, $context);
66+
67+
// FIXME: function_exists conditional can be dropped once we don't need to support <8.4.0
68+
if (function_exists('http_get_last_response_headers')) {
69+
/** @var array|null */
70+
$http_response_header = \http_get_last_response_headers();
71+
}
72+
73+
$status = isset($http_response_header) && is_array($http_response_header)
74+
? parse_http_status($http_response_header)
75+
: 0;
76+
77+
if ($status === 200) {
78+
$data = json_decode($response, true);
79+
$publishedAt = $data['published_at'] ?? $data['created_at'] ?? null;
80+
return ['date' => $publishedAt ? strtotime($publishedAt) : time()];
81+
}
82+
83+
if ($status === 404) {
84+
return null;
85+
}
86+
87+
fwrite(STDERR, "GitHub API error (HTTP $status) checking v$version.0.0 — aborting\n");
88+
exit(1);
89+
}
90+
91+
/**
92+
* Detect version metadata from a list of stable branch numbers.
93+
*
94+
* @param int[] $branches All known stable branch numbers (any order)
95+
* @return array{
96+
* highest_stable: int|null,
97+
* lowest_stable: int,
98+
* dev_version: int,
99+
* released: array<int, int>
100+
* }
101+
*/
102+
function detect_versions(array $branches): array {
103+
rsort($branches, SORT_NUMERIC);
104+
$oneYearAgo = time() - (365 * 24 * 60 * 60);
105+
$released = [];
106+
$firstOutOfSupportTime = null;
107+
108+
foreach ($branches as $branch) {
109+
if ($firstOutOfSupportTime !== null) {
110+
// Older than the first out-of-support version — skip API call,
111+
// store with the same timestamp (also out of support).
112+
fwrite(STDERR, "🛑 Version $branch is unsupported\n");
113+
$released[$branch] = $firstOutOfSupportTime;
114+
continue;
115+
}
116+
117+
$info = fetch_release_info($branch);
118+
if ($info === null) {
119+
fwrite(STDERR, "⏳ Version $branch is not released (tag v$branch.0.0 not found)\n");
120+
continue;
121+
}
122+
123+
$released[$branch] = $info['date'];
124+
if ($info['date'] < $oneYearAgo) {
125+
fwrite(STDERR, "🛑 Version $branch is unsupported (released on " . date('Y-m-d', $info['date']) . ")\n");
126+
$firstOutOfSupportTime = $info['date'];
127+
} else {
128+
fwrite(STDERR, "✅ Version $branch is supported (released on " . date('Y-m-d', $info['date']) . ")\n");
129+
}
130+
}
131+
132+
// highest_stable: highest branch with a confirmed release
133+
$highestStable = null;
134+
foreach ($branches as $b) {
135+
if (isset($released[$b])) {
136+
$highestStable = $b;
137+
break;
138+
}
139+
}
140+
141+
// dev_version: if the highest branch has a release, dev = highest + 1;
142+
// otherwise the branch exists but isn't released yet (upcoming).
143+
$devVersion = isset($released[$branches[0]]) ? $branches[0] + 1 : $branches[0];
144+
145+
// lowest_stable: lowest version still within the support window.
146+
// Using min($branches) would include ancient branches (e.g. stable10) that still
147+
// exist on the remote but are long out of support.
148+
$supportedVersions = array_keys(array_filter($released, fn($date) => $date >= $oneYearAgo));
149+
$lowestStable = !empty($supportedVersions) ? min($supportedVersions) : $highestStable;
150+
151+
return [
152+
'highest_stable' => $highestStable,
153+
'lowest_stable' => $lowestStable,
154+
'dev_version' => $devVersion,
155+
'released' => $released,
156+
];
157+
}
158+
159+
// CLI entry point — only runs when invoked directly, not when require_once'd.
160+
if (basename(__FILE__) === basename($argv[0])) {
161+
$branches = array_values(array_filter(
162+
array_map('intval', array_slice($argv, 1)),
163+
fn($b) => $b >= 12
164+
));
165+
166+
if (empty($branches)) {
167+
fwrite(STDERR, "Error: No valid stable branches provided (expected numeric args >= 12).\n");
168+
exit(1);
169+
}
170+
171+
$result = detect_versions($branches);
172+
173+
if ($result['highest_stable'] === null) {
174+
fwrite(STDERR, "Error: No released stable branch found.\n");
175+
exit(1);
176+
}
177+
178+
fwrite(STDERR, "➡️ Version {$result['dev_version']} (development)\n");
179+
180+
echo "highest_stable={$result['highest_stable']}\n";
181+
echo "lowest_stable={$result['lowest_stable']}\n";
182+
echo "dev_version={$result['dev_version']}\n";
183+
}

conf.py

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -56,35 +56,61 @@
5656
# disable including the reST sources in HTML builds (in _sources/) (default is True)
5757
html_copy_source = False
5858

59+
# building the versions list
60+
# CI validates both constants against actual stableNN branches (see sphinxbuild.yml).
61+
# Update version_start when the lowest stableNN branch is deleted (version goes EoL).
62+
# Update version_stable when a new NC release ships (highest stableNN branch added).
63+
version_start = 32 # oldest documented version
64+
65+
# latest released stable — CHANGING IT MUST RESULT IN A CHANGE OF THE SYMLINK ON THE LIVE SERVER
66+
version_stable = 34 # mapped to https://docs.nextcloud.com/server/stable/
67+
import re as _re
68+
# Detect stable branch version for display purposes.
69+
# For PRs: GITHUB_BASE_REF is the target branch (e.g. 'stable34').
70+
# For direct pushes: GITHUB_REF is 'refs/heads/stable34'.
71+
_base = os.environ.get('GITHUB_BASE_REF', '')
72+
_ref = os.environ.get('GITHUB_REF', '')
73+
_stable_ver = (
74+
_re.match(r'^stable(\d+)$', _base)
75+
or _re.match(r'^refs/heads/stable(\d+)$', _ref)
76+
)
77+
display_version = (
78+
release if release != 'latest' # PDF/ePub builds (DOCS_RELEASE set)
79+
else _stable_ver.group(1) if _stable_ver # stableNN branches and PRs targeting them
80+
else str(version_stable + 1) # master
81+
)
82+
83+
# Also search for "TODO ON RELEASE" in the rst files
84+
5985
# substitutions go here
6086
rst_epilog = """
6187
.. |version| replace:: %s
62-
""" % (release)
88+
""" % (display_version)
6389

6490
# Replace hardcoded /latest/ URLs in all .rst source files with the actual release
6591
def replace_latest(app, docname, source):
6692
if release != 'latest':
6793
source[0] = source[0].replace('/server/latest/', '/server/%s/' % release)
68-
94+
6995
def setup(app):
7096
app.connect('source-read', replace_latest)
7197

72-
73-
# building the versions list
74-
version_start = 32 # THIS IS THE OLDEST SUPPORTED VERSION NUMBER
75-
76-
# THIS IS THE VERSION THAT IS MAPPED TO https://docs.nextcloud.com/server/stable/
77-
version_stable = 33 # CHANGING IT MUST RESULT IN A CHANGE OF THE SYMLINK ON THE LIVE SERVER
78-
79-
# Also search for "TODO ON RELEASE" in the rst files
80-
8198
def generateVersionsDocs(current_docs):
8299
versions_doc = []
83-
for v in range(version_start, version_stable + 1):
100+
101+
# If viewing an unsupported (older than version_start) branch, prepend it so it
102+
# appears last after the template's |reverse — e.g. "26 (unsupported)" at the bottom.
103+
if _stable_ver:
104+
branch_ver = int(_stable_ver.group(1))
105+
if branch_ver < version_start:
106+
url = 'https://docs.nextcloud.com/server/%s/%s' % (str(branch_ver), current_docs)
107+
versions_doc.append((branch_ver, url, '%s (unsupported)' % branch_ver))
108+
109+
for v in range(version_start, version_stable):
84110
url = 'https://docs.nextcloud.com/server/%s/%s' % (str(v), current_docs)
85-
versions_doc.append(tuple((v, url)))
86-
versions_doc.append(tuple(('stable', 'https://docs.nextcloud.com/server/%s/%s' % ('stable', current_docs))))
87-
versions_doc.append(tuple(('latest', 'https://docs.nextcloud.com/server/%s/%s' % ('latest', current_docs))))
111+
versions_doc.append((v, url, str(v)))
112+
versions_doc.append(('stable', 'https://docs.nextcloud.com/server/stable/%s' % current_docs, '%s (stable)' % version_stable))
113+
versions_doc.append(('latest', 'https://docs.nextcloud.com/server/latest/%s' % current_docs, '%s (latest)' % str(version_stable + 1)))
88114
return versions_doc
89115

90116
if version.isdigit():
@@ -93,7 +119,8 @@ def generateVersionsDocs(current_docs):
93119
github_branch = 'master'
94120

95121
html_context = {
96-
'current_version': version,
122+
'current_version': int(_stable_ver.group(1)) if _stable_ver else version,
123+
'display_version': display_version,
97124
'READTHEDOCS': True,
98125

99126
# force github plugin

developer_manual/_templates/versions.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,20 @@
99
data-toggle="rst-current-version"
1010
aria-expanded="false"
1111
aria-controls="rst-other-versions-dev">
12-
☁️ {{ current_version }}
12+
☁️ {{ display_version }}
1313
<span class="fa fa-caret-down" aria-hidden="true"></span>
1414
</button>
1515
<div id="rst-other-versions-dev" class="rst-other-versions">
1616
<dl>
1717
<dt>☁️ {{ _('Versions') }}</dt>
18-
{% for slug, url in versions|reverse %}
18+
{% for slug, url, label in versions|reverse %}
1919
<dd style="width: 32%">
2020
<a href="{{ url }}"
2121
{% if current_version == slug %}
2222
style="color: var(--dark-link-color);"
2323
{% endif %}
2424
>
25-
{{ slug }}
25+
{{ label }}
2626
</a>
2727
</dd>
2828
{% endfor %}

0 commit comments

Comments
 (0)