#!/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(); }