@redpanda-data/docs-extensions-and-macros 4.0.0 → 4.2.0

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/README.adoc CHANGED
@@ -254,6 +254,90 @@ antora:
254
254
  - require: '@redpanda-data/docs-extensions-and-macros/extensions/generate-rp-connect-categories'
255
255
  ```
256
256
 
257
+ === Compute end-of-life extension
258
+
259
+ This extension calculates and attaches metadata related to the end-of-life (EoL) status of docs pages, such as nearing EoL, past EoL, and associated EoL dates. This metadata can be used to display relevant banners or messages in docs to inform users about the lifecycle of each version.
260
+
261
+ The extension leverages configuration settings provided in the Antora playbook to apply EoL calculations, specify the warning period, and include links to upgrade documentation and EoL policies.
262
+
263
+ The extension computes whether a page is nearing EoL or past EoL based on the `page-release-date` attribute and configured settings.
264
+ It injects the following attributes into each page, making them available for use in UI templates:
265
+
266
+ - `page-is-nearing-eol`: Indicates if the page is within the warning period before EoL. Calculated using `(page-release-date + supported_months) - warning_weeks`.
267
+ - `page-is-past-eol`: Indicates if the page has passed its EoL. Calculated using `today > (page-release-date + supported_months)`.
268
+ - `page-eol-date`: The calculated EoL date in a human-readable format. Calculated using `page-release-date + supported_months`.
269
+ - `page-eol-doc`: The URL to the supported versions policy or EoL documentation.
270
+ - `page-upgrade-doc`: The Antora resource ID to a document containing upgrade instructions.
271
+
272
+ ==== Environment variables
273
+
274
+ This extension does not require any environment variables.
275
+
276
+ ==== Configuration options
277
+
278
+ To enable and configure the extension, add it to the `antora.extensions` section of your Antora playbook. Define the EoL settings under the `data.eol_settings` key with the following options:
279
+
280
+ `component` (required):: The component name to which the configuration applies.
281
+ `eol_doc` (required):: A link to the supported versions policy or EoL documentation.
282
+ `upgrade_doc` (required):: A link to the upgrade instructions.
283
+ `supported_months` (optional, default: 12):: The number of months after the publish date when the documentation reaches its EoL.
284
+ `warning_weeks` (optional, default: 6):: The number of weeks before EoL when the documentation is considered to be nearing EoL. Can be used to decide when to notify users of the upcoming EoL status.
285
+
286
+ [,yaml]
287
+ ----
288
+ antora:
289
+ extensions:
290
+ - require: '@redpanda-data/docs-extensions-and-macros/extensions/compute-end-of-life'
291
+ data:
292
+ eol_settings:
293
+ - component: 'ROOT'
294
+ supported_months: 18
295
+ warning_weeks: 8
296
+ eol_doc: https://support.redpanda.com/hc/en-us/articles/20617574366743-Redpanda-Supported-Versions
297
+ upgrade_doc: ROOT:upgrade:index.adoc
298
+ ----
299
+
300
+ ==== Registration example
301
+
302
+ You can register the extension with a customized configuration for different components in your playbook:
303
+
304
+ [,yaml]
305
+ ----
306
+ antora:
307
+ extensions:
308
+ - require: '@redpanda-data/docs-extensions-and-macros/extensions/compute-end-of-life'
309
+ data:
310
+ eol_settings:
311
+ - component: 'ROOT'
312
+ supported_months: 12
313
+ warning_weeks: 6
314
+ eol_doc: https://example.com/supported-versions
315
+ upgrade_doc: ROOT:upgrade:index.adoc
316
+ - component: 'example-docs'
317
+ supported_months: 24
318
+ warning_weeks: 12
319
+ eol_doc: https://example.com/example-supported-versions
320
+ upgrade_doc: example-docs:upgrade:index.adoc
321
+ ----
322
+
323
+
324
+ ==== Example Handlebars template:
325
+
326
+ [,handlebars]
327
+ ----
328
+ {{#if page.attributes.is-nearing-eol}}
329
+ <div class="banner-container nearing-eol">
330
+ This documentation will reach its end of life on {{page.attributes.eol-date}}.
331
+ Please <a href="{{resolve-resource page.attributes.upgrade-doc}}">upgrade to a supported version</a>.
332
+ </div>
333
+ {{else if page.attributes.is-past-eol}}
334
+ <div class="banner-container past-eol">
335
+ This documentation reached its end of life on {{page.attributes.eol-date}}.
336
+ See our <a href="{{page.attributes.eol-doc}}" target="_blank">supported versions policy</a>.
337
+ </div>
338
+ {{/if}}
339
+ ----
340
+
257
341
  === Generate index data
258
342
 
259
343
  The `generate-index-data` extension creates structured index data about doc pages based on configurable filters. The indexed data is saved to a specified attribute in all component versions, enabling the dynamic generation of categorized links and descriptions within your docs using UI templates.
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+
3
+ const calculateEOL = require('./util/calculate-eol.js');
4
+
5
+ module.exports.register = function ({ config }) {
6
+ this.on('contentClassified', ({ contentCatalog }) => {
7
+ const logger = this.getLogger("compute-end-of-life-extension");
8
+
9
+ // Extract EOL configuration from the config object
10
+ const eolConfigs = config.data?.eol_settings || [];
11
+ if (!Array.isArray(eolConfigs) || eolConfigs.length === 0) {
12
+ logger.warn('No end-of-life settings found in configuration.');
13
+ return;
14
+ }
15
+
16
+ eolConfigs.forEach(({ component: componentName, supported_months, warning_weeks, eol_doc, upgrade_doc }) => {
17
+ if (!eol_doc || !upgrade_doc) {
18
+ logger.error(
19
+ `End-of-life configuration for component "${component}" is missing required attributes. ` +
20
+ `Ensure both "eol_doc" and "upgrade_doc" are specified.`
21
+ );
22
+ return;
23
+ }
24
+ const resolvedEOLMonths = supported_months && supported_months > 0 ? supported_months : 12; // Default: 12 months
25
+ const resolvedWarningWeeks = warning_weeks && warning_weeks > 0 ? warning_weeks : 6; // Default: 6 weeks
26
+
27
+ logger.info(
28
+ `Processing component: ${componentName} with end-of-life months: ${resolvedEOLMonths}, Warning weeks: ${resolvedWarningWeeks}`
29
+ );
30
+
31
+ const component = contentCatalog.getComponents().find((c) => c.name === componentName);
32
+
33
+ if (!component) {
34
+ logger.warn(`Component not found: ${componentName}`);
35
+ return;
36
+ }
37
+
38
+ component.versions.forEach(({ asciidoc, version }) => {
39
+ const releaseDate = asciidoc.attributes['page-release-date'];
40
+ if (releaseDate) {
41
+ // Pass resolved configuration to calculateEOL
42
+ const eolInfo = calculateEOL(releaseDate, resolvedEOLMonths, resolvedWarningWeeks, logger);
43
+ Object.assign(asciidoc.attributes, {
44
+ 'page-is-nearing-eol': eolInfo.isNearingEOL.toString(),
45
+ 'page-is-past-eol': eolInfo.isPastEOL.toString(),
46
+ 'page-eol-date': eolInfo.eolDate,
47
+ 'page-eol-doc': eol_doc,
48
+ 'page-upgrade-doc': upgrade_doc,
49
+ });
50
+ } else {
51
+ logger.warn(`No release date found for component: ${componentName}. Make sure to set {page-release-date} in the antora.yml of the component version ${version}.`);
52
+ }
53
+ });
54
+ });
55
+ });
56
+ };
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ module.exports = (releaseDate, eolMonths, warningWeeks, logger) => {
4
+ if (!releaseDate) {
5
+ logger.warn(
6
+ 'No release date provided. Make sure to set {page-release-date} in the antora.yml of the component.'
7
+ );
8
+ return null;
9
+ }
10
+
11
+ // Parse the input date string YYYY-MM-DD in UTC
12
+ const parseUTCDate = (dateString) => {
13
+ const [year, month, day] = dateString.split('-').map(Number);
14
+ // month - 1 because JS months are 0-based
15
+ // https://www.w3schools.com/jsref/jsref_getmonth.asp
16
+ return new Date(Date.UTC(year, month - 1, day));
17
+ };
18
+
19
+ const targetDate = parseUTCDate(releaseDate);
20
+
21
+ if (isNaN(targetDate.getTime())) {
22
+ logger.warn('Invalid release date format:', releaseDate);
23
+ return null;
24
+ }
25
+
26
+ // Calculate EoL date in UTC
27
+ const eolDate = new Date(targetDate.getTime()); // clone
28
+ eolDate.setUTCMonth(eolDate.getUTCMonth() + eolMonths);
29
+
30
+ // Calculate the threshold for warning (X weeks before EoL) in UTC
31
+ const weeksBeforeEOL = new Date(eolDate.getTime());
32
+ weeksBeforeEOL.setUTCDate(weeksBeforeEOL.getUTCDate() - warningWeeks * 7);
33
+
34
+ // Compare times in milliseconds to avoid timezone confusion
35
+ const nowMs = Date.now();
36
+ const eolMs = eolDate.getTime();
37
+ const warningMs = weeksBeforeEOL.getTime();
38
+
39
+ const isNearingEOL = nowMs >= warningMs && nowMs < eolMs;
40
+ const isPastEOL = nowMs > eolMs;
41
+
42
+ // Format the EoL date in UTC
43
+ const humanReadableEOLDate = new Intl.DateTimeFormat('en-US', {
44
+ year: 'numeric',
45
+ month: 'long',
46
+ day: 'numeric',
47
+ timeZone: 'UTC', // Ensure UTC in output
48
+ }).format(eolDate);
49
+
50
+ return {
51
+ isNearingEOL,
52
+ isPastEOL,
53
+ eolDate: humanReadableEOLDate, // For example "March 1, 2025"
54
+ };
55
+ };
@@ -0,0 +1,19 @@
1
+ function customStringify(obj) {
2
+ return JSON.stringify(obj, (key, value) => {
3
+ if (value instanceof Map) {
4
+ return {
5
+ type: 'Map',
6
+ value: Array.from(value.entries())
7
+ };
8
+ } else if (value instanceof Set) {
9
+ return {
10
+ type: 'Set',
11
+ value: Array.from(value)
12
+ };
13
+ } else if (typeof value === 'function') {
14
+ return value.toString();
15
+ } else {
16
+ return value;
17
+ }
18
+ }, 2);
19
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Fetches the latest version tag from Docker Hub for a given repository.
3
+ *
4
+ *
5
+ * @param {string} dockerNamespace - The Docker Hub namespace (organization or username)
6
+ * @param {string} dockerRepo - The repository name on Docker Hub
7
+ * @returns {Promise<string|null>} The latest version tag or null if none is found.
8
+ */
9
+ module.exports = async (dockerNamespace, dockerRepo) => {
10
+ const { default: fetch } = await import('node-fetch');
11
+
12
+ try {
13
+ // Fetch a list of tags from Docker Hub.
14
+ const url = `https://hub.docker.com/v2/repositories/${dockerNamespace}/${dockerRepo}/tags?page_size=100`;
15
+ const response = await fetch(url);
16
+
17
+ if (!response.ok) {
18
+ throw new Error(`Docker Hub API responded with status ${response.status}`);
19
+ }
20
+
21
+ const data = await response.json();
22
+
23
+ // Define a regular expression to capture the major and minor version numbers.
24
+ // The regex /^v(\d+)\.(\d+)/ matches tags that start with "v", followed by digits (major),
25
+ // a period, and more digits (minor). It works for tags like "v2.3.8-24.3.6" or "v25.1-k8s4".
26
+ const versionRegex = /^v(\d+)\.(\d+)/;
27
+
28
+ // Filter the list of tags to include only those that match our expected version pattern.
29
+ // This helps ensure we work only with tags that represent valid version numbers.
30
+ let versionTags = data.results.filter(tag => versionRegex.test(tag.name));
31
+
32
+ // If the repository is "redpanda-operator", ignore any tags starting with the old "v22 or "v23" versions.
33
+ if (dockerRepo === 'redpanda-operator') {
34
+ versionTags = versionTags.filter(tag => !/^(v22|v23)/.test(tag.name));
35
+ }
36
+
37
+ if (versionTags.length === 0) {
38
+ console.warn('No version tags found.');
39
+ return null;
40
+ }
41
+
42
+ // Sort the filtered tags in descending order based on their major and minor version numbers.
43
+ // This sorting ignores any additional patch or suffix details and focuses only on the major.minor value.
44
+ versionTags.sort((a, b) => {
45
+ const aMatch = a.name.match(versionRegex);
46
+ const bMatch = b.name.match(versionRegex);
47
+ const aMajor = parseInt(aMatch[1], 10);
48
+ const aMinor = parseInt(aMatch[2], 10);
49
+ const bMajor = parseInt(bMatch[1], 10);
50
+ const bMinor = parseInt(bMatch[2], 10);
51
+
52
+ // Compare by major version first; if equal, compare by minor version.
53
+ if (aMajor !== bMajor) {
54
+ return bMajor - aMajor;
55
+ }
56
+ return bMinor - aMinor;
57
+ });
58
+
59
+ // Return the name of the tag with the highest major.minor version.
60
+ return versionTags[0].name;
61
+
62
+ } catch (error) {
63
+ console.error('Error fetching latest Docker tag:', error);
64
+ return null;
65
+ }
66
+ };
@@ -3,7 +3,7 @@
3
3
  module.exports.register = function ({ config }) {
4
4
  const GetLatestRedpandaVersion = require('./get-latest-redpanda-version');
5
5
  const GetLatestConsoleVersion = require('./get-latest-console-version');
6
- const GetLatestOperatorVersion = require('./get-latest-operator-version');
6
+ const GetLatestOperatorVersion = require('./fetch-latest-docker-tag');
7
7
  const GetLatestHelmChartVersion = require('./get-latest-redpanda-helm-version');
8
8
  const GetLatestConnectVersion = require('./get-latest-connect');
9
9
  const logger = this.getLogger('set-latest-version-extension');
@@ -25,6 +25,7 @@ module.exports.register = function ({ config }) {
25
25
  auth: process.env.REDPANDA_GITHUB_TOKEN || undefined,
26
26
  };
27
27
  const github = new OctokitWithRetries(githubOptions);
28
+ const dockerNamespace = 'redpandadata'
28
29
 
29
30
  try {
30
31
  const [
@@ -36,7 +37,7 @@ module.exports.register = function ({ config }) {
36
37
  ] = await Promise.allSettled([
37
38
  GetLatestRedpandaVersion(github, owner, 'redpanda'),
38
39
  GetLatestConsoleVersion(github, owner, 'console'),
39
- GetLatestOperatorVersion(github, owner, 'redpanda-operator'),
40
+ GetLatestOperatorVersion(dockerNamespace, 'redpanda-operator'),
40
41
  GetLatestHelmChartVersion(github, owner, 'helm-charts', 'charts/redpanda/Chart.yaml'),
41
42
  GetLatestConnectVersion(github, owner, 'connect'),
42
43
  ]);
@@ -49,6 +50,8 @@ module.exports.register = function ({ config }) {
49
50
  connect: latestConnectResult.status === 'fulfilled' ? latestConnectResult.value : undefined,
50
51
  };
51
52
 
53
+ console.log(latestVersions)
54
+
52
55
  const components = await contentCatalog.getComponents();
53
56
  components.forEach(component => {
54
57
  const prerelease = component.latestPrerelease;
@@ -58,24 +61,25 @@ module.exports.register = function ({ config }) {
58
61
  asciidoc.attributes['page-component-version-is-prerelease'] = 'true';
59
62
  }
60
63
 
61
- // Set operator and helm chart attributes
62
- if (latestVersions.operator) {
63
- asciidoc.attributes['latest-operator-version'] = latestVersions.operator;
64
- }
65
- if (latestVersions.helmChart) {
66
- asciidoc.attributes['latest-redpanda-helm-chart-version'] = latestVersions.helmChart;
67
- }
64
+ // Set operator and helm chart attributes via helper function
65
+ updateAttributes(asciidoc, [
66
+ { condition: latestVersions.operator, key: 'latest-operator-version', value: latestVersions.operator },
67
+ { condition: latestVersions.helmChart, key: 'latest-redpanda-helm-chart-version', value: latestVersions.helmChart }
68
+ ]);
68
69
 
69
70
  // Set attributes for console and connect versions
70
- if (latestVersions.console) {
71
- setVersionAndTagAttributes(asciidoc, 'latest-console', latestVersions.console.latestStableRelease, name, version);
72
- }
73
- if (latestVersions.connect) {
74
- setVersionAndTagAttributes(asciidoc, 'latest-connect', latestVersions.connect, name, version);
75
- }
71
+ [
72
+ { condition: latestVersions.console, baseName: 'latest-console', value: latestVersions.console?.latestStableRelease },
73
+ { condition: latestVersions.connect, baseName: 'latest-connect', value: latestVersions.connect }
74
+ ].forEach(mapping => {
75
+ if (mapping.condition && mapping.value) {
76
+ setVersionAndTagAttributes(asciidoc, mapping.baseName, mapping.value, name, version);
77
+ }
78
+ });
79
+
76
80
  // Special handling for Redpanda RC versions if in beta
77
81
  if (latestVersions.redpanda?.latestRcRelease?.version) {
78
- setVersionAndTagAttributes(asciidoc, 'redpanda-beta', latestVersions.redpanda.latestRcRelease.version, name, version)
82
+ setVersionAndTagAttributes(asciidoc, 'redpanda-beta', latestVersions.redpanda.latestRcRelease.version, name, version);
79
83
  setVersionAndTagAttributes(asciidoc, 'console-beta', latestVersions.console.latestBetaRelease, name, version);
80
84
  asciidoc.attributes['redpanda-beta-commit'] = latestVersions.redpanda.latestRcRelease.commitHash;
81
85
  }
@@ -127,4 +131,13 @@ module.exports.register = function ({ config }) {
127
131
  function sanitizeVersion(version) {
128
132
  return version.replace(/^v/, '');
129
133
  }
130
- };
134
+
135
+ // Helper function to update multiple attributes based on a list of mappings
136
+ function updateAttributes(asciidoc, mappings) {
137
+ mappings.forEach(({ condition, key, value }) => {
138
+ if (condition) {
139
+ asciidoc.attributes[key] = value;
140
+ }
141
+ });
142
+ }
143
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redpanda-data/docs-extensions-and-macros",
3
- "version": "4.0.0",
3
+ "version": "4.2.0",
4
4
  "description": "Antora extensions and macros developed for Redpanda documentation.",
5
5
  "keywords": [
6
6
  "antora",
@@ -33,6 +33,7 @@
33
33
  "./extensions/archive-attachments": "./extensions/archive-attachments.js",
34
34
  "./extensions/add-pages-to-root": "./extensions/add-pages-to-root.js",
35
35
  "./extensions/collect-bloblang-samples": "./extensions/collect-bloblang-samples.js",
36
+ "./extensions/compute-end-of-life": "./extensions/compute-end-of-life.js",
36
37
  "./extensions/generate-rp-connect-categories": "./extensions/generate-rp-connect-categories.js",
37
38
  "./extensions/generate-index-data": "./extensions/generate-index-data.js",
38
39
  "./extensions/generate-rp-connect-info": "./extensions/generate-rp-connect-info.js",
@@ -74,6 +75,7 @@
74
75
  "js-yaml": "^4.1.0",
75
76
  "lodash": "^4.17.21",
76
77
  "micromatch": "^4.0.8",
78
+ "node-fetch": "^3.3.2",
77
79
  "node-html-parser": "5.4.2-0",
78
80
  "papaparse": "^5.4.1",
79
81
  "semver": "^7.6.0",
@@ -1,19 +0,0 @@
1
- function customStringify(obj) {
2
- return JSON.stringify(obj, (key, value) => {
3
- if (value instanceof Map) {
4
- return {
5
- type: 'Map',
6
- value: Array.from(value.entries())
7
- };
8
- } else if (value instanceof Set) {
9
- return {
10
- type: 'Set',
11
- value: Array.from(value)
12
- };
13
- } else if (typeof value === 'function') {
14
- return value.toString();
15
- } else {
16
- return value;
17
- }
18
- }, 2);
19
- }
@@ -1,10 +0,0 @@
1
- module.exports = async (github, owner, repo) => {
2
- try {
3
- const release = await github.rest.repos.getLatestRelease({ owner, repo });
4
- latestOperatorReleaseVersion = release.data.tag_name;
5
- return latestOperatorReleaseVersion;
6
- } catch (error) {
7
- console.error(error);
8
- return null;
9
- }
10
- };