@redpanda-data/docs-extensions-and-macros 5.13.2 → 5.13.4

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.
@@ -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`: The latest release version of Redpanda Console.
434
- `latest-connect-version`: The latest release version of Redpanda Connect.
435
- `redpanda-beta-version`: The latest RC version of Redpanda.
436
- `redpanda-beta-commit`: The commit hash for the latest RC version of Redpanda.
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
- The following attributes are available to the latest version of the `ROOT` component (Redpanda docs):
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`: The latest release version of Redpanda.
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
 
@@ -13,8 +13,13 @@ module.exports = async (github, owner, repo, logger = null) => {
13
13
  per_page: 50
14
14
  });
15
15
 
16
- // Filter valid semver tags and sort them to find the highest version
16
+ // Filter valid semver tags and sort them to find the highest version.
17
+ // Drafts are dropped first, before the map discards the flag: a draft has
18
+ // no published Docker image, so returning its tag hands the caller a
19
+ // CONSOLE_VERSION that cannot be pulled. Drafts are only visible to
20
+ // callers whose token has push access to the repository.
17
21
  const sortedReleases = releases.data
22
+ .filter(release => !release.draft)
18
23
  .map(release => release.tag_name)
19
24
  .filter(tag => semver.valid(tag.replace(/^v/, '')))
20
25
  .sort((a, b) => semver.rcompare(a.replace(/^v/, ''), b.replace(/^v/, '')));
@@ -26,39 +26,85 @@ module.exports = async (github, owner, repo, logger = null) => {
26
26
  release => !release.tag_name.includes('-rc') && !release.draft
27
27
  );
28
28
 
29
- // Find latest RC release (can be draft or not, adjust if needed)
29
+ // Find latest RC release that is NOT a draft. The primary risk is a draft
30
+ // outranking the real latest RC (a token with push access sees drafts;
31
+ // unauthenticated CI does not, which is why this stays invisible there).
32
+ // A draft's git tag usually already exists, but isn't guaranteed to --
33
+ // when it's missing, the getRef below 404s, which the caller now handles
34
+ // per-RC instead of letting it take down the whole lookup.
30
35
  const latestRcRelease = sortedReleases.find(
31
- release => release.tag_name.includes('-rc')
36
+ release => release.tag_name.includes('-rc') && !release.draft
32
37
  );
33
38
 
34
- let latestRedpandaReleaseCommitHash = null;
35
- if (latestRedpandaRelease) {
36
- const commitData = await github.rest.git.getRef({
37
- owner,
38
- repo,
39
- ref: `tags/${latestRedpandaRelease.tag_name}`
40
- });
41
- latestRedpandaReleaseCommitHash = commitData.data.object.sha;
42
- }
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);
43
70
 
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.
44
78
  let latestRcReleaseCommitHash = null;
79
+ let resolvedRcRelease = latestRcRelease;
45
80
  if (latestRcRelease) {
46
- const rcCommitData = await github.rest.git.getRef({
47
- owner,
48
- repo,
49
- ref: `tags/${latestRcRelease.tag_name}`
50
- });
51
- latestRcReleaseCommitHash = rcCommitData.data.object.sha;
81
+ try {
82
+ const rcCommitData = await github.rest.git.getRef({
83
+ owner,
84
+ repo,
85
+ ref: `tags/${latestRcRelease.tag_name}`
86
+ });
87
+ latestRcReleaseCommitHash = rcCommitData.data.object.sha.substring(0, 7);
88
+ } catch (error) {
89
+ if (error.status !== 404) throw error;
90
+ const message = `No git tag for RC release ${latestRcRelease.tag_name}; ignoring it.`;
91
+ if (logger) {
92
+ logger.warn(message);
93
+ } else {
94
+ console.error(`⚠️ ${message}`);
95
+ }
96
+ resolvedRcRelease = null;
97
+ }
52
98
  }
53
99
 
54
100
  return {
55
101
  latestRedpandaRelease: latestRedpandaRelease ? {
56
102
  version: latestRedpandaRelease.tag_name,
57
- commitHash: latestRedpandaReleaseCommitHash.substring(0, 7)
103
+ commitHash: latestRedpandaReleaseCommitHash
58
104
  } : null,
59
- latestRcRelease: latestRcRelease ? {
60
- version: latestRcRelease.tag_name,
61
- commitHash: latestRcReleaseCommitHash.substring(0, 7)
105
+ latestRcRelease: resolvedRcRelease ? {
106
+ version: resolvedRcRelease.tag_name,
107
+ commitHash: latestRcReleaseCommitHash
62
108
  } : null
63
109
  };
64
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
- asciidoc.attributes['redpanda-beta-commit'] = latestVersions.redpanda.latestRcRelease.commitHash;
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 version, set both latest-redpanda-version and latest-redpanda-tag if available
130
- if (semver.valid(latestVersions.redpanda?.latestRedpandaRelease?.version)) {
131
- const currentVersion = component.latest.asciidoc.attributes['full-version'] || '0.0.0';
132
- if (semver.gt(latestVersions.redpanda.latestRedpandaRelease.version, currentVersion)) {
133
- // Required for backwards compatibility. Some docs still use full-version
134
- component.latest.asciidoc.attributes['full-version'] = sanitizeVersion(latestVersions.redpanda.latestRedpandaRelease.version);
135
- setVersionAndTagAttributes(component.latest.asciidoc, 'latest-redpanda', latestVersions.redpanda.latestRedpandaRelease.version, component.latest.name, component.latest.version);
136
- component.latest.asciidoc.attributes['latest-release-commit'] = latestVersions.redpanda.latestRedpandaRelease.commitHash;
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
- logger.info('Updated Redpanda documentation versions successfully:');
142
- logger.info(`- Redpanda: ${latestVersions.redpanda.latestRedpandaRelease.version}${latestVersions.redpanda.latestRcRelease ? ', beta: ' + latestVersions.redpanda.latestRcRelease.version : ''}`);
143
- logger.info(`- Connect: ${latestVersions.connect}`);
144
- logger.info(`- Console: ${latestVersions.console.latestStableRelease}${latestVersions.console.latestBetaRelease ? ', beta: ' + latestVersions.console.latestBetaRelease : ''}`);
145
- logger.info(`- Operator: ${latestVersions.operator?.latestStableRelease || 'unknown'}${latestVersions.operator?.latestBetaRelease ? ', beta: ' + latestVersions.operator.latestBetaRelease : ''}`);
146
- logger.info(`- Helm chart: ${latestVersions.helmChart?.latestStableRelease || 'unknown'}${latestVersions.helmChart?.latestBetaRelease ? ', beta: ' + latestVersions.helmChart.latestBetaRelease : ''}`);
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 both latest-*version and latest-*tag attributes
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redpanda-data/docs-extensions-and-macros",
3
- "version": "5.13.2",
3
+ "version": "5.13.4",
4
4
  "description": "Antora extensions and macros developed for Redpanda documentation.",
5
5
  "keywords": [
6
6
  "antora",
@@ -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
- * Return the major.minor portion of a semantic version string.
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.