0xray 4.0.0 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.mcp.json +15 -55
- package/.xray/features.json +2 -2
- package/AGENTS-consumer.md +6 -6
- package/AGENTS.md +9 -9
- package/README.md +19 -19
- package/SKILLS.md +2 -2
- package/dist/AGENTS.md +9 -9
- package/dist/CHANGELOG.md +54 -2
- package/dist/README.md +19 -19
- package/dist/SKILLS.md +2 -2
- package/dist/cli/commands/foundry-mint-wear.d.ts +2 -0
- package/dist/cli/commands/foundry-mint-wear.js +14 -0
- package/dist/cli/commands/hermes-install.js +2 -0
- package/dist/cli/commands/openclaw-install.js +2 -0
- package/dist/cli/commands/opencode-install.js +2 -0
- package/dist/cli/index.js +10 -1
- package/dist/enforcement/enforcer-tools.d.ts +7 -0
- package/dist/enforcement/enforcer-tools.js +18 -109
- package/dist/integrations/grok/grok-cli.js +2 -0
- package/dist/integrations/hermes-agent/bridge.mjs +7 -0
- package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +4 -2
- package/dist/processors/implementations/version-compliance-processor.d.ts +3 -34
- package/dist/processors/implementations/version-compliance-processor.js +8 -135
- package/opencode.json +0 -8
- package/package.json +15 -14
- package/scripts/foundry/CHANGELOG.md +34 -0
- package/scripts/foundry/LICENSE +21 -0
- package/scripts/foundry/README.md +67 -0
- package/scripts/foundry/ci-monitor.mjs +115 -0
- package/scripts/foundry/cli.js +2 -0
- package/scripts/foundry/cli.mjs +55 -0
- package/scripts/foundry/docs-build.mjs +38 -0
- package/scripts/foundry/hooks.mjs +36 -0
- package/scripts/foundry/mill-root.mjs +62 -0
- package/scripts/foundry/mint-suit.cjs +313 -0
- package/scripts/foundry/mint.mjs +33 -0
- package/scripts/foundry/package.json +53 -0
- package/scripts/foundry/reconcile-version.mjs +150 -0
- package/scripts/foundry/release-gate.mjs +100 -0
- package/scripts/foundry/release.mjs +266 -0
- package/scripts/foundry/validate-release-docs.mjs +316 -0
- package/scripts/foundry/version-manager.mjs +444 -0
- package/scripts/node/ci-cd-auto-fix.cjs +12 -257
- package/scripts/node/consumer-install-smoke.mjs +57 -2
- package/scripts/node/enforce-version-compliance.sh +5 -20
- package/scripts/node/enforce-version-compliance.ts +13 -113
- package/scripts/node/foundry-shim.mjs +19 -0
- package/scripts/node/github-actions-monitor.cjs +13 -18
- package/scripts/node/postinstall.cjs +36 -1
- package/scripts/node/reconcile-version.mjs +3 -146
- package/scripts/node/release-gate.mjs +3 -74
- package/scripts/node/release.mjs +3 -171
- package/scripts/node/validate-release-docs.mjs +3 -285
- package/scripts/node/version-manager.mjs +3 -708
- package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +4 -2
- package/xray/features.json +2 -2
- package/scripts/node/release.js +0 -17
- package/scripts/node/sync-versions.mjs +0 -18
- package/scripts/node/universal-version-manager.js +0 -1043
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@0xray/foundry",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "0xRay mill: mint their suit params, stamp, gate, publish, CI report, git hooks. Not the worn exo.",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/0xRayAI/xray.git",
|
|
11
|
+
"directory": "scripts/foundry"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/0xRayAI/xray/tree/main/scripts/foundry",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/0xRayAI/xray/issues"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"bin": {
|
|
21
|
+
"0xray-foundry": "cli.js"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"*.mjs",
|
|
25
|
+
"*.cjs",
|
|
26
|
+
"cli.js",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"CHANGELOG.md"
|
|
30
|
+
],
|
|
31
|
+
"exports": {
|
|
32
|
+
"./version-manager": "./version-manager.mjs",
|
|
33
|
+
"./validate-release-docs": "./validate-release-docs.mjs",
|
|
34
|
+
"./reconcile-version": "./reconcile-version.mjs",
|
|
35
|
+
"./release": "./release.mjs",
|
|
36
|
+
"./release-gate": "./release-gate.mjs",
|
|
37
|
+
"./mill-root": "./mill-root.mjs",
|
|
38
|
+
"./mint-suit": "./mint-suit.cjs",
|
|
39
|
+
"./ci-monitor": "./ci-monitor.mjs",
|
|
40
|
+
"./hooks": "./hooks.mjs",
|
|
41
|
+
"./docs-build": "./docs-build.mjs"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18"
|
|
45
|
+
},
|
|
46
|
+
"keywords": [
|
|
47
|
+
"0xray",
|
|
48
|
+
"foundry",
|
|
49
|
+
"mill",
|
|
50
|
+
"release",
|
|
51
|
+
"changelog"
|
|
52
|
+
]
|
|
53
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* reconcile-version.mjs — Single version truth before bump/tag/publish.
|
|
4
|
+
*
|
|
5
|
+
* Sources: npm registry, package.json, latest git tag.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* npx @0xray/foundry reconcile
|
|
9
|
+
* npx @0xray/foundry reconcile --check
|
|
10
|
+
* npx @0xray/foundry reconcile patch
|
|
11
|
+
* npx @0xray/foundry reconcile patch --apply
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { execSync } from "child_process";
|
|
15
|
+
import fs from "fs";
|
|
16
|
+
import path from "path";
|
|
17
|
+
import { fileURLToPath } from "url";
|
|
18
|
+
import { readRootPackage, resolveMillRoot } from "./mill-root.mjs";
|
|
19
|
+
|
|
20
|
+
const rootDir = resolveMillRoot();
|
|
21
|
+
const pkgPath = path.join(rootDir, "package.json");
|
|
22
|
+
|
|
23
|
+
const args = process.argv.slice(2);
|
|
24
|
+
const checkMode = args.includes("--check");
|
|
25
|
+
const applyMode = args.includes("--apply");
|
|
26
|
+
const printTarget = args.includes("--print-target");
|
|
27
|
+
const bumpType = args.find((a) => ["major", "minor", "patch"].includes(a)) || null;
|
|
28
|
+
|
|
29
|
+
function run(cmd) {
|
|
30
|
+
try {
|
|
31
|
+
return execSync(cmd, { cwd: rootDir, encoding: "utf-8" }).trim();
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readLocalVersion() {
|
|
38
|
+
return JSON.parse(fs.readFileSync(pkgPath, "utf-8")).version;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readPublishedVersion() {
|
|
42
|
+
const name = readRootPackage(rootDir).name;
|
|
43
|
+
if (!name) return "0.0.0";
|
|
44
|
+
const v = run(`npm view ${name} version`);
|
|
45
|
+
return v || "0.0.0";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readLatestTag() {
|
|
49
|
+
const tag = run('git describe --tags --abbrev=0 2>/dev/null');
|
|
50
|
+
if (!tag) return null;
|
|
51
|
+
return tag.replace(/^v/, "");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseVersion(version) {
|
|
55
|
+
const [major, minor, patch] = version.split(".").map((n) => parseInt(n, 10));
|
|
56
|
+
return { major, minor, patch };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function bumpVersion(version, type) {
|
|
60
|
+
const v = parseVersion(version);
|
|
61
|
+
switch (type) {
|
|
62
|
+
case "major":
|
|
63
|
+
return `${v.major + 1}.0.0`;
|
|
64
|
+
case "minor":
|
|
65
|
+
return `${v.major}.${v.minor + 1}.0`;
|
|
66
|
+
case "patch":
|
|
67
|
+
return `${v.major}.${v.minor}.${v.patch + 1}`;
|
|
68
|
+
default:
|
|
69
|
+
return version;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function compare(a, b) {
|
|
74
|
+
const va = parseVersion(a);
|
|
75
|
+
const vb = parseVersion(b);
|
|
76
|
+
if (va.major !== vb.major) return va.major - vb.major;
|
|
77
|
+
if (va.minor !== vb.minor) return va.minor - vb.minor;
|
|
78
|
+
return va.patch - vb.patch;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function maxVersion(...versions) {
|
|
82
|
+
return versions.reduce((best, cur) => (compare(cur, best) > 0 ? cur : best));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function fail(msg) {
|
|
86
|
+
console.error(`❌ ${msg}`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function main() {
|
|
91
|
+
const local = readLocalVersion();
|
|
92
|
+
const npm = readPublishedVersion();
|
|
93
|
+
const tag = readLatestTag();
|
|
94
|
+
|
|
95
|
+
const baseline = maxVersion(npm, tag || "0.0.0");
|
|
96
|
+
const target = bumpType ? bumpVersion(baseline, bumpType) : local;
|
|
97
|
+
|
|
98
|
+
if (printTarget && bumpType) {
|
|
99
|
+
console.log(target);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log("\n=== Version Reconcile ===\n");
|
|
104
|
+
console.log(` npm registry : ${npm}`);
|
|
105
|
+
console.log(` package.json : ${local}`);
|
|
106
|
+
console.log(` latest tag : ${tag ? `v${tag}` : "(none)"}`);
|
|
107
|
+
if (bumpType) {
|
|
108
|
+
console.log(` bump ${bumpType} : ${target} (from baseline ${baseline})`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (applyMode && bumpType) {
|
|
112
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
113
|
+
pkg.version = target;
|
|
114
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
115
|
+
console.log(`\n✅ Updated package.json → ${target}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!checkMode) {
|
|
119
|
+
if (compare(local, npm) <= 0 && !bumpType) {
|
|
120
|
+
console.log("\n⚠️ Local version is not ahead of npm — bump before publishing.");
|
|
121
|
+
} else {
|
|
122
|
+
console.log("\n✅ Reconcile report complete.");
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Strict checks for release gate / pre-tag
|
|
128
|
+
if (compare(local, npm) <= 0) {
|
|
129
|
+
fail(`package.json (${local}) must be > npm (${npm}). Run: npx @0xray/foundry reconcile patch --apply`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (tag && compare(local, tag) < 0) {
|
|
133
|
+
fail(`package.json (${local}) is behind latest tag v${tag}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (tag && compare(local, tag) > 0 && compare(npm, tag) === 0) {
|
|
137
|
+
console.log(`✅ Ready to tag v${local} (npm at ${npm})`);
|
|
138
|
+
} else if (compare(local, npm) > 0) {
|
|
139
|
+
console.log(`✅ Ready to publish ${local} (npm at ${npm})`);
|
|
140
|
+
} else {
|
|
141
|
+
console.log("✅ Version reconcile OK");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const isMain =
|
|
146
|
+
process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
147
|
+
|
|
148
|
+
if (isMain) {
|
|
149
|
+
main();
|
|
150
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* release-gate.mjs — Single release checkpoint.
|
|
4
|
+
*
|
|
5
|
+
* Modes:
|
|
6
|
+
* (default) build + test + consumer smoke (fresh + upgrade merge)
|
|
7
|
+
* --verify-only git + reconcile + release docs + consumer smoke — after push, before publish
|
|
8
|
+
* --skip-smoke skip consumer install smoke
|
|
9
|
+
* --skip-docs skip release docs validation (not recommended for publish)
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* npx @0xray/foundry gate
|
|
13
|
+
* npx @0xray/foundry gate --verify-only
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { execSync } from "child_process";
|
|
17
|
+
import path from "path";
|
|
18
|
+
import { fileURLToPath } from "url";
|
|
19
|
+
import { isXrayExoRepo, millScript, readRootPackage, resolveMillRoot } from "./mill-root.mjs";
|
|
20
|
+
|
|
21
|
+
const rootDir = resolveMillRoot();
|
|
22
|
+
const verifyOnly = process.argv.includes("--verify-only");
|
|
23
|
+
const skipSmoke = process.argv.includes("--skip-smoke");
|
|
24
|
+
const skipDocs = process.argv.includes("--skip-docs");
|
|
25
|
+
|
|
26
|
+
function step(label, cmd) {
|
|
27
|
+
console.log(`\n${"─".repeat(60)}\n🔄 ${label}\n${"─".repeat(60)}\n`);
|
|
28
|
+
execSync(cmd, { cwd: rootDir, stdio: "inherit", env: { ...process.env, FOUNDRY_ROOT: rootDir } });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function docsCheckCmd() {
|
|
32
|
+
return `${JSON.stringify(process.execPath)} ${JSON.stringify(millScript("validate-release-docs.mjs"))}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function runLightGate() {
|
|
36
|
+
const pkg = readRootPackage(rootDir);
|
|
37
|
+
if (verifyOnly) {
|
|
38
|
+
if (!skipDocs) step("1/1 Release docs (light)", docsCheckCmd());
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (pkg.scripts.build) step("1/3 Build", "npm run build");
|
|
42
|
+
if (pkg.scripts.test) step("2/3 Tests", "npm test");
|
|
43
|
+
if (!skipDocs) step("3/3 Release docs (light)", docsCheckCmd());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function main() {
|
|
47
|
+
const mode = verifyOnly ? "verify" : "full";
|
|
48
|
+
console.log("\n╔════════════════════════════════════════════════════════╗");
|
|
49
|
+
console.log(`║ 🛡️ 0xRay Release Gate (${mode})`.padEnd(57) + "║");
|
|
50
|
+
console.log("╚════════════════════════════════════════════════════════╝");
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
if (!isXrayExoRepo(rootDir)) {
|
|
54
|
+
runLightGate();
|
|
55
|
+
} else if (verifyOnly) {
|
|
56
|
+
step("1/5 Git + reconcile", "node scripts/node/pre-publish-guard.js --verify-only");
|
|
57
|
+
if (!skipDocs) {
|
|
58
|
+
step("2/5 Release docs", docsCheckCmd());
|
|
59
|
+
}
|
|
60
|
+
step(
|
|
61
|
+
"3/5 Plugin infrastructure",
|
|
62
|
+
"node scripts/test/validate-plugins-e2e.cjs --structural-only",
|
|
63
|
+
);
|
|
64
|
+
if (!skipSmoke) {
|
|
65
|
+
step("4/5 Consumer install smoke", "node scripts/node/consumer-install-smoke.mjs");
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
step("1/7 Build", "npm run build");
|
|
69
|
+
step("2/7 Tests", "npm test");
|
|
70
|
+
if (!skipDocs) {
|
|
71
|
+
step("3/7 Release docs", docsCheckCmd());
|
|
72
|
+
}
|
|
73
|
+
step(
|
|
74
|
+
"4/7 Consumer hook verifiers",
|
|
75
|
+
"npm run verify:pre-commit-diff && npm run verify:pre-push-diff && node scripts/mjs/verify-delegation-gate-core.mjs --host=grok && node scripts/mjs/verify-delegation-gate-core.mjs --host=hermes && node scripts/mjs/verify-delegation-gate-core.mjs --host=opencode && node scripts/mjs/verify-hermes-session-start.mjs && node scripts/mjs/verify-confer-core.mjs && npm run verify:user-aside && npm run build && node scripts/mjs/verify-pipeline-facets.mjs --package-only",
|
|
76
|
+
);
|
|
77
|
+
step(
|
|
78
|
+
"5/7 Plugin infrastructure",
|
|
79
|
+
"node scripts/test/validate-plugins-e2e.cjs --structural-only",
|
|
80
|
+
);
|
|
81
|
+
if (!skipSmoke) {
|
|
82
|
+
step("6/7 Consumer install smoke (fresh + upgrade)", "node scripts/node/consumer-install-smoke.mjs");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log("\n╔════════════════════════════════════════════════════════╗");
|
|
87
|
+
console.log("║ ✅ RELEASE GATE PASSED ║");
|
|
88
|
+
console.log("╚════════════════════════════════════════════════════════╝\n");
|
|
89
|
+
} catch {
|
|
90
|
+
console.error("\n❌ RELEASE GATE FAILED — do not tag or publish\n");
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const isMain =
|
|
96
|
+
process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
97
|
+
|
|
98
|
+
if (isMain) {
|
|
99
|
+
main();
|
|
100
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Canonical 0xRay release script.
|
|
5
|
+
*
|
|
6
|
+
* 1. Bump version from npm registry baseline (reconcile-version)
|
|
7
|
+
* 2. Release artifacts (CHANGELOG / README / AGENTS / docs) — before gate so docs tests pass
|
|
8
|
+
* 3. Release gate (build + test + consumer smoke)
|
|
9
|
+
* 4. Commit release artifacts → push
|
|
10
|
+
* 5. Verify gate (reconcile + git + release docs + smoke)
|
|
11
|
+
* 6. npm publish (idempotent — skip if version already on registry)
|
|
12
|
+
* 7. Tag → push tag (only after successful publish)
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* npx @0xray/foundry release [patch|minor|major] --dry-run
|
|
16
|
+
* npx @0xray/foundry release [patch|minor|major] --i-mean-it
|
|
17
|
+
* npx @0xray/foundry release --publish-only --dry-run
|
|
18
|
+
* npx @0xray/foundry release --publish-only --i-mean-it
|
|
19
|
+
*
|
|
20
|
+
* Live bump/commit/push/publish requires --i-mean-it or FOUNDRY_RELEASE=1.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { execSync } from "child_process";
|
|
24
|
+
import fs from "fs";
|
|
25
|
+
import path from "path";
|
|
26
|
+
import { fileURLToPath } from "url";
|
|
27
|
+
import { getReleaseArtifactPaths } from "./version-manager.mjs";
|
|
28
|
+
import {
|
|
29
|
+
isXrayExoRepo,
|
|
30
|
+
millScript,
|
|
31
|
+
readRootPackage,
|
|
32
|
+
resolveMillRoot,
|
|
33
|
+
} from "./mill-root.mjs";
|
|
34
|
+
|
|
35
|
+
const rootDir = resolveMillRoot();
|
|
36
|
+
const args = process.argv.slice(2);
|
|
37
|
+
const dryRun = args.includes("--dry-run");
|
|
38
|
+
const publishOnly = args.includes("--publish-only");
|
|
39
|
+
const releaseType = args.find((a) => ["major", "minor", "patch"].includes(a));
|
|
40
|
+
const optedIn = args.includes("--i-mean-it") || process.env.FOUNDRY_RELEASE === "1";
|
|
41
|
+
|
|
42
|
+
function writeReleaseTrap() {
|
|
43
|
+
process.stderr.write(
|
|
44
|
+
"npx @0xray/foundry release bumps, commits, pushes, and publishes the milled cwd.\n" +
|
|
45
|
+
"Pass --i-mean-it or set FOUNDRY_RELEASE=1. Preview with --dry-run.\n",
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function run(cmd, label) {
|
|
50
|
+
console.log(`\n> ${cmd}`);
|
|
51
|
+
if (dryRun) {
|
|
52
|
+
console.log(` (dry-run skip: ${label})`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
execSync(cmd, {
|
|
56
|
+
cwd: rootDir,
|
|
57
|
+
stdio: "inherit",
|
|
58
|
+
encoding: "utf-8",
|
|
59
|
+
env: { ...process.env, FOUNDRY_ROOT: rootDir },
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function runMill(script, extraArgs, label) {
|
|
64
|
+
const quoted = [millScript(script), ...extraArgs].map((a) => JSON.stringify(a)).join(" ");
|
|
65
|
+
run(`${JSON.stringify(process.execPath)} ${quoted}`, label);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readVersion() {
|
|
69
|
+
return JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf-8")).version;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function packageName() {
|
|
73
|
+
const name = readRootPackage(rootDir).name;
|
|
74
|
+
if (!name) {
|
|
75
|
+
process.stderr.write("package.json name is required to mill publish\n");
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
return name;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function npmPublishCmd() {
|
|
82
|
+
const pkg = readRootPackage(rootDir);
|
|
83
|
+
if (isXrayExoRepo(rootDir) || pkg.name === "@0xray/foundry") {
|
|
84
|
+
return "npm publish --access public";
|
|
85
|
+
}
|
|
86
|
+
return "npm publish";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function currentBranch() {
|
|
90
|
+
return execSync("git rev-parse --abbrev-ref HEAD", { cwd: rootDir, encoding: "utf-8" }).trim();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function npmVersionPublished(version) {
|
|
94
|
+
try {
|
|
95
|
+
const out = execSync(`npm view ${packageName()}@${version} version`, {
|
|
96
|
+
cwd: rootDir,
|
|
97
|
+
encoding: "utf-8",
|
|
98
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
99
|
+
}).trim();
|
|
100
|
+
return out === version;
|
|
101
|
+
} catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function publishIdempotent(version) {
|
|
107
|
+
if (dryRun) {
|
|
108
|
+
console.log(` would: ${npmPublishCmd()} (check ${packageName()}@${version} first)`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (npmVersionPublished(version)) {
|
|
112
|
+
console.log(`ℹ️ ${packageName()}@${version} already on npm — skipping publish`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
execSync(npmPublishCmd(), { cwd: rootDir, stdio: "inherit", encoding: "utf-8" });
|
|
116
|
+
console.log(`✅ Published ${packageName()}@${version}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function main() {
|
|
120
|
+
const live = (publishOnly || Boolean(releaseType)) && !dryRun;
|
|
121
|
+
if (live && !optedIn) {
|
|
122
|
+
writeReleaseTrap();
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (publishOnly) {
|
|
127
|
+
packageName();
|
|
128
|
+
const version = readVersion();
|
|
129
|
+
runMill("release-gate.mjs", [], "release gate");
|
|
130
|
+
if (isXrayExoRepo(rootDir)) {
|
|
131
|
+
run("npm run prepare-consumer && npm run build", "safe-publish");
|
|
132
|
+
} else if (readRootPackage(rootDir).scripts.build) {
|
|
133
|
+
run("npm run build", "build");
|
|
134
|
+
}
|
|
135
|
+
publishIdempotent(version);
|
|
136
|
+
if (!dryRun) {
|
|
137
|
+
try {
|
|
138
|
+
execSync(`git tag -a v${version} -m "Release v${version}"`, {
|
|
139
|
+
cwd: rootDir,
|
|
140
|
+
stdio: "inherit",
|
|
141
|
+
});
|
|
142
|
+
} catch (e) {
|
|
143
|
+
const msg = String(e.stderr || e.message || "");
|
|
144
|
+
if (!msg.includes("already exists")) throw e;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
run(`git push origin v${version}`, "push tag");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!releaseType || !["major", "minor", "patch"].includes(releaseType)) {
|
|
152
|
+
process.stderr.write(
|
|
153
|
+
"Usage: npx @0xray/foundry release [patch|minor|major] [--dry-run|--i-mean-it]\n",
|
|
154
|
+
);
|
|
155
|
+
process.stderr.write(
|
|
156
|
+
" npx @0xray/foundry release --publish-only [--dry-run|--i-mean-it]\n",
|
|
157
|
+
);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
console.log("╔════════════════════════════════════════════════════════╗");
|
|
162
|
+
console.log("║ 🚀 0xRay Release (canonical) ║");
|
|
163
|
+
console.log("╚════════════════════════════════════════════════════════╝");
|
|
164
|
+
if (dryRun) console.log("\n⚠️ DRY RUN — no writes, commits, tags, or publish\n");
|
|
165
|
+
|
|
166
|
+
// 1. Version bump from npm SSOT
|
|
167
|
+
console.log(`\n📦 Step 1: Bump ${releaseType} from npm baseline`);
|
|
168
|
+
if (dryRun) {
|
|
169
|
+
runMill("reconcile-version.mjs", [releaseType], "version bump (dry)");
|
|
170
|
+
} else {
|
|
171
|
+
runMill("reconcile-version.mjs", [releaseType, "--apply"], "version bump");
|
|
172
|
+
}
|
|
173
|
+
const newVersion = dryRun
|
|
174
|
+
? execSync(
|
|
175
|
+
`${JSON.stringify(process.execPath)} ${JSON.stringify(millScript("reconcile-version.mjs"))} ${JSON.stringify(releaseType)} --print-target`,
|
|
176
|
+
{
|
|
177
|
+
cwd: rootDir,
|
|
178
|
+
encoding: "utf-8",
|
|
179
|
+
env: { ...process.env, FOUNDRY_ROOT: rootDir },
|
|
180
|
+
},
|
|
181
|
+
).trim()
|
|
182
|
+
: readVersion();
|
|
183
|
+
console.log(`📌 Release version: ${newVersion}`);
|
|
184
|
+
|
|
185
|
+
// 2. Artifacts BEFORE gate (docs vitest matches package.json version)
|
|
186
|
+
console.log("\n📦 Step 2: Release artifacts (CHANGELOG, README, AGENTS, docs)");
|
|
187
|
+
runMill("version-manager.mjs", ["--artifacts-only"], "release artifacts");
|
|
188
|
+
|
|
189
|
+
// 3. Gate
|
|
190
|
+
console.log("\n📦 Step 3: Release gate (build + test + smoke)");
|
|
191
|
+
runMill("release-gate.mjs", [], "release gate");
|
|
192
|
+
|
|
193
|
+
// 4. Commit + push
|
|
194
|
+
console.log("\n📦 Step 4: Commit & push");
|
|
195
|
+
const releaseFiles = getReleaseArtifactPaths();
|
|
196
|
+
if (!dryRun) {
|
|
197
|
+
run(`git add ${releaseFiles.join(" ")}`, "git add");
|
|
198
|
+
try {
|
|
199
|
+
execSync(`git commit --no-verify -m "release: v${newVersion}"`, { cwd: rootDir, stdio: "inherit" });
|
|
200
|
+
} catch (e) {
|
|
201
|
+
const out = String(e.stdout || e.stderr || "");
|
|
202
|
+
if (!out.includes("nothing to commit")) throw e;
|
|
203
|
+
console.log("ℹ️ Nothing to commit (release files unchanged)");
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
console.log(` would: git add ${releaseFiles.join(" ")} && git commit -m "release: v${newVersion}"`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const branch = currentBranch();
|
|
210
|
+
run(`git push origin ${branch}`, "git push");
|
|
211
|
+
|
|
212
|
+
// 5. Post-push verify before publish
|
|
213
|
+
console.log("\n📦 Step 5: Verify gate (reconcile + git + docs + smoke)");
|
|
214
|
+
runMill("release-gate.mjs", ["--verify-only"], "verify gate");
|
|
215
|
+
|
|
216
|
+
// 6. Publish (idempotent)
|
|
217
|
+
console.log("\n📦 Step 6: npm publish");
|
|
218
|
+
publishIdempotent(newVersion); // name from package.json via packageName()
|
|
219
|
+
|
|
220
|
+
// 7. Tag after publish
|
|
221
|
+
console.log("\n📦 Step 7: Tag");
|
|
222
|
+
if (!dryRun) {
|
|
223
|
+
try {
|
|
224
|
+
execSync(`git tag -a v${newVersion} -m "Release v${newVersion}"`, { cwd: rootDir, stdio: "inherit" });
|
|
225
|
+
console.log(`✅ Created tag v${newVersion}`);
|
|
226
|
+
} catch (e) {
|
|
227
|
+
const msg = String(e.stderr || e.message || "");
|
|
228
|
+
if (msg.includes("already exists")) {
|
|
229
|
+
console.log(`ℹ️ Tag v${newVersion} already exists`);
|
|
230
|
+
} else {
|
|
231
|
+
throw e;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
console.log(` would: git tag -a v${newVersion}`);
|
|
236
|
+
}
|
|
237
|
+
run(`git push origin v${newVersion}`, "push tag");
|
|
238
|
+
|
|
239
|
+
console.log("\n╔════════════════════════════════════════════════════════╗");
|
|
240
|
+
console.log("║ ✅ Release Complete! ║");
|
|
241
|
+
console.log("╚════════════════════════════════════════════════════════╝");
|
|
242
|
+
if (!dryRun) {
|
|
243
|
+
process.stdout.write(`\n📦 ${packageName()}@${newVersion} 🏷 v${newVersion}\n`);
|
|
244
|
+
if (isXrayExoRepo(rootDir)) {
|
|
245
|
+
const tweetPath = path.join(rootDir, "tweets", `v${newVersion}.md`);
|
|
246
|
+
if (!fs.existsSync(tweetPath)) {
|
|
247
|
+
fs.mkdirSync(path.join(rootDir, "tweets"), { recursive: true });
|
|
248
|
+
fs.writeFileSync(
|
|
249
|
+
tweetPath,
|
|
250
|
+
`🎉 0xRay v${newVersion} is LIVE - consumer-safe upgrades!\n...\n\`\`\`\nnpm install 0xray@latest\n\`\`\`\n`,
|
|
251
|
+
);
|
|
252
|
+
process.stdout.write(`📝 Tweet template: tweets/v${newVersion}.md\n`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const isMain =
|
|
259
|
+
process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
260
|
+
|
|
261
|
+
if (isMain) {
|
|
262
|
+
main().catch((err) => {
|
|
263
|
+
process.stderr.write(`\n❌ Release failed: ${err.message}\n`);
|
|
264
|
+
process.exit(1);
|
|
265
|
+
});
|
|
266
|
+
}
|