Auto-publishing had a version comparison bug, causing it to attempt to republish old versions instead using next avaiable version number.
217 lines
6.3 KiB
JavaScript
217 lines
6.3 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 compareVersions(a, b) {
|
|
const [aMajor, aMinor, aPatch] = a.split('.').map(Number);
|
|
const [bMajor, bMinor, bPatch] = b.split('.').map(Number);
|
|
if (aMajor !== bMajor) return aMajor - bMajor;
|
|
if (aMinor !== bMinor) return aMinor - bMinor;
|
|
return aPatch - bPatch;
|
|
}
|
|
|
|
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);
|
|
const cmp = compareVersions(currentVersion, latestNpmVersion);
|
|
|
|
if (cmp > 0) {
|
|
return `${major}.${minor}.${patch + 1}`;
|
|
}
|
|
|
|
if (cmp < 0) {
|
|
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();
|
|
}
|