BMAD-METHOD/tools/cli/scripts/migrate-workflows.js

274 lines
7.5 KiB
JavaScript
Executable File

/**
* Workflow Migration Script
*
* Updates workflow.yaml files to support the multi-scope system.
* Primarily updates test_dir and other path variables to use scope-aware paths.
*
* Usage:
* node migrate-workflows.js [--dry-run] [--verbose]
*
* Options:
* --dry-run Show what would be changed without making changes
* --verbose Show detailed output
*/
const fs = require('fs-extra');
const path = require('node:path');
const yaml = require('yaml');
const chalk = require('chalk');
// Configuration
const SRC_PATH = path.resolve(__dirname, '../../../src');
const WORKFLOW_PATTERN = '**/workflow.yaml';
// Path mappings for migration
const PATH_MIGRATIONS = [
// Test directory migrations
{
pattern: /\{output_folder\}\/tests/g,
replacement: '{scope_tests}',
description: 'test directory to scope_tests',
},
{
pattern: /\{config_source:implementation_artifacts\}\/tests/g,
replacement: '{config_source:scope_tests}',
description: 'implementation_artifacts tests to scope_tests',
},
// Planning artifacts
{
pattern: /\{output_folder\}\/planning-artifacts/g,
replacement: '{config_source:planning_artifacts}',
description: 'output_folder planning to config_source',
},
// Implementation artifacts
{
pattern: /\{output_folder\}\/implementation-artifacts/g,
replacement: '{config_source:implementation_artifacts}',
description: 'output_folder implementation to config_source',
},
];
// Variables that indicate scope requirement
const SCOPE_INDICATORS = ['{scope}', '{scope_path}', '{scope_tests}', '{scope_planning}', '{scope_implementation}'];
/**
* Find all workflow.yaml files
*/
async function findWorkflowFiles(basePath) {
const files = [];
async function walk(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
// Skip node_modules and hidden directories
if (!entry.name.startsWith('.') && entry.name !== 'node_modules') {
await walk(fullPath);
}
} else if (entry.name === 'workflow.yaml') {
files.push(fullPath);
}
}
}
await walk(basePath);
return files;
}
/**
* Check if a workflow already uses scope variables
*/
function usesScope(content) {
return SCOPE_INDICATORS.some((indicator) => content.includes(indicator));
}
/**
* Analyze a workflow file and suggest migrations
*/
function analyzeWorkflow(content, filePath) {
const analysis = {
filePath,
needsMigration: false,
alreadyScoped: false,
suggestions: [],
currentVariables: [],
};
// Check if already uses scope
if (usesScope(content)) {
analysis.alreadyScoped = true;
return analysis;
}
// Find variables that might need migration
const variablePattern = /\{[^}]+\}/g;
const matches = content.match(variablePattern) || [];
analysis.currentVariables = [...new Set(matches)];
// Check each migration pattern
for (const migration of PATH_MIGRATIONS) {
if (migration.pattern.test(content)) {
analysis.needsMigration = true;
analysis.suggestions.push({
description: migration.description,
pattern: migration.pattern.toString(),
replacement: migration.replacement,
});
}
}
// Check for test_dir variable
if (content.includes('test_dir:') || content.includes('test_dir:')) {
analysis.needsMigration = true;
analysis.suggestions.push({
description: 'Has test_dir variable - may need scope_tests',
pattern: 'test_dir',
replacement: 'scope_tests via config_source',
});
}
return analysis;
}
/**
* Apply migrations to workflow content
*/
function migrateWorkflow(content) {
let migrated = content;
let changes = [];
for (const migration of PATH_MIGRATIONS) {
if (migration.pattern.test(migrated)) {
migrated = migrated.replace(migration.pattern, migration.replacement);
changes.push(migration.description);
}
}
return { content: migrated, changes };
}
/**
* Add scope_required marker to workflow
*/
function addScopeMarker(content) {
try {
const parsed = yaml.parse(content);
// Add scope_required if not present
if (!parsed.scope_required) {
parsed.scope_required = false; // Default to false for backward compatibility
}
return yaml.stringify(parsed, { lineWidth: 120 });
} catch {
// If YAML parsing fails, return original
return content;
}
}
/**
* Main migration function
*/
async function main() {
const args = new Set(process.argv.slice(2));
const dryRun = args.has('--dry-run');
const verbose = args.has('--verbose');
console.log(chalk.bold('\nWorkflow Migration Script'));
console.log(chalk.dim('Updating workflow.yaml files for multi-scope support\n'));
if (dryRun) {
console.log(chalk.yellow('DRY RUN MODE - No changes will be made\n'));
}
// Find all workflow files
console.log(chalk.blue('Scanning for workflow.yaml files...'));
const files = await findWorkflowFiles(SRC_PATH);
console.log(chalk.green(`Found ${files.length} workflow.yaml files\n`));
// Analysis results
const results = {
analyzed: 0,
alreadyScoped: 0,
migrated: 0,
noChanges: 0,
errors: [],
};
// Process each file
for (const filePath of files) {
const relativePath = path.relative(SRC_PATH, filePath);
results.analyzed++;
try {
const content = await fs.readFile(filePath, 'utf8');
const analysis = analyzeWorkflow(content, filePath);
if (analysis.alreadyScoped) {
results.alreadyScoped++;
if (verbose) {
console.log(chalk.dim(`${relativePath} - already scope-aware`));
}
continue;
}
if (!analysis.needsMigration) {
results.noChanges++;
if (verbose) {
console.log(chalk.dim(`${relativePath} - no changes needed`));
}
continue;
}
// Apply migration
const { content: migrated, changes } = migrateWorkflow(content);
if (changes.length > 0) {
console.log(chalk.cyan(`${relativePath}`));
for (const change of changes) {
console.log(chalk.dim(`${change}`));
}
if (!dryRun) {
await fs.writeFile(filePath, migrated, 'utf8');
}
results.migrated++;
} else {
results.noChanges++;
}
} catch (error) {
results.errors.push({ file: relativePath, error: error.message });
console.log(chalk.red(`${relativePath} - Error: ${error.message}`));
}
}
// Print summary
console.log(chalk.bold('\n─────────────────────────────────────'));
console.log(chalk.bold('Summary'));
console.log(chalk.dim('─────────────────────────────────────'));
console.log(` Files analyzed: ${results.analyzed}`);
console.log(` Already scope-aware: ${results.alreadyScoped}`);
console.log(` Migrated: ${results.migrated}`);
console.log(` No changes needed: ${results.noChanges}`);
if (results.errors.length > 0) {
console.log(chalk.red(` Errors: ${results.errors.length}`));
}
console.log();
if (dryRun && results.migrated > 0) {
console.log(chalk.yellow('Run without --dry-run to apply changes\n'));
}
// Exit with error code if there were errors
process.exit(results.errors.length > 0 ? 1 : 0);
}
// Run
main().catch((error) => {
console.error(chalk.red('Fatal error:'), error.message);
process.exit(1);
});