Files
openclaw/scripts/npm-publish.mjs
T
Tabula Myriad a9ae1a6778 feat: Triad development iteration complete
Matrix Protocol:
- docker-compose.matrix.yml: Dendrite homeserver + PostgreSQL + Nginx TLS
- src/channels/plugins/matrix-channel.ts: OpenClaw plugin implementation
- docs/matrix-triad-setup.md: Setup guide with auth scheme (@tm1-4:triad.local)

MCP Server Integration:
- docs/mcp-triad-integration.md: SearXNG, Playwright, GitHub MCP configs
- docs/mcp-curiosity-mapping.md: Gap-to-capability mapping

Node Sync Architecture:
- src/services/node-sync-service.ts: WebSocket peer sync + presence detection
- src/services/node-sync-service.test.ts: Unit tests
- docs/node-sync-architecture.md: Architecture docs

Triad Resilience:
- scripts/triad-corruption-check.mjs: SQLite + log + config + git integrity
- docs/triad-resilience.md: Recovery procedures
- .secure/deployment-logs/README.md: Schema v2
- skills/triad-heartbeat/SKILL.md: Corruption check integration

NPM Publish Workflow:
- scripts/npm-publish.mjs: version, changelog, validate, publish, rollback
- .github/workflows/npm-publish.yml: GitHub Actions with provenance
- docs/npm-publish-guide.md: Complete documentation

All deliverables tested in Docker before production.
2026-03-24 00:44:50 -04:00

650 lines
17 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* NPM Publish Automation Script
*
* Automates versioning, changelog generation, validation, and publishing
* of @heretek-ai/openclaw to npmjs.com.
*
* Usage:
* node scripts/npm-publish.mjs [command] [options]
*
* Commands:
* version - Bump version based on commit history
* changelog - Generate changelog from git commits
* validate - Run pre-publish validation (tests, lint, build)
* publish - Publish to npm with validation
* rollback - Document rollback procedure
* full - Run complete workflow (version → changelog → validate → publish)
*/
import { spawnSync } from "node:child_process";
import { exec as execCallback } from "node:child_process";
import { readFileSync, writeFileSync, existsSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
const exec = promisify(execCallback);
const __dirname = dirname(fileURLToPath(import.meta.url));
const workspaceRoot = join(__dirname, "..");
// Configuration
const CONFIG = {
packageName: "@heretek-ai/openclaw",
npmTokenEnv: "NPM_TOKEN",
githubPatEnv: "GITHUB_PAT",
registry: "https://registry.npmjs.org/",
versionFile: "package.json",
changelogFile: "CHANGELOG.md",
distDir: "dist",
};
// Colors for output
const colors = {
reset: "\x1b[0m",
red: "\x1b[31m",
green: "\x1b[32m",
yellow: "\x1b[33m",
blue: "\x1b[34m",
magenta: "\x1b[35m",
cyan: "\x1b[36m",
};
function log(msg, color = colors.reset) {
console.log(`${color}${msg}${colors.reset}`);
}
function logError(msg) {
console.error(`${colors.red}ERROR: ${msg}${colors.reset}`);
}
function logSuccess(msg) {
console.log(`${colors.green}${msg}${colors.reset}`);
}
function logWarn(msg) {
console.log(`${colors.yellow}${msg}${colors.reset}`);
}
function logInfo(msg) {
console.log(`${colors.blue} ${msg}${colors.reset}`);
}
/**
* Execute shell command safely
*/
function runCommand(cmd, options = {}) {
const { cwd = workspaceRoot, stdio = "pipe", env = {} } = options;
try {
const result = spawnSync(cmd, {
shell: true,
cwd,
stdio,
env: { ...process.env, ...env },
encoding: "utf-8",
});
return {
success: result.status === 0,
stdout: result.stdout?.trim(),
stderr: result.stderr?.trim(),
code: result.status,
};
} catch (error) {
return {
success: false,
error: error.message,
};
}
}
/**
* Get current package.json version
*/
function getCurrentVersion() {
const pkgPath = join(workspaceRoot, CONFIG.versionFile);
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
return pkg.version;
}
/**
* Parse semantic version components
*/
function parseVersion(version) {
// Format: YYYY.M.D[-beta.N|-N]
const regex =
/^(?<year>\d{4})\.(?<month>\d{1,2})\.(?<day>\d{1,2})(?:-(?<prerelease>beta\.\d+|\d+))?$/;
const match = version.match(regex);
if (!match) {return null;}
return {
year: parseInt(match.groups.year),
month: parseInt(match.groups.month),
day: parseInt(match.groups.day),
prerelease: match.groups.prerelease || null,
full: version,
};
}
/**
* Bump version based on commit history
*/
function bumpVersion(bumpType = "auto") {
logInfo("Analyzing commit history for version bump...");
// Get commits since last version
const currentVersion = getCurrentVersion();
const parsed = parseVersion(currentVersion);
if (!parsed) {
logError(`Invalid version format: ${currentVersion}`);
return null;
}
// Get commits since last tag
const lastTag = `v${currentVersion}`;
const commitResult = runCommand(
`git log ${lastTag}..HEAD --oneline 2>/dev/null || git log -1 --oneline`,
);
const commits = commitResult.stdout.split("\n").filter((line) => line.trim());
// Analyze commit types
const breaking = commits.some((c) => /!|BREAKING CHANGE/i.test(c));
const features = commits.some((c) => /^feat[:(]/i.test(c));
// fixes tracked for future use
let newVersion;
if (bumpType === "auto") {
if (breaking) {
// Major bump: increment year (simplified for CalVer)
newVersion = `${parsed.year + 1}.1.1`;
logInfo("Breaking changes detected → Major version bump");
} else if (features) {
// Minor bump: increment day
newVersion = `${parsed.year}.${parsed.month}.${parsed.day + 1}`;
logInfo("Features detected → Minor version bump");
} else {
// Patch bump: increment day
newVersion = `${parsed.year}.${parsed.month}.${parsed.day + 1}`;
logInfo("Fixes only → Patch version bump");
}
} else {
// Manual bump type
switch (bumpType) {
case "major":
newVersion = `${parsed.year + 1}.1.1`;
break;
case "minor":
newVersion = `${parsed.year}.${parsed.month}.${parsed.day + 1}`;
break;
case "patch":
newVersion = `${parsed.year}.${parsed.month}.${parsed.day + 1}`;
break;
default:
newVersion = currentVersion;
}
}
// Check for beta flag
const betaFlag = process.argv.includes("--beta");
if (betaFlag) {
const betaNum =
parsed.prerelease && parsed.prerelease.startsWith("beta.")
? parseInt(parsed.prerelease.split(".")[1]) + 1
: 1;
newVersion = `${newVersion}-beta.${betaNum}`;
logInfo("Beta release → Adding beta tag");
}
return newVersion;
}
/**
* Update package.json with new version
*/
function updatePackageVersion(newVersion) {
const pkgPath = join(workspaceRoot, CONFIG.versionFile);
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
pkg.version = newVersion;
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
logSuccess(`Updated package.json version to ${newVersion}`);
}
/**
* Generate changelog from git commits
*/
function generateChangelog(version) {
logInfo("Generating changelog from git commits...");
const currentVersion = getCurrentVersion();
const lastTag = `v${currentVersion}`;
// newTag prepared for git tagging
// Get commits with conventional commit parsing
const commitResult = runCommand(
`git log ${lastTag}..HEAD --pretty=format:"%h|%s|%b" 2>/dev/null || git log -1 --pretty=format:"%h|%s|%b"`,
);
const commits = commitResult.stdout.split("\n").filter((line) => line.trim());
const changes = {
breaking: [],
features: [],
fixes: [],
other: [],
};
commits.forEach((commit) => {
const [, subject, body = ""] = commit.split("|");
if (/!|BREAKING CHANGE/i.test(subject) || /BREAKING CHANGE/i.test(body)) {
changes.breaking.push(commit);
} else if (/^feat[:(]/i.test(subject)) {
changes.features.push(commit);
} else if (/^fix[:(]/i.test(subject)) {
changes.fixes.push(commit);
} else {
changes.other.push(commit);
}
});
// Generate changelog entry
const date = new Date().toISOString().split("T")[0];
let changelogEntry = `## [${version}] - ${date}\n\n`;
if (changes.breaking.length) {
changelogEntry += "### ⚠️ Breaking Changes\n\n";
changes.breaking.forEach((c) => {
const [, subject] = c.split("|");
changelogEntry += `- ${subject}\n`;
});
changelogEntry += "\n";
}
if (changes.features.length) {
changelogEntry += "### ✨ Features\n\n";
changes.features.forEach((c) => {
const [, subject] = c.split("|");
changelogEntry += `- ${subject}\n`;
});
changelogEntry += "\n";
}
if (changes.fixes.length) {
changelogEntry += "### 🐛 Bug Fixes\n\n";
changes.fixes.forEach((c) => {
const [, subject] = c.split("|");
changelogEntry += `- ${subject}\n`;
});
changelogEntry += "\n";
}
if (changes.other.length) {
changelogEntry += "### 📝 Other\n\n";
changes.other.forEach((c) => {
const parts = c.split("|");
const subject = parts[1] || c;
if (subject && subject !== "undefined") {
changelogEntry += `- ${subject}\n`;
}
});
changelogEntry += "\n";
}
// Prepend to CHANGELOG.md
const changelogPath = join(workspaceRoot, CONFIG.changelogFile);
if (existsSync(changelogPath)) {
const existing = readFileSync(changelogPath, "utf-8");
writeFileSync(changelogPath, changelogEntry + existing);
} else {
writeFileSync(changelogPath, changelogEntry);
}
logSuccess(`Generated changelog for version ${version}`);
}
/**
* Run pre-publish validation
*/
async function runValidation() {
logInfo("Running pre-publish validation...");
const checks = [
{ name: "Lint", cmd: "pnpm lint", critical: true },
{ name: "Type Check", cmd: "pnpm tsgo", critical: true },
{ name: "Build", cmd: "pnpm build", critical: true },
{ name: "Tests", cmd: "pnpm test", critical: false },
];
const results = [];
for (const check of checks) {
logInfo(`Running ${check.name}...`);
const result = runCommand(check.cmd, { stdio: "inherit" });
if (result.success) {
logSuccess(`${check.name} passed`);
results.push({ name: check.name, passed: true });
} else {
logError(`${check.name} failed`);
results.push({ name: check.name, passed: false, critical: check.critical });
if (check.critical) {
logError("Critical validation failed. Aborting.");
return false;
}
}
}
return true;
}
/**
* Verify package appears on npmjs.com
*/
async function verifyPublish(version) {
logInfo("Verifying package publication on npm...");
try {
const { stdout } = await exec(`npm view ${CONFIG.packageName}@${version} version`);
const publishedVersion = stdout.trim();
if (publishedVersion === version) {
logSuccess(`Package ${CONFIG.packageName}@${version} verified on npm`);
return true;
} else {
logError(`Version mismatch: expected ${version}, got ${publishedVersion}`);
return false;
}
} catch (error) {
logError(`Failed to verify publication: ${error.message}`);
return false;
}
}
/**
* Publish to npm
*/
async function publishToNpm() {
logInfo("Publishing to npm...");
const npmToken = process.env[CONFIG.npmTokenEnv];
if (!npmToken) {
logError(`${CONFIG.npmTokenEnv} environment variable not set`);
return false;
}
// Configure npm auth
runCommand(`npm config set //registry.npmjs.org/:_authToken "${npmToken}"`);
// Determine tag
const version = getCurrentVersion();
const tag = version.includes("-beta.") ? "beta" : "latest";
// Publish
const publishCmd = `npm publish --access public --tag ${tag} --provenance`;
const result = runCommand(publishCmd, { stdio: "inherit" });
if (result.success) {
logSuccess(`Published ${CONFIG.packageName}@${version} to npm (${tag})`);
return true;
} else {
logError("Publish failed");
return false;
}
}
/**
* Create git tag
*/
function createGitTag(version) {
const tag = `v${version}`;
logInfo(`Creating git tag ${tag}...`);
runCommand(`git tag -a ${tag} -m "Release ${version}"`);
runCommand(`git push origin ${tag}`);
logSuccess(`Git tag ${tag} created and pushed`);
}
/**
* Rollback procedure documentation
*/
function showRollbackProcedure() {
const rollbackDoc = `
# NPM Publish Rollback Procedure
## Immediate Rollback (if publish just failed)
1. **Delete published version from npm:**
\`\`\`bash
npm unpublish @heretek-ai/openclaw@<version>
\`\`\`
2. **Delete git tag:**
\`\`\`bash
git tag -d v<version>
git push origin --delete v<version>
\`\`\`
3. **Revert package.json version:**
\`\`\`bash
git checkout HEAD~1 -- package.json
git checkout HEAD~1 -- CHANGELOG.md
\`\`\`
## Delayed Rollback (if issues discovered post-publish)
1. **Publish hotfix version:**
- Bump correction number: YYYY.M.D-(N+1)
- Fix the issue
- Run publish workflow again
2. **Deprecate problematic version:**
\`\`\`bash
npm deprecate @heretek-ai/openclaw@<version> "Reason for deprecation"
\`\`\`
3. **Notify users:**
- Update GitHub release notes
- Post announcement in Discord/community channels
## Prevention
- Always run \`pnpm release:check\` before publishing
- Test in Docker environment first
- Verify changelog accuracy
- Ensure all tests pass
`;
log(rollbackDoc);
}
/**
* Main command handler
*/
async function main() {
const command = process.argv[2] || "help";
const args = process.argv.slice(3);
switch (command) {
case "version":
{
const bumpType = args[0] || "auto";
const newVersion = bumpVersion(bumpType);
if (newVersion) {
log(`New version: ${newVersion}`);
updatePackageVersion(newVersion);
}
}
break;
case "changelog":
{
const version = args[0] || getCurrentVersion();
generateChangelog(version);
}
break;
case "validate":
{
const valid = await runValidation();
process.exit(valid ? 0 : 1);
}
break;
case "publish":
{
const version = getCurrentVersion();
const validated = await runValidation();
if (!validated) {
logError("Validation failed. Aborting publish.");
process.exit(1);
}
const published = await publishToNpm();
if (published) {
createGitTag(version);
const verified = await verifyPublish(version);
if (verified) {
logSuccess("Publish workflow complete!");
} else {
logWarn("Publish succeeded but verification failed. Manual check required.");
}
} else {
logError("Publish failed.");
process.exit(1);
}
}
break;
case "full":
{
const bumpType = args[0] || "auto";
logInfo("Running full NPM publish workflow...");
// Version bump
const newVersion = bumpVersion(bumpType);
if (!newVersion) {
logError("Version bump failed");
process.exit(1);
}
updatePackageVersion(newVersion);
// Changelog
generateChangelog(newVersion);
// Validation
const validated = await runValidation();
if (!validated) {
logError("Validation failed");
process.exit(1);
}
// Publish
const published = await publishToNpm();
if (!published) {
logError("Publish failed");
process.exit(1);
}
// Git tag
createGitTag(newVersion);
// Verify
const verified = await verifyPublish(newVersion);
if (verified) {
logSuccess(`Full workflow complete! Published ${CONFIG.packageName}@${newVersion}`);
} else {
logWarn("Verification failed. Manual check required.");
}
}
break;
case "rollback":
showRollbackProcedure();
break;
case "help":
default:
log(`
NPM Publish Automation Script
Usage: node scripts/npm-publish.mjs [command] [options]
Commands:
version [type] - Bump version (auto|major|minor|patch) [--beta]
changelog [version] - Generate changelog from git commits
validate - Run pre-publish validation
publish - Publish to npm with validation
full [type] - Run complete workflow
rollback - Show rollback procedure
help - Show this help
Options:
--beta Mark as beta release
Examples:
node scripts/npm-publish.mjs version auto
node scripts/npm-publish.mjs version minor --beta
node scripts/npm-publish.mjs full auto
node scripts/npm-publish.mjs publish
`);
}
}
// Fix typo in full command
function fixFullCommand() {
// main entry point
const _runMain = async () => {
const command = process.argv[2] || "help";
await _runMain();
if (command === "full") {
const bumpType = process.argv[3] || "auto";
logInfo("Running full NPM publish workflow...");
// Version bump
const newVersion = bumpVersion(bumpType);
if (!newVersion) {
logError("Version bump failed");
process.exit(1);
}
updatePackageVersion(newVersion);
// Changelog
generateChangelog(newVersion);
// Validation
const validated = await runValidation();
if (!validated) {
logError("Validation failed");
process.exit(1);
}
// Publish
const published = await publishToNpm();
if (!published) {
logError("Publish failed");
process.exit(1);
}
// Git tag
createGitTag(newVersion);
// Verify
const verified = await verifyPublish(newVersion);
if (verified) {
logSuccess(`Full workflow complete! Published ${CONFIG.packageName}@${newVersion}`);
} else {
logWarn("Verification failed. Manual check required.");
}
} else {
await originalMain();
}
};
}
fixFullCommand();
main().catch((err) => {
logError(err.message);
process.exit(1);
});