Files
oxc-project__oxc/.github/scripts/check-npm-packages.js
Boshen 4da5da8eed fix(ci): skip already-published npm packages instead of erroring (#20173)
## Summary

- CI publish workflow fails when re-run after a partial publish because
npm rejects republishing an existing version
- Add version-published checks to `check-npm-packages.js` so
already-published packages are skipped in the dry-run step
- Extract a shared `publish-if-needed.sh` script that checks the npm
registry before publishing, used by all three trusted publish steps
- Consolidate the old `checkPackageExists` and new version check into a
single `npmViewVersion` helper to avoid redundant npm registry calls

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 13:20:59 +08:00

205 lines
6.1 KiB
JavaScript
Executable File

#!/usr/bin/env node
/* eslint-disable no-console */
const { execSync, execFileSync } = require("child_process");
const fs = require("fs");
const path = require("path");
/**
* Check npm packages before publishing
* Usage: node check-npm-packages.js <package_dir_pattern> [root_package_path]
* Example: node check-npm-packages.js "npm/oxlint*"
* Example: node check-npm-packages.js "release-dir/*" "napi/parser"
*/
/**
* Query npm registry for a package version.
* Returns the version string if found, or null if the package/version does not exist.
* Throws on unexpected errors (network issues, auth failures, etc).
*/
function npmViewVersion(spec) {
try {
return execFileSync("npm", ["view", spec, "version"], {
stdio: "pipe",
encoding: "utf-8",
}).trim();
} catch (error) {
// npm exits with E404 when the package or version doesn't exist
if (error.stderr && error.stderr.includes("E404")) {
return null;
}
throw error;
}
}
function checkPackageFiles(packageDir, packageJson) {
const files = packageJson.files || [];
let allFilesExist = true;
for (const file of files) {
const filePath = path.join(packageDir, file);
if (!fs.existsSync(filePath)) {
console.error(`::error::File does not exist: ${filePath}`);
allFilesExist = false;
}
}
return allFilesExist;
}
function dryRunPublish(packageDir) {
try {
const flags = "--provenance --access public --no-git-checks";
console.log(`Running dry-run publish for ${packageDir}...`);
execSync(`pnpm publish ${packageDir}/ ${flags} --dry-run`, {
stdio: "inherit",
});
return true;
} catch (error) {
console.error(`::error::Dry-run publish failed for ${packageDir}:`, error.message);
return false;
}
}
function checkPackage(packageDir, skipExistenceCheck = false) {
console.log(packageDir);
if (!fs.existsSync(packageDir)) {
console.warn(`::warning::Package directory not found: ${packageDir}`);
return false;
}
const packageJsonPath = path.join(packageDir, "package.json");
if (!fs.existsSync(packageJsonPath)) {
console.warn(`::warning::package.json not found in: ${packageDir}`);
return false;
}
// List directory contents
try {
execSync(`ls ${packageDir}`, { stdio: "inherit" });
} catch (error) {
console.error(`Failed to list directory ${packageDir}:`, error.message);
}
// Show package.json
try {
execSync(`cat ${packageJsonPath}`, { stdio: "inherit" });
} catch (error) {
console.error(`Failed to read ${packageJsonPath}:`, error.message);
}
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
const packageName = packageJson.name;
const packageVersion = packageJson.version;
// Check if this exact version is already published (single npm registry call)
if (npmViewVersion(`${packageName}@${packageVersion}`) === packageVersion) {
console.log(`⏭ ${packageName}@${packageVersion} already published, skipping.`);
console.log("");
return true;
}
// Check if package exists on npm (unless skipped)
if (!skipExistenceCheck) {
console.log(`Checking if package '${packageName}' exists on npm...`);
if (!npmViewVersion(packageName)) {
console.error(`::error::Package '${packageName}' does not exist on npm!`);
console.log("");
console.log(
"======================================================================================================",
);
console.log("FIRST PUBLISH REQUIRED");
console.log(
"======================================================================================================",
);
console.log("");
console.log(`Package: ${packageName}`);
console.log("This package needs to be published to npm for the first time manually.");
console.log("");
console.log("Instructions:");
console.log(
` 1. Create a local project with package name '${packageName}' and version '0.0.1'`,
);
console.log(" 2. Publish it manually with: npm publish --access public");
console.log(" 3. After successful publish, delete the token from:");
console.log(" - ~/.npmrc (local file)");
console.log(" - npm settings (https://www.npmjs.com/settings/YOUR_USERNAME/tokens)");
console.log(
" Note: Manual tokens don't expire automatically and should be deleted for security",
);
console.log("");
console.log(
"After the initial publish, this workflow will handle all subsequent releases automatically.",
);
console.log(
"======================================================================================================",
);
console.log("");
return false;
}
console.log(`✓ Package '${packageName}' exists on npm`);
}
// Check files exist
if (!checkPackageFiles(packageDir, packageJson)) {
return false;
}
// Run dry-run publish
if (!dryRunPublish(packageDir)) {
return false;
}
console.log("");
return true;
}
function main() {
const args = process.argv.slice(2);
if (args.length === 0) {
console.error("Usage: node check-npm-packages.js <package_dir_pattern> [root_package_path]");
process.exit(1);
}
const packagePattern = args[0];
const rootPackagePath = args[1];
let exitCode = 0;
// Expand glob pattern using shell
let packageDirs = [];
try {
const result = execSync(`ls -d ${packagePattern}`, { encoding: "utf-8" });
packageDirs = result.trim().split("\n").filter(Boolean);
} catch (error) {
// Pattern might not match anything, which is ok if we have a root package
console.error(`Pattern expansion failed for ${packagePattern}:`, error.message);
if (!rootPackagePath) {
console.error(`::error::No packages found matching pattern: ${packagePattern}`);
process.exit(1);
}
}
// Check subpackages
for (const packageDir of packageDirs) {
if (!checkPackage(packageDir)) {
exitCode = 1;
}
}
// Check root package if specified
if (rootPackagePath) {
console.log("# Checking root package");
if (!checkPackage(rootPackagePath, true)) {
exitCode = 1;
}
}
process.exit(exitCode);
}
main();