WizardConnect/contrib/auto-publish.js
2026-03-09 17:05:45 +01:00

208 lines
6 KiB
JavaScript

#!/usr/bin/env node
/**
* Fully automated package publishing for monorepo
* - Detects changes automatically
* - Gets latest version from npm and bumps appropriately
* - Skips private packages
* - Uses npm trusted publishing (OIDC provenance) — no NPM_TOKEN needed
* - No git commits, just publishes
* - No manual interaction required
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
function discoverPackages() {
const packagesDir = 'packages';
const packagePaths = {};
const packageDirs = fs.readdirSync(packagesDir, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
for (const packageDir of packageDirs) {
const packageJsonPath = path.join(packagesDir, packageDir, 'package.json');
if (fs.existsSync(packageJsonPath)) {
try {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
if (packageJson.private) {
log(`Skipping private package: ${packageJson.name || packageDir}`);
continue;
}
if (packageJson.name) {
packagePaths[packageJson.name] = path.join(packagesDir, packageDir);
}
} catch (error) {
log(`Warning: Could not parse package.json in ${packageDir}: ${error.message}`);
}
}
}
return packagePaths;
}
function log(message) {
console.log(`[AUTO-PUBLISH] ${message}`);
}
function execCommand(command, cwd = process.cwd()) {
try {
return execSync(command, { cwd, stdio: 'pipe', encoding: 'utf8' });
} catch (error) {
throw new Error(`Command failed: ${command}\n${error.message}`);
}
}
function checkPackageChanges(packageName, packagePath) {
log(`Checking changes for ${packageName} in ${packagePath}`);
const latestNpmVersion = getLatestVersionFromNpm(packageName);
if (latestNpmVersion === null) {
log(`New package ${packageName} detected - will be published`);
return true;
}
try {
execCommand(`git diff --quiet HEAD~1 HEAD -- ${packagePath}/src/`);
return false;
} catch (error) {
return true;
}
}
function getLatestVersionFromNpm(packageName) {
try {
const result = execCommand(`npm view ${packageName} version`);
return result.trim();
} catch (error) {
const errorMessage = error.message || error.toString();
if (errorMessage.includes('404')) {
log(`Package ${packageName} not found on npm registry (404) - treating as new package`);
return null;
}
log(`ERROR: Failed to check npm registry for ${packageName}: ${errorMessage}`);
throw new Error(`Failed to check npm registry for ${packageName}: ${errorMessage}`);
}
}
function getCurrentVersion(packagePath) {
const packageJson = JSON.parse(fs.readFileSync(path.join(packagePath, 'package.json'), 'utf8'));
return packageJson.version;
}
function bumpVersion(currentVersion, latestNpmVersion) {
const [major, minor, patch] = currentVersion.split('.').map(Number);
if (latestNpmVersion === null) {
return currentVersion;
}
const [npmMajor, npmMinor, npmPatch] = latestNpmVersion.split('.').map(Number);
if (currentVersion > latestNpmVersion) {
return `${major}.${minor}.${patch + 1}`;
}
if (latestNpmVersion > currentVersion) {
return `${npmMajor}.${npmMinor}.${npmPatch + 1}`;
}
return `${major}.${minor}.${patch + 1}`;
}
function updatePackageVersion(packagePath, packageName, newVersion) {
log(`Updating ${packageName} to version ${newVersion}`);
const packageJsonPath = path.join(packagePath, 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
packageJson.version = newVersion;
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
return newVersion;
}
function publishPackage(packagePath, packageName) {
log(`Publishing ${packageName}`);
try {
execCommand('npm publish --access public', packagePath);
log(`Successfully published ${packageName}`);
} catch (error) {
log(`Failed to publish ${packageName}: ${error.message}`);
throw error;
}
}
function main() {
const isDryRun = process.argv.includes('--dry-run');
if (isDryRun) {
log('DRY RUN MODE - No packages will be published');
} else {
log('Starting fully automated package publishing');
}
log('Discovering packages...');
const packagePaths = discoverPackages();
log(`Found ${Object.keys(packagePaths).length} publishable packages: ${Object.keys(packagePaths).join(', ')}`);
log('Installing dependencies...');
execCommand('npm ci');
log('Building all packages...');
execCommand('npm run build');
const packagesToPublish = [];
for (const [packageName, packagePath] of Object.entries(packagePaths)) {
if (checkPackageChanges(packageName, packagePath)) {
log(`Changes detected for ${packageName}`);
packagesToPublish.push(packageName);
} else {
log(`No changes detected for ${packageName}`);
}
}
if (packagesToPublish.length === 0) {
log('No packages have changes, skipping publish');
process.exit(0);
}
log(`Found ${packagesToPublish.length} packages with changes: ${packagesToPublish.join(', ')}`);
for (const packageName of packagesToPublish) {
const packagePath = packagePaths[packageName];
const currentVersion = getCurrentVersion(packagePath);
const latestNpmVersion = getLatestVersionFromNpm(packageName);
if (latestNpmVersion === null) {
log(`Current version: ${currentVersion}, Latest on npm: (new package)`);
} else {
log(`Current version: ${currentVersion}, Latest on npm: ${latestNpmVersion}`);
}
const newVersion = bumpVersion(currentVersion, latestNpmVersion);
updatePackageVersion(packagePath, packageName, newVersion);
if (!isDryRun) {
publishPackage(packagePath, packageName);
} else {
log(`[DRY RUN] Would publish ${packageName}@${newVersion}`);
}
}
if (isDryRun) {
log('Dry run completed - no packages were actually published');
} else {
log('Automated publishing completed successfully');
}
}
if (require.main === module) {
main();
}