mirror of
https://github.com/Heretek-AI/openclaw.git
synced 2026-07-19 23:43:32 -04:00
a9ae1a6778
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.
650 lines
17 KiB
JavaScript
Executable File
650 lines
17 KiB
JavaScript
Executable File
#!/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);
|
||
});
|