@redpanda-data/docs-extensions-and-macros 5.13.3 → 5.13.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli-utils/version.js +73 -0
- package/extensions/REFERENCE.adoc +43 -9
- package/extensions/version-fetcher/get-latest-redpanda-version.js +41 -16
- package/extensions/version-fetcher/set-latest-version.js +68 -18
- package/macros/enterprise.js +105 -1
- package/package.json +1 -1
- package/tools/bundle-openapi.js +4 -30
- package/tools/enterprise-features/verify.js +9 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared `major.minor` derivation.
|
|
5
|
+
*
|
|
6
|
+
* Two variants exist because the callers have genuinely different contracts, and
|
|
7
|
+
* they live side by side here so a third copy never gets written:
|
|
8
|
+
*
|
|
9
|
+
* - `getMajorMinor` is strict. It is used by the OpenAPI bundler, where a bad
|
|
10
|
+
* version is a build error and a Git branch name (for example, `dev`) must
|
|
11
|
+
* pass through untouched.
|
|
12
|
+
* - `toShortVersion` is lenient. It is used by the Antora version fetcher, where
|
|
13
|
+
* a non-semver release channel (for example, `nightly`) must simply not
|
|
14
|
+
* produce a `-version-short` attribute rather than throw or leak the raw
|
|
15
|
+
* value into the docs.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// A complete semantic version, optionally with prerelease and build metadata.
|
|
19
|
+
const SEMVER_PATTERN = /^\d+\.\d+\.\d+(-[\w.-]+)?(\+[\w.-]+)?$/;
|
|
20
|
+
|
|
21
|
+
// A leading `<major>.<minor>` pair that is followed by a separator or the end of
|
|
22
|
+
// the string. This also accepts two-part versions such as `26.2`, which Antora
|
|
23
|
+
// components use, and which `SEMVER_PATTERN` rejects.
|
|
24
|
+
const MAJOR_MINOR_PATTERN = /^(\d+)\.(\d+)(?:[.+-]|$)/;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Return the major.minor portion of a semantic version string.
|
|
28
|
+
*
|
|
29
|
+
* Accepts a semantic version like `25.1.1` and yields `25.1`. A value that is
|
|
30
|
+
* not a semantic version (for example, the branch name `dev`) is returned
|
|
31
|
+
* unchanged.
|
|
32
|
+
* @param {string} version - Semantic version (for example, `'25.1.1'`) or a branch name.
|
|
33
|
+
* @returns {string} The `major.minor` string (for example, `'25.1'`) or the input unchanged.
|
|
34
|
+
* @throws {Error} If `version` is not a non-empty string, lacks major/minor parts, or if major/minor are not numeric.
|
|
35
|
+
*/
|
|
36
|
+
function getMajorMinor(version) {
|
|
37
|
+
if (!version || typeof version !== 'string') {
|
|
38
|
+
throw new Error('Version must be a non-empty string');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Only process if valid semver, else return as-is (branch name)
|
|
42
|
+
if (!SEMVER_PATTERN.test(version)) {
|
|
43
|
+
return version;
|
|
44
|
+
}
|
|
45
|
+
const parts = version.split('.');
|
|
46
|
+
if (parts.length < 2) {
|
|
47
|
+
throw new Error(`Invalid version format: ${version}. Expected X.Y.Z format`);
|
|
48
|
+
}
|
|
49
|
+
const major = parseInt(parts[0], 10);
|
|
50
|
+
const minor = parseInt(parts[1], 10);
|
|
51
|
+
if (isNaN(major) || isNaN(minor)) {
|
|
52
|
+
throw new Error(`Major and minor versions must be numbers: ${version}`);
|
|
53
|
+
}
|
|
54
|
+
return `${major}.${minor}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Return the major.minor portion of a version, or `null` when there is none.
|
|
59
|
+
*
|
|
60
|
+
* Unlike `getMajorMinor`, this never throws and never returns a value that is
|
|
61
|
+
* not a `major.minor` pair, so a caller can treat `null` as "no short version
|
|
62
|
+
* exists for this release channel". Leading zeros are normalized, so `26.02.1`
|
|
63
|
+
* yields `26.2`. The `v` prefix is not accepted: sanitize it first.
|
|
64
|
+
* @param {string} version - Version without a `v` prefix (for example, `'26.2.1'`).
|
|
65
|
+
* @returns {string|null} The `major.minor` string, or `null` for anything else.
|
|
66
|
+
*/
|
|
67
|
+
function toShortVersion(version) {
|
|
68
|
+
if (!version || typeof version !== 'string') return null;
|
|
69
|
+
const match = MAJOR_MINOR_PATTERN.exec(version);
|
|
70
|
+
return match ? `${Number(match[1])}.${Number(match[2])}` : null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { getMajorMinor, toShortVersion };
|
|
@@ -428,19 +428,53 @@ antora:
|
|
|
428
428
|
|
|
429
429
|
This extension fetches the latest release versions from GitHub.
|
|
430
430
|
|
|
431
|
+
=== Attributes
|
|
432
|
+
|
|
431
433
|
The following attributes are available to all versions of all Antora components:
|
|
432
434
|
|
|
433
|
-
`latest-console-version
|
|
434
|
-
`latest-connect-version
|
|
435
|
-
`redpanda-beta-version
|
|
436
|
-
`redpanda-beta-commit
|
|
435
|
+
`latest-console-version`:: The latest release version of Redpanda Console.
|
|
436
|
+
`latest-connect-version`:: The latest release version of Redpanda Connect.
|
|
437
|
+
`redpanda-beta-version`:: The latest RC version of Redpanda.
|
|
438
|
+
`redpanda-beta-commit`:: The commit hash for the latest RC version of Redpanda.
|
|
439
|
+
`console-beta-version`:: The latest beta version of Redpanda Console.
|
|
440
|
+
`operator-beta-version`:: The latest beta version of the Redpanda Operator.
|
|
441
|
+
`helm-beta-version`:: The latest beta version of the Redpanda Helm chart.
|
|
442
|
+
`latest-operator-version`:: The latest release version of the Redpanda Operator. This value keeps its `v` prefix (for example, `v25.1.3`) because pages pass it straight to `helm --version`.
|
|
443
|
+
`latest-redpanda-helm-chart-version`:: The latest release version of the Redpanda Helm chart.
|
|
444
|
+
|
|
445
|
+
The following attributes are available to the latest version of each Antora component:
|
|
446
|
+
|
|
447
|
+
`full-version`:: The latest release version of Redpanda. Only ever moves forwards: if the component's `antora.yml` already pins a higher version, that pin wins.
|
|
448
|
+
`latest-redpanda-version`:: The latest release version of Redpanda.
|
|
449
|
+
`latest-release-commit`:: The commit hash for the latest release version of Redpanda.
|
|
450
|
+
|
|
451
|
+
==== Tag and short-version siblings
|
|
452
|
+
|
|
453
|
+
Most `<base>-version` attributes in the previous lists also get two siblings.
|
|
454
|
+
`full-version` is the exception and gets neither, because it is set directly
|
|
455
|
+
rather than through the shared helper, and it is the one attribute that only
|
|
456
|
+
ever moves forwards.
|
|
457
|
+
|
|
458
|
+
`<base>-tag`:: The version exactly as the release publishes it, including any `v` prefix (for example, `latest-redpanda-tag` is `v26.2.1`). Use this for container image tags and Git references. There is no tag attribute for `latest-operator-version` or `latest-redpanda-helm-chart-version`, which are already raw values.
|
|
459
|
+
`<base>-version-short`:: Only the `major.minor` part (for example, `latest-redpanda-version-short` is `26.2` when `latest-redpanda-version` is `26.2.1`). Use this where a page needs the release line rather than an exact patch, such as a download directory or an install pack name.
|
|
437
460
|
|
|
438
|
-
|
|
461
|
+
A short attribute is only set when a `major.minor` pair can be derived. Release channels that are not versions, such as `nightly` or `latest`, get a `-version` and a `-tag` but no `-version-short`, so a page never renders a fabricated release line.
|
|
462
|
+
|
|
463
|
+
==== Seed a fallback in antora.yml
|
|
464
|
+
|
|
465
|
+
Every attribute here depends on a successful GitHub lookup at build time. If a lookup fails, for example because of a rate limit or a missing token, the attribute is never set and any page that references it renders the attribute name in braces.
|
|
466
|
+
|
|
467
|
+
To keep a page buildable, seed a soft default in the consuming component's `antora.yml`. The trailing `@` makes it a fallback that the extension overwrites when the fetch succeeds:
|
|
468
|
+
|
|
469
|
+
[,yaml]
|
|
470
|
+
----
|
|
471
|
+
asciidoc:
|
|
472
|
+
attributes:
|
|
473
|
+
latest-redpanda-version: 26.2.1@
|
|
474
|
+
latest-redpanda-version-short: 26.2@
|
|
475
|
+
----
|
|
439
476
|
|
|
440
|
-
`full-version
|
|
441
|
-
`latest-release-commit`: The commit hash for the latest release version of Redpanda.
|
|
442
|
-
`latest-operator-version`: The latest release version of the Redpanda Operator.
|
|
443
|
-
`latest-redpanda-helm-chart-version`: The latest release version of the Redpanda Helm chart.
|
|
477
|
+
CAUTION: Do not seed `full-version` with a value higher than the current release. `full-version` is the one attribute the extension refuses to move backwards, so a pin that is ahead of the current release stays ahead.
|
|
444
478
|
|
|
445
479
|
=== Environment variables
|
|
446
480
|
|
|
@@ -36,20 +36,45 @@ module.exports = async (github, owner, repo, logger = null) => {
|
|
|
36
36
|
release => release.tag_name.includes('-rc') && !release.draft
|
|
37
37
|
);
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
39
|
+
// A release can exist before its tag is pushed, which is routine for draft
|
|
40
|
+
// RCs, and GitHub answers the ref lookup with a 404. Losing the commit hash
|
|
41
|
+
// must not cost us the version: an uncaught 404 here aborts the whole
|
|
42
|
+
// lookup and every latest-redpanda-* attribute goes unset for the build.
|
|
43
|
+
const resolveCommitHash = async (release) => {
|
|
44
|
+
if (!release) return null;
|
|
45
|
+
try {
|
|
46
|
+
// Retry the ref lookup on its own. A rate limit or a 5xx is transient
|
|
47
|
+
// and deserves the attempts; swallowing it immediately turned one
|
|
48
|
+
// blip into a permanently unset commit attribute. Retrying HERE
|
|
49
|
+
// rather than letting the error escape is what keeps a exhausted
|
|
50
|
+
// retry from also costing us the version, which is strictly worse
|
|
51
|
+
// than losing the commit hash.
|
|
52
|
+
const commitData = await retryWithBackoff(
|
|
53
|
+
() => github.rest.git.getRef({ owner, repo, ref: `tags/${release.tag_name}` }),
|
|
54
|
+
{ shouldRetry: isRetryableGitHubError, operationName: `getRef ${release.tag_name}` },
|
|
55
|
+
logger
|
|
56
|
+
);
|
|
57
|
+
return commitData.data.object.sha.substring(0, 7);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const message = `Could not resolve the commit for ${release.tag_name}, so its commit attribute is unset: ${error.status || ''} ${error.message || error}`;
|
|
60
|
+
if (logger) {
|
|
61
|
+
logger.warn(message);
|
|
62
|
+
} else {
|
|
63
|
+
console.warn(message);
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const latestRedpandaReleaseCommitHash = await resolveCommitHash(latestRedpandaRelease);
|
|
48
70
|
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
71
|
+
// The RC's tag is resolved separately, not through resolveCommitHash:
|
|
72
|
+
// a missing tag must exclude the RC entirely (it's an unpublished
|
|
73
|
+
// draft slipping through by version alone, the bug this guards
|
|
74
|
+
// against), not just null out its commit hash while still reporting
|
|
75
|
+
// the draft's version as latest. Only a missing tag (404) is
|
|
76
|
+
// tolerated; any other error still propagates so retryWithBackoff can
|
|
77
|
+
// retry it.
|
|
53
78
|
let latestRcReleaseCommitHash = null;
|
|
54
79
|
let resolvedRcRelease = latestRcRelease;
|
|
55
80
|
if (latestRcRelease) {
|
|
@@ -59,7 +84,7 @@ module.exports = async (github, owner, repo, logger = null) => {
|
|
|
59
84
|
repo,
|
|
60
85
|
ref: `tags/${latestRcRelease.tag_name}`
|
|
61
86
|
});
|
|
62
|
-
latestRcReleaseCommitHash = rcCommitData.data.object.sha;
|
|
87
|
+
latestRcReleaseCommitHash = rcCommitData.data.object.sha.substring(0, 7);
|
|
63
88
|
} catch (error) {
|
|
64
89
|
if (error.status !== 404) throw error;
|
|
65
90
|
const message = `No git tag for RC release ${latestRcRelease.tag_name}; ignoring it.`;
|
|
@@ -75,11 +100,11 @@ module.exports = async (github, owner, repo, logger = null) => {
|
|
|
75
100
|
return {
|
|
76
101
|
latestRedpandaRelease: latestRedpandaRelease ? {
|
|
77
102
|
version: latestRedpandaRelease.tag_name,
|
|
78
|
-
commitHash: latestRedpandaReleaseCommitHash
|
|
103
|
+
commitHash: latestRedpandaReleaseCommitHash
|
|
79
104
|
} : null,
|
|
80
105
|
latestRcRelease: resolvedRcRelease ? {
|
|
81
106
|
version: resolvedRcRelease.tag_name,
|
|
82
|
-
commitHash: latestRcReleaseCommitHash
|
|
107
|
+
commitHash: latestRcReleaseCommitHash
|
|
83
108
|
} : null
|
|
84
109
|
};
|
|
85
110
|
},
|
|
@@ -5,13 +5,14 @@ const { raiseListenerLimit } = require('../util/raise-listener-limit')
|
|
|
5
5
|
module.exports.register = function ({ config }) {
|
|
6
6
|
raiseListenerLimit(this)
|
|
7
7
|
const GetLatestRedpandaVersion = require('./get-latest-redpanda-version');
|
|
8
|
-
const GetLatestConsoleVersion = require('./get-latest-console-version');
|
|
9
8
|
const GetLatestDockerTag = require('./fetch-latest-docker-tag');
|
|
10
9
|
const GetLatestHelmChartVersionFromOperator = require('./get-latest-redpanda-helm-version-from-operator');
|
|
11
10
|
const GetLatestConnectVersion = require('./get-latest-connect');
|
|
12
11
|
const logger = this.getLogger('set-latest-version-extension');
|
|
13
12
|
|
|
14
13
|
const { getGitHubToken } = require('../../cli-utils/github-token');
|
|
14
|
+
// Shared with tools/bundle-openapi.js so there is one major.minor derivation.
|
|
15
|
+
const { toShortVersion } = require('../../cli-utils/version');
|
|
15
16
|
const token = getGitHubToken();
|
|
16
17
|
|
|
17
18
|
if (!token) {
|
|
@@ -92,7 +93,10 @@ module.exports.register = function ({ config }) {
|
|
|
92
93
|
asciidoc.attributes['page-component-version-is-prerelease'] = 'true';
|
|
93
94
|
}
|
|
94
95
|
|
|
95
|
-
// Set operator and helm chart attributes via helper function
|
|
96
|
+
// Set operator and helm chart attributes via helper function. These keep
|
|
97
|
+
// their raw fetched form, including the "v" prefix on
|
|
98
|
+
// latest-operator-version, because docs pages pass that value straight
|
|
99
|
+
// to `helm --version`. Only the short sibling is derived.
|
|
96
100
|
updateAttributes(asciidoc, [
|
|
97
101
|
{ condition: latestVersions.operator, key: 'latest-operator-version', value: latestVersions.operator?.latestStableRelease },
|
|
98
102
|
{ condition: latestVersions.helmChart, key: 'latest-redpanda-helm-chart-version', value: latestVersions.helmChart?.latestStableRelease }
|
|
@@ -111,7 +115,9 @@ module.exports.register = function ({ config }) {
|
|
|
111
115
|
// Special handling for Redpanda RC versions if in beta
|
|
112
116
|
if (latestVersions.redpanda?.latestRcRelease?.version) {
|
|
113
117
|
setVersionAndTagAttributes(asciidoc, 'redpanda-beta', latestVersions.redpanda.latestRcRelease.version, name, version);
|
|
114
|
-
|
|
118
|
+
if (latestVersions.redpanda.latestRcRelease.commitHash) {
|
|
119
|
+
asciidoc.attributes['redpanda-beta-commit'] = latestVersions.redpanda.latestRcRelease.commitHash;
|
|
120
|
+
}
|
|
115
121
|
}
|
|
116
122
|
if (latestVersions.console?.latestBetaRelease) {
|
|
117
123
|
setVersionAndTagAttributes(asciidoc, 'console-beta', latestVersions.console.latestBetaRelease, name, version);
|
|
@@ -126,36 +132,66 @@ module.exports.register = function ({ config }) {
|
|
|
126
132
|
|
|
127
133
|
if (!component.latest.asciidoc) component.latest.asciidoc = { attributes: {} };
|
|
128
134
|
|
|
129
|
-
// For Redpanda GA
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
// For the Redpanda GA release, always publish latest-redpanda-version,
|
|
136
|
+
// latest-redpanda-tag, latest-redpanda-version-short and
|
|
137
|
+
// latest-release-commit. They describe the newest release, so they must
|
|
138
|
+
// not depend on how the component pins full-version: consuming repos seed
|
|
139
|
+
// full-version at the current GA (both docs and cloud-docs sit at 26.2.1
|
|
140
|
+
// today), which made the whole block unreachable and left those
|
|
141
|
+
// attributes unset until Redpanda shipped something strictly newer.
|
|
142
|
+
const gaRelease = latestVersions.redpanda?.latestRedpandaRelease;
|
|
143
|
+
if (semver.valid(gaRelease?.version)) {
|
|
144
|
+
setVersionAndTagAttributes(component.latest.asciidoc, 'latest-redpanda', gaRelease.version, component.latest.name, component.latest.version);
|
|
145
|
+
// An unresolvable tag leaves the commit hash null. Leave any antora.yml
|
|
146
|
+
// fallback in place rather than overwriting it with nothing.
|
|
147
|
+
if (gaRelease.commitHash) {
|
|
148
|
+
component.latest.asciidoc.attributes['latest-release-commit'] = gaRelease.commitHash;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Required for backwards compatibility. Some docs still use full-version,
|
|
152
|
+
// and treat it as a floor, so only ever move it forwards. An unparseable
|
|
153
|
+
// pin counts as no pin, otherwise semver.gt throws and aborts the run.
|
|
154
|
+
const pinnedVersion = component.latest.asciidoc.attributes['full-version'];
|
|
155
|
+
if (!semver.valid(pinnedVersion) || semver.gt(gaRelease.version, pinnedVersion)) {
|
|
156
|
+
component.latest.asciidoc.attributes['full-version'] = sanitizeVersion(gaRelease.version);
|
|
137
157
|
}
|
|
138
158
|
}
|
|
139
159
|
});
|
|
140
160
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
161
|
+
// Report what resolved and what did not. A failed lookup resolves to a null
|
|
162
|
+
// release rather than rejecting, so dereferencing it here used to throw and
|
|
163
|
+
// replace the real cause with a bare TypeError from the logger.
|
|
164
|
+
const summary = [
|
|
165
|
+
['Redpanda', latestVersions.redpanda?.latestRedpandaRelease?.version, latestVersions.redpanda?.latestRcRelease?.version],
|
|
166
|
+
['Connect', latestVersions.connect],
|
|
167
|
+
['Console', latestVersions.console?.latestStableRelease, latestVersions.console?.latestBetaRelease],
|
|
168
|
+
['Operator', latestVersions.operator?.latestStableRelease, latestVersions.operator?.latestBetaRelease],
|
|
169
|
+
['Helm chart', latestVersions.helmChart?.latestStableRelease, latestVersions.helmChart?.latestBetaRelease],
|
|
170
|
+
];
|
|
171
|
+
const unresolved = summary.filter(([, stableVersion]) => !stableVersion).map(([label]) => label);
|
|
172
|
+
if (unresolved.length) {
|
|
173
|
+
logger.error(`Could not resolve the latest version of: ${unresolved.join(', ')}. Pages that reference the matching attributes render the attribute name instead of a version unless antora.yml seeds a fallback.`);
|
|
174
|
+
logger.info('Updated Redpanda documentation versions with gaps:');
|
|
175
|
+
} else {
|
|
176
|
+
logger.info('Updated Redpanda documentation versions successfully:');
|
|
177
|
+
}
|
|
178
|
+
summary.forEach(([label, stableVersion, betaVersion]) => {
|
|
179
|
+
logger.info(`- ${label}: ${stableVersion || 'unknown'}${betaVersion ? ', beta: ' + betaVersion : ''}`);
|
|
180
|
+
});
|
|
147
181
|
} catch (error) {
|
|
148
182
|
logger.error(`Error updating versions: ${error}`);
|
|
149
183
|
}
|
|
150
184
|
});
|
|
151
185
|
|
|
152
|
-
// Helper function to set
|
|
186
|
+
// Helper function to set latest-*version, latest-*tag, and latest-*version-short attributes
|
|
153
187
|
function setVersionAndTagAttributes(asciidoc, baseName, versionData, name = '', version = '') {
|
|
154
188
|
if (versionData) {
|
|
155
189
|
const versionWithoutPrefix = sanitizeVersion(versionData);
|
|
156
190
|
asciidoc.attributes[`${baseName}-version`] = versionWithoutPrefix; // Without "v" prefix
|
|
157
191
|
asciidoc.attributes[`${baseName}-tag`] = `${versionData}`;
|
|
158
192
|
|
|
193
|
+
setShortVersionAttribute(asciidoc, `${baseName}-version`, versionWithoutPrefix);
|
|
194
|
+
|
|
159
195
|
if (name && version) {
|
|
160
196
|
logger.debug(`Set ${baseName}-version to ${versionWithoutPrefix} and ${baseName}-tag to ${versionData} in ${name} ${version}`);
|
|
161
197
|
} else {
|
|
@@ -164,6 +200,19 @@ module.exports.register = function ({ config }) {
|
|
|
164
200
|
}
|
|
165
201
|
}
|
|
166
202
|
|
|
203
|
+
// Helper function to set the major.minor sibling of a *-version attribute (for
|
|
204
|
+
// example, latest-operator-version v25.1.3 -> latest-operator-version-short
|
|
205
|
+
// 25.1). Release channels that are not versions (for example, nightly) get no
|
|
206
|
+
// short attribute rather than a misleading one.
|
|
207
|
+
function setShortVersionAttribute(asciidoc, versionKey, versionData) {
|
|
208
|
+
if (!versionData || typeof versionData !== 'string') return;
|
|
209
|
+
if (!versionKey.endsWith('-version')) return;
|
|
210
|
+
const shortVersion = toShortVersion(sanitizeVersion(versionData));
|
|
211
|
+
if (shortVersion) {
|
|
212
|
+
asciidoc.attributes[`${versionKey}-short`] = shortVersion;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
167
216
|
// Helper function to sanitize version by removing "v" prefix
|
|
168
217
|
function sanitizeVersion(version) {
|
|
169
218
|
return version.replace(/^v/, '');
|
|
@@ -174,6 +223,7 @@ module.exports.register = function ({ config }) {
|
|
|
174
223
|
mappings.forEach(({ condition, key, value }) => {
|
|
175
224
|
if (condition) {
|
|
176
225
|
asciidoc.attributes[key] = value;
|
|
226
|
+
setShortVersionAttribute(asciidoc, key, value);
|
|
177
227
|
}
|
|
178
228
|
});
|
|
179
229
|
}
|
package/macros/enterprise.js
CHANGED
|
@@ -31,6 +31,12 @@
|
|
|
31
31
|
* registry is available (for example, outside an Antora build), the macros
|
|
32
32
|
* fall back to unvalidated rendering with a single warning.
|
|
33
33
|
*
|
|
34
|
+
* A registry entry's `since` field, when set, is checked against the page
|
|
35
|
+
* being converted, not the newest release: a mention or table row for a
|
|
36
|
+
* feature whose `since` postdates the page's own version renders unstyled,
|
|
37
|
+
* the same way an unreleased feature renders on released docs, because as
|
|
38
|
+
* of that page's version the feature had not shipped yet.
|
|
39
|
+
*
|
|
34
40
|
* Document or site attributes:
|
|
35
41
|
*
|
|
36
42
|
* enterprise-validate 'warn' (default) to log unknown feature names,
|
|
@@ -55,6 +61,7 @@
|
|
|
55
61
|
|
|
56
62
|
const yaml = require('js-yaml')
|
|
57
63
|
const chalk = require('chalk')
|
|
64
|
+
const semver = require('semver')
|
|
58
65
|
const { buildBadgeHtml, DEFAULT_TOOLTIPS } = require('./badge')
|
|
59
66
|
|
|
60
67
|
const $enterpriseRegistry = Symbol('$enterpriseRegistry')
|
|
@@ -211,6 +218,81 @@ function reportUnreleasedFeature ({ feature, mode, filePath }) {
|
|
|
211
218
|
console.warn(chalk.yellow(message))
|
|
212
219
|
}
|
|
213
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Parse a version string into a semver for comparison, or undefined when it
|
|
223
|
+
* cannot be parsed. Lenient the same way `semver.coerce` is: Redpanda docs
|
|
224
|
+
* versions are "MAJOR.MINOR", not full semver, so an exact-match parse would
|
|
225
|
+
* reject every one of them.
|
|
226
|
+
*/
|
|
227
|
+
function coerceVersion (raw) {
|
|
228
|
+
if (raw === undefined || raw === null) return undefined
|
|
229
|
+
return semver.coerce(String(raw)) || undefined
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Whether a registry entry's `since` version has shipped as of the page
|
|
234
|
+
* being converted.
|
|
235
|
+
*
|
|
236
|
+
* enterprise-features.yml lives in the non-versioned 'shared' component, so
|
|
237
|
+
* every version branch reads the exact same entry -- unlike property data,
|
|
238
|
+
* which has one JSON file per release and is checked against the page's
|
|
239
|
+
* own version (see PROPERTY_AND_ENTERPRISE_REFERENCES.adoc). Without this
|
|
240
|
+
* check, a feature that ships in 26.3 renders as fully available -- linked,
|
|
241
|
+
* badge-free, no warning -- on a 24.1 page that predates it by two years.
|
|
242
|
+
*
|
|
243
|
+
* Returns true (shipped) when `since` is absent, unparsable, or the page's
|
|
244
|
+
* own version cannot be determined. That default favors data quality over
|
|
245
|
+
* release-readiness: unlike an unrecognized `status`, which fails closed
|
|
246
|
+
* because it is the one mistake that could publish an unreleased feature,
|
|
247
|
+
* a `since` typo or a build with no page-version context has no such
|
|
248
|
+
* asymmetry to protect against, so it defaults to not gating rather than
|
|
249
|
+
* hiding a shipped feature over unrelated bad data. An unparsable `since`
|
|
250
|
+
* is still reported, the same way an unrecognized `status` is: the failure
|
|
251
|
+
* mode differs (not gated, instead of gated), but a silent typo is exactly
|
|
252
|
+
* as easy to miss either way.
|
|
253
|
+
*
|
|
254
|
+
* @param {object} entry - Registry entry.
|
|
255
|
+
* @param {object} [config] - Extension config, used to read the page's own
|
|
256
|
+
* version.
|
|
257
|
+
* @param {object} [report] - {mode, filePath, contentCatalog} to report an
|
|
258
|
+
* unparsable since value. Omit to check silently.
|
|
259
|
+
*/
|
|
260
|
+
function isFeatureShippedOnPage (entry, config, report) {
|
|
261
|
+
const since = entry && entry.since
|
|
262
|
+
if (since === undefined || since === null || String(since).trim() === '') return true
|
|
263
|
+
const sinceVersion = coerceVersion(since)
|
|
264
|
+
if (!sinceVersion) {
|
|
265
|
+
if (report && report.mode !== 'off') {
|
|
266
|
+
const where = report.filePath ? ` in ${report.filePath}` : ''
|
|
267
|
+
const message =
|
|
268
|
+
`enterprise:${entry.name}[]${where}: registry has since: '${since}', which is not a parsable version, so the since-gating check is skipped and the feature renders as already shipped. ` +
|
|
269
|
+
'Fix the since value in enterprise-features.yml (e.g. "26.3"), or remove the key if the feature has always been available.'
|
|
270
|
+
if (report.contentCatalog) warnOnce(report.contentCatalog, `since:${entry.name}`, message)
|
|
271
|
+
else console.warn(chalk.yellow(message))
|
|
272
|
+
}
|
|
273
|
+
return true
|
|
274
|
+
}
|
|
275
|
+
const src = config && config.file && config.file.src
|
|
276
|
+
const pageVersion = src && coerceVersion(src.version)
|
|
277
|
+
if (!pageVersion) return true
|
|
278
|
+
return semver.gte(pageVersion, sinceVersion)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Report an inline mention of a feature before its `since` version, on a
|
|
283
|
+
* page whose own version predates it.
|
|
284
|
+
*/
|
|
285
|
+
function reportFeatureNotYetShipped ({ feature, since, pageVersion, mode, filePath }) {
|
|
286
|
+
if (mode === 'off') return
|
|
287
|
+
const where = filePath ? ` in ${filePath}` : ''
|
|
288
|
+
const message =
|
|
289
|
+
`enterprise:${feature}[]${where}: '${feature}' is marked since: ${since} in the enterprise features registry, but this page is version ${pageVersion}, which predates it. ` +
|
|
290
|
+
'The mention renders as plain text with no enterprise styling, tooltip, or link. ' +
|
|
291
|
+
'Move the mention to a page whose version is at or after the since version, or remove since if the feature covers this version too.'
|
|
292
|
+
if (mode === 'error') throw new Error(message)
|
|
293
|
+
console.warn(chalk.yellow(message))
|
|
294
|
+
}
|
|
295
|
+
|
|
214
296
|
/**
|
|
215
297
|
* The badge for a registry entry, wrapped for use in generated AsciiDoc.
|
|
216
298
|
*
|
|
@@ -436,6 +518,8 @@ function buildEnterpriseContent ({ feature, text, xref, url, tooltip, licensingP
|
|
|
436
518
|
* @param {object} [opts]
|
|
437
519
|
* @param {string} [opts.title] - Table title override.
|
|
438
520
|
* @param {string} [opts.heading] - Third column heading override.
|
|
521
|
+
* @param {object} [opts.config] - Extension config, used to read the page's
|
|
522
|
+
* own version for since-gating (see isFeatureShippedOnPage).
|
|
439
523
|
* @returns {string}
|
|
440
524
|
*/
|
|
441
525
|
function buildFeatureTable (features, scope, opts = {}) {
|
|
@@ -456,7 +540,13 @@ function buildFeatureTable (features, scope, opts = {}) {
|
|
|
456
540
|
// did not fail there either.
|
|
457
541
|
.filter((feature) => {
|
|
458
542
|
const status = entryStatus(feature, report)
|
|
459
|
-
|
|
543
|
+
if (!includeUnreleased && status === STATUS_UNRELEASED) return false
|
|
544
|
+
// since only describes a shipped feature's first version, so it does
|
|
545
|
+
// not apply to one that has not released at all yet. Silent, like the
|
|
546
|
+
// unreleased filter above: an older version's table naturally does not
|
|
547
|
+
// list a feature that did not exist yet on that version, which is not
|
|
548
|
+
// a writer mistake worth warning about.
|
|
549
|
+
return status === STATUS_UNRELEASED || isFeatureShippedOnPage(feature, opts.config, report)
|
|
460
550
|
})
|
|
461
551
|
.sort((a, b) => a.name.localeCompare(b.name, 'en', { sensitivity: 'base' }))
|
|
462
552
|
const title = opts.title || TABLE_TITLES[scope]
|
|
@@ -517,6 +607,18 @@ function enterpriseInlineMacro (config) {
|
|
|
517
607
|
reportUnreleasedFeature({ feature, mode, filePath })
|
|
518
608
|
return self.createInline(parent, 'quoted', attributes.text || feature)
|
|
519
609
|
}
|
|
610
|
+
if (!isFeatureShippedOnPage(entry, config, { mode, filePath, contentCatalog: config && config.contentCatalog })) {
|
|
611
|
+
// isFeatureShippedOnPage only returns false when entry.since parsed,
|
|
612
|
+
// so entry is defined here.
|
|
613
|
+
reportFeatureNotYetShipped({
|
|
614
|
+
feature,
|
|
615
|
+
since: entry.since,
|
|
616
|
+
pageVersion: config && config.file && config.file.src && config.file.src.version,
|
|
617
|
+
mode,
|
|
618
|
+
filePath,
|
|
619
|
+
})
|
|
620
|
+
return self.createInline(parent, 'quoted', attributes.text || feature)
|
|
621
|
+
}
|
|
520
622
|
let content = buildEnterpriseContent({
|
|
521
623
|
feature,
|
|
522
624
|
text: attributes.text,
|
|
@@ -568,6 +670,7 @@ function enterpriseFeaturesBlockMacro (config) {
|
|
|
568
670
|
// Prerelease docs describe the upcoming release, so they list features
|
|
569
671
|
// that are still only in a release candidate.
|
|
570
672
|
includeUnreleased: isPrereleasePage(config, parent.getDocument()),
|
|
673
|
+
config,
|
|
571
674
|
})
|
|
572
675
|
// A block anchor before the macro call ([[my-id]]) is consumed into the
|
|
573
676
|
// macro's id attribute. Re-emit it so crossrefs to the table keep working.
|
|
@@ -598,3 +701,4 @@ module.exports.buildFeatureTable = buildFeatureTable
|
|
|
598
701
|
module.exports.parseRegistry = parseRegistry
|
|
599
702
|
module.exports.resolveEntryXref = resolveEntryXref
|
|
600
703
|
module.exports.resolveTooltipAttribute = resolveTooltipAttribute
|
|
704
|
+
module.exports.isFeatureShippedOnPage = isFeatureShippedOnPage
|
package/package.json
CHANGED
package/tools/bundle-openapi.js
CHANGED
|
@@ -4,6 +4,7 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { execSync, spawnSync } = require('child_process');
|
|
6
6
|
const yaml = require('yaml');
|
|
7
|
+
const { getMajorMinor } = require('../cli-utils/version');
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Normalize a Git tag into a semantic version string.
|
|
@@ -39,36 +40,9 @@ function normalizeTag(tag) {
|
|
|
39
40
|
return tag;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
* Accepts a semantic version like `25.1.1` and yields `25.1`. The special value
|
|
46
|
-
* `'dev'` is returned unchanged.
|
|
47
|
-
* @param {string} version - Semantic version (for example, `'25.1.1'`) or `'dev'`.
|
|
48
|
-
* @returns {string} The `major.minor` string (for example, `'25.1'`) or `'dev'`.
|
|
49
|
-
* @throws {Error} If `version` is not a non-empty string, lacks major/minor parts, or if major/minor are not numeric.
|
|
50
|
-
*/
|
|
51
|
-
function getMajorMinor(version) {
|
|
52
|
-
if (!version || typeof version !== 'string') {
|
|
53
|
-
throw new Error('Version must be a non-empty string');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Only process if valid semver, else return as-is (branch name)
|
|
57
|
-
const semverPattern = /^\d+\.\d+\.\d+(-[\w\.-]+)?(\+[\w\.-]+)?$/;
|
|
58
|
-
if (!semverPattern.test(version)) {
|
|
59
|
-
return version;
|
|
60
|
-
}
|
|
61
|
-
const parts = version.split('.');
|
|
62
|
-
if (parts.length < 2) {
|
|
63
|
-
throw new Error(`Invalid version format: ${version}. Expected X.Y.Z format`);
|
|
64
|
-
}
|
|
65
|
-
const major = parseInt(parts[0], 10);
|
|
66
|
-
const minor = parseInt(parts[1], 10);
|
|
67
|
-
if (isNaN(major) || isNaN(minor)) {
|
|
68
|
-
throw new Error(`Major and minor versions must be numbers: ${version}`);
|
|
69
|
-
}
|
|
70
|
-
return `${major}.${minor}`;
|
|
71
|
-
}
|
|
43
|
+
// `getMajorMinor` lives in cli-utils/version.js so the Antora version fetcher and
|
|
44
|
+
// this bundler share one major.minor derivation instead of drifting apart.
|
|
45
|
+
// That module also exports the lenient `toShortVersion` variant.
|
|
72
46
|
|
|
73
47
|
/**
|
|
74
48
|
* Produce a new value with object keys sorted recursively for deterministic output.
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
const yaml = require('js-yaml')
|
|
15
|
+
const semver = require('semver')
|
|
15
16
|
|
|
16
17
|
const VALID_SCOPES = ['redpanda', 'console', 'connect', 'operator', 'cloud']
|
|
17
18
|
const SOURCE_KINDS = ['core-enum', 'core-property', 'connect-plugin', 'manual']
|
|
@@ -58,6 +59,14 @@ function lintRegistry (source) {
|
|
|
58
59
|
} else if (!feature.source.value || !String(feature.source.value).trim()) {
|
|
59
60
|
findings.push(finding('error', 'registry-lint', `'${name}' has an empty source value; manual entries need a justification.`))
|
|
60
61
|
}
|
|
62
|
+
if (feature.since !== undefined && feature.since !== null && String(feature.since).trim() !== '') {
|
|
63
|
+
const since = String(feature.since).trim()
|
|
64
|
+
// Lenient the same way the enterprise macro's own coerceVersion is:
|
|
65
|
+
// docs versions are "MAJOR.MINOR", not full semver.
|
|
66
|
+
if (!semver.coerce(since)) {
|
|
67
|
+
findings.push(finding('error', 'registry-lint', `'${name}' has since '${since}', which is not a parsable version.`))
|
|
68
|
+
}
|
|
69
|
+
}
|
|
61
70
|
}
|
|
62
71
|
return { features: data.features, findings }
|
|
63
72
|
}
|