@kungfu-tech/buildchain 0.0.0-bootstrap.0 → 2.0.13-alpha.11
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/README.md +262 -0
- package/bin/buildchain.mjs +222 -0
- package/docs/cli.md +124 -0
- package/docs/lifecycle-protocol.md +422 -0
- package/docs/publish-transaction.md +285 -0
- package/docs/reusable-build-surface.md +350 -0
- package/docs/web-surface-deployments.md +211 -0
- package/package.json +52 -1
- package/packages/core/README.md +15 -0
- package/packages/core/buildchain-config.js +721 -0
- package/packages/core/index.js +40 -0
- package/packages/core/package-manager.js +291 -0
- package/packages/core/publish-transaction.js +418 -0
- package/packages/core/release-line-dry-run.js +296 -0
- package/scripts/aggregate-build-summary.mjs +88 -0
- package/scripts/build-contract-core.mjs +731 -0
- package/scripts/check-inventory.mjs +325 -0
- package/scripts/init-repo.mjs +316 -0
- package/scripts/npm-publish-dry-run.mjs +176 -0
- package/scripts/npm-publish-transaction.mjs +268 -0
- package/scripts/publish-source-ref-resolver.mjs +113 -0
- package/scripts/release-line-dry-run.mjs +53 -0
- package/scripts/release-line-policy.mjs +141 -0
- package/scripts/release-transaction.mjs +212 -0
- package/scripts/resolve-build-contract.mjs +63 -0
- package/scripts/resolve-publish-gate.mjs +33 -0
- package/scripts/resolve-publish-source.mjs +99 -0
- package/scripts/run-lifecycle-core.mjs +162 -0
- package/scripts/run-lifecycle.mjs +40 -0
- package/scripts/strip-trailing-whitespace.mjs +14 -0
- package/scripts/tsup-action.config.mjs +19 -0
- package/scripts/verify-publish-source-lock.mjs +37 -0
- package/scripts/verify-release-pr.mjs +34 -0
- package/scripts/web-surface-core.mjs +382 -0
- package/scripts/web-surface.mjs +112 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
|
|
7
|
+
const EXACT_TAG_PATTERN = /^v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
8
|
+
|
|
9
|
+
function readPackageJson(cwd) {
|
|
10
|
+
const filePath = path.join(cwd, "package.json");
|
|
11
|
+
if (!fs.existsSync(filePath)) {
|
|
12
|
+
throw new Error(`package.json not found: ${filePath}`);
|
|
13
|
+
}
|
|
14
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readArg(argv, name, fallback = "") {
|
|
18
|
+
const index = argv.indexOf(`--${name}`);
|
|
19
|
+
if (index === -1) {
|
|
20
|
+
return fallback;
|
|
21
|
+
}
|
|
22
|
+
return argv[index + 1] || "";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function hasFlag(argv, name) {
|
|
26
|
+
return argv.includes(`--${name}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function runCommand({ cwd, cmd, args }) {
|
|
30
|
+
const result = spawnSync(cmd, args, {
|
|
31
|
+
cwd,
|
|
32
|
+
env: process.env,
|
|
33
|
+
encoding: "utf8",
|
|
34
|
+
});
|
|
35
|
+
if (result.error) {
|
|
36
|
+
throw result.error;
|
|
37
|
+
}
|
|
38
|
+
if (result.status !== 0) {
|
|
39
|
+
throw new Error(`${cmd} ${args.join(" ")} failed\n${result.stdout || ""}${result.stderr || ""}`.trim());
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
command: [cmd, ...args],
|
|
43
|
+
stdout: result.stdout,
|
|
44
|
+
stderr: result.stderr,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parsePackResult(stdout) {
|
|
49
|
+
const parsed = JSON.parse(stdout);
|
|
50
|
+
const pack = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
51
|
+
const files = Array.isArray(pack?.files) ? pack.files : [];
|
|
52
|
+
const bin = files.find((file) => file.path === "bin/buildchain.mjs");
|
|
53
|
+
return {
|
|
54
|
+
filename: pack?.filename || "",
|
|
55
|
+
name: pack?.name || "",
|
|
56
|
+
version: pack?.version || "",
|
|
57
|
+
size: pack?.size || 0,
|
|
58
|
+
unpackedSize: pack?.unpackedSize || 0,
|
|
59
|
+
entryCount: pack?.entryCount || files.length,
|
|
60
|
+
bundled: pack?.bundled || [],
|
|
61
|
+
binMode: bin?.mode,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function writeGitHubOutputs(outputs) {
|
|
66
|
+
const outputPath = process.env.GITHUB_OUTPUT;
|
|
67
|
+
if (!outputPath) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
fs.appendFileSync(outputPath, Object.entries(outputs)
|
|
71
|
+
.map(([key, value]) => `${key}=${String(value).replace(/\r?\n/g, " ")}`)
|
|
72
|
+
.join("\n") + "\n");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function npmPublishDryRun({
|
|
76
|
+
cwd = process.cwd(),
|
|
77
|
+
expectedTag = "",
|
|
78
|
+
registry = "https://registry.npmjs.org/",
|
|
79
|
+
distTag = "",
|
|
80
|
+
skipNpmPublishDryRun = false,
|
|
81
|
+
} = {}) {
|
|
82
|
+
const resolvedCwd = path.resolve(cwd);
|
|
83
|
+
const pkg = readPackageJson(resolvedCwd);
|
|
84
|
+
if (pkg.private === true) {
|
|
85
|
+
throw new Error("package.json private must be false before npm publish");
|
|
86
|
+
}
|
|
87
|
+
if (!pkg.name || typeof pkg.name !== "string") {
|
|
88
|
+
throw new Error("package.json name must be a non-empty string");
|
|
89
|
+
}
|
|
90
|
+
if (!pkg.version || typeof pkg.version !== "string") {
|
|
91
|
+
throw new Error("package.json version must be a non-empty string");
|
|
92
|
+
}
|
|
93
|
+
const exactTag = `v${pkg.version}`;
|
|
94
|
+
if (!EXACT_TAG_PATTERN.test(exactTag)) {
|
|
95
|
+
throw new Error(`unsupported release tag for npm publish: ${exactTag}`);
|
|
96
|
+
}
|
|
97
|
+
if (expectedTag && expectedTag !== exactTag) {
|
|
98
|
+
throw new Error(`npm publish tag must match package.json version: tag=${expectedTag} expected=${exactTag}`);
|
|
99
|
+
}
|
|
100
|
+
const resolvedDistTag = distTag || (pkg.version.includes("-") ? "alpha" : "latest");
|
|
101
|
+
const packCommand = runCommand({
|
|
102
|
+
cwd: resolvedCwd,
|
|
103
|
+
cmd: "npm",
|
|
104
|
+
args: ["pack", "--dry-run", "--json", `--registry=${registry}`],
|
|
105
|
+
});
|
|
106
|
+
const pack = parsePackResult(packCommand.stdout);
|
|
107
|
+
let publishCommand;
|
|
108
|
+
if (!skipNpmPublishDryRun) {
|
|
109
|
+
publishCommand = runCommand({
|
|
110
|
+
cwd: resolvedCwd,
|
|
111
|
+
cmd: "npm",
|
|
112
|
+
args: ["publish", "--dry-run", "--access", "public", "--tag", resolvedDistTag, `--registry=${registry}`],
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const result = {
|
|
116
|
+
schemaVersion: 1,
|
|
117
|
+
dryRun: true,
|
|
118
|
+
wouldPublish: !skipNpmPublishDryRun,
|
|
119
|
+
package: {
|
|
120
|
+
name: pkg.name,
|
|
121
|
+
version: pkg.version,
|
|
122
|
+
private: pkg.private === true,
|
|
123
|
+
},
|
|
124
|
+
exactTag,
|
|
125
|
+
distTag: resolvedDistTag,
|
|
126
|
+
registry,
|
|
127
|
+
pack,
|
|
128
|
+
commands: {
|
|
129
|
+
pack: packCommand.command,
|
|
130
|
+
publishDryRun: publishCommand?.command || [],
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
writeGitHubOutputs({
|
|
134
|
+
version: pkg.version,
|
|
135
|
+
"exact-tag": exactTag,
|
|
136
|
+
"dist-tag": resolvedDistTag,
|
|
137
|
+
registry,
|
|
138
|
+
"pack-entry-count": pack.entryCount,
|
|
139
|
+
"publish-dry-run": String(!skipNpmPublishDryRun),
|
|
140
|
+
});
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function usage() {
|
|
145
|
+
return `Usage:
|
|
146
|
+
node scripts/npm-publish-dry-run.mjs [--cwd <dir>] [--expected-tag <tag>]
|
|
147
|
+
[--registry <url>] [--dist-tag <tag>]
|
|
148
|
+
[--skip-npm-publish-dry-run] [--json]
|
|
149
|
+
`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
153
|
+
try {
|
|
154
|
+
const argv = process.argv.slice(2);
|
|
155
|
+
if (hasFlag(argv, "help") || hasFlag(argv, "h")) {
|
|
156
|
+
process.stdout.write(usage());
|
|
157
|
+
process.exit(0);
|
|
158
|
+
}
|
|
159
|
+
const result = npmPublishDryRun({
|
|
160
|
+
cwd: readArg(argv, "cwd", process.cwd()),
|
|
161
|
+
expectedTag: readArg(argv, "expected-tag", ""),
|
|
162
|
+
registry: readArg(argv, "registry", "https://registry.npmjs.org/"),
|
|
163
|
+
distTag: readArg(argv, "dist-tag", ""),
|
|
164
|
+
skipNpmPublishDryRun: hasFlag(argv, "skip-npm-publish-dry-run"),
|
|
165
|
+
});
|
|
166
|
+
if (hasFlag(argv, "json")) {
|
|
167
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
168
|
+
} else {
|
|
169
|
+
process.stdout.write(`npm publish dry-run ok: ${result.package.name}@${result.package.version} -> ${result.distTag}\n`);
|
|
170
|
+
process.stdout.write(`pack entries: ${result.pack.entryCount}\n`);
|
|
171
|
+
}
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error(`npm-publish-dry-run: ${error.message}`);
|
|
174
|
+
process.exitCode = 1;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
|
|
7
|
+
const EXACT_TAG_PATTERN = /^v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
8
|
+
|
|
9
|
+
function readArg(argv, name, fallback = "") {
|
|
10
|
+
const index = argv.indexOf(`--${name}`);
|
|
11
|
+
if (index === -1) {
|
|
12
|
+
return fallback;
|
|
13
|
+
}
|
|
14
|
+
return argv[index + 1] || "";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function hasFlag(argv, name) {
|
|
18
|
+
return argv.includes(`--${name}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readEnv(name, fallback = "") {
|
|
22
|
+
return process.env[name] || fallback;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function runNpm({ cwd, args, allowFailure = false }) {
|
|
26
|
+
const result = spawnSync("npm", args, {
|
|
27
|
+
cwd,
|
|
28
|
+
env: process.env,
|
|
29
|
+
encoding: "utf8",
|
|
30
|
+
});
|
|
31
|
+
if (result.error) {
|
|
32
|
+
throw result.error;
|
|
33
|
+
}
|
|
34
|
+
if (!allowFailure && result.status !== 0) {
|
|
35
|
+
throw new Error(`npm ${args.join(" ")} failed\n${result.stdout || ""}${result.stderr || ""}`.trim());
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readPackageJson(cwd) {
|
|
41
|
+
const filePath = path.join(cwd, "package.json");
|
|
42
|
+
if (!fs.existsSync(filePath)) {
|
|
43
|
+
throw new Error(`package.json not found: ${filePath}`);
|
|
44
|
+
}
|
|
45
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parsePackResult(stdout) {
|
|
49
|
+
const parsed = JSON.parse(stdout);
|
|
50
|
+
const pack = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
51
|
+
if (!pack?.name || !pack?.version) {
|
|
52
|
+
throw new Error("npm pack did not return package name and version");
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
name: pack.name,
|
|
56
|
+
version: pack.version,
|
|
57
|
+
filename: pack.filename || "",
|
|
58
|
+
integrity: pack.integrity || "",
|
|
59
|
+
shasum: pack.shasum || "",
|
|
60
|
+
entryCount: pack.entryCount || pack.files?.length || 0,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function artifactDigest(pack) {
|
|
65
|
+
if (pack.integrity) {
|
|
66
|
+
return pack.integrity;
|
|
67
|
+
}
|
|
68
|
+
if (pack.shasum) {
|
|
69
|
+
return `sha1:${pack.shasum}`;
|
|
70
|
+
}
|
|
71
|
+
throw new Error("npm pack did not return integrity or shasum");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parseNpmView(stdout) {
|
|
75
|
+
const raw = String(stdout || "").trim();
|
|
76
|
+
if (!raw) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
const parsed = JSON.parse(raw);
|
|
80
|
+
const dist = parsed?.dist || parsed;
|
|
81
|
+
return {
|
|
82
|
+
integrity: dist?.integrity || "",
|
|
83
|
+
shasum: dist?.shasum || "",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function publishedDigest({ cwd, name, version, registry }) {
|
|
88
|
+
const result = runNpm({
|
|
89
|
+
cwd,
|
|
90
|
+
args: ["view", `${name}@${version}`, "dist.integrity", "dist.shasum", "--json", `--registry=${registry}`],
|
|
91
|
+
allowFailure: true,
|
|
92
|
+
});
|
|
93
|
+
if (result.status !== 0) {
|
|
94
|
+
const output = `${result.stdout || ""}\n${result.stderr || ""}`;
|
|
95
|
+
if (/\bE404\b|404 Not Found|is not in this registry/i.test(output)) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
throw new Error(`npm view ${name}@${version} failed\n${output}`.trim());
|
|
99
|
+
}
|
|
100
|
+
const view = parseNpmView(result.stdout);
|
|
101
|
+
if (!view) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
return view.integrity || (view.shasum ? `sha1:${view.shasum}` : "");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function assertPackageVersion({ pkg, expectedVersion }) {
|
|
108
|
+
if (pkg.private === true) {
|
|
109
|
+
throw new Error("package.json private must be false before npm publish");
|
|
110
|
+
}
|
|
111
|
+
if (!pkg.name || typeof pkg.name !== "string") {
|
|
112
|
+
throw new Error("package.json name must be a non-empty string");
|
|
113
|
+
}
|
|
114
|
+
if (!pkg.version || typeof pkg.version !== "string") {
|
|
115
|
+
throw new Error("package.json version must be a non-empty string");
|
|
116
|
+
}
|
|
117
|
+
if (expectedVersion && pkg.version !== expectedVersion) {
|
|
118
|
+
throw new Error(`package.json version must match Buildchain version: package=${pkg.version} buildchain=${expectedVersion}`);
|
|
119
|
+
}
|
|
120
|
+
const exactTag = `v${pkg.version}`;
|
|
121
|
+
if (!EXACT_TAG_PATTERN.test(exactTag)) {
|
|
122
|
+
throw new Error(`unsupported release tag for npm publish: ${exactTag}`);
|
|
123
|
+
}
|
|
124
|
+
return exactTag;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function writeEvidence({ cwd, evidencePath, evidence }) {
|
|
128
|
+
const resolved = path.resolve(cwd, evidencePath);
|
|
129
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
130
|
+
fs.writeFileSync(resolved, `${JSON.stringify(evidence, null, 2)}\n`);
|
|
131
|
+
return resolved;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function writeGitHubOutputs(outputs) {
|
|
135
|
+
const outputPath = process.env.GITHUB_OUTPUT;
|
|
136
|
+
if (!outputPath) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
fs.appendFileSync(outputPath, Object.entries(outputs)
|
|
140
|
+
.map(([key, value]) => `${key}=${String(value).replace(/\r?\n/g, " ")}`)
|
|
141
|
+
.join("\n") + "\n");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function npmPublishTransaction({
|
|
145
|
+
cwd = process.cwd(),
|
|
146
|
+
registry = "https://registry.npmjs.org/",
|
|
147
|
+
dryRunPublish = false,
|
|
148
|
+
access = "public",
|
|
149
|
+
skipRegistryLookup = false,
|
|
150
|
+
} = {}) {
|
|
151
|
+
const resolvedCwd = path.resolve(cwd);
|
|
152
|
+
const pkg = readPackageJson(resolvedCwd);
|
|
153
|
+
const expectedVersion = readEnv("BUILDCHAIN_VERSION");
|
|
154
|
+
const exactTag = assertPackageVersion({ pkg, expectedVersion });
|
|
155
|
+
const distTag = pkg.version.includes("-") ? "alpha" : "latest";
|
|
156
|
+
|
|
157
|
+
const pack = parsePackResult(runNpm({
|
|
158
|
+
cwd: resolvedCwd,
|
|
159
|
+
args: ["pack", "--dry-run", "--json", `--registry=${registry}`],
|
|
160
|
+
}).stdout);
|
|
161
|
+
const digest = artifactDigest(pack);
|
|
162
|
+
|
|
163
|
+
const existingDigest = skipRegistryLookup
|
|
164
|
+
? undefined
|
|
165
|
+
: publishedDigest({
|
|
166
|
+
cwd: resolvedCwd,
|
|
167
|
+
name: pkg.name,
|
|
168
|
+
version: pkg.version,
|
|
169
|
+
registry,
|
|
170
|
+
});
|
|
171
|
+
let publishAction = "already-published";
|
|
172
|
+
if (existingDigest) {
|
|
173
|
+
if (existingDigest !== digest) {
|
|
174
|
+
throw new Error(`artifact digest mismatch: npm:${pkg.name}@${pkg.version}`);
|
|
175
|
+
}
|
|
176
|
+
} else if (dryRunPublish) {
|
|
177
|
+
publishAction = "dry-run";
|
|
178
|
+
} else {
|
|
179
|
+
runNpm({
|
|
180
|
+
cwd: resolvedCwd,
|
|
181
|
+
args: ["publish", "--access", access, "--tag", distTag, `--registry=${registry}`],
|
|
182
|
+
});
|
|
183
|
+
publishAction = "published";
|
|
184
|
+
const registryDigest = skipRegistryLookup
|
|
185
|
+
? undefined
|
|
186
|
+
: publishedDigest({
|
|
187
|
+
cwd: resolvedCwd,
|
|
188
|
+
name: pkg.name,
|
|
189
|
+
version: pkg.version,
|
|
190
|
+
registry,
|
|
191
|
+
});
|
|
192
|
+
if (registryDigest && registryDigest !== digest) {
|
|
193
|
+
throw new Error(`artifact digest mismatch: npm:${pkg.name}@${pkg.version}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const evidencePath = readEnv("BUILDCHAIN_PUBLISH_EVIDENCE");
|
|
198
|
+
if (!evidencePath) {
|
|
199
|
+
throw new Error("BUILDCHAIN_PUBLISH_EVIDENCE is required");
|
|
200
|
+
}
|
|
201
|
+
const evidence = {
|
|
202
|
+
schema: 1,
|
|
203
|
+
version: expectedVersion || pkg.version,
|
|
204
|
+
channel: readEnv("BUILDCHAIN_CHANNEL"),
|
|
205
|
+
source_sha: readEnv("BUILDCHAIN_SOURCE_SHA"),
|
|
206
|
+
release_sha: readEnv("BUILDCHAIN_RELEASE_SHA"),
|
|
207
|
+
target_ref: readEnv("BUILDCHAIN_TARGET_REF"),
|
|
208
|
+
release_material_sha: readEnv("BUILDCHAIN_RELEASE_MATERIAL_SHA", readEnv("BUILDCHAIN_RELEASE_SHA")),
|
|
209
|
+
publish_tooling_sha: readEnv("BUILDCHAIN_PUBLISH_TOOLING_SHA", readEnv("BUILDCHAIN_RELEASE_SHA")),
|
|
210
|
+
artifacts: [{
|
|
211
|
+
group: "node",
|
|
212
|
+
kind: "npm",
|
|
213
|
+
name: pkg.name,
|
|
214
|
+
ref: pkg.version,
|
|
215
|
+
digest,
|
|
216
|
+
}],
|
|
217
|
+
};
|
|
218
|
+
const resolvedEvidencePath = writeEvidence({ cwd: resolvedCwd, evidencePath, evidence });
|
|
219
|
+
writeGitHubOutputs({
|
|
220
|
+
version: pkg.version,
|
|
221
|
+
"exact-tag": exactTag,
|
|
222
|
+
"dist-tag": distTag,
|
|
223
|
+
"artifact-digest": digest,
|
|
224
|
+
"publish-action": publishAction,
|
|
225
|
+
"publish-evidence": resolvedEvidencePath,
|
|
226
|
+
});
|
|
227
|
+
return {
|
|
228
|
+
schemaVersion: 1,
|
|
229
|
+
package: {
|
|
230
|
+
name: pkg.name,
|
|
231
|
+
version: pkg.version,
|
|
232
|
+
},
|
|
233
|
+
exactTag,
|
|
234
|
+
distTag,
|
|
235
|
+
registry,
|
|
236
|
+
publishAction,
|
|
237
|
+
pack,
|
|
238
|
+
evidencePath: resolvedEvidencePath,
|
|
239
|
+
evidence,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function usage() {
|
|
244
|
+
return `Usage:
|
|
245
|
+
node scripts/npm-publish-transaction.mjs [--cwd <dir>] [--registry <url>]
|
|
246
|
+
[--dry-run-publish] [--skip-registry-lookup]
|
|
247
|
+
`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
251
|
+
try {
|
|
252
|
+
const argv = process.argv.slice(2);
|
|
253
|
+
if (hasFlag(argv, "help") || argv.includes("-h")) {
|
|
254
|
+
process.stdout.write(usage());
|
|
255
|
+
process.exit(0);
|
|
256
|
+
}
|
|
257
|
+
const result = npmPublishTransaction({
|
|
258
|
+
cwd: readArg(argv, "cwd", process.cwd()),
|
|
259
|
+
registry: readArg(argv, "registry", "https://registry.npmjs.org/"),
|
|
260
|
+
dryRunPublish: hasFlag(argv, "dry-run-publish"),
|
|
261
|
+
skipRegistryLookup: hasFlag(argv, "skip-registry-lookup"),
|
|
262
|
+
});
|
|
263
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
console.error(`npm-publish-transaction: ${error.message}`);
|
|
266
|
+
process.exitCode = 1;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const DEFAULT_GITHUB_API_URL = "https://api.github.com";
|
|
2
|
+
|
|
3
|
+
function readEnv(env, name, fallback = "") {
|
|
4
|
+
return env[name] || fallback;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function assertGitSha(sha, label = "sourceSha") {
|
|
8
|
+
const value = String(sha || "").trim();
|
|
9
|
+
if (!/^[0-9a-f]{40}$/i.test(value)) {
|
|
10
|
+
throw new Error(`${label} must be a 40-character Git SHA`);
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function normalizeSourceRef(value, fallback = "") {
|
|
16
|
+
return String(value || fallback || "")
|
|
17
|
+
.trim()
|
|
18
|
+
.replace(/^refs\/heads\//, "")
|
|
19
|
+
.replace(/^refs\/tags\//, "");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function splitRepository(repository) {
|
|
23
|
+
const value = String(repository || "").trim();
|
|
24
|
+
const match = value.match(/^([^/\s]+)\/([^/\s]+)$/);
|
|
25
|
+
if (!match) {
|
|
26
|
+
throw new Error(`BUILDCHAIN_SOURCE_REPOSITORY must be owner/repo, got: ${value || "<empty>"}`);
|
|
27
|
+
}
|
|
28
|
+
return { owner: match[1], repo: match[2] };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function encodeRefPath(ref) {
|
|
32
|
+
return String(ref)
|
|
33
|
+
.split("/")
|
|
34
|
+
.map((segment) => encodeURIComponent(segment))
|
|
35
|
+
.join("/");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function sourceRefFromEnv(env = process.env) {
|
|
39
|
+
const configured = normalizeSourceRef(readEnv(env, "BUILDCHAIN_PUBLISH_SOURCE_REF"));
|
|
40
|
+
if (configured) {
|
|
41
|
+
return configured;
|
|
42
|
+
}
|
|
43
|
+
const refName = normalizeSourceRef(readEnv(env, "GITHUB_REF_NAME"));
|
|
44
|
+
if (refName.startsWith("publish-gate/") || refName === "major-gate") {
|
|
45
|
+
return refName;
|
|
46
|
+
}
|
|
47
|
+
return "";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function currentGitHubRefSha(sourceRef, env = process.env) {
|
|
51
|
+
const ref = normalizeSourceRef(sourceRef);
|
|
52
|
+
const githubRefName = normalizeSourceRef(readEnv(env, "GITHUB_REF_NAME"));
|
|
53
|
+
const githubRef = String(readEnv(env, "GITHUB_REF")).trim();
|
|
54
|
+
if (ref && ref === githubRefName && githubRef === `refs/heads/${ref}`) {
|
|
55
|
+
return assertGitSha(readEnv(env, "GITHUB_SHA"), "GITHUB_SHA");
|
|
56
|
+
}
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function resolveRemoteGitHubRefSha({
|
|
61
|
+
repository,
|
|
62
|
+
sourceRef,
|
|
63
|
+
env = process.env,
|
|
64
|
+
fetchImpl = globalThis.fetch,
|
|
65
|
+
} = {}) {
|
|
66
|
+
const ref = normalizeSourceRef(sourceRef);
|
|
67
|
+
if (!ref) {
|
|
68
|
+
throw new Error("publish source ref is required");
|
|
69
|
+
}
|
|
70
|
+
if (typeof fetchImpl !== "function") {
|
|
71
|
+
throw new Error("fetch is required to resolve publish source ref through GitHub REST API");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const { owner, repo } = splitRepository(repository);
|
|
75
|
+
const apiBase = String(readEnv(env, "GITHUB_API_URL", DEFAULT_GITHUB_API_URL)).replace(/\/+$/, "");
|
|
76
|
+
const url = `${apiBase}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/git/ref/heads/${encodeRefPath(ref)}`;
|
|
77
|
+
const headers = {
|
|
78
|
+
Accept: "application/vnd.github+json",
|
|
79
|
+
"User-Agent": "buildchain-publish-source-resolver",
|
|
80
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
81
|
+
};
|
|
82
|
+
const token = readEnv(env, "GITHUB_TOKEN");
|
|
83
|
+
if (token) {
|
|
84
|
+
headers.Authorization = `Bearer ${token}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const response = await fetchImpl(url, { headers });
|
|
88
|
+
if (!response?.ok) {
|
|
89
|
+
let detail = "";
|
|
90
|
+
try {
|
|
91
|
+
const body = await response.json();
|
|
92
|
+
detail = body?.message ? `: ${body.message}` : "";
|
|
93
|
+
} catch {
|
|
94
|
+
detail = "";
|
|
95
|
+
}
|
|
96
|
+
throw new Error(`publish source ref not found: ${ref} (GitHub API ${response?.status || "unknown"}${detail})`);
|
|
97
|
+
}
|
|
98
|
+
const body = await response.json();
|
|
99
|
+
return assertGitSha(body?.object?.sha, `GitHub ref ${ref} object.sha`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function resolvePublishSourceRefSha({
|
|
103
|
+
repository,
|
|
104
|
+
sourceRef,
|
|
105
|
+
env = process.env,
|
|
106
|
+
fetchImpl = globalThis.fetch,
|
|
107
|
+
} = {}) {
|
|
108
|
+
const currentSha = currentGitHubRefSha(sourceRef, env);
|
|
109
|
+
if (currentSha) {
|
|
110
|
+
return currentSha;
|
|
111
|
+
}
|
|
112
|
+
return await resolveRemoteGitHubRefSha({ repository, sourceRef, env, fetchImpl });
|
|
113
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
explainReleaseLineDryRun,
|
|
4
|
+
formatReleaseLineDryRun,
|
|
5
|
+
} from "../packages/core/release-line-dry-run.js";
|
|
6
|
+
|
|
7
|
+
function readFlag(args, name, fallback = "") {
|
|
8
|
+
const index = args.indexOf(`--${name}`);
|
|
9
|
+
if (index === -1) {
|
|
10
|
+
return fallback;
|
|
11
|
+
}
|
|
12
|
+
return args[index + 1] || "";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function hasFlag(args, name) {
|
|
16
|
+
return args.includes(`--${name}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function usage() {
|
|
20
|
+
return `Usage:
|
|
21
|
+
node scripts/release-line-dry-run.mjs --target-ref <ref> [--cwd <dir>]
|
|
22
|
+
[--sha <sha>] [--source-ref <ref>]
|
|
23
|
+
[--tags <comma-list>] [--json]
|
|
24
|
+
`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function main(argv = process.argv.slice(2)) {
|
|
28
|
+
if (hasFlag(argv, "help") || argv.includes("-h")) {
|
|
29
|
+
process.stdout.write(usage());
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const plan = explainReleaseLineDryRun({
|
|
33
|
+
cwd: readFlag(argv, "cwd", process.cwd()),
|
|
34
|
+
targetRef: readFlag(argv, "target-ref", ""),
|
|
35
|
+
sourceRef: readFlag(argv, "source-ref", ""),
|
|
36
|
+
sha: readFlag(argv, "sha", ""),
|
|
37
|
+
tags: readFlag(argv, "tags", ""),
|
|
38
|
+
publishTransaction: hasFlag(argv, "publish-transaction"),
|
|
39
|
+
publishCommand: readFlag(argv, "publish-command", ""),
|
|
40
|
+
});
|
|
41
|
+
if (hasFlag(argv, "json")) {
|
|
42
|
+
process.stdout.write(`${JSON.stringify(plan, null, 2)}\n`);
|
|
43
|
+
} else {
|
|
44
|
+
process.stdout.write(formatReleaseLineDryRun(plan));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
main();
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error(`release-line-dry-run: ${error.message}`);
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
}
|