386 lines
11 KiB
JavaScript
386 lines
11 KiB
JavaScript
/**
|
|
* Matrix Generator for Link Validation Workflows
|
|
* Replaces complex bash scripting with maintainable JavaScript
|
|
* Includes cache-aware optimization to skip validation of unchanged files
|
|
*/
|
|
|
|
import { spawn } from 'child_process';
|
|
import process from 'process';
|
|
import { fileURLToPath } from 'url'; // Used for main execution check at bottom of file
|
|
|
|
// Product configuration mapping file paths to products
|
|
const PRODUCT_MAPPING = {
|
|
'content/influxdb3/core': {
|
|
key: 'influxdb3-core',
|
|
name: 'InfluxDB 3 Core',
|
|
},
|
|
'content/influxdb3/enterprise': {
|
|
key: 'influxdb3-enterprise',
|
|
name: 'InfluxDB 3 Enterprise',
|
|
},
|
|
'content/influxdb3/cloud-dedicated': {
|
|
key: 'influxdb3-cloud-dedicated',
|
|
name: 'InfluxDB 3 Cloud Dedicated',
|
|
},
|
|
'content/influxdb3/cloud-serverless': {
|
|
key: 'influxdb3-cloud-serverless',
|
|
name: 'InfluxDB 3 Cloud Serverless',
|
|
},
|
|
'content/influxdb3/clustered': {
|
|
key: 'influxdb3-clustered',
|
|
name: 'InfluxDB 3 Clustered',
|
|
},
|
|
'content/influxdb3/explorer': {
|
|
key: 'influxdb3-explorer',
|
|
name: 'InfluxDB 3 Explorer',
|
|
},
|
|
'content/influxdb/v2': {
|
|
key: 'influxdb-v2',
|
|
name: 'InfluxDB v2',
|
|
},
|
|
'content/influxdb/cloud': {
|
|
key: 'influxdb-cloud',
|
|
name: 'InfluxDB Cloud',
|
|
},
|
|
'content/influxdb/v1': {
|
|
key: 'influxdb-v1',
|
|
name: 'InfluxDB v1',
|
|
},
|
|
'content/influxdb/enterprise_influxdb': {
|
|
key: 'influxdb-enterprise-v1',
|
|
name: 'InfluxDB Enterprise v1',
|
|
},
|
|
'content/telegraf': {
|
|
key: 'telegraf',
|
|
name: 'Telegraf',
|
|
},
|
|
'content/kapacitor': {
|
|
key: 'kapacitor',
|
|
name: 'Kapacitor',
|
|
},
|
|
'content/chronograf': {
|
|
key: 'chronograf',
|
|
name: 'Chronograf',
|
|
},
|
|
'content/flux': {
|
|
key: 'flux',
|
|
name: 'Flux',
|
|
},
|
|
'content/shared': {
|
|
key: 'shared',
|
|
name: 'Shared Content',
|
|
},
|
|
'api-docs': {
|
|
key: 'api-docs',
|
|
name: 'API Documentation',
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Group files by product based on their path
|
|
* @param {string[]} files - Array of file paths
|
|
* @returns {Object} - Object with product keys and arrays of files
|
|
*/
|
|
function groupFilesByProduct(files) {
|
|
const productFiles = {};
|
|
|
|
// Initialize all products
|
|
Object.values(PRODUCT_MAPPING).forEach((product) => {
|
|
productFiles[product.key] = [];
|
|
});
|
|
|
|
files.forEach((file) => {
|
|
let matched = false;
|
|
|
|
// Check each product mapping
|
|
for (const [pathPrefix, product] of Object.entries(PRODUCT_MAPPING)) {
|
|
if (file.startsWith(pathPrefix + '/')) {
|
|
productFiles[product.key].push(file);
|
|
matched = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Handle edge case for api-docs (no trailing slash)
|
|
if (!matched && file.startsWith('api-docs/')) {
|
|
productFiles['api-docs'].push(file);
|
|
}
|
|
});
|
|
|
|
return productFiles;
|
|
}
|
|
|
|
/**
|
|
* Run incremental validation analysis
|
|
* @param {string[]} files - Array of file paths to analyze
|
|
* @returns {Promise<Object>} - Incremental validation results
|
|
*/
|
|
async function runIncrementalAnalysis(files) {
|
|
return new Promise((resolve) => {
|
|
const child = spawn(
|
|
'node',
|
|
['.github/scripts/incremental-validator.cjs', ...files],
|
|
{
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
env: process.env,
|
|
}
|
|
);
|
|
|
|
let stdout = '';
|
|
let stderr = '';
|
|
|
|
child.stdout.on('data', (data) => {
|
|
stdout += data.toString();
|
|
});
|
|
|
|
child.stderr.on('data', (data) => {
|
|
stderr += data.toString();
|
|
});
|
|
|
|
child.on('close', (code) => {
|
|
if (code === 0) {
|
|
try {
|
|
// Parse the JSON output from the validation script
|
|
const lines = stdout.trim().split('\n');
|
|
const jsonLine = lines.find((line) => line.startsWith('{'));
|
|
|
|
if (jsonLine) {
|
|
const results = JSON.parse(jsonLine);
|
|
resolve(results);
|
|
} else {
|
|
resolve({ filesToValidate: files.map((f) => ({ filePath: f })) });
|
|
}
|
|
} catch (error) {
|
|
console.warn(
|
|
`Warning: Could not parse incremental validation results: ${error.message}`
|
|
);
|
|
resolve({ filesToValidate: files.map((f) => ({ filePath: f })) });
|
|
}
|
|
} else {
|
|
console.warn(
|
|
`Incremental validation failed with code ${code}: ${stderr}`
|
|
);
|
|
resolve({ filesToValidate: files.map((f) => ({ filePath: f })) });
|
|
}
|
|
});
|
|
|
|
child.on('error', (error) => {
|
|
console.warn(`Incremental validation error: ${error.message}`);
|
|
resolve({ filesToValidate: files.map((f) => ({ filePath: f })) });
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Generate matrix configuration for GitHub Actions with cache awareness
|
|
* @param {string[]} changedFiles - Array of changed file paths
|
|
* @param {Object} options - Configuration options
|
|
* @returns {Promise<Object>} - Matrix configuration object
|
|
*/
|
|
async function generateMatrix(changedFiles, options = {}) {
|
|
const {
|
|
maxConcurrentJobs = 5,
|
|
forceSequential = false,
|
|
minFilesForParallel = 10,
|
|
useCache = true,
|
|
} = options;
|
|
|
|
if (!changedFiles || changedFiles.length === 0) {
|
|
return {
|
|
strategy: 'none',
|
|
hasChanges: false,
|
|
matrix: { include: [] },
|
|
cacheStats: { hitRate: 100, cacheHits: 0, cacheMisses: 0 },
|
|
};
|
|
}
|
|
|
|
let filesToValidate = changedFiles;
|
|
let cacheStats = {
|
|
hitRate: 0,
|
|
cacheHits: 0,
|
|
cacheMisses: changedFiles.length,
|
|
};
|
|
|
|
// Run incremental analysis if cache is enabled
|
|
if (useCache) {
|
|
try {
|
|
console.log(
|
|
`🔍 Running cache analysis for ${changedFiles.length} files...`
|
|
);
|
|
const analysisResults = await runIncrementalAnalysis(changedFiles);
|
|
|
|
if (analysisResults.filesToValidate) {
|
|
filesToValidate = analysisResults.filesToValidate.map(
|
|
(f) => f.filePath
|
|
);
|
|
cacheStats = analysisResults.cacheStats || cacheStats;
|
|
|
|
console.log(
|
|
`📊 Cache analysis complete: ${cacheStats.hitRate}% hit rate`
|
|
);
|
|
console.log(
|
|
`✅ ${cacheStats.cacheHits} files cached, ${cacheStats.cacheMisses} need validation`
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.warn(
|
|
`Cache analysis failed: ${error.message}, proceeding without cache optimization`
|
|
);
|
|
}
|
|
}
|
|
|
|
// If no files need validation after cache analysis
|
|
if (filesToValidate.length === 0) {
|
|
return {
|
|
strategy: 'cache-hit',
|
|
hasChanges: false,
|
|
matrix: { include: [] },
|
|
cacheStats,
|
|
message: '✨ All files are cached - no validation needed!',
|
|
};
|
|
}
|
|
|
|
const productFiles = groupFilesByProduct(filesToValidate);
|
|
const productsWithFiles = Object.entries(productFiles).filter(
|
|
([key, files]) => files.length > 0
|
|
);
|
|
|
|
// Determine strategy based on file count and configuration
|
|
const totalFiles = filesToValidate.length;
|
|
const shouldUseParallel =
|
|
!forceSequential &&
|
|
totalFiles >= minFilesForParallel &&
|
|
productsWithFiles.length > 1;
|
|
|
|
if (shouldUseParallel) {
|
|
// Parallel strategy: create matrix with products
|
|
const matrixIncludes = productsWithFiles.map(([productKey, files]) => {
|
|
const product = Object.values(PRODUCT_MAPPING).find(
|
|
(p) => p.key === productKey
|
|
);
|
|
return {
|
|
product: productKey,
|
|
name: product?.name || productKey,
|
|
files: files.join(' '),
|
|
cacheEnabled: useCache,
|
|
};
|
|
});
|
|
|
|
return {
|
|
strategy: 'parallel',
|
|
hasChanges: true,
|
|
matrix: { include: matrixIncludes.slice(0, maxConcurrentJobs) },
|
|
cacheStats,
|
|
originalFileCount: changedFiles.length,
|
|
validationFileCount: filesToValidate.length,
|
|
};
|
|
} else {
|
|
// Sequential strategy: single job with all files
|
|
return {
|
|
strategy: 'sequential',
|
|
hasChanges: true,
|
|
matrix: {
|
|
include: [
|
|
{
|
|
product: 'all',
|
|
name: 'All Files',
|
|
files: filesToValidate.join(' '),
|
|
cacheEnabled: useCache,
|
|
},
|
|
],
|
|
},
|
|
cacheStats,
|
|
originalFileCount: changedFiles.length,
|
|
validationFileCount: filesToValidate.length,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* CLI interface for the matrix generator
|
|
*/
|
|
async function main() {
|
|
const args = process.argv.slice(2);
|
|
|
|
if (args.includes('--help') || args.includes('-h')) {
|
|
console.log(`
|
|
Usage: node matrix-generator.js [options] <file1> <file2> ...
|
|
|
|
Options:
|
|
--max-concurrent <n> Maximum concurrent jobs (default: 5)
|
|
--force-sequential Force sequential execution
|
|
--min-files-parallel <n> Minimum files needed for parallel (default: 10)
|
|
--output-format <format> Output format: json, github (default: github)
|
|
--no-cache Disable cache-aware optimization
|
|
--help, -h Show this help message
|
|
|
|
Examples:
|
|
node matrix-generator.js content/influxdb3/core/file1.md content/influxdb/v2/file2.md
|
|
node matrix-generator.js --force-sequential content/shared/file.md
|
|
node matrix-generator.js --no-cache --output-format json *.md
|
|
`);
|
|
process.exit(0);
|
|
}
|
|
|
|
// Parse options
|
|
const options = {};
|
|
const files = [];
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
const arg = args[i];
|
|
|
|
if (arg === '--max-concurrent' && i + 1 < args.length) {
|
|
options.maxConcurrentJobs = parseInt(args[++i]);
|
|
} else if (arg === '--force-sequential') {
|
|
options.forceSequential = true;
|
|
} else if (arg === '--min-files-parallel' && i + 1 < args.length) {
|
|
options.minFilesForParallel = parseInt(args[++i]);
|
|
} else if (arg === '--output-format' && i + 1 < args.length) {
|
|
options.outputFormat = args[++i];
|
|
} else if (arg === '--no-cache') {
|
|
options.useCache = false;
|
|
} else if (!arg.startsWith('--')) {
|
|
files.push(arg);
|
|
}
|
|
}
|
|
|
|
try {
|
|
const result = await generateMatrix(files, options);
|
|
|
|
if (options.outputFormat === 'json') {
|
|
console.log(JSON.stringify(result, null, 2));
|
|
} else {
|
|
// GitHub Actions format
|
|
console.log(`strategy=${result.strategy}`);
|
|
console.log(`has-changes=${result.hasChanges}`);
|
|
console.log(`matrix=${JSON.stringify(result.matrix)}`);
|
|
|
|
// Add cache statistics
|
|
if (result.cacheStats) {
|
|
console.log(`cache-hit-rate=${result.cacheStats.hitRate}`);
|
|
console.log(`cache-hits=${result.cacheStats.cacheHits}`);
|
|
console.log(`cache-misses=${result.cacheStats.cacheMisses}`);
|
|
}
|
|
|
|
if (result.originalFileCount !== undefined) {
|
|
console.log(`original-file-count=${result.originalFileCount}`);
|
|
console.log(`validation-file-count=${result.validationFileCount}`);
|
|
}
|
|
|
|
if (result.message) {
|
|
console.log(`message=${result.message}`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error generating matrix: ${error.message}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Run CLI if this file is executed directly
|
|
if (fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
main().catch(console.error);
|
|
}
|
|
|
|
export { generateMatrix, groupFilesByProduct, PRODUCT_MAPPING };
|