@redpanda-data/docs-extensions-and-macros
Version:
Antora extensions and macros developed for Redpanda documentation.
1,314 lines (1,145 loc) • 47.7 kB
JavaScript
/**
* Format diff and cloud support data into a PR-friendly summary
* Outputs a console-parseable format for GitHub Actions
*/
/**
* Render Bloblang changes section for PR summary
* @param {object} data - Bloblang change data
* @param {array} data.newMethods - Array of new methods (with .name and optional .version)
* @param {array} data.newFunctions - Array of new functions (with .name and optional .version)
* @param {array} data.removedMethods - Array of removed methods
* @param {array} data.removedFunctions - Array of removed functions
* @param {array} data.deprecatedMethods - Array of deprecated methods
* @param {array} data.deprecatedFunctions - Array of deprecated functions
* @param {boolean} includeVersion - Whether to include version info in output
* @returns {array} Array of lines to add to the summary
*/
function renderBloblangChanges(data, includeVersion = false) {
const lines = [];
const {
newMethods = [],
newFunctions = [],
removedMethods = [],
removedFunctions = [],
deprecatedMethods = [],
deprecatedFunctions = []
} = data;
const hasChanges = newMethods.length > 0 || newFunctions.length > 0 ||
removedMethods.length > 0 || removedFunctions.length > 0 ||
deprecatedMethods.length > 0 || deprecatedFunctions.length > 0;
if (!hasChanges) {
return lines;
}
lines.push('**Update Bloblang Guide Pages:**');
lines.push('');
if (newMethods.length > 0) {
lines.push(`New methods to add to \`modules/guides/pages/bloblang/methods.adoc\` (${newMethods.length}):`);
newMethods.forEach(method => {
const versionInfo = includeVersion && method.version ? ` — introduced in **${method.version}**` : '';
const name = typeof method === 'string' ? method : method.name;
lines.push(`- [ ] \`${name}\`${versionInfo}`);
});
lines.push('');
}
if (newFunctions.length > 0) {
lines.push(`New functions to add to \`modules/guides/pages/bloblang/functions.adoc\` (${newFunctions.length}):`);
newFunctions.forEach(func => {
const versionInfo = includeVersion && func.version ? ` — introduced in **${func.version}**` : '';
const name = typeof func === 'string' ? func : func.name;
lines.push(`- [ ] \`${name}\`${versionInfo}`);
});
lines.push('');
}
if (removedMethods.length > 0) {
lines.push(`Removed methods to delete from \`modules/guides/pages/bloblang/methods.adoc\` (${removedMethods.length}):`);
removedMethods.forEach(method => {
const versionInfo = includeVersion && method.version ? ` — removed in **${method.version}**` : '';
const name = typeof method === 'string' ? method : method.name;
lines.push(`- [ ] \`${name}\`${versionInfo}`);
});
lines.push('');
}
if (removedFunctions.length > 0) {
lines.push(`Removed functions to delete from \`modules/guides/pages/bloblang/functions.adoc\` (${removedFunctions.length}):`);
removedFunctions.forEach(func => {
const versionInfo = includeVersion && func.version ? ` — removed in **${func.version}**` : '';
const name = typeof func === 'string' ? func : func.name;
lines.push(`- [ ] \`${name}\`${versionInfo}`);
});
lines.push('');
}
if (deprecatedMethods.length > 0) {
lines.push(`Deprecated methods - add deprecation notice to \`modules/guides/pages/bloblang/methods.adoc\` (${deprecatedMethods.length}):`);
deprecatedMethods.forEach(method => {
const versionInfo = includeVersion && method.version ? ` — deprecated in **${method.version}**` : '';
const name = typeof method === 'string' ? method : method.name;
lines.push(`- [ ] \`${name}\`${versionInfo}`);
});
lines.push('');
}
if (deprecatedFunctions.length > 0) {
lines.push(`Deprecated functions - add deprecation notice to \`modules/guides/pages/bloblang/functions.adoc\` (${deprecatedFunctions.length}):`);
deprecatedFunctions.forEach(func => {
const versionInfo = includeVersion && func.version ? ` — deprecated in **${func.version}**` : '';
const name = typeof func === 'string' ? func : func.name;
lines.push(`- [ ] \`${name}\`${versionInfo}`);
});
lines.push('');
}
lines.push('**How to add includes:**');
lines.push('');
lines.push('For methods, find the appropriate category section in `methods.adoc` and add:');
lines.push('```asciidoc');
lines.push('include::connect:partial$bloblang-methods/method_name.adoc[leveloffset=+2]');
lines.push('```');
lines.push('');
lines.push('For functions, add to the Functions section in `functions.adoc`:');
lines.push('```asciidoc');
lines.push('include::connect:partial$bloblang-functions/function_name.adoc[leveloffset=+2]');
lines.push('```');
lines.push('');
lines.push('**Note:** Partials are auto-generated. Includes must be added manually in alphabetical order within their section.');
lines.push('');
return lines;
}
/**
* Generate PR summary for multiple releases
* @param {object} masterDiff - Master diff with releases array
* @param {object} binaryAnalysis - Cloud support data (from latest release)
* @param {array} draftedConnectors - Array of newly drafted connectors
* @returns {string} Formatted summary
*/
function generateMultiVersionPRSummary(masterDiff, binaryAnalysis = null, draftedConnectors = null) {
const lines = [];
// Defensive: ensure metadata exists
const metadata = masterDiff?.metadata || {};
const startVersion = metadata.startVersion || 'unknown';
const endVersion = metadata.endVersion || 'unknown';
const processedReleases = metadata.processedReleases || 0;
lines.push('<!-- PR_SUMMARY_START -->');
lines.push('');
lines.push('## Redpanda Connect Documentation Update');
lines.push('');
lines.push(`**Multi-Release Update:** ${startVersion} → ${endVersion}`);
lines.push(`**Releases Processed:** ${processedReleases}`);
if (binaryAnalysis) {
lines.push(`**Cloud Version:** ${binaryAnalysis.cloudVersion}`);
}
lines.push('');
// Total summary across all releases
lines.push('### Total Changes Across All Releases');
lines.push('');
const total = masterDiff?.totalSummary || {};
if (total.newComponents > 0) {
lines.push(`- **${total.newComponents}** new connectors`);
}
if (total.newFields > 0) {
lines.push(`- **${total.newFields}** new fields across ${total.releaseCount || 0} release(s)`);
}
if (total.removedComponents > 0) {
lines.push(`- **${total.removedComponents}** removed connectors`);
}
if (total.removedFields > 0) {
lines.push(`- **${total.removedFields}** removed fields`);
}
if (total.deprecatedComponents > 0) {
lines.push(`- **${total.deprecatedComponents}** deprecated connectors`);
}
if (total.deprecatedFields > 0) {
lines.push(`- **${total.deprecatedFields}** deprecated fields`);
}
if (total.changedDefaults > 0) {
lines.push(`- **${total.changedDefaults}** changed default values`);
}
if (total.newBloblangMethods > 0) {
lines.push(`- **${total.newBloblangMethods}** new Bloblang methods`);
}
if (total.removedBloblangMethods > 0) {
lines.push(`- **${total.removedBloblangMethods}** removed Bloblang methods`);
}
if (total.newBloblangFunctions > 0) {
lines.push(`- **${total.newBloblangFunctions}** new Bloblang functions`);
}
if (total.removedBloblangFunctions > 0) {
lines.push(`- **${total.removedBloblangFunctions}** removed Bloblang functions`);
}
lines.push('');
// Guard: ensure releases array exists
const releases = masterDiff?.releases || [];
if (releases.length === 0) {
lines.push('_No releases to process_');
lines.push('');
lines.push('<!-- PR_SUMMARY_END -->');
return lines.join('\n');
}
// Per-release detailed breakdown
lines.push('### Changes Per Release');
lines.push('');
for (const release of releases) {
const releaseNotesUrl = `https://github.com/redpanda-data/connect/releases/tag/v${release.toVersion}`;
lines.push(`#### Version ${release.toVersion}`);
lines.push(`> [Release notes](${releaseNotesUrl})`);
lines.push('');
const summary = release.summary || {};
const details = release.details || {};
const hasChanges = Object.values(summary).some(v => v > 0);
if (!hasChanges) {
lines.push('_No documentation changes in this release_');
lines.push('');
continue;
}
// New connectors with descriptions
const newComponents = details.newComponents || [];
if (summary.newComponents > 0 && newComponents.length > 0) {
lines.push(`**New Connectors (${summary.newComponents}):**`);
lines.push('');
const inCloud = release.binaryAnalysis?.comparison?.inCloud || [];
const cloudOnly = release.binaryAnalysis?.comparison?.cloudOnly || [];
const notInCloud = release.binaryAnalysis?.comparison?.notInCloud || [];
const cgoOnly = release.binaryAnalysis?.cgoOnly || [];
newComponents.forEach(comp => {
const isInCloud = inCloud.some(c => c.type === comp.type && c.name === comp.name) ||
cloudOnly.some(c => c.type === comp.type && c.name === comp.name);
const isSelfHostedOnly = notInCloud.some(c => c.type === comp.type && c.name === comp.name);
const isCgoOnly = cgoOnly.some(c => c.type === comp.type && c.name === comp.name);
let platformIndicator = '';
if (isInCloud) platformIndicator = ' ☁️';
else if (isSelfHostedOnly) platformIndicator = ' 🖥️';
if (isCgoOnly) platformIndicator += ' 🔧';
lines.push(`##### \`${comp.name}\` (${comp.type}, ${comp.status})${platformIndicator}`);
// Add description (truncated to 2 sentences)
if (comp.description) {
const shortDesc = truncateToSentence(comp.description, 2);
lines.push(`> ${shortDesc}`);
}
lines.push('');
});
}
// New fields with details
const newFields = details.newFields || [];
if (summary.newFields > 0 && newFields.length > 0) {
lines.push(`**New Fields (${summary.newFields}):**`);
lines.push('');
lines.push('| Component | Field | Description |');
lines.push('|-----------|-------|-------------|');
newFields.forEach(field => {
const desc = field.description ? truncateToSentence(field.description, 1).replace(/\|/g, '\\|') : '_No description_';
lines.push(`| \`${field.component}\` | \`${field.field}\` | ${desc} |`);
});
lines.push('');
}
// Removed components
const removedComponents = details.removedComponents || [];
if (summary.removedComponents > 0 && removedComponents.length > 0) {
lines.push(`**Removed Connectors (${summary.removedComponents}):**`);
lines.push('');
removedComponents.forEach(comp => {
lines.push(`- \`${comp.name}\` (${comp.type})`);
});
lines.push('');
}
// Removed fields
const removedFields = details.removedFields || [];
if (summary.removedFields > 0 && removedFields.length > 0) {
lines.push(`**Removed Fields (${summary.removedFields}):**`);
lines.push('');
lines.push('| Component | Field |');
lines.push('|-----------|-------|');
removedFields.forEach(field => {
lines.push(`| \`${field.component}\` | \`${field.field}\` |`);
});
lines.push('');
}
// Deprecated fields with migration guidance
const deprecatedFields = details.deprecatedFields || [];
if (summary.deprecatedFields > 0 && deprecatedFields.length > 0) {
lines.push(`**Deprecated Fields (${summary.deprecatedFields}):**`);
lines.push('');
deprecatedFields.forEach(field => {
const guidance = field.description ? ` — ${truncateToSentence(field.description, 1)}` : '';
lines.push(`- \`${field.component}.${field.field}\`${guidance}`);
});
lines.push('');
}
// Changed defaults
const changedDefaults = details.changedDefaults || [];
if (summary.changedDefaults > 0 && changedDefaults.length > 0) {
lines.push(`**Changed Defaults (${summary.changedDefaults}):**`);
lines.push('');
lines.push('| Component | Field | Old Default | New Default |');
lines.push('|-----------|-------|-------------|-------------|');
changedDefaults.forEach(change => {
const oldVal = change.oldDefault !== undefined ? `\`${JSON.stringify(change.oldDefault)}\`` : '_none_';
const newVal = change.newDefault !== undefined ? `\`${JSON.stringify(change.newDefault)}\`` : '_none_';
lines.push(`| \`${change.component}\` | \`${change.field}\` | ${oldVal} | ${newVal} |`);
});
lines.push('');
}
}
// Writer action items (aggregate)
lines.push('---');
lines.push('');
lines.push('### Writer Action Items');
lines.push('');
// Collect all new connectors across all releases with full details
const allNewConnectors = [];
const allRemovedConnectors = [];
const allDeprecatedFields = [];
const allChangedDefaults = [];
const allNewBloblangMethods = [];
const allRemovedBloblangMethods = [];
const allNewBloblangFunctions = [];
const allRemovedBloblangFunctions = [];
const allDeprecatedBloblangMethods = [];
const allDeprecatedBloblangFunctions = [];
releases.forEach(release => {
const details = release.details || {};
const releaseInCloud = release.binaryAnalysis?.comparison?.inCloud || [];
const releaseCloudOnly = release.binaryAnalysis?.comparison?.cloudOnly || [];
const releaseNotInCloud = release.binaryAnalysis?.comparison?.notInCloud || [];
// New connectors
(details.newComponents || []).forEach(comp => {
const isCloud = releaseInCloud.some(c => c.type === comp.type && c.name === comp.name) ||
releaseCloudOnly.some(c => c.type === comp.type && c.name === comp.name);
const isSelfHostedOnly = releaseNotInCloud.some(c => c.type === comp.type && c.name === comp.name);
allNewConnectors.push({
name: comp.name,
type: comp.type,
status: comp.status,
description: comp.description,
version: release.toVersion,
isCloud,
isSelfHostedOnly
});
});
// Removed connectors
(details.removedComponents || []).forEach(comp => {
allRemovedConnectors.push({
name: comp.name,
type: comp.type,
version: release.toVersion
});
});
// Deprecated fields
(details.deprecatedFields || []).forEach(field => {
allDeprecatedFields.push({
component: field.component,
field: field.field,
description: field.description,
version: release.toVersion
});
});
// Changed defaults
(details.changedDefaults || []).forEach(change => {
allChangedDefaults.push({
component: change.component,
field: change.field,
oldDefault: change.oldDefault,
newDefault: change.newDefault,
version: release.toVersion
});
});
// New/removed Bloblang methods
(details.newBloblangMethods || []).forEach(methodName => {
allNewBloblangMethods.push({
name: methodName,
version: release.toVersion
});
});
(details.removedBloblangMethods || []).forEach(methodName => {
allRemovedBloblangMethods.push({
name: methodName,
version: release.toVersion
});
});
// New/removed Bloblang functions
(details.newBloblangFunctions || []).forEach(funcName => {
allNewBloblangFunctions.push({
name: funcName,
version: release.toVersion
});
});
(details.removedBloblangFunctions || []).forEach(funcName => {
allRemovedBloblangFunctions.push({
name: funcName,
version: release.toVersion
});
});
// Deprecated Bloblang methods/functions
(details.deprecatedBloblangMethods || []).forEach(methodName => {
allDeprecatedBloblangMethods.push({
name: methodName,
version: release.toVersion
});
});
(details.deprecatedBloblangFunctions || []).forEach(funcName => {
allDeprecatedBloblangFunctions.push({
name: funcName,
version: release.toVersion
});
});
});
// Priority 1: New connectors needing documentation
if (allNewConnectors.length > 0) {
lines.push('**📝 Document New Connectors:**');
lines.push('');
// Group by cloud vs self-hosted
const cloudConnectors = allNewConnectors.filter(c => c.isCloud);
const selfHostedConnectors = allNewConnectors.filter(c => c.isSelfHostedOnly);
const otherConnectors = allNewConnectors.filter(c => !c.isCloud && !c.isSelfHostedOnly);
if (cloudConnectors.length > 0) {
lines.push('_Cloud-supported (higher priority):_');
cloudConnectors.forEach(conn => {
lines.push(`- [ ] \`${conn.name}\` ${conn.type} ☁️ — introduced in **${conn.version}**`);
});
lines.push('');
}
if (selfHostedConnectors.length > 0) {
lines.push('_Self-hosted only:_');
selfHostedConnectors.forEach(conn => {
lines.push(`- [ ] \`${conn.name}\` ${conn.type} 🖥️ — introduced in **${conn.version}**`);
});
lines.push('');
}
if (otherConnectors.length > 0) {
lines.push('_Other connectors:_');
otherConnectors.forEach(conn => {
lines.push(`- [ ] \`${conn.name}\` ${conn.type} — introduced in **${conn.version}**`);
});
lines.push('');
}
}
// Priority 2: Removed connectors needing migration docs
if (allRemovedConnectors.length > 0) {
lines.push('**Update Migration Guide for Removed Connectors:**');
lines.push('');
allRemovedConnectors.forEach(conn => {
lines.push(`- [ ] \`${conn.name}\` ${conn.type} — removed in **${conn.version}**`);
});
lines.push('');
}
// Priority 3: Deprecated fields needing docs update
if (allDeprecatedFields.length > 0) {
lines.push('**Update Docs for Deprecated Fields:**');
lines.push('');
allDeprecatedFields.forEach(field => {
const guidance = field.description ? ` (${truncateToSentence(field.description, 1)})` : '';
lines.push(`- [ ] \`${field.component}.${field.field}\`${guidance} — deprecated in **${field.version}**`);
});
lines.push('');
}
// Priority 4: Changed defaults that may affect users
if (allChangedDefaults.length > 0) {
lines.push('**Review Changed Defaults for Breaking Changes:**');
lines.push('');
allChangedDefaults.forEach(change => {
const oldVal = change.oldDefault !== undefined ? JSON.stringify(change.oldDefault) : 'none';
const newVal = change.newDefault !== undefined ? JSON.stringify(change.newDefault) : 'none';
lines.push(`- [ ] \`${change.component}.${change.field}\`: \`${oldVal}\` → \`${newVal}\` — changed in **${change.version}**`);
});
lines.push('');
}
// Bloblang methods and functions
const bloblangLines = renderBloblangChanges({
newMethods: allNewBloblangMethods,
newFunctions: allNewBloblangFunctions,
removedMethods: allRemovedBloblangMethods,
removedFunctions: allRemovedBloblangFunctions,
deprecatedMethods: allDeprecatedBloblangMethods,
deprecatedFunctions: allDeprecatedBloblangFunctions
}, true); // includeVersion = true for multi-version summaries
lines.push(...bloblangLines);
// Add commercial name reminder if there are new connectors
if (allNewConnectors.length > 0) {
lines.push('---');
lines.push('');
lines.push('**💡 Reminder:** For each new connector, add the `:page-commercial-names:` attribute:');
lines.push('');
lines.push('```asciidoc');
lines.push('= Connector Name');
lines.push(':type: input');
lines.push(':page-commercial-names: Commercial Name, Alternative Name');
lines.push('```');
lines.push('');
}
lines.push('<!-- PR_SUMMARY_END -->');
return lines.join('\n');
}
/**
* Generate a PR-friendly summary for connector changes
* @param {object} diffData - Diff data from generateConnectorDiffJson OR master diff with multiple releases
* @param {object} binaryAnalysis - Cloud support data from getCloudSupport
* @param {array} draftedConnectors - Array of newly drafted connectors
* @param {boolean} isMultiVersion - Whether diffData is a master diff with multiple releases
* @returns {string} Formatted summary
*/
function generatePRSummary(diffData, binaryAnalysis = null, draftedConnectors = null, isMultiVersion = false) {
const lines = [];
// Header with delimiters for GitHub Action parsing
lines.push('<!-- PR_SUMMARY_START -->');
lines.push('');
// Detect if this is a master diff
if (!isMultiVersion && diffData.releases && diffData.totalSummary) {
isMultiVersion = true;
}
if (isMultiVersion) {
// Multi-version format
return generateMultiVersionPRSummary(diffData, binaryAnalysis, draftedConnectors);
}
// Single version format (original logic)
// Quick Summary Section
lines.push('## Redpanda Connect Documentation Update');
lines.push('');
lines.push(`**OSS Version:** ${diffData.comparison.oldVersion} → ${diffData.comparison.newVersion}`);
if (binaryAnalysis) {
lines.push(`**Cloud Version:** ${binaryAnalysis.cloudVersion}`);
}
lines.push('');
// High-level stats
const stats = diffData.summary;
const hasChanges = Object.values(stats).some(v => v > 0) || (draftedConnectors && draftedConnectors.length > 0);
if (!hasChanges) {
lines.push('**No changes detected** - Documentation is up to date');
lines.push('');
lines.push('<!-- PR_SUMMARY_END -->');
return lines.join('\n');
}
lines.push('### Summary');
lines.push('');
if (stats.newComponents > 0) {
lines.push(`- **${stats.newComponents}** new connector${stats.newComponents !== 1 ? 's' : ''}`);
if (binaryAnalysis && binaryAnalysis.comparison) {
const newConnectorKeys = diffData.details.newComponents.map(c => `${c.type}:${c.name}`);
const cloudSupported = newConnectorKeys.filter(key => {
const inCloud = binaryAnalysis.comparison.inCloud.some(c => `${c.type}:${c.name}` === key);
const cloudOnly = binaryAnalysis.comparison.cloudOnly &&
binaryAnalysis.comparison.cloudOnly.some(c => `${c.type}:${c.name}` === key);
return inCloud || cloudOnly;
}).length;
const needsCloudDocs = cloudSupported;
if (needsCloudDocs > 0) {
lines.push(` - ${needsCloudDocs} need${needsCloudDocs !== 1 ? '' : 's'} cloud docs ☁️`);
}
}
}
if (stats.newFields > 0) {
const affectedComponents = new Set(diffData.details.newFields.map(f => f.component)).size;
lines.push(`- **${stats.newFields}** new field${stats.newFields !== 1 ? 's' : ''} across ${affectedComponents} connector${affectedComponents !== 1 ? 's' : ''}`);
}
if (stats.removedComponents > 0) {
lines.push(`- **${stats.removedComponents}** removed connector${stats.removedComponents !== 1 ? 's' : ''}`);
}
if (stats.removedFields > 0) {
lines.push(`- **${stats.removedFields}** removed field${stats.removedFields !== 1 ? 's' : ''}`);
}
if (stats.deprecatedComponents > 0) {
lines.push(`- **${stats.deprecatedComponents}** deprecated connector${stats.deprecatedComponents !== 1 ? 's' : ''}`);
}
if (stats.deprecatedFields > 0) {
lines.push(`- **${stats.deprecatedFields}** deprecated field${stats.deprecatedFields !== 1 ? 's' : ''}`);
}
if (stats.newBloblangMethods > 0) {
lines.push(`- **${stats.newBloblangMethods}** new Bloblang method${stats.newBloblangMethods !== 1 ? 's' : ''}`);
}
if (stats.newBloblangFunctions > 0) {
lines.push(`- **${stats.newBloblangFunctions}** new Bloblang function${stats.newBloblangFunctions !== 1 ? 's' : ''}`);
}
if (stats.removedBloblangMethods > 0) {
lines.push(`- **${stats.removedBloblangMethods}** removed Bloblang method${stats.removedBloblangMethods !== 1 ? 's' : ''}`);
}
if (stats.removedBloblangFunctions > 0) {
lines.push(`- **${stats.removedBloblangFunctions}** removed Bloblang function${stats.removedBloblangFunctions !== 1 ? 's' : ''}`);
}
if (stats.deprecatedBloblangMethods > 0) {
lines.push(`- **${stats.deprecatedBloblangMethods}** deprecated Bloblang method${stats.deprecatedBloblangMethods !== 1 ? 's' : ''}`);
}
if (stats.deprecatedBloblangFunctions > 0) {
lines.push(`- **${stats.deprecatedBloblangFunctions}** deprecated Bloblang function${stats.deprecatedBloblangFunctions !== 1 ? 's' : ''}`);
}
if (stats.changedDefaults > 0) {
lines.push(`- **${stats.changedDefaults}** default value change${stats.changedDefaults !== 1 ? 's' : ''}`);
}
lines.push('');
// Writer Reminder for Commercial Names
if (stats.newComponents > 0) {
lines.push('### Writer Action Required');
lines.push('');
lines.push('For each new connector, add the `:page-commercial-names:` attribute to the frontmatter:');
lines.push('');
lines.push('```asciidoc');
lines.push('= Connector Name');
lines.push(':type: input');
lines.push(':page-commercial-names: Commercial Name, Alternative Name');
lines.push('```');
lines.push('');
lines.push('_This helps improve discoverability and ensures proper categorization._');
lines.push('');
// Check if any new connectors are cloud-supported
if (binaryAnalysis && binaryAnalysis.comparison) {
const newConnectorKeys = diffData.details.newComponents.map(c => ({
key: `${c.type}:${c.name}`,
type: c.type,
name: c.name
}));
const cloudSupported = newConnectorKeys.filter(item => {
// Check both inCloud (OSS+Cloud) and cloudOnly (Cloud-only)
const inCloud = binaryAnalysis.comparison.inCloud.some(c => `${c.type}:${c.name}` === item.key);
const cloudOnly = binaryAnalysis.comparison.cloudOnly &&
binaryAnalysis.comparison.cloudOnly.some(c => `${c.type}:${c.name}` === item.key);
return inCloud || cloudOnly;
});
if (cloudSupported.length > 0) {
lines.push('### ☁️ Cloud Docs Update Required');
lines.push('');
lines.push(`**${cloudSupported.length}** new connector${cloudSupported.length !== 1 ? 's are' : ' is'} available in Redpanda Cloud.`);
lines.push('');
lines.push('**Action:** Submit a separate PR to https://github.com/redpanda-data/cloud-docs to add the connector pages using include syntax:');
lines.push('');
// Check if any are cloud-only (need partial syntax)
const cloudOnly = cloudSupported.filter(item =>
binaryAnalysis.comparison.cloudOnly &&
binaryAnalysis.comparison.cloudOnly.some(c => `${c.type}:${c.name}` === item.key)
);
const regularCloud = cloudSupported.filter(item =>
!binaryAnalysis.comparison.cloudOnly ||
!binaryAnalysis.comparison.cloudOnly.some(c => `${c.type}:${c.name}` === item.key)
);
if (regularCloud.length > 0) {
lines.push('**For connectors in pages:**');
lines.push('```asciidoc');
lines.push('= Connector Name');
lines.push('');
lines.push('include::connect:components:page$type/connector-name.adoc[tag=single-source]');
lines.push('```');
lines.push('');
}
if (cloudOnly.length > 0) {
lines.push('**For cloud-only connectors (in partials):**');
lines.push('```asciidoc');
lines.push('= Connector Name');
lines.push('');
lines.push('include::connect:components:partial$components/cloud-only/type/connector-name.adoc[tag=single-source]');
lines.push('```');
lines.push('');
}
// Add instruction to update cloud whats-new
lines.push('**Also update the cloud whats-new file:**');
lines.push('');
lines.push('Add entries for the new cloud-supported connectors to the Redpanda Cloud whats-new file in the cloud-docs repository.');
lines.push('');
}
}
}
// Breaking Changes Section
const breakingChanges = [];
if (stats.removedComponents > 0) breakingChanges.push('removed connectors');
if (stats.removedFields > 0) breakingChanges.push('removed fields');
if (stats.changedDefaults > 0) breakingChanges.push('changed defaults');
if (breakingChanges.length > 0) {
lines.push('### Breaking Changes Detected');
lines.push('');
lines.push(`This update includes **${breakingChanges.join(', ')}** that may affect existing configurations.`);
lines.push('');
}
// Newly Drafted Connectors Section
if (draftedConnectors && draftedConnectors.length > 0) {
lines.push('### 📝 Newly Drafted - Needs Review');
lines.push('');
lines.push(`**${draftedConnectors.length}** connector${draftedConnectors.length !== 1 ? 's have' : ' has'} been auto-generated and placed in the proper location. These drafts need writer review:`);
lines.push('');
// Group by type
const draftsByType = {};
draftedConnectors.forEach(draft => {
const type = draft.type || 'unknown';
if (!draftsByType[type]) {
draftsByType[type] = [];
}
draftsByType[type].push(draft);
});
// List drafts by type
Object.entries(draftsByType).forEach(([type, drafts]) => {
lines.push(`**${type}:**`);
drafts.forEach(draft => {
// Check both inCloud (OSS+Cloud) and cloudOnly (Cloud-only)
const isInCloud = binaryAnalysis?.comparison.inCloud.some(c =>
c.type === type && c.name === draft.name
);
const isCloudOnly = binaryAnalysis?.comparison.cloudOnly &&
binaryAnalysis.comparison.cloudOnly.some(c =>
c.type === type && c.name === draft.name
);
const cloudIndicator = (isInCloud || isCloudOnly) ? ' ☁️' : '';
const cgoIndicator = draft.requiresCgo ? ' 🔧' : '';
const statusBadge = draft.status && draft.status !== 'stable' ? ` (${draft.status})` : '';
lines.push(`- \`${draft.name}\`${statusBadge}${cloudIndicator}${cgoIndicator} → \`${draft.path}\``);
});
lines.push('');
});
}
// Missing Descriptions Warning
const missingDescriptions = [];
// Check for new components with missing descriptions
if (stats.newComponents > 0) {
diffData.details.newComponents.forEach(connector => {
if (!connector.description || connector.description.trim() === '') {
missingDescriptions.push({
type: 'component',
name: connector.name,
componentType: connector.type
});
}
});
}
// Check for new fields with missing descriptions
if (stats.newFields > 0) {
diffData.details.newFields.forEach(field => {
if (!field.description || field.description.trim() === '') {
missingDescriptions.push({
type: 'field',
name: field.field,
component: field.component
});
}
});
}
if (missingDescriptions.length > 0) {
lines.push('### Missing Descriptions');
lines.push('');
lines.push(`**${missingDescriptions.length}** item${missingDescriptions.length !== 1 ? 's' : ''} missing descriptions - these need writer attention:`);
lines.push('');
const componentsMissing = missingDescriptions.filter(m => m.type === 'component');
const fieldsMissing = missingDescriptions.filter(m => m.type === 'field');
if (componentsMissing.length > 0) {
lines.push('**Components:**');
componentsMissing.forEach(m => {
lines.push(`- \`${m.name}\` (${m.componentType})`);
});
lines.push('');
}
if (fieldsMissing.length > 0) {
lines.push('**Fields:**');
// Group by component
const fieldsByComponent = {};
fieldsMissing.forEach(m => {
if (!fieldsByComponent[m.component]) {
fieldsByComponent[m.component] = [];
}
fieldsByComponent[m.component].push(m.name);
});
Object.entries(fieldsByComponent).forEach(([component, fields]) => {
const [type, name] = component.split(':');
lines.push(`- **${type}/${name}:** ${fields.map(f => `\`${f}\``).join(', ')}`);
});
lines.push('');
}
}
// Action Items
lines.push('### 📝 Action Items for Writers');
lines.push('');
const actionItems = [];
// Add action items for missing descriptions
if (missingDescriptions.length > 0) {
actionItems.push({
priority: 0,
text: `Add descriptions for ${missingDescriptions.length} component${missingDescriptions.length !== 1 ? 's' : ''}/field${missingDescriptions.length !== 1 ? 's' : ''} (see Missing Descriptions section)`
});
}
// New connectors that need cloud docs
if (binaryAnalysis && stats.newComponents > 0) {
diffData.details.newComponents.forEach(connector => {
const key = `${connector.type}:${connector.name}`;
const inCloud = binaryAnalysis.comparison.inCloud.some(c => `${c.type}:${c.name}` === key);
if (inCloud) {
actionItems.push({
priority: 1,
text: `Document new \`${connector.name}\` ${connector.type} (☁️ **CLOUD SUPPORTED**)`
});
}
});
}
// New connectors without cloud support (self-hosted only)
if (binaryAnalysis && stats.newComponents > 0) {
diffData.details.newComponents.forEach(connector => {
const key = `${connector.type}:${connector.name}`;
// Check if it's explicitly in the self-hosted only list
const isSelfHostedOnly = binaryAnalysis.comparison.notInCloud &&
binaryAnalysis.comparison.notInCloud.some(c => `${c.type}:${c.name}` === key);
if (isSelfHostedOnly) {
actionItems.push({
priority: 2,
text: `Document new \`${connector.name}\` ${connector.type} (self-hosted only)`
});
}
});
}
// Deprecated connectors
if (stats.deprecatedComponents > 0) {
diffData.details.deprecatedComponents.forEach(connector => {
actionItems.push({
priority: 3,
text: `Update docs for deprecated \`${connector.name}\` ${connector.type}`
});
});
}
// Removed connectors
if (stats.removedComponents > 0) {
diffData.details.removedComponents.forEach(connector => {
actionItems.push({
priority: 3,
text: `Update migration guide for removed \`${connector.name}\` ${connector.type}`
});
});
}
// Changed defaults that may break configs
if (stats.changedDefaults > 0) {
actionItems.push({
priority: 4,
text: `Review ${stats.changedDefaults} default value change${stats.changedDefaults !== 1 ? 's' : ''} for breaking changes`
});
}
// Sort by priority and output
actionItems.sort((a, b) => a.priority - b.priority);
if (actionItems.length > 0) {
actionItems.forEach(item => {
lines.push(`- [ ] ${item.text}`);
});
} else {
lines.push('- [ ] Review generated documentation');
}
lines.push('');
// Bloblang methods and functions
const bloblangLines = renderBloblangChanges({
newMethods: diffData.details.newBloblangMethods || [],
newFunctions: diffData.details.newBloblangFunctions || [],
removedMethods: diffData.details.removedBloblangMethods || [],
removedFunctions: diffData.details.removedBloblangFunctions || [],
deprecatedMethods: diffData.details.deprecatedBloblangMethods || [],
deprecatedFunctions: diffData.details.deprecatedBloblangFunctions || []
}, false); // includeVersion = false for single-version summaries
lines.push(...bloblangLines);
// Detailed breakdown (expandable)
lines.push('<details>');
lines.push('<summary><strong>Detailed Changes</strong> (click to expand)</summary>');
lines.push('');
// New Connectors
if (stats.newComponents > 0) {
lines.push('#### New Connectors');
lines.push('');
if (binaryAnalysis) {
const cloudSupportedNew = [];
const selfHostedOnlyNew = [];
diffData.details.newComponents.forEach(connector => {
const key = `${connector.type}:${connector.name}`;
// Check explicit categories
const inCloud = binaryAnalysis.comparison.inCloud.some(c => `${c.type}:${c.name}` === key);
const isSelfHostedOnly = binaryAnalysis.comparison.notInCloud &&
binaryAnalysis.comparison.notInCloud.some(c => `${c.type}:${c.name}` === key);
const isCloudOnly = binaryAnalysis.comparison.cloudOnly &&
binaryAnalysis.comparison.cloudOnly.some(c => `${c.type}:${c.name}` === key);
const entry = {
name: connector.name,
type: connector.type,
status: connector.status,
description: connector.description
};
// Use explicit positive checks instead of !inCloud
if (inCloud || isCloudOnly) {
cloudSupportedNew.push(entry);
} else if (isSelfHostedOnly) {
selfHostedOnlyNew.push(entry);
}
});
if (cloudSupportedNew.length > 0) {
lines.push('**☁️ Cloud Supported:**');
lines.push('');
cloudSupportedNew.forEach(c => {
lines.push(`- **${c.name}** (${c.type}, ${c.status})`);
const desc = c.summary || c.description;
if (desc) {
const shortDesc = truncateToSentence(desc, 2);
lines.push(` - ${shortDesc}`);
}
});
lines.push('');
}
if (selfHostedOnlyNew.length > 0) {
lines.push('**Self-Hosted Only:**');
lines.push('');
selfHostedOnlyNew.forEach(c => {
lines.push(`- **${c.name}** (${c.type}, ${c.status})`);
const desc = c.summary || c.description;
if (desc) {
const shortDesc = truncateToSentence(desc, 2);
lines.push(` - ${shortDesc}`);
}
});
lines.push('');
}
} else {
// No cloud support info, just list all
diffData.details.newComponents.forEach(c => {
lines.push(`- **${c.name}** (${c.type}, ${c.status})`);
const desc = c.summary || c.description;
if (desc) {
const shortDesc = truncateToSentence(desc, 2);
lines.push(` - ${shortDesc}`);
}
});
lines.push('');
}
}
// cgo-only connectors (if any new connectors require cgo)
if (binaryAnalysis && binaryAnalysis.cgoOnly && binaryAnalysis.cgoOnly.length > 0 && stats.newComponents > 0) {
// Find new connectors that are cgo-only
const newCgoConnectors = diffData.details.newComponents.filter(connector => {
const key = `${connector.type}:${connector.name}`;
return binaryAnalysis.cgoOnly.some(cgo => `${cgo.type}:${cgo.name}` === key);
});
if (newCgoConnectors.length > 0) {
lines.push('#### 🔧 Cgo Requirements');
lines.push('');
lines.push('The following new connectors require cgo-enabled builds:');
lines.push('');
newCgoConnectors.forEach(connector => {
// Convert type to singular form for better grammar (e.g., "inputs" -> "input")
const typeSingular = connector.type.endsWith('s') ? connector.type.slice(0, -1) : connector.type;
lines.push(`**${connector.name}** (${connector.type}):`);
lines.push('');
lines.push('[NOTE]');
lines.push('====');
lines.push(`The \`${connector.name}\` ${typeSingular} requires a cgo-enabled build of Redpanda Connect.`);
lines.push('');
lines.push('For instructions, see:');
lines.push('');
lines.push('* xref:install:prebuilt-binary.adoc[Download a cgo-enabled binary]');
lines.push('* xref:install:build-from-source.adoc[Build Redpanda Connect from source]');
lines.push('====');
lines.push('');
});
}
}
// New Fields
if (stats.newFields > 0) {
lines.push('#### New Fields');
lines.push('');
// Group by component
const fieldsByComponent = {};
diffData.details.newFields.forEach(field => {
if (!fieldsByComponent[field.component]) {
fieldsByComponent[field.component] = [];
}
fieldsByComponent[field.component].push(field);
});
Object.entries(fieldsByComponent).forEach(([component, fields]) => {
const [type, name] = component.split(':');
lines.push(`**${type}/${name}:**`);
fields.forEach(f => {
lines.push(`- \`${f.field}\`${f.introducedIn ? ` (since ${f.introducedIn})` : ''}`);
});
lines.push('');
});
}
// Removed Connectors
if (stats.removedComponents > 0) {
lines.push('#### Removed Connectors');
lines.push('');
diffData.details.removedComponents.forEach(c => {
lines.push(`- **${c.name}** (${c.type})`);
});
lines.push('');
}
// Removed Fields
if (stats.removedFields > 0) {
lines.push('#### Removed Fields');
lines.push('');
const fieldsByComponent = {};
diffData.details.removedFields.forEach(field => {
if (!fieldsByComponent[field.component]) {
fieldsByComponent[field.component] = [];
}
fieldsByComponent[field.component].push(field);
});
Object.entries(fieldsByComponent).forEach(([component, fields]) => {
const [type, name] = component.split(':');
lines.push(`**${type}/${name}:**`);
fields.forEach(f => {
lines.push(`- \`${f.field}\``);
});
lines.push('');
});
}
// Deprecated Connectors
if (stats.deprecatedComponents > 0) {
lines.push('#### Deprecated Connectors');
lines.push('');
diffData.details.deprecatedComponents.forEach(c => {
lines.push(`- **${c.name}** (${c.type})`);
});
lines.push('');
}
// Deprecated Fields
if (stats.deprecatedFields > 0) {
lines.push('#### Deprecated Fields');
lines.push('');
const fieldsByComponent = {};
diffData.details.deprecatedFields.forEach(field => {
if (!fieldsByComponent[field.component]) {
fieldsByComponent[field.component] = [];
}
fieldsByComponent[field.component].push(field);
});
Object.entries(fieldsByComponent).forEach(([component, fields]) => {
const [type, name] = component.split(':');
lines.push(`**${type}/${name}:**`);
fields.forEach(f => {
lines.push(`- \`${f.field}\``);
});
lines.push('');
});
}
// Changed Defaults
if (stats.changedDefaults > 0) {
lines.push('#### Changed Default Values');
lines.push('');
const changesByComponent = {};
diffData.details.changedDefaults.forEach(change => {
if (!changesByComponent[change.component]) {
changesByComponent[change.component] = [];
}
changesByComponent[change.component].push(change);
});
Object.entries(changesByComponent).forEach(([component, changes]) => {
const [type, name] = component.split(':');
lines.push(`**${type}/${name}:**`);
changes.forEach(c => {
const oldStr = JSON.stringify(c.oldDefault);
const newStr = JSON.stringify(c.newDefault);
lines.push(`- \`${c.field}\`: ${oldStr} → ${newStr}`);
});
lines.push('');
});
}
// New Bloblang Methods
if (stats.newBloblangMethods > 0 && diffData.details.newBloblangMethods) {
lines.push('#### New Bloblang Methods');
lines.push('');
diffData.details.newBloblangMethods.forEach(methodName => {
lines.push(`- \`${methodName}\``);
});
lines.push('');
}
// New Bloblang Functions
if (stats.newBloblangFunctions > 0 && diffData.details.newBloblangFunctions) {
lines.push('#### New Bloblang Functions');
lines.push('');
diffData.details.newBloblangFunctions.forEach(funcName => {
lines.push(`- \`${funcName}\``);
});
lines.push('');
}
// Removed Bloblang Methods
if (stats.removedBloblangMethods > 0 && diffData.details.removedBloblangMethods) {
lines.push('#### Removed Bloblang Methods');
lines.push('');
diffData.details.removedBloblangMethods.forEach(methodName => {
lines.push(`- \`${methodName}\``);
});
lines.push('');
}
// Removed Bloblang Functions
if (stats.removedBloblangFunctions > 0 && diffData.details.removedBloblangFunctions) {
lines.push('#### Removed Bloblang Functions');
lines.push('');
diffData.details.removedBloblangFunctions.forEach(funcName => {
lines.push(`- \`${funcName}\``);
});
lines.push('');
}
// Deprecated Bloblang Methods
if (stats.deprecatedBloblangMethods > 0 && diffData.details.deprecatedBloblangMethods) {
lines.push('#### Deprecated Bloblang Methods');
lines.push('');
diffData.details.deprecatedBloblangMethods.forEach(methodName => {
lines.push(`- \`${methodName}\``);
});
lines.push('');
}
// Deprecated Bloblang Functions
if (stats.deprecatedBloblangFunctions > 0 && diffData.details.deprecatedBloblangFunctions) {
lines.push('#### Deprecated Bloblang Functions');
lines.push('');
diffData.details.deprecatedBloblangFunctions.forEach(funcName => {
lines.push(`- \`${funcName}\``);
});
lines.push('');
}
// Cloud Support Gap Analysis
if (binaryAnalysis && binaryAnalysis.comparison.notInCloud.length > 0) {
lines.push('#### 🔍 Cloud Support Gap Analysis');
lines.push('');
lines.push(`**${binaryAnalysis.comparison.notInCloud.length} connector${binaryAnalysis.comparison.notInCloud.length !== 1 ? 's' : ''} available in OSS but not in cloud:**`);
lines.push('');
// Group by type
const gapsByType = {};
binaryAnalysis.comparison.notInCloud.forEach(connector => {
if (!gapsByType[connector.type]) {
gapsByType[connector.type] = [];
}
gapsByType[connector.type].push(connector);
});
Object.entries(gapsByType).forEach(([type, connectors]) => {
lines.push(`**${type}:**`);
connectors.forEach(c => {
lines.push(`- ${c.name} (${c.status})`);
});
lines.push('');
});
}
lines.push('</details>');
lines.push('');
// Footer
lines.push('---');
lines.push('');
lines.push(`*Generated: ${diffData.comparison.timestamp}*`);
lines.push('');
lines.push('<!-- PR_SUMMARY_END -->');
return lines.join('\n');
}
/**
* Truncate description to specified number of sentences
* @param {string} text - Text to truncate
* @param {number} sentences - Number of sentences to keep
* @returns {string} Truncated text
*/
function truncateToSentence(text, sentences = 2) {
if (!text) return '';
// Remove markdown formatting
let clean = text
.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1') // Remove links
.replace(/[*_`]/g, '') // Remove emphasis
.replace(/\n/g, ' '); // Replace newlines with spaces
// Split by sentence boundaries
const sentenceRegex = /[^.!?]+[.!?]+/g;
const matches = clean.match(sentenceRegex);
if (!matches || matches.length === 0) {
return clean.substring(0, 150);
}
const truncated = matches.slice(0, sentences).join(' ').trim();
return truncated.length > 200 ? truncated.substring(0, 200) + '...' : truncated;
}
/**
* Print the PR summary to console
* @param {object} diffData - Diff data
* @param {object} binaryAnalysis - Cloud support data
* @param {array} draftedConnectors - Array of newly drafted connectors
*/
function printPRSummary(diffData, binaryAnalysis = null, draftedConnectors = null) {
const summary = generatePRSummary(diffData, binaryAnalysis, draftedConnectors);
console.log('\n' + summary + '\n');
}
module.exports = {
generatePRSummary,
generateMultiVersionPRSummary,
printPRSummary,
truncateToSentence
};