@redpanda-data/docs-extensions-and-macros
Version:
Antora extensions and macros developed for Redpanda documentation.
637 lines (563 loc) • 23.1 kB
JavaScript
const { execSync } = require('child_process');
/**
* Generate a JSON diff report between two connector index objects.
* Includes platform metadata (CGO, cloud-only) to detect transitions.
* @param {object} oldIndex - Previous version connector index
* @param {object} newIndex - Current version connector index
* @param {object} opts - { oldVersion, newVersion, timestamp, binaryAnalysis, oldBinaryAnalysis }
* @returns {object} JSON diff report
*/
function generateConnectorDiffJson(oldIndex, newIndex, opts = {}) {
const oldMap = buildComponentMap(oldIndex);
const newMap = buildComponentMap(newIndex);
// New components (include platform metadata)
const newComponentKeys = Object.keys(newMap).filter(k => !(k in oldMap));
const newComponents = newComponentKeys.map(key => {
const [type, name] = key.split(':');
const raw = newMap[key].raw;
const metadata = newMap[key].metadata || {};
return {
name,
type,
status: raw.status || raw.type || '',
version: raw.version || raw.introducedInVersion || '',
description: raw.description || '',
requiresCgo: metadata.requiresCgo || false,
cloudOnly: metadata.cloudOnly || false,
cloudSupported: metadata.cloudSupported || false
};
});
// Removed components (include platform metadata to understand why removed)
const removedComponentKeys = Object.keys(oldMap).filter(k => !(k in newMap));
const removedComponents = removedComponentKeys.map(key => {
const [type, name] = key.split(':');
const raw = oldMap[key].raw;
const metadata = oldMap[key].metadata || {};
return {
name,
type,
status: raw.status || raw.type || '',
version: raw.version || raw.introducedInVersion || '',
description: raw.description || '',
requiresCgo: metadata.requiresCgo || false,
cloudOnly: metadata.cloudOnly || false,
cloudSupported: metadata.cloudSupported || false
};
});
// New fields under existing components
const newFields = [];
Object.keys(newMap).forEach(cKey => {
if (!(cKey in oldMap)) return;
const oldFields = new Set(oldMap[cKey].fields || []);
const newFieldsArr = newMap[cKey].fields || [];
newFieldsArr.forEach(fName => {
if (!oldFields.has(fName)) {
const [type, compName] = cKey.split(':');
let rawFieldObj = null;
if (type === 'config') {
rawFieldObj = (newMap[cKey].raw.children || []).find(f => f.name === fName);
} else {
rawFieldObj = (newMap[cKey].raw.config?.children || []).find(f => f.name === fName);
}
newFields.push({
component: cKey,
field: fName,
introducedIn: rawFieldObj && (rawFieldObj.introducedInVersion || rawFieldObj.version),
description: rawFieldObj && rawFieldObj.description
});
}
});
});
// Removed fields under existing components
const removedFields = [];
Object.keys(oldMap).forEach(cKey => {
if (!(cKey in newMap)) return;
const newFieldsSet = new Set(newMap[cKey].fields || []);
const oldFieldsArr = oldMap[cKey].fields || [];
oldFieldsArr.forEach(fName => {
if (!newFieldsSet.has(fName)) {
removedFields.push({
component: cKey,
field: fName
});
}
});
});
// Newly deprecated components (exist in both versions but became deprecated)
const deprecatedComponents = [];
Object.keys(newMap).forEach(cKey => {
if (!(cKey in oldMap)) return;
const oldStatus = (oldMap[cKey].raw.status || '').toLowerCase();
const newStatus = (newMap[cKey].raw.status || '').toLowerCase();
if (oldStatus !== 'deprecated' && newStatus === 'deprecated') {
const [type, name] = cKey.split(':');
const raw = newMap[cKey].raw;
deprecatedComponents.push({
name,
type,
status: raw.status || raw.type || '',
version: raw.version || raw.introducedInVersion || '',
description: raw.description || ''
});
}
});
// Platform transitions (CGO, cloud support changes)
const platformTransitions = [];
Object.keys(newMap).forEach(cKey => {
if (!(cKey in oldMap)) return;
const oldMeta = oldMap[cKey].metadata || {};
const newMeta = newMap[cKey].metadata || {};
const [type, name] = cKey.split(':');
const transitions = [];
// CGO requirement changes
if (!oldMeta.requiresCgo && newMeta.requiresCgo) {
transitions.push('became_cgo_only');
} else if (oldMeta.requiresCgo && !newMeta.requiresCgo) {
transitions.push('no_longer_cgo_only');
}
// Cloud support changes
if (!oldMeta.cloudSupported && newMeta.cloudSupported) {
transitions.push('added_cloud_support');
} else if (oldMeta.cloudSupported && !newMeta.cloudSupported) {
transitions.push('removed_cloud_support');
}
// Cloud-only status changes
if (!oldMeta.cloudOnly && newMeta.cloudOnly) {
transitions.push('became_cloud_only');
} else if (oldMeta.cloudOnly && !newMeta.cloudOnly) {
transitions.push('no_longer_cloud_only');
}
if (transitions.length > 0) {
platformTransitions.push({
name,
type,
transitions,
oldPlatform: {
requiresCgo: oldMeta.requiresCgo || false,
cloudSupported: oldMeta.cloudSupported || false,
cloudOnly: oldMeta.cloudOnly || false
},
newPlatform: {
requiresCgo: newMeta.requiresCgo || false,
cloudSupported: newMeta.cloudSupported || false,
cloudOnly: newMeta.cloudOnly || false
}
});
}
});
// Newly deprecated fields (exist in both versions but became deprecated)
const deprecatedFields = [];
// Changed default values
const changedDefaults = [];
Object.keys(newMap).forEach(cKey => {
if (!(cKey in oldMap)) return;
const oldFieldsArr = oldMap[cKey].fields || [];
const newFieldsArr = newMap[cKey].fields || [];
// Check fields that exist in both versions
const commonFields = oldFieldsArr.filter(f => newFieldsArr.includes(f));
commonFields.forEach(fName => {
const [type, compName] = cKey.split(':');
// Get old field object
let oldFieldObj = null;
if (type === 'config') {
oldFieldObj = (oldMap[cKey].raw.children || []).find(f => f.name === fName);
} else {
oldFieldObj = (oldMap[cKey].raw.config?.children || []).find(f => f.name === fName);
}
// Get new field object
let newFieldObj = null;
if (type === 'config') {
newFieldObj = (newMap[cKey].raw.children || []).find(f => f.name === fName);
} else {
newFieldObj = (newMap[cKey].raw.config?.children || []).find(f => f.name === fName);
}
const oldDeprecated = oldFieldObj && (oldFieldObj.is_deprecated === true || oldFieldObj.deprecated === true || (oldFieldObj.status || '').toLowerCase() === 'deprecated');
const newDeprecated = newFieldObj && (newFieldObj.is_deprecated === true || newFieldObj.deprecated === true || (newFieldObj.status || '').toLowerCase() === 'deprecated');
if (!oldDeprecated && newDeprecated) {
deprecatedFields.push({
component: cKey,
field: fName,
description: newFieldObj && newFieldObj.description
});
}
// Check for changed default values
if (oldFieldObj && newFieldObj) {
const oldDefault = oldFieldObj.default;
const newDefault = newFieldObj.default;
// Compare defaults using JSON stringification to handle objects/arrays
const oldDefaultStr = JSON.stringify(oldDefault);
const newDefaultStr = JSON.stringify(newDefault);
if (oldDefaultStr !== newDefaultStr) {
changedDefaults.push({
component: cKey,
field: fName,
oldDefault: oldDefault,
newDefault: newDefault,
description: newFieldObj && newFieldObj.description
});
}
}
});
});
// Detect new/removed Bloblang methods and functions
const oldMethods = new Set((oldIndex['bloblang-methods'] || []).filter(Boolean).map(m => m.name).filter(Boolean));
const newMethods = new Set((newIndex['bloblang-methods'] || []).filter(Boolean).map(m => m.name).filter(Boolean));
const oldFunctions = new Set((oldIndex['bloblang-functions'] || []).filter(Boolean).map(f => f.name).filter(Boolean));
const newFunctions = new Set((newIndex['bloblang-functions'] || []).filter(Boolean).map(f => f.name).filter(Boolean));
const newBloblangMethods = Array.from(newMethods).filter(m => !oldMethods.has(m)).sort();
const removedBloblangMethods = Array.from(oldMethods).filter(m => !newMethods.has(m)).sort();
const newBloblangFunctions = Array.from(newFunctions).filter(f => !oldFunctions.has(f)).sort();
const removedBloblangFunctions = Array.from(oldFunctions).filter(f => !newFunctions.has(f)).sort();
// Detect deprecated Bloblang methods and functions
const deprecatedBloblangMethods = [];
const deprecatedBloblangFunctions = [];
const oldMethodsMap = new Map((oldIndex['bloblang-methods'] || []).filter(Boolean).filter(m => m.name).map(m => [m.name, m]));
const newMethodsMap = new Map((newIndex['bloblang-methods'] || []).filter(Boolean).filter(m => m.name).map(m => [m.name, m]));
const oldFunctionsMap = new Map((oldIndex['bloblang-functions'] || []).filter(Boolean).filter(f => f.name).map(f => [f.name, f]));
const newFunctionsMap = new Map((newIndex['bloblang-functions'] || []).filter(Boolean).filter(f => f.name).map(f => [f.name, f]));
// Check methods for newly deprecated status
newMethodsMap.forEach((newMethod, name) => {
const oldMethod = oldMethodsMap.get(name);
if (oldMethod) {
const oldStatus = (oldMethod.status || '').toLowerCase();
const newStatus = (newMethod.status || '').toLowerCase();
if (oldStatus !== 'deprecated' && newStatus === 'deprecated') {
deprecatedBloblangMethods.push(name);
}
}
});
// Check functions for newly deprecated status
newFunctionsMap.forEach((newFunction, name) => {
const oldFunction = oldFunctionsMap.get(name);
if (oldFunction) {
const oldStatus = (oldFunction.status || '').toLowerCase();
const newStatus = (newFunction.status || '').toLowerCase();
if (oldStatus !== 'deprecated' && newStatus === 'deprecated') {
deprecatedBloblangFunctions.push(name);
}
}
});
const result = {
comparison: {
oldVersion: opts.oldVersion || '',
newVersion: opts.newVersion || '',
timestamp: opts.timestamp || new Date().toISOString()
},
summary: {
newComponents: newComponents.length,
removedComponents: removedComponents.length,
newFields: newFields.length,
removedFields: removedFields.length,
deprecatedComponents: deprecatedComponents.length,
deprecatedFields: deprecatedFields.length,
changedDefaults: changedDefaults.length,
platformTransitions: platformTransitions.length,
newBloblangMethods: newBloblangMethods.length,
removedBloblangMethods: removedBloblangMethods.length,
newBloblangFunctions: newBloblangFunctions.length,
removedBloblangFunctions: removedBloblangFunctions.length,
deprecatedBloblangMethods: deprecatedBloblangMethods.length,
deprecatedBloblangFunctions: deprecatedBloblangFunctions.length
},
details: {
newComponents,
removedComponents,
newFields,
removedFields,
deprecatedComponents,
deprecatedFields,
changedDefaults,
platformTransitions,
newBloblangMethods,
removedBloblangMethods,
newBloblangFunctions,
removedBloblangFunctions,
deprecatedBloblangMethods,
deprecatedBloblangFunctions
}
};
// Include binary analysis data if provided
if (opts.binaryAnalysis) {
const ba = opts.binaryAnalysis;
const oldBa = opts.oldBinaryAnalysis || {};
result.binaryAnalysis = {
versions: {
oss: ba.ossVersion,
cloud: ba.cloudVersion || null,
cgo: ba.cgoVersion || null
},
current: {
cloudSupported: ba.comparison?.inCloud?.length || 0,
selfHostedOnly: ba.comparison?.notInCloud?.length || 0,
cgoOnly: ba.cgoOnly?.length || 0
},
changes: {}
};
// Calculate cloud support changes
if (oldBa.comparison && ba.comparison) {
const oldCloudSet = new Set(oldBa.comparison.inCloud?.map(c => `${c.type}:${c.name}`) || []);
const newCloudSet = new Set(ba.comparison.inCloud?.map(c => `${c.type}:${c.name}`) || []);
const addedToCloud = ba.comparison.inCloud?.filter(c =>
!oldCloudSet.has(`${c.type}:${c.name}`)
) || [];
const removedFromCloud = oldBa.comparison.inCloud?.filter(c =>
!newCloudSet.has(`${c.type}:${c.name}`)
) || [];
result.binaryAnalysis.changes.cloud = {
added: addedToCloud.map(c => ({ type: c.type, name: c.name, status: c.status })),
removed: removedFromCloud.map(c => ({ type: c.type, name: c.name, status: c.status }))
};
}
// Calculate cgo-only changes
if (oldBa.cgoOnly && ba.cgoOnly) {
const oldCgoSet = new Set(oldBa.cgoOnly.map(c => `${c.type}:${c.name}`));
const newCgoSet = new Set(ba.cgoOnly.map(c => `${c.type}:${c.name}`));
const newCgoOnly = ba.cgoOnly.filter(c =>
!oldCgoSet.has(`${c.type}:${c.name}`)
);
const removedCgoOnly = oldBa.cgoOnly.filter(c =>
!newCgoSet.has(`${c.type}:${c.name}`)
);
result.binaryAnalysis.changes.cgo = {
newCgoOnly: newCgoOnly.map(c => ({ type: c.type, name: c.name, status: c.status })),
removedCgoOnly: removedCgoOnly.map(c => ({ type: c.type, name: c.name, status: c.status }))
};
}
// Include full lists for reference
result.binaryAnalysis.details = {
cloudSupported: ba.comparison?.inCloud?.map(c => ({ type: c.type, name: c.name, status: c.status })) || [],
selfHostedOnly: ba.comparison?.notInCloud?.map(c => ({ type: c.type, name: c.name, status: c.status })) || [],
cloudOnly: ba.comparison?.cloudOnly?.map(c => ({ type: c.type, name: c.name, status: c.status })) || [],
cgoOnly: ba.cgoOnly?.map(c => ({ type: c.type, name: c.name, status: c.status })) || []
};
}
return result;
}
function discoverComponentKeys(obj) {
return Object.keys(obj).filter(key => Array.isArray(obj[key]));
}
function buildComponentMap(indexObj) {
const map = {};
const types = discoverComponentKeys(indexObj);
types.forEach(type => {
(indexObj[type] || []).forEach(component => {
const name = component.name;
if (!name) return;
const lookupKey = `${type}:${name}`;
let childArray = [];
if (type === 'config') {
if (Array.isArray(component.children)) {
childArray = component.children;
}
} else {
if (component.config && Array.isArray(component.config.children)) {
childArray = component.config.children;
}
}
const fieldNames = childArray.map(f => f.name);
// Preserve platform metadata for accurate diff comparison
const metadata = {
requiresCgo: component.requiresCgo || false,
cloudSupported: component.cloudSupported || false,
cloudOnly: component.cloudOnly || false
};
map[lookupKey] = {
raw: component,
fields: fieldNames,
metadata: metadata
};
});
});
return map;
}
function getRpkConnectVersion() {
try {
// Make sure the connect plugin is upgraded first (silent)
execSync('rpk connect upgrade', { stdio: 'ignore' });
// Now capture the --version output
const raw = execSync('rpk connect --version', {
stdio: ['ignore', 'pipe', 'ignore'],
})
.toString()
.trim();
// raw looks like:
// Version: 4.53.0
// Date: 2025-04-18T17:49:53Z
// We want to extract “4.53.0”
const match = raw.match(/^Version:\s*(.+)$/m);
if (!match) {
throw new Error(`Unexpected format from "rpk connect --version":\n${raw}`);
}
return match[1];
} catch (err) {
throw new Error(`Unable to run "rpk connect --version": ${err.message}`);
}
}
/**
* Given two “index objects” (parsed from connect.json), produce a console summary of:
* • which connectors/components are brand-new
* • which new fields appeared under existing connectors (including “config” entries)
* • for each new component/field, if the raw object contains “version” or “introducedInVersion” or “requiresVersion” metadata, print it
*/
function printDeltaReport(oldIndex, newIndex) {
const oldMap = buildComponentMap(oldIndex);
const newMap = buildComponentMap(newIndex);
// 1) brand-new components
const newComponentKeys = Object.keys(newMap).filter(k => !(k in oldMap));
// 2) brand-new fields under shared components
const newFields = [];
Object.keys(newMap).forEach(cKey => {
if (!(cKey in oldMap)) return; // skip brand-new components here
const oldFields = new Set(oldMap[cKey].fields || []);
const newFieldsArr = newMap[cKey].fields || [];
newFieldsArr.forEach(fName => {
if (!oldFields.has(fName)) {
// fetch raw field metadata if available
const [type, compName] = cKey.split(':');
let rawFieldObj = null;
if (type === 'config') {
rawFieldObj = (newMap[cKey].raw.children || []).find(f => f.name === fName);
} else {
rawFieldObj = (newMap[cKey].raw.config?.children || []).find(f => f.name === fName);
}
let introducedIn = rawFieldObj && (rawFieldObj.introducedInVersion || rawFieldObj.version);
let requiresVer = rawFieldObj && rawFieldObj.requiresVersion;
newFields.push({
component: cKey,
field: fName,
introducedIn,
requiresVersion: requiresVer,
});
}
});
});
// Newly deprecated components
const deprecatedComponentKeys = [];
Object.keys(newMap).forEach(cKey => {
if (!(cKey in oldMap)) return;
const oldStatus = (oldMap[cKey].raw.status || '').toLowerCase();
const newStatus = (newMap[cKey].raw.status || '').toLowerCase();
if (oldStatus !== 'deprecated' && newStatus === 'deprecated') {
deprecatedComponentKeys.push(cKey);
}
});
// Newly deprecated fields
const deprecatedFieldsList = [];
// Changed default values
const changedDefaultsList = [];
Object.keys(newMap).forEach(cKey => {
if (!(cKey in oldMap)) return;
const oldFieldsArr = oldMap[cKey].fields || [];
const newFieldsArr = newMap[cKey].fields || [];
const commonFields = oldFieldsArr.filter(f => newFieldsArr.includes(f));
commonFields.forEach(fName => {
const [type, compName] = cKey.split(':');
let oldFieldObj = null;
if (type === 'config') {
oldFieldObj = (oldMap[cKey].raw.children || []).find(f => f.name === fName);
} else {
oldFieldObj = (oldMap[cKey].raw.config?.children || []).find(f => f.name === fName);
}
let newFieldObj = null;
if (type === 'config') {
newFieldObj = (newMap[cKey].raw.children || []).find(f => f.name === fName);
} else {
newFieldObj = (newMap[cKey].raw.config?.children || []).find(f => f.name === fName);
}
const oldDeprecated = oldFieldObj && (oldFieldObj.is_deprecated === true || oldFieldObj.deprecated === true || (oldFieldObj.status || '').toLowerCase() === 'deprecated');
const newDeprecated = newFieldObj && (newFieldObj.is_deprecated === true || newFieldObj.deprecated === true || (newFieldObj.status || '').toLowerCase() === 'deprecated');
if (!oldDeprecated && newDeprecated) {
deprecatedFieldsList.push({ component: cKey, field: fName });
}
// Check for changed default values
if (oldFieldObj && newFieldObj) {
const oldDefault = oldFieldObj.default;
const newDefault = newFieldObj.default;
// Compare defaults using JSON stringification to handle objects/arrays
const oldDefaultStr = JSON.stringify(oldDefault);
const newDefaultStr = JSON.stringify(newDefault);
if (oldDefaultStr !== newDefaultStr) {
changedDefaultsList.push({
component: cKey,
field: fName,
oldDefault: oldDefault,
newDefault: newDefault
});
}
}
});
});
console.log('\n📋 RPCN Connector Delta Report\n');
if (newComponentKeys.length) {
console.log('➤ Newly added components:');
newComponentKeys.forEach(key => {
const [type, name] = key.split(':');
const raw = newMap[key].raw;
const status = raw.status || raw.type || '';
const version = raw.version || raw.introducedInVersion || '';
console.log(
` • ${type}/${name}${
status ? ` (${status})` : ''
}${version ? ` — introduced in ${version}` : ''}`
);
});
console.log('');
} else {
console.log('➤ No newly added components.\n');
}
if (newFields.length) {
console.log('➤ Newly added fields:');
newFields.forEach(entry => {
const { component, field, introducedIn, requiresVersion } = entry;
process.stdout.write(` • ${component} → ${field}`);
if (introducedIn) process.stdout.write(` (introducedIn: ${introducedIn})`);
if (requiresVersion) process.stdout.write(` (requiresVersion: ${requiresVersion})`);
console.log('');
});
console.log('');
} else {
console.log('➤ No newly added fields.\n');
}
if (deprecatedComponentKeys.length) {
console.log('➤ Newly deprecated components:');
deprecatedComponentKeys.forEach(key => {
const [type, name] = key.split(':');
const raw = newMap[key].raw;
console.log(` • ${type}/${name}`);
});
console.log('');
} else {
console.log('➤ No newly deprecated components.\n');
}
if (deprecatedFieldsList.length) {
console.log('➤ Newly deprecated fields:');
deprecatedFieldsList.forEach(entry => {
const { component, field } = entry;
console.log(` • ${component} → ${field}`);
});
console.log('');
} else {
console.log('➤ No newly deprecated fields.\n');
}
if (changedDefaultsList.length) {
console.log('➤ Changed default values:');
changedDefaultsList.forEach(entry => {
const { component, field, oldDefault, newDefault } = entry;
const oldStr = JSON.stringify(oldDefault);
const newStr = JSON.stringify(newDefault);
console.log(` • ${component} → ${field}`);
console.log(` Old: ${oldStr}`);
console.log(` New: ${newStr}`);
});
console.log('');
} else {
console.log('➤ No changed default values.\n');
}
}
module.exports = {
discoverComponentKeys,
buildComponentMap,
getRpkConnectVersion,
printDeltaReport,
generateConnectorDiffJson,
};