@kungfu-tech/buildchain 3.0.2 → 3.0.3-alpha.1
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/AGENTS.md +1 -1
- package/CONTRIBUTING.md +1 -1
- package/README.md +27 -1
- package/actions/promote-buildchain-ref/README.md +90 -19
- package/actions/report-buildchain-issue/README.md +20 -2
- package/actions/run-lifecycle/README.md +5 -0
- package/actions/validate-config/README.md +19 -1
- package/bin/buildchain.mjs +56 -2
- package/bin/internal/command-registry.mjs +82 -0
- package/bin/internal/trust-release-cli.mjs +1 -0
- package/contracts/auditable-demo-media-profiles-v1.json +116 -1
- package/contracts/buildchain-v2-residuals-v1.json +146 -0
- package/contracts/evidence/auditable-demo-responsive-web-delivery-v1.json +143 -0
- package/contracts/evidence/auditable-demo-web-delivery-v1.json +7 -3
- package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/complete-transcript.txt +2 -0
- package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/public-projection.json +16 -0
- package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/scene.json +12 -0
- package/dist/site/buildchain-contract.json +188 -46
- package/dist/site/buildchain-site.json +181 -90
- package/dist/site/capability-registry.json +5 -5
- package/dist/site/cli-registry.json +860 -98
- package/dist/site/controller-registry.json +118 -5
- package/dist/site/kfd-claims.json +233 -19
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +20 -20
- package/dist/site/node-api-registry.json +374 -59
- package/dist/site/page-registry.json +156 -65
- package/dist/site/public-surface-audit.json +231 -22
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +3 -0
- package/dist/site/site-manifest.json +24 -24
- package/dist/site/workflow-registry.json +575 -79
- package/docs/MAP.md +22 -4
- package/docs/auditable-demo.md +54 -13
- package/docs/aws-us-elastic-runner-burst-plane.md +381 -0
- package/docs/cli.md +119 -12
- package/docs/consumer-issue-reporting.md +20 -2
- package/docs/dev-alpha-candidate-patrol.md +24 -10
- package/docs/github-governance-authority.md +24 -31
- package/docs/lifecycle-protocol.md +20 -2
- package/docs/migration-inventory.md +24 -5
- package/docs/observed-evidence-patrol.md +28 -12
- package/docs/ownership.md +19 -1
- package/docs/publication-artifacts.md +158 -18
- package/docs/publication-authority.md +4 -4
- package/docs/publish-transaction.md +78 -31
- package/docs/release-candidate.md +24 -0
- package/docs/release-flow.md +50 -32
- package/docs/release-governance.md +39 -32
- package/docs/release-passport.md +4 -0
- package/docs/release-propagation.md +57 -12
- package/docs/reusable-build-surface.md +218 -90
- package/docs/runtime-train-validation.md +20 -2
- package/docs/shifu-gate-profiles.md +7 -1
- package/docs/stable-candidate-patrol.md +6 -6
- package/docs/toolkit-observability.md +22 -0
- package/docs/versioning.md +46 -24
- package/docs/web-surface-deployments.md +82 -1
- package/package.json +7 -5
- package/packages/core/README.md +60 -4
- package/packages/core/artifact-signing.js +1 -0
- package/packages/core/buildchain-contract.js +9 -4
- package/packages/core/cache-evidence.js +1 -0
- package/packages/core/controller-evidence.js +2 -0
- package/packages/core/diagnostics.js +105 -30
- package/packages/core/github-governance-authority.js +25 -17
- package/packages/core/index.js +40 -0
- package/packages/core/paper.js +3226 -0
- package/packages/core/public-surface-audit.js +3 -0
- package/packages/core/publication-artifact.js +1 -6
- package/packages/core/publication-authority.js +1 -0
- package/packages/core/publication-control-plane-audit.js +28 -0
- package/packages/core/publication-reproducibility.js +947 -0
- package/packages/core/publication-sealed-bundle.js +190 -0
- package/packages/core/publish-transaction.js +147 -17
- package/packages/core/release-candidate.js +234 -0
- package/packages/core/release-passport.js +519 -228
- package/packages/core/release-propagation.js +175 -1
- package/packages/core/stable-release-gate.js +4 -1
- package/scripts/artifact-signing-delegation.mjs +268 -0
- package/scripts/audit-github-governance.mjs +32 -13
- package/scripts/audit-publication-control-plane.mjs +17 -8
- package/scripts/auditable-demo.mjs +266 -7
- package/scripts/aws-codebuild-toolchain.mjs +285 -0
- package/scripts/aws-macos-jit-core.mjs +378 -0
- package/scripts/aws-macos-jit.mjs +123 -0
- package/scripts/aws-runner-burst-core.mjs +248 -0
- package/scripts/aws-runner-burst.mjs +79 -0
- package/scripts/aws-windows-jit-core.mjs +374 -0
- package/scripts/aws-windows-jit.mjs +121 -0
- package/scripts/build-contract-core.mjs +69 -6
- package/scripts/build-standalone-binary.mjs +23 -0
- package/scripts/buildchain-contract-lock.mjs +1 -1
- package/scripts/buildchain-patrol.mjs +1 -1
- package/scripts/check-internal-architecture.mjs +69 -2
- package/scripts/check-inventory.mjs +70 -2
- package/scripts/check-javascript-syntax.mjs +31 -0
- package/scripts/check-maintainability.mjs +371 -0
- package/scripts/compiler-cache-evidence.mjs +221 -0
- package/scripts/dev-alpha-candidate-patrol.mjs +288 -43
- package/scripts/dispatch-artifact-signing-authority.mjs +7 -1
- package/scripts/finalize-native-artifact-signing-result.mjs +185 -31
- package/scripts/gate-profile-core.mjs +6 -1
- package/scripts/generate-channel-promotion-workflow.mjs +3 -12
- package/scripts/generate-release-candidate-passport.mjs +4 -0
- package/scripts/generate-site-bundle.mjs +84 -18
- package/scripts/import-artifact-signing-results.mjs +64 -5
- package/scripts/inspect-artifact-signing-requests.mjs +52 -14
- package/scripts/maintainability-metrics.mjs +339 -0
- package/scripts/npm-publish-transaction.mjs +57 -6
- package/scripts/observed-evidence.mjs +151 -33
- package/scripts/paper.mjs +645 -0
- package/scripts/promotion-channel-router.mjs +16 -4
- package/scripts/publication-reproducibility.mjs +62 -0
- package/scripts/reconcile-github-governance.mjs +8 -1
- package/scripts/release-propagation.mjs +39 -0
- package/scripts/resolve-artifact-signing-upload-route.mjs +55 -0
- package/scripts/resolve-build-contract.mjs +6 -0
- package/scripts/route-offline-runners.mjs +236 -0
- package/scripts/run-candidate-body-prefix-renderer.mjs +187 -0
- package/scripts/run-lifecycle-core.mjs +62 -1
- package/scripts/runtime-ref-core.mjs +36 -4
- package/scripts/seal-artifact-signing-requests.mjs +64 -12
- package/scripts/stable-candidate-qualification.mjs +50 -0
- package/scripts/verify-artifact-signing-results.mjs +26 -1
- package/scripts/web-surface-core.mjs +272 -86
- package/scripts/web-surface-production-decision.mjs +19 -3
|
@@ -0,0 +1,3226 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import {
|
|
7
|
+
loadBuildchainConfig,
|
|
8
|
+
validateBuildchainConfig,
|
|
9
|
+
} from "./buildchain-config.js";
|
|
10
|
+
import {
|
|
11
|
+
createBuildchainContractLock,
|
|
12
|
+
createBuildchainContractWorld,
|
|
13
|
+
evaluateBuildchainContractLock,
|
|
14
|
+
readBuildchainContractLock,
|
|
15
|
+
} from "./buildchain-contract.js";
|
|
16
|
+
import {
|
|
17
|
+
releaseTransactionPublicationState,
|
|
18
|
+
readReleaseTransaction,
|
|
19
|
+
} from "./publish-transaction.js";
|
|
20
|
+
import { PUBLICATION_REPRODUCIBILITY_RECEIPT_CONTRACT } from "./publication-reproducibility.js";
|
|
21
|
+
import {
|
|
22
|
+
PUBLICATION_SEALED_BUNDLE_CONTRACT,
|
|
23
|
+
verifyPublicationSealedBundle,
|
|
24
|
+
} from "./publication-sealed-bundle.js";
|
|
25
|
+
|
|
26
|
+
export const PAPER_SCAFFOLD_CONTRACT = "kungfu-buildchain-paper-scaffold";
|
|
27
|
+
export const PAPER_MIGRATION_CONTRACT = "kungfu-buildchain-paper-migration";
|
|
28
|
+
export const PAPER_PREFLIGHT_CONTRACT = "kungfu-buildchain-paper-preflight";
|
|
29
|
+
export const PAPER_STATUS_CONTRACT = "kungfu-buildchain-paper-status";
|
|
30
|
+
export const PAPER_NPM_BOOTSTRAP_CONTRACT =
|
|
31
|
+
"kungfu-buildchain-paper-npm-bootstrap";
|
|
32
|
+
export const PAPER_PROVISIONING_CONTRACT =
|
|
33
|
+
"kungfu-buildchain-paper-provisioning-authority";
|
|
34
|
+
export const PAPER_BUILD_PLAN_CONTRACT = "kungfu-buildchain-paper-build-plan";
|
|
35
|
+
export const PAPER_ALPHA_PLAN_CONTRACT = "kungfu-buildchain-paper-alpha-plan";
|
|
36
|
+
export const PAPER_RESUME_PLAN_CONTRACT = "kungfu-buildchain-paper-resume-plan";
|
|
37
|
+
export const PAPER_VISIBILITY_CONTRACT = "kungfu-buildchain-paper-visibility";
|
|
38
|
+
|
|
39
|
+
export const PAPER_STATE_ORDER = Object.freeze([
|
|
40
|
+
"scaffolded",
|
|
41
|
+
"governed",
|
|
42
|
+
"admitted",
|
|
43
|
+
"bootstrapped",
|
|
44
|
+
"trust-bound",
|
|
45
|
+
"content-ready",
|
|
46
|
+
"artifact-sealed",
|
|
47
|
+
"package-published",
|
|
48
|
+
"alpha-complete",
|
|
49
|
+
"staging-visible",
|
|
50
|
+
"production-visible",
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
export const PAPER_PATHS = Object.freeze({
|
|
54
|
+
config: ".buildchain/buildchain.toml",
|
|
55
|
+
versionPin: ".buildchain-version",
|
|
56
|
+
contractLock: ".buildchain/contract-lock.json",
|
|
57
|
+
buildWorkflow: ".github/workflows/build.yml",
|
|
58
|
+
releaseWorkflow: ".github/workflows/paper-release.yml",
|
|
59
|
+
reproducibilityReceipt:
|
|
60
|
+
".buildchain/publication/reproducibility-receipt.json",
|
|
61
|
+
sealedBundle: ".buildchain/admitted/sealed-bundle.json",
|
|
62
|
+
admission: ".buildchain/admitted/publication-admission.json",
|
|
63
|
+
capability: ".buildchain/admitted/publication-capability.json",
|
|
64
|
+
npmBootstrap: ".buildchain/paper/npm-bootstrap.json",
|
|
65
|
+
npmTrust: ".buildchain/paper/npm-trust.json",
|
|
66
|
+
provisioningAuthority: ".buildchain/paper/provisioning-authority.json",
|
|
67
|
+
visibility: ".buildchain/paper/visibility.json",
|
|
68
|
+
});
|
|
69
|
+
const PAPER_SCAFFOLD_PATHS = Object.freeze([
|
|
70
|
+
PAPER_PATHS.config,
|
|
71
|
+
PAPER_PATHS.contractLock,
|
|
72
|
+
PAPER_PATHS.versionPin,
|
|
73
|
+
PAPER_PATHS.buildWorkflow,
|
|
74
|
+
PAPER_PATHS.releaseWorkflow,
|
|
75
|
+
PAPER_PATHS.provisioningAuthority,
|
|
76
|
+
"Makefile",
|
|
77
|
+
"package.json",
|
|
78
|
+
"README.md",
|
|
79
|
+
"docs/MAP.md",
|
|
80
|
+
"paper/main.tex",
|
|
81
|
+
"paper/references.bib",
|
|
82
|
+
"LICENSE",
|
|
83
|
+
".gitignore",
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
const NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
87
|
+
const DEFAULT_BOOTSTRAP_VERSION = "0.0.0-bootstrap.0";
|
|
88
|
+
const DEFAULT_TOOLCHAIN_IMAGE =
|
|
89
|
+
"ghcr.io/kungfu-systems/build-images/latex-pdf-builder";
|
|
90
|
+
const DEFAULT_TOOLCHAIN_DIGEST =
|
|
91
|
+
"sha256:c20f3809e96836c1c78e97c76939d12f1de3fed0ea9b7c40c43332ec2ea480f8";
|
|
92
|
+
const DEFAULT_TOOLCHAIN_COMMAND = "latexmk -pdf -outdir=_build paper/main.tex";
|
|
93
|
+
const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/i;
|
|
94
|
+
const GIT_SHA_PATTERN = /^[0-9a-f]{40}$/i;
|
|
95
|
+
const PACKAGE_PATTERN = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
|
|
96
|
+
|
|
97
|
+
function toPosix(value) {
|
|
98
|
+
return String(value || "")
|
|
99
|
+
.split(path.sep)
|
|
100
|
+
.join("/");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function stableJson(value) {
|
|
104
|
+
if (Array.isArray(value)) {
|
|
105
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
106
|
+
}
|
|
107
|
+
if (value && typeof value === "object") {
|
|
108
|
+
return `{${Object.keys(value)
|
|
109
|
+
.sort()
|
|
110
|
+
.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
|
|
111
|
+
.join(",")}}`;
|
|
112
|
+
}
|
|
113
|
+
return JSON.stringify(value);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function sha256Text(value) {
|
|
117
|
+
return `sha256:${crypto.createHash("sha256").update(String(value)).digest("hex")}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function sha256File(filePath) {
|
|
121
|
+
return `sha256:${crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex")}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function jsonText(value) {
|
|
125
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function readJson(filePath) {
|
|
129
|
+
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
|
130
|
+
return { exists: false, value: undefined, error: "" };
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
return {
|
|
134
|
+
exists: true,
|
|
135
|
+
value: JSON.parse(fs.readFileSync(filePath, "utf8")),
|
|
136
|
+
error: "",
|
|
137
|
+
};
|
|
138
|
+
} catch (error) {
|
|
139
|
+
return { exists: true, value: undefined, error: error.message };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function existingFileFact(cwd, relativePath) {
|
|
144
|
+
const filePath = path.resolve(cwd, relativePath);
|
|
145
|
+
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
path: toPosix(relativePath),
|
|
150
|
+
bytes: fs.statSync(filePath).size,
|
|
151
|
+
sha256: sha256File(filePath),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function normalizePackageName(value, label = "package name") {
|
|
156
|
+
const normalized = String(value || "").trim();
|
|
157
|
+
if (!PACKAGE_PATTERN.test(normalized) || normalized.length > 214) {
|
|
158
|
+
throw new Error(`${label} is invalid`);
|
|
159
|
+
}
|
|
160
|
+
return normalized;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function normalizeRepository(value) {
|
|
164
|
+
const normalized = String(value || "")
|
|
165
|
+
.trim()
|
|
166
|
+
.replace(/^git\+/, "")
|
|
167
|
+
.replace(/^git@github\.com:/, "")
|
|
168
|
+
.replace(/^ssh:\/\/git@github\.com\//, "")
|
|
169
|
+
.replace(/^https?:\/\/github\.com\//, "")
|
|
170
|
+
.replace(/\.git$/, "")
|
|
171
|
+
.replace(/^\/+|\/+$/g, "");
|
|
172
|
+
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(normalized)) {
|
|
173
|
+
return "";
|
|
174
|
+
}
|
|
175
|
+
return normalized;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function commandResult(
|
|
179
|
+
command,
|
|
180
|
+
args,
|
|
181
|
+
{ cwd, env = process.env, timeout = 15000 } = {},
|
|
182
|
+
) {
|
|
183
|
+
const result = spawnSync(command, args, {
|
|
184
|
+
cwd,
|
|
185
|
+
env,
|
|
186
|
+
encoding: "utf8",
|
|
187
|
+
timeout,
|
|
188
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
189
|
+
});
|
|
190
|
+
return {
|
|
191
|
+
ok: result.status === 0,
|
|
192
|
+
status: result.status ?? 1,
|
|
193
|
+
stdout: String(result.stdout || "").trim(),
|
|
194
|
+
stderr: String(result.stderr || "").trim(),
|
|
195
|
+
error: result.error?.message || "",
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function gitResult(cwd, args) {
|
|
200
|
+
return commandResult("git", args, { cwd });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function gitValue(cwd, args) {
|
|
204
|
+
const result = gitResult(cwd, args);
|
|
205
|
+
return result.ok ? result.stdout : "";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function buildchainPackageIdentity(buildchainRoot, explicitVersion = "") {
|
|
209
|
+
const packageJson = readJson(
|
|
210
|
+
path.resolve(buildchainRoot, "package.json"),
|
|
211
|
+
).value;
|
|
212
|
+
return {
|
|
213
|
+
name: String(packageJson?.name || "@kungfu-tech/buildchain"),
|
|
214
|
+
version: String(explicitVersion || packageJson?.version || ""),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function runtimeGitSha(buildchainRoot, buildchainVersion = "") {
|
|
219
|
+
const value = gitValue(buildchainRoot, ["rev-parse", "HEAD"]);
|
|
220
|
+
if (GIT_SHA_PATTERN.test(value)) return value;
|
|
221
|
+
const identity = buildchainPackageIdentity(buildchainRoot, buildchainVersion);
|
|
222
|
+
if (!identity.version) return "";
|
|
223
|
+
const observed = commandResult(
|
|
224
|
+
"npm",
|
|
225
|
+
[
|
|
226
|
+
"view",
|
|
227
|
+
`${identity.name}@${identity.version}`,
|
|
228
|
+
"gitHead",
|
|
229
|
+
"--json",
|
|
230
|
+
`--registry=${NPM_REGISTRY}`,
|
|
231
|
+
],
|
|
232
|
+
{ cwd: buildchainRoot },
|
|
233
|
+
);
|
|
234
|
+
if (!observed.ok) return "";
|
|
235
|
+
const parsed = safeParseJson(observed.stdout);
|
|
236
|
+
const gitHead =
|
|
237
|
+
typeof parsed === "string" ? parsed : String(parsed?.gitHead || "");
|
|
238
|
+
return GIT_SHA_PATTERN.test(gitHead) ? gitHead : "";
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function runtimeAcceptedAt(buildchainRoot, sha) {
|
|
242
|
+
if (!sha) return "1970-01-01T00:00:00.000Z";
|
|
243
|
+
const value = gitValue(buildchainRoot, ["show", "-s", "--format=%cI", sha]);
|
|
244
|
+
if (!value) return "1970-01-01T00:00:00.000Z";
|
|
245
|
+
const parsed = new Date(value);
|
|
246
|
+
return Number.isNaN(parsed.valueOf())
|
|
247
|
+
? "1970-01-01T00:00:00.000Z"
|
|
248
|
+
: parsed.toISOString();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function runtimeContractWorld(buildchainRoot) {
|
|
252
|
+
const embedded = String(process.env.BUILDCHAIN_EMBEDDED_CONTRACT_WORLD || "");
|
|
253
|
+
if (embedded) {
|
|
254
|
+
const parsed = safeParseJson(embedded);
|
|
255
|
+
if (parsed?.contract) return parsed;
|
|
256
|
+
throw new Error("embedded Buildchain contract world is invalid");
|
|
257
|
+
}
|
|
258
|
+
return createBuildchainContractWorld({ root: buildchainRoot });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function runtimeLicenseText(buildchainRoot) {
|
|
262
|
+
const licensePath = path.join(buildchainRoot, "LICENSE");
|
|
263
|
+
if (fs.existsSync(licensePath) && fs.statSync(licensePath).isFile()) {
|
|
264
|
+
return fs.readFileSync(licensePath, "utf8");
|
|
265
|
+
}
|
|
266
|
+
const embedded = String(process.env.BUILDCHAIN_EMBEDDED_LICENSE_TEXT || "");
|
|
267
|
+
if (embedded) return embedded;
|
|
268
|
+
throw new Error(
|
|
269
|
+
"Buildchain package is missing LICENSE; cannot create a governed paper scaffold",
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function tomlString(value) {
|
|
274
|
+
return JSON.stringify(String(value || ""));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function texEscape(value) {
|
|
278
|
+
return String(value || "")
|
|
279
|
+
.replaceAll("\\", "\\textbackslash{}")
|
|
280
|
+
.replaceAll("&", "\\&")
|
|
281
|
+
.replaceAll("%", "\\%")
|
|
282
|
+
.replaceAll("$", "\\$")
|
|
283
|
+
.replaceAll("#", "\\#")
|
|
284
|
+
.replaceAll("_", "\\_")
|
|
285
|
+
.replaceAll("{", "\\{")
|
|
286
|
+
.replaceAll("}", "\\}");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function joinUrl(base, suffix = "") {
|
|
290
|
+
const normalizedBase = String(base || "")
|
|
291
|
+
.trim()
|
|
292
|
+
.replace(/\/+$/, "");
|
|
293
|
+
const normalizedSuffix = String(suffix || "")
|
|
294
|
+
.trim()
|
|
295
|
+
.replace(/^\/+|\/+$/g, "");
|
|
296
|
+
if (!normalizedBase) return "";
|
|
297
|
+
return normalizedSuffix
|
|
298
|
+
? `${normalizedBase}/${normalizedSuffix}/`
|
|
299
|
+
: `${normalizedBase}/`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function scaffoldConfig({ name, title, packageName, version, siteBaseUrl }) {
|
|
303
|
+
const archiveId = name.replace(/^paper-/, "");
|
|
304
|
+
const canonicalUrl = joinUrl(siteBaseUrl, archiveId);
|
|
305
|
+
const archive = canonicalUrl
|
|
306
|
+
? `
|
|
307
|
+
[publication.archive]
|
|
308
|
+
id = ${tomlString(archiveId)}
|
|
309
|
+
canonical_url = ${tomlString(canonicalUrl)}
|
|
310
|
+
latest_url = ${tomlString(joinUrl(canonicalUrl, "latest"))}
|
|
311
|
+
latest_evidence_url = ${tomlString(`${joinUrl(canonicalUrl, "latest")}buildchain.release.json`)}
|
|
312
|
+
immutable_base_url = ${tomlString(joinUrl(siteBaseUrl, "archive").replace(/\/$/, ""))}
|
|
313
|
+
registry_path = ".buildchain/publication/publication-registry.json"
|
|
314
|
+
`
|
|
315
|
+
: "";
|
|
316
|
+
return `schema = 1
|
|
317
|
+
|
|
318
|
+
[project]
|
|
319
|
+
type = "publication-artifact"
|
|
320
|
+
name = ${tomlString(name)}
|
|
321
|
+
|
|
322
|
+
[publication]
|
|
323
|
+
kind = "paper"
|
|
324
|
+
title = ${tomlString(title)}
|
|
325
|
+
version = ${tomlString(version)}
|
|
326
|
+
primary_artifact = "_build/main.pdf"
|
|
327
|
+
artifact_paths = ["_build/main.pdf"]
|
|
328
|
+
metadata_paths = ["README.md", "docs/MAP.md"]
|
|
329
|
+
source_paths = ["paper", "README.md", "LICENSE", "Makefile"]
|
|
330
|
+
site_consumers = ${siteBaseUrl ? `[${tomlString(siteBaseUrl)}]` : "[]"}
|
|
331
|
+
manifest_path = ".buildchain/publication/publication-artifact.json"
|
|
332
|
+
source_bundle_path = ".buildchain/publication/source.tar.gz"
|
|
333
|
+
${archive}
|
|
334
|
+
[publication.toolchain]
|
|
335
|
+
type = "latex-docker"
|
|
336
|
+
image = "${DEFAULT_TOOLCHAIN_IMAGE}"
|
|
337
|
+
digest = "${DEFAULT_TOOLCHAIN_DIGEST}"
|
|
338
|
+
command = "${DEFAULT_TOOLCHAIN_COMMAND}"
|
|
339
|
+
|
|
340
|
+
[publish]
|
|
341
|
+
kind = "npm-paper-package"
|
|
342
|
+
package = ${tomlString(packageName)}
|
|
343
|
+
auth = "trusted-publishing"
|
|
344
|
+
|
|
345
|
+
[lifecycle.build]
|
|
346
|
+
command = "make pdf"
|
|
347
|
+
|
|
348
|
+
[lifecycle.verify]
|
|
349
|
+
command = "make check"
|
|
350
|
+
`;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function scaffoldBuildWorkflow(
|
|
354
|
+
buildchainSha,
|
|
355
|
+
{ artifactName = "paper-publication" } = {},
|
|
356
|
+
) {
|
|
357
|
+
return `name: Build
|
|
358
|
+
|
|
359
|
+
on:
|
|
360
|
+
workflow_dispatch:
|
|
361
|
+
pull_request:
|
|
362
|
+
push:
|
|
363
|
+
branches:
|
|
364
|
+
- "dev/**"
|
|
365
|
+
- "alpha/**"
|
|
366
|
+
- "release/**"
|
|
367
|
+
|
|
368
|
+
permissions:
|
|
369
|
+
contents: read
|
|
370
|
+
issues: write
|
|
371
|
+
|
|
372
|
+
jobs:
|
|
373
|
+
publication:
|
|
374
|
+
uses: kungfu-systems/buildchain/.github/workflows/publication-artifact.yml@${buildchainSha}
|
|
375
|
+
with:
|
|
376
|
+
buildchain-ref: ${buildchainSha}
|
|
377
|
+
buildchain-contract-lock-path: .buildchain/contract-lock.json
|
|
378
|
+
toolchain-type: config
|
|
379
|
+
verify-command: make check
|
|
380
|
+
artifact-name: ${JSON.stringify(artifactName)}
|
|
381
|
+
`;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function scaffoldReleaseWorkflow(
|
|
385
|
+
buildchainSha,
|
|
386
|
+
{ artifactPaths = "_build/main.pdf", releasePassportProductName = "" } = {},
|
|
387
|
+
) {
|
|
388
|
+
const passportInput = releasePassportProductName
|
|
389
|
+
? ` release-passport-product-name: ${JSON.stringify(releasePassportProductName)}\n`
|
|
390
|
+
: "";
|
|
391
|
+
return `name: Paper Release
|
|
392
|
+
|
|
393
|
+
on:
|
|
394
|
+
workflow_dispatch:
|
|
395
|
+
push:
|
|
396
|
+
branches:
|
|
397
|
+
- "alpha/**"
|
|
398
|
+
- "release/**"
|
|
399
|
+
|
|
400
|
+
permissions:
|
|
401
|
+
contents: read
|
|
402
|
+
|
|
403
|
+
jobs:
|
|
404
|
+
paper-release:
|
|
405
|
+
uses: kungfu-systems/buildchain/.github/workflows/paper-release-sealed.yml@${buildchainSha}
|
|
406
|
+
permissions:
|
|
407
|
+
actions: read
|
|
408
|
+
checks: write
|
|
409
|
+
contents: read
|
|
410
|
+
id-token: write
|
|
411
|
+
issues: write
|
|
412
|
+
pull-requests: write
|
|
413
|
+
with:
|
|
414
|
+
buildchain-ref: ${buildchainSha}
|
|
415
|
+
buildchain-contract-lock-path: .buildchain/contract-lock.json
|
|
416
|
+
publisher-workflow-path: .github/workflows/paper-release.yml
|
|
417
|
+
toolchain-type: config
|
|
418
|
+
verify-command: make check
|
|
419
|
+
artifact-paths: ${JSON.stringify(artifactPaths)}
|
|
420
|
+
${passportInput} secrets:
|
|
421
|
+
KUNGFU_GOVERNANCE_AUDITOR_APP_PRIVATE_KEY: \${{ secrets.KUNGFU_GOVERNANCE_AUDITOR_APP_PRIVATE_KEY }}
|
|
422
|
+
BUILDCHAIN_GENERATED_WRITE_APP_CLIENT_ID: \${{ secrets.BUILDCHAIN_GENERATED_WRITE_APP_CLIENT_ID }}
|
|
423
|
+
BUILDCHAIN_GENERATED_WRITE_APP_PRIVATE_KEY: \${{ secrets.BUILDCHAIN_GENERATED_WRITE_APP_PRIVATE_KEY }}
|
|
424
|
+
BUILDCHAIN_GENERATED_WRITE_TOKEN: \${{ secrets.BUILDCHAIN_GENERATED_WRITE_TOKEN }}
|
|
425
|
+
`;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function paperProvisioningPolicy() {
|
|
429
|
+
return {
|
|
430
|
+
repositoryActions: {
|
|
431
|
+
defaultWorkflowPermissions: "read",
|
|
432
|
+
canApprovePullRequestReviews: false,
|
|
433
|
+
},
|
|
434
|
+
generatedWrites: {
|
|
435
|
+
preferredAuthority: "github-app",
|
|
436
|
+
compatibilityAuthority: "narrow-token",
|
|
437
|
+
githubTokenFallback: false,
|
|
438
|
+
permissions: ["checks:write", "contents:write", "pull-requests:write"],
|
|
439
|
+
tokenPersistence: "runtime-only",
|
|
440
|
+
},
|
|
441
|
+
release: {
|
|
442
|
+
protectedReviewRequired: true,
|
|
443
|
+
versionState: "not-required",
|
|
444
|
+
identityOnlyPullRequests: false,
|
|
445
|
+
manualVersionStateRepairPullRequests: false,
|
|
446
|
+
},
|
|
447
|
+
roles: {
|
|
448
|
+
actor: "repository-development-role",
|
|
449
|
+
pusher: "repository-development-role",
|
|
450
|
+
reviewer: "independent-review-role",
|
|
451
|
+
generatedWriteAuthority: "github-app-or-narrow-token",
|
|
452
|
+
},
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export function createPaperProvisioningAuthority({
|
|
457
|
+
repository,
|
|
458
|
+
packageName,
|
|
459
|
+
buildchainVersion,
|
|
460
|
+
buildchainSha,
|
|
461
|
+
contractLock,
|
|
462
|
+
buildWorkflow,
|
|
463
|
+
releaseWorkflow,
|
|
464
|
+
environment = "",
|
|
465
|
+
}) {
|
|
466
|
+
const policy = paperProvisioningPolicy();
|
|
467
|
+
const payload = {
|
|
468
|
+
schemaVersion: 1,
|
|
469
|
+
contract: PAPER_PROVISIONING_CONTRACT,
|
|
470
|
+
repository,
|
|
471
|
+
package: {
|
|
472
|
+
name: packageName,
|
|
473
|
+
registry: NPM_REGISTRY,
|
|
474
|
+
bootstrapVersion: DEFAULT_BOOTSTRAP_VERSION,
|
|
475
|
+
},
|
|
476
|
+
runtime: {
|
|
477
|
+
repository: "kungfu-systems/buildchain",
|
|
478
|
+
version: buildchainVersion,
|
|
479
|
+
ref: buildchainSha,
|
|
480
|
+
resolvedSha: buildchainSha,
|
|
481
|
+
},
|
|
482
|
+
workflows: {
|
|
483
|
+
build: {
|
|
484
|
+
path: PAPER_PATHS.buildWorkflow,
|
|
485
|
+
sourceDigest: sha256Text(buildWorkflow),
|
|
486
|
+
reusablePath: ".github/workflows/publication-artifact.yml",
|
|
487
|
+
reusableRef: buildchainSha,
|
|
488
|
+
},
|
|
489
|
+
release: {
|
|
490
|
+
path: PAPER_PATHS.releaseWorkflow,
|
|
491
|
+
sourceDigest: sha256Text(releaseWorkflow),
|
|
492
|
+
reusablePath: ".github/workflows/paper-release-sealed.yml",
|
|
493
|
+
reusableRef: buildchainSha,
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
admission: {
|
|
497
|
+
contractLockPath: PAPER_PATHS.contractLock,
|
|
498
|
+
contractLockDigest: sha256Text(contractLock),
|
|
499
|
+
acceptedRef: buildchainSha,
|
|
500
|
+
acceptedSha: buildchainSha,
|
|
501
|
+
},
|
|
502
|
+
trustedPublisher: {
|
|
503
|
+
type: "github",
|
|
504
|
+
repository,
|
|
505
|
+
workflow: path.posix.basename(PAPER_PATHS.releaseWorkflow),
|
|
506
|
+
environment,
|
|
507
|
+
},
|
|
508
|
+
policy: {
|
|
509
|
+
...policy,
|
|
510
|
+
policyDigest: sha256Text(stableJson(policy)),
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
return {
|
|
514
|
+
...payload,
|
|
515
|
+
authorityDigest: sha256Text(stableJson(payload)),
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function scaffoldMakefile() {
|
|
520
|
+
const image = `${DEFAULT_TOOLCHAIN_IMAGE}@${DEFAULT_TOOLCHAIN_DIGEST}`;
|
|
521
|
+
return `.PHONY: check pdf clean
|
|
522
|
+
|
|
523
|
+
BUILDER_IMAGE := ${image}
|
|
524
|
+
SOURCE_DATE_EPOCH ?= 0
|
|
525
|
+
|
|
526
|
+
check:
|
|
527
|
+
\t@test -f paper/main.tex
|
|
528
|
+
\t@test -f paper/references.bib
|
|
529
|
+
\t@git diff --check
|
|
530
|
+
|
|
531
|
+
pdf:
|
|
532
|
+
\t@mkdir -p _build
|
|
533
|
+
\tdocker run --rm --network=none \\
|
|
534
|
+
\t\t-e SOURCE_DATE_EPOCH="$(SOURCE_DATE_EPOCH)" \\
|
|
535
|
+
\t\t-e TZ=UTC -e LANG=C.UTF-8 -e LC_ALL=C.UTF-8 -e HOME=/tmp \\
|
|
536
|
+
\t\t-v "$(CURDIR):/workspace" -w /workspace \\
|
|
537
|
+
\t\t"$(BUILDER_IMAGE)" bash -lc '${DEFAULT_TOOLCHAIN_COMMAND}'
|
|
538
|
+
|
|
539
|
+
clean:
|
|
540
|
+
\trm -rf _build
|
|
541
|
+
`;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function scaffoldPackageJson({ name, packageName, repository }) {
|
|
545
|
+
return jsonText({
|
|
546
|
+
name: packageName,
|
|
547
|
+
private: true,
|
|
548
|
+
description: `${name} publication source repository.`,
|
|
549
|
+
repository: {
|
|
550
|
+
type: "git",
|
|
551
|
+
url: `git+https://github.com/${repository}.git`,
|
|
552
|
+
},
|
|
553
|
+
license: "Apache-2.0",
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function scaffoldReadme({ title, packageName }) {
|
|
558
|
+
return `# ${title}
|
|
559
|
+
|
|
560
|
+
This repository is a Buildchain-governed publication artifact source.
|
|
561
|
+
|
|
562
|
+
## Local workflow
|
|
563
|
+
|
|
564
|
+
\`\`\`sh
|
|
565
|
+
buildchain paper preflight --json
|
|
566
|
+
buildchain paper build
|
|
567
|
+
buildchain paper status --json
|
|
568
|
+
\`\`\`
|
|
569
|
+
|
|
570
|
+
The public package identity is \`${packageName}\`. Buildchain owns reproducible
|
|
571
|
+
artifact generation, sealed publication, npm Trusted Publishing, and recovery;
|
|
572
|
+
this repository owns the paper source and review history.
|
|
573
|
+
|
|
574
|
+
See [docs/MAP.md](docs/MAP.md) for the repository map.
|
|
575
|
+
`;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function scaffoldMap() {
|
|
579
|
+
return `# Repository Map
|
|
580
|
+
|
|
581
|
+
- \`paper/main.tex\`: paper source entrypoint.
|
|
582
|
+
- \`paper/references.bib\`: bibliography source.
|
|
583
|
+
- \`.buildchain/buildchain.toml\`: publication identity, toolchain, package, and lifecycle contract.
|
|
584
|
+
- \`.buildchain/contract-lock.json\`: accepted Buildchain runtime contract.
|
|
585
|
+
- \`.github/workflows/build.yml\`: thin read-only build and reproducibility caller.
|
|
586
|
+
- \`.github/workflows/paper-release.yml\`: thin protected sealed-release caller.
|
|
587
|
+
|
|
588
|
+
The repository does not own npm transaction logic, publication authority,
|
|
589
|
+
release-state recovery, or site deployment mechanics. Those remain Buildchain,
|
|
590
|
+
npm/GitHub, and downstream site responsibilities respectively.
|
|
591
|
+
`;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function scaffoldMainTex(title) {
|
|
595
|
+
return `\\documentclass[11pt]{article}
|
|
596
|
+
\\usepackage[T1]{fontenc}
|
|
597
|
+
\\usepackage{lmodern}
|
|
598
|
+
\\usepackage{hyperref}
|
|
599
|
+
|
|
600
|
+
\\title{${texEscape(title)}}
|
|
601
|
+
\\author{}
|
|
602
|
+
\\date{}
|
|
603
|
+
|
|
604
|
+
\\begin{document}
|
|
605
|
+
\\maketitle
|
|
606
|
+
|
|
607
|
+
\\begin{abstract}
|
|
608
|
+
Replace this paragraph with the paper abstract.
|
|
609
|
+
\\end{abstract}
|
|
610
|
+
|
|
611
|
+
\\section{Introduction}
|
|
612
|
+
Replace this section with the reviewed paper content.
|
|
613
|
+
|
|
614
|
+
\\bibliographystyle{plain}
|
|
615
|
+
\\bibliography{paper/references}
|
|
616
|
+
\\end{document}
|
|
617
|
+
`;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function scaffoldFiles({
|
|
621
|
+
buildchainRoot,
|
|
622
|
+
buildchainVersion,
|
|
623
|
+
buildchainRef,
|
|
624
|
+
buildchainSha,
|
|
625
|
+
cwd,
|
|
626
|
+
name,
|
|
627
|
+
title,
|
|
628
|
+
packageName,
|
|
629
|
+
repository,
|
|
630
|
+
version,
|
|
631
|
+
siteBaseUrl,
|
|
632
|
+
}) {
|
|
633
|
+
const contractWorld = runtimeContractWorld(buildchainRoot);
|
|
634
|
+
const existingLock = readJson(
|
|
635
|
+
path.resolve(cwd, PAPER_PATHS.contractLock),
|
|
636
|
+
).value;
|
|
637
|
+
const acceptedAt =
|
|
638
|
+
existingLock?.buildchain?.acceptedAt ||
|
|
639
|
+
runtimeAcceptedAt(buildchainRoot, buildchainSha);
|
|
640
|
+
const contractLock = createBuildchainContractLock({
|
|
641
|
+
buildchainRef: buildchainSha,
|
|
642
|
+
resolvedSha: buildchainSha,
|
|
643
|
+
contractWorld,
|
|
644
|
+
acceptedAt,
|
|
645
|
+
});
|
|
646
|
+
const contractLockText = jsonText(contractLock);
|
|
647
|
+
const buildWorkflow = scaffoldBuildWorkflow(buildchainSha);
|
|
648
|
+
const releaseWorkflow = scaffoldReleaseWorkflow(buildchainSha);
|
|
649
|
+
const provisioningAuthority = createPaperProvisioningAuthority({
|
|
650
|
+
repository,
|
|
651
|
+
packageName,
|
|
652
|
+
buildchainVersion,
|
|
653
|
+
buildchainSha,
|
|
654
|
+
contractLock: contractLockText,
|
|
655
|
+
buildWorkflow,
|
|
656
|
+
releaseWorkflow,
|
|
657
|
+
});
|
|
658
|
+
const licenseText = runtimeLicenseText(buildchainRoot);
|
|
659
|
+
return new Map([
|
|
660
|
+
[
|
|
661
|
+
PAPER_PATHS.config,
|
|
662
|
+
scaffoldConfig({
|
|
663
|
+
name,
|
|
664
|
+
title,
|
|
665
|
+
packageName,
|
|
666
|
+
version,
|
|
667
|
+
siteBaseUrl,
|
|
668
|
+
}),
|
|
669
|
+
],
|
|
670
|
+
[PAPER_PATHS.contractLock, contractLockText],
|
|
671
|
+
[PAPER_PATHS.versionPin, `${buildchainVersion}\n`],
|
|
672
|
+
[PAPER_PATHS.buildWorkflow, buildWorkflow],
|
|
673
|
+
[PAPER_PATHS.releaseWorkflow, releaseWorkflow],
|
|
674
|
+
[PAPER_PATHS.provisioningAuthority, jsonText(provisioningAuthority)],
|
|
675
|
+
["Makefile", scaffoldMakefile()],
|
|
676
|
+
["package.json", scaffoldPackageJson({ name, packageName, repository })],
|
|
677
|
+
["README.md", scaffoldReadme({ title, packageName })],
|
|
678
|
+
["docs/MAP.md", scaffoldMap()],
|
|
679
|
+
["paper/main.tex", scaffoldMainTex(title)],
|
|
680
|
+
["paper/references.bib", "% Add reviewed bibliography entries here.\n"],
|
|
681
|
+
["LICENSE", licenseText],
|
|
682
|
+
[
|
|
683
|
+
".gitignore",
|
|
684
|
+
"_build/\n.buildchain/publication/\n.buildchain/release-state/\n.buildchain/release-evidence/\n.buildchain/paper/npm-bootstrap.json\n.buildchain/paper/npm-trust.json\n",
|
|
685
|
+
],
|
|
686
|
+
]);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
export function planPaperScaffold({
|
|
690
|
+
cwd = process.cwd(),
|
|
691
|
+
buildchainRoot = process.cwd(),
|
|
692
|
+
buildchainVersion = "",
|
|
693
|
+
buildchainRef = "v3",
|
|
694
|
+
buildchainSha = "",
|
|
695
|
+
name = path.basename(path.resolve(cwd)),
|
|
696
|
+
title = "",
|
|
697
|
+
packageName = "",
|
|
698
|
+
repository = "",
|
|
699
|
+
version = "0.1.0-alpha.0",
|
|
700
|
+
siteBaseUrl = "",
|
|
701
|
+
} = {}) {
|
|
702
|
+
const resolvedCwd = path.resolve(cwd);
|
|
703
|
+
const normalizedName = String(name || "").trim();
|
|
704
|
+
if (!normalizedName) throw new Error("paper scaffold requires --name");
|
|
705
|
+
const normalizedPackage = normalizePackageName(packageName);
|
|
706
|
+
const normalizedRepository = normalizeRepository(repository);
|
|
707
|
+
if (!normalizedRepository) {
|
|
708
|
+
throw new Error("paper scaffold requires --repository <owner/repo>");
|
|
709
|
+
}
|
|
710
|
+
const normalizedVersion = String(version || "")
|
|
711
|
+
.trim()
|
|
712
|
+
.replace(/^v/, "");
|
|
713
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(normalizedVersion)) {
|
|
714
|
+
throw new Error("paper scaffold --version must be semver");
|
|
715
|
+
}
|
|
716
|
+
const runtimeIdentity = buildchainPackageIdentity(
|
|
717
|
+
buildchainRoot,
|
|
718
|
+
buildchainVersion,
|
|
719
|
+
);
|
|
720
|
+
const runtimeSha =
|
|
721
|
+
buildchainSha || runtimeGitSha(buildchainRoot, runtimeIdentity.version);
|
|
722
|
+
if (!GIT_SHA_PATTERN.test(runtimeSha)) {
|
|
723
|
+
throw new Error(
|
|
724
|
+
"paper scaffold cannot resolve the exact Buildchain runtime SHA; pass buildchainSha or use a published Buildchain package with npm gitHead provenance",
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
const files = scaffoldFiles({
|
|
728
|
+
buildchainRoot,
|
|
729
|
+
buildchainVersion: runtimeIdentity.version,
|
|
730
|
+
buildchainRef,
|
|
731
|
+
buildchainSha: runtimeSha,
|
|
732
|
+
cwd: resolvedCwd,
|
|
733
|
+
name: normalizedName,
|
|
734
|
+
title: title || normalizedName,
|
|
735
|
+
packageName: normalizedPackage,
|
|
736
|
+
repository: normalizedRepository,
|
|
737
|
+
version: normalizedVersion,
|
|
738
|
+
siteBaseUrl,
|
|
739
|
+
});
|
|
740
|
+
const changes = [...files.entries()].map(([relativePath, content]) => {
|
|
741
|
+
const filePath = path.resolve(resolvedCwd, relativePath);
|
|
742
|
+
const exists = fs.existsSync(filePath);
|
|
743
|
+
if (!exists) {
|
|
744
|
+
return {
|
|
745
|
+
path: relativePath,
|
|
746
|
+
action: "create",
|
|
747
|
+
sha256: sha256Text(content),
|
|
748
|
+
content,
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
if (!fs.statSync(filePath).isFile()) {
|
|
752
|
+
return {
|
|
753
|
+
path: relativePath,
|
|
754
|
+
action: "conflict",
|
|
755
|
+
reason: "path-exists-and-is-not-a-file",
|
|
756
|
+
sha256: sha256Text(content),
|
|
757
|
+
content,
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
const current = fs.readFileSync(filePath, "utf8");
|
|
761
|
+
return current === content
|
|
762
|
+
? {
|
|
763
|
+
path: relativePath,
|
|
764
|
+
action: "unchanged",
|
|
765
|
+
sha256: sha256Text(content),
|
|
766
|
+
content,
|
|
767
|
+
}
|
|
768
|
+
: {
|
|
769
|
+
path: relativePath,
|
|
770
|
+
action: "conflict",
|
|
771
|
+
reason: "existing-content-differs",
|
|
772
|
+
currentSha256: sha256Text(current),
|
|
773
|
+
sha256: sha256Text(content),
|
|
774
|
+
content,
|
|
775
|
+
};
|
|
776
|
+
});
|
|
777
|
+
const publicChanges = changes.map(({ content: _content, ...entry }) => entry);
|
|
778
|
+
const conflicts = publicChanges.filter(
|
|
779
|
+
(entry) => entry.action === "conflict",
|
|
780
|
+
);
|
|
781
|
+
const result = {
|
|
782
|
+
schemaVersion: 1,
|
|
783
|
+
contract: PAPER_SCAFFOLD_CONTRACT,
|
|
784
|
+
ok: conflicts.length === 0,
|
|
785
|
+
cwd: resolvedCwd,
|
|
786
|
+
dryRun: true,
|
|
787
|
+
identity: {
|
|
788
|
+
project: normalizedName,
|
|
789
|
+
title: title || normalizedName,
|
|
790
|
+
package: normalizedPackage,
|
|
791
|
+
repository: normalizedRepository,
|
|
792
|
+
version: normalizedVersion,
|
|
793
|
+
},
|
|
794
|
+
buildchain: {
|
|
795
|
+
version: runtimeIdentity.version,
|
|
796
|
+
ref: runtimeSha,
|
|
797
|
+
resolvedSha: runtimeSha,
|
|
798
|
+
},
|
|
799
|
+
summary: {
|
|
800
|
+
create: publicChanges.filter((entry) => entry.action === "create").length,
|
|
801
|
+
unchanged: publicChanges.filter((entry) => entry.action === "unchanged")
|
|
802
|
+
.length,
|
|
803
|
+
conflict: conflicts.length,
|
|
804
|
+
},
|
|
805
|
+
changes: publicChanges,
|
|
806
|
+
conflicts,
|
|
807
|
+
nextActions:
|
|
808
|
+
conflicts.length > 0
|
|
809
|
+
? [
|
|
810
|
+
{
|
|
811
|
+
id: "resolve-scaffold-conflicts",
|
|
812
|
+
command: "",
|
|
813
|
+
description:
|
|
814
|
+
"Resolve the listed semantic conflicts; scaffold never overwrites them.",
|
|
815
|
+
},
|
|
816
|
+
]
|
|
817
|
+
: [
|
|
818
|
+
{
|
|
819
|
+
id: "write-scaffold",
|
|
820
|
+
command: "buildchain paper scaffold --write <same arguments>",
|
|
821
|
+
description:
|
|
822
|
+
"Write only missing files after the conflict-free plan is reviewed.",
|
|
823
|
+
},
|
|
824
|
+
],
|
|
825
|
+
};
|
|
826
|
+
Object.defineProperty(result, "_plannedFiles", {
|
|
827
|
+
value: changes,
|
|
828
|
+
enumerable: false,
|
|
829
|
+
});
|
|
830
|
+
return result;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
export function writePaperScaffold(plan) {
|
|
834
|
+
if (!plan || plan.contract !== PAPER_SCAFFOLD_CONTRACT) {
|
|
835
|
+
throw new Error("paper scaffold plan contract mismatch");
|
|
836
|
+
}
|
|
837
|
+
if (!plan.ok || plan.conflicts.length > 0) {
|
|
838
|
+
return {
|
|
839
|
+
...plan,
|
|
840
|
+
dryRun: false,
|
|
841
|
+
written: [],
|
|
842
|
+
ok: false,
|
|
843
|
+
errorCode: "paper-scaffold-conflict",
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
const resolvedCwd = path.resolve(plan.cwd);
|
|
847
|
+
const creates = plan._plannedFiles.filter(
|
|
848
|
+
(entry) => entry.action === "create",
|
|
849
|
+
);
|
|
850
|
+
for (const entry of creates) {
|
|
851
|
+
const target = path.resolve(resolvedCwd, entry.path);
|
|
852
|
+
if (fs.existsSync(target)) {
|
|
853
|
+
const current = fs.statSync(target).isFile()
|
|
854
|
+
? fs.readFileSync(target, "utf8")
|
|
855
|
+
: undefined;
|
|
856
|
+
if (current !== entry.content) {
|
|
857
|
+
throw new Error(
|
|
858
|
+
`paper scaffold race detected at ${entry.path}; no file was overwritten`,
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
const written = [];
|
|
865
|
+
for (const entry of creates) {
|
|
866
|
+
const target = path.resolve(resolvedCwd, entry.path);
|
|
867
|
+
if (fs.existsSync(target)) continue;
|
|
868
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
869
|
+
fs.writeFileSync(target, entry.content, { flag: "wx" });
|
|
870
|
+
written.push(entry.path);
|
|
871
|
+
}
|
|
872
|
+
return {
|
|
873
|
+
...plan,
|
|
874
|
+
ok: true,
|
|
875
|
+
dryRun: false,
|
|
876
|
+
written,
|
|
877
|
+
idempotent: written.length === 0,
|
|
878
|
+
nextActions: [
|
|
879
|
+
{
|
|
880
|
+
id: "paper-preflight",
|
|
881
|
+
command: `buildchain paper preflight --cwd ${JSON.stringify(resolvedCwd)} --json`,
|
|
882
|
+
description:
|
|
883
|
+
"Verify the generated repository before any external mutation.",
|
|
884
|
+
},
|
|
885
|
+
],
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function migrationFiles({
|
|
890
|
+
cwd,
|
|
891
|
+
buildchainRoot,
|
|
892
|
+
buildchainVersion,
|
|
893
|
+
buildchainSha,
|
|
894
|
+
}) {
|
|
895
|
+
const configResult = paperConfig(cwd);
|
|
896
|
+
if (configResult.error) {
|
|
897
|
+
throw new Error(
|
|
898
|
+
`paper migration requires a valid publication config: ${configResult.error}`,
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
validateBuildchainConfig(cwd, { requireLifecycleStages: ["verify"] });
|
|
902
|
+
const config = configResult.loaded.config;
|
|
903
|
+
const repository = resolvePaperRepository(cwd);
|
|
904
|
+
if (!repository) {
|
|
905
|
+
throw new Error(
|
|
906
|
+
"paper migration requires an exact GitHub repository identity",
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
const packageName = normalizePackageName(
|
|
910
|
+
config.publish?.package || config.publish?.mainPackage || "",
|
|
911
|
+
"paper publish package",
|
|
912
|
+
);
|
|
913
|
+
const runtimeSha =
|
|
914
|
+
buildchainSha || runtimeGitSha(buildchainRoot, buildchainVersion);
|
|
915
|
+
if (!GIT_SHA_PATTERN.test(runtimeSha)) {
|
|
916
|
+
throw new Error("paper migration requires an exact Buildchain source SHA");
|
|
917
|
+
}
|
|
918
|
+
const runtimeIdentity = buildchainPackageIdentity(
|
|
919
|
+
buildchainRoot,
|
|
920
|
+
buildchainVersion,
|
|
921
|
+
);
|
|
922
|
+
if (!runtimeIdentity.version) {
|
|
923
|
+
throw new Error(
|
|
924
|
+
"paper migration requires an exact Buildchain package version",
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
const existingLock = readJson(
|
|
928
|
+
path.resolve(cwd, PAPER_PATHS.contractLock),
|
|
929
|
+
).value;
|
|
930
|
+
const contractLock = createBuildchainContractLock({
|
|
931
|
+
buildchainRef: runtimeSha,
|
|
932
|
+
resolvedSha: runtimeSha,
|
|
933
|
+
contractWorld: runtimeContractWorld(buildchainRoot),
|
|
934
|
+
acceptedAt:
|
|
935
|
+
existingLock?.buildchain?.resolvedSha === runtimeSha
|
|
936
|
+
? existingLock.buildchain.acceptedAt
|
|
937
|
+
: runtimeAcceptedAt(buildchainRoot, runtimeSha),
|
|
938
|
+
});
|
|
939
|
+
const contractLockText = jsonText(contractLock);
|
|
940
|
+
const buildWorkflow = scaffoldBuildWorkflow(runtimeSha, {
|
|
941
|
+
artifactName: config.project.name,
|
|
942
|
+
});
|
|
943
|
+
const releaseWorkflow = scaffoldReleaseWorkflow(runtimeSha, {
|
|
944
|
+
artifactPaths: config.publication.artifactPaths.join(","),
|
|
945
|
+
releasePassportProductName: config.publication.title,
|
|
946
|
+
});
|
|
947
|
+
const provisioningAuthority = createPaperProvisioningAuthority({
|
|
948
|
+
repository,
|
|
949
|
+
packageName,
|
|
950
|
+
buildchainVersion: runtimeIdentity.version,
|
|
951
|
+
buildchainSha: runtimeSha,
|
|
952
|
+
contractLock: contractLockText,
|
|
953
|
+
buildWorkflow,
|
|
954
|
+
releaseWorkflow,
|
|
955
|
+
});
|
|
956
|
+
return new Map([
|
|
957
|
+
[PAPER_PATHS.contractLock, contractLockText],
|
|
958
|
+
[PAPER_PATHS.versionPin, `${runtimeIdentity.version}\n`],
|
|
959
|
+
[PAPER_PATHS.buildWorkflow, buildWorkflow],
|
|
960
|
+
[PAPER_PATHS.releaseWorkflow, releaseWorkflow],
|
|
961
|
+
[PAPER_PATHS.provisioningAuthority, jsonText(provisioningAuthority)],
|
|
962
|
+
]);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
export function planPaperMigration({
|
|
966
|
+
cwd = process.cwd(),
|
|
967
|
+
buildchainRoot = process.cwd(),
|
|
968
|
+
buildchainVersion = "",
|
|
969
|
+
buildchainSha = "",
|
|
970
|
+
} = {}) {
|
|
971
|
+
const resolvedCwd = path.resolve(cwd);
|
|
972
|
+
const repositoryRoot = gitValue(resolvedCwd, [
|
|
973
|
+
"rev-parse",
|
|
974
|
+
"--show-toplevel",
|
|
975
|
+
]);
|
|
976
|
+
if (
|
|
977
|
+
!repositoryRoot ||
|
|
978
|
+
fs.realpathSync(repositoryRoot) !== fs.realpathSync(resolvedCwd)
|
|
979
|
+
) {
|
|
980
|
+
throw new Error("paper migration must target the exact repository root");
|
|
981
|
+
}
|
|
982
|
+
const source = {
|
|
983
|
+
head: gitValue(resolvedCwd, ["rev-parse", "HEAD"]),
|
|
984
|
+
clean: gitResult(resolvedCwd, ["status", "--porcelain"]).stdout === "",
|
|
985
|
+
};
|
|
986
|
+
if (!GIT_SHA_PATTERN.test(source.head)) {
|
|
987
|
+
throw new Error("paper migration requires a committed Git source");
|
|
988
|
+
}
|
|
989
|
+
const plannedFiles = [
|
|
990
|
+
...migrationFiles({
|
|
991
|
+
cwd: resolvedCwd,
|
|
992
|
+
buildchainRoot,
|
|
993
|
+
buildchainVersion,
|
|
994
|
+
buildchainSha,
|
|
995
|
+
}),
|
|
996
|
+
].map(([relativePath, content]) => {
|
|
997
|
+
const target = path.resolve(resolvedCwd, relativePath);
|
|
998
|
+
if (fs.existsSync(target) && !fs.statSync(target).isFile()) {
|
|
999
|
+
return {
|
|
1000
|
+
path: relativePath,
|
|
1001
|
+
action: "conflict",
|
|
1002
|
+
currentSha256: "",
|
|
1003
|
+
sha256: sha256Text(content),
|
|
1004
|
+
content,
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
const current = fs.existsSync(target)
|
|
1008
|
+
? fs.readFileSync(target, "utf8")
|
|
1009
|
+
: undefined;
|
|
1010
|
+
return {
|
|
1011
|
+
path: relativePath,
|
|
1012
|
+
action:
|
|
1013
|
+
current === undefined
|
|
1014
|
+
? "create"
|
|
1015
|
+
: current === content
|
|
1016
|
+
? "unchanged"
|
|
1017
|
+
: "update",
|
|
1018
|
+
currentSha256: current === undefined ? "" : sha256Text(current),
|
|
1019
|
+
sha256: sha256Text(content),
|
|
1020
|
+
content,
|
|
1021
|
+
};
|
|
1022
|
+
});
|
|
1023
|
+
const changes = plannedFiles.map(({ content: _content, ...entry }) => entry);
|
|
1024
|
+
const conflicts = changes.filter((entry) => entry.action === "conflict");
|
|
1025
|
+
const ok = source.clean && conflicts.length === 0;
|
|
1026
|
+
const result = {
|
|
1027
|
+
schemaVersion: 1,
|
|
1028
|
+
contract: PAPER_MIGRATION_CONTRACT,
|
|
1029
|
+
ok,
|
|
1030
|
+
cwd: resolvedCwd,
|
|
1031
|
+
dryRun: true,
|
|
1032
|
+
source,
|
|
1033
|
+
summary: {
|
|
1034
|
+
create: changes.filter((entry) => entry.action === "create").length,
|
|
1035
|
+
update: changes.filter((entry) => entry.action === "update").length,
|
|
1036
|
+
unchanged: changes.filter((entry) => entry.action === "unchanged").length,
|
|
1037
|
+
conflict: conflicts.length,
|
|
1038
|
+
},
|
|
1039
|
+
changes,
|
|
1040
|
+
conflicts,
|
|
1041
|
+
nextActions: !source.clean
|
|
1042
|
+
? [
|
|
1043
|
+
{
|
|
1044
|
+
id: "commit-source",
|
|
1045
|
+
command: "git status --short",
|
|
1046
|
+
description:
|
|
1047
|
+
"Migration only rewrites Buildchain-owned control files from a clean committed source.",
|
|
1048
|
+
},
|
|
1049
|
+
]
|
|
1050
|
+
: conflicts.length > 0
|
|
1051
|
+
? [
|
|
1052
|
+
{
|
|
1053
|
+
id: "resolve-migration-conflicts",
|
|
1054
|
+
command: "",
|
|
1055
|
+
description:
|
|
1056
|
+
"Resolve non-file targets; migration never replaces a directory or special path.",
|
|
1057
|
+
},
|
|
1058
|
+
]
|
|
1059
|
+
: [
|
|
1060
|
+
{
|
|
1061
|
+
id: "write-migration",
|
|
1062
|
+
command: "buildchain paper migrate --write --json",
|
|
1063
|
+
description:
|
|
1064
|
+
"Write the reviewed Buildchain-owned control files without changing paper content or publication configuration.",
|
|
1065
|
+
},
|
|
1066
|
+
],
|
|
1067
|
+
};
|
|
1068
|
+
Object.defineProperty(result, "_plannedFiles", {
|
|
1069
|
+
value: plannedFiles,
|
|
1070
|
+
enumerable: false,
|
|
1071
|
+
});
|
|
1072
|
+
return result;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
export function writePaperMigration(plan) {
|
|
1076
|
+
if (!plan || plan.contract !== PAPER_MIGRATION_CONTRACT) {
|
|
1077
|
+
throw new Error("paper migration plan contract mismatch");
|
|
1078
|
+
}
|
|
1079
|
+
if (!plan.ok) {
|
|
1080
|
+
return {
|
|
1081
|
+
...plan,
|
|
1082
|
+
dryRun: false,
|
|
1083
|
+
written: [],
|
|
1084
|
+
updated: [],
|
|
1085
|
+
ok: false,
|
|
1086
|
+
errorCode: "paper-migration-blocked",
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
const written = [];
|
|
1090
|
+
const updated = [];
|
|
1091
|
+
for (const entry of plan._plannedFiles) {
|
|
1092
|
+
if (entry.action === "unchanged") continue;
|
|
1093
|
+
const target = path.resolve(plan.cwd, entry.path);
|
|
1094
|
+
const exists = fs.existsSync(target);
|
|
1095
|
+
const current =
|
|
1096
|
+
exists && fs.statSync(target).isFile()
|
|
1097
|
+
? fs.readFileSync(target, "utf8")
|
|
1098
|
+
: undefined;
|
|
1099
|
+
const currentSha256 = current === undefined ? "" : sha256Text(current);
|
|
1100
|
+
if (
|
|
1101
|
+
(entry.action === "create" && exists) ||
|
|
1102
|
+
(entry.action === "update" && currentSha256 !== entry.currentSha256)
|
|
1103
|
+
) {
|
|
1104
|
+
throw new Error(
|
|
1105
|
+
`paper migration race detected at ${entry.path}; no stale plan was applied`,
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
for (const entry of plan._plannedFiles) {
|
|
1110
|
+
if (entry.action === "unchanged") continue;
|
|
1111
|
+
const target = path.resolve(plan.cwd, entry.path);
|
|
1112
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
1113
|
+
fs.writeFileSync(target, entry.content, {
|
|
1114
|
+
flag: entry.action === "create" ? "wx" : "w",
|
|
1115
|
+
});
|
|
1116
|
+
(entry.action === "create" ? written : updated).push(entry.path);
|
|
1117
|
+
}
|
|
1118
|
+
return {
|
|
1119
|
+
...plan,
|
|
1120
|
+
ok: true,
|
|
1121
|
+
dryRun: false,
|
|
1122
|
+
written,
|
|
1123
|
+
updated,
|
|
1124
|
+
idempotent: written.length === 0 && updated.length === 0,
|
|
1125
|
+
nextActions: [
|
|
1126
|
+
{
|
|
1127
|
+
id: "paper-preflight",
|
|
1128
|
+
command: `buildchain paper preflight --cwd ${JSON.stringify(plan.cwd)} --offline --json`,
|
|
1129
|
+
description:
|
|
1130
|
+
"Verify the migrated repository before any external mutation.",
|
|
1131
|
+
},
|
|
1132
|
+
],
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function paperConfig(cwd) {
|
|
1137
|
+
const loaded = loadBuildchainConfig(cwd);
|
|
1138
|
+
if (!loaded) {
|
|
1139
|
+
return {
|
|
1140
|
+
loaded: undefined,
|
|
1141
|
+
error: `${PAPER_PATHS.config} is missing`,
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
if (loaded.config.project?.type !== "publication-artifact") {
|
|
1145
|
+
return {
|
|
1146
|
+
loaded,
|
|
1147
|
+
error: 'project.type must be "publication-artifact"',
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
if (!loaded.config.publication) {
|
|
1151
|
+
return { loaded, error: "[publication] is missing" };
|
|
1152
|
+
}
|
|
1153
|
+
return { loaded, error: "" };
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function state(id, status, reason, evidence = []) {
|
|
1157
|
+
return {
|
|
1158
|
+
id,
|
|
1159
|
+
status,
|
|
1160
|
+
satisfied: status === "satisfied",
|
|
1161
|
+
reason,
|
|
1162
|
+
evidence: evidence.filter(Boolean),
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
function transactionCandidates(cwd, version) {
|
|
1167
|
+
const stateDir = path.resolve(cwd, ".buildchain/release-state");
|
|
1168
|
+
if (!fs.existsSync(stateDir) || !fs.statSync(stateDir).isDirectory()) {
|
|
1169
|
+
return [];
|
|
1170
|
+
}
|
|
1171
|
+
const preferred = new Set([`${version}.json`, `v${version}.json`]);
|
|
1172
|
+
return fs
|
|
1173
|
+
.readdirSync(stateDir)
|
|
1174
|
+
.filter((entry) => entry.endsWith(".json"))
|
|
1175
|
+
.sort((left, right) => {
|
|
1176
|
+
const leftRank = preferred.has(left) ? 0 : 1;
|
|
1177
|
+
const rightRank = preferred.has(right) ? 0 : 1;
|
|
1178
|
+
return leftRank - rightRank || left.localeCompare(right);
|
|
1179
|
+
})
|
|
1180
|
+
.map((entry) => {
|
|
1181
|
+
const filePath = path.join(stateDir, entry);
|
|
1182
|
+
try {
|
|
1183
|
+
const transaction = readReleaseTransaction(filePath);
|
|
1184
|
+
return {
|
|
1185
|
+
path: toPosix(path.relative(cwd, filePath)),
|
|
1186
|
+
transaction,
|
|
1187
|
+
error: "",
|
|
1188
|
+
};
|
|
1189
|
+
} catch (error) {
|
|
1190
|
+
return {
|
|
1191
|
+
path: toPosix(path.relative(cwd, filePath)),
|
|
1192
|
+
transaction: undefined,
|
|
1193
|
+
error: error.message,
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function explicitVisibilityState(value, channel) {
|
|
1200
|
+
const entry = value?.channels?.[channel];
|
|
1201
|
+
return Boolean(
|
|
1202
|
+
value?.contract === PAPER_VISIBILITY_CONTRACT &&
|
|
1203
|
+
entry?.status === "visible" &&
|
|
1204
|
+
typeof entry.url === "string" &&
|
|
1205
|
+
entry.url &&
|
|
1206
|
+
SHA256_PATTERN.test(String(entry.evidenceDigest || "")),
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function admissionSatisfied(value) {
|
|
1211
|
+
const digestPattern = /^(?:sha256:)?[0-9a-f]{64}$/i;
|
|
1212
|
+
if (value?.contract === "kungfu-buildchain-publication-admission") {
|
|
1213
|
+
return digestPattern.test(String(value.admissionDigest || ""));
|
|
1214
|
+
}
|
|
1215
|
+
if (value?.contract === "kungfu-buildchain-publication-capability") {
|
|
1216
|
+
return (
|
|
1217
|
+
value.decision === "allow" &&
|
|
1218
|
+
digestPattern.test(String(value.capabilityDigest || ""))
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
return false;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function bootstrapSatisfied(value) {
|
|
1225
|
+
return Boolean(
|
|
1226
|
+
value?.contract === PAPER_NPM_BOOTSTRAP_CONTRACT &&
|
|
1227
|
+
["existing", "published"].includes(value?.publish?.status) &&
|
|
1228
|
+
value?.package?.name,
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
function trustSatisfied(value) {
|
|
1233
|
+
return Boolean(
|
|
1234
|
+
value?.contract === PAPER_NPM_BOOTSTRAP_CONTRACT &&
|
|
1235
|
+
value?.trust?.status === "configured",
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
function contentReadiness(cwd, publication) {
|
|
1240
|
+
const required = [...publication.sourcePaths, ...publication.metadataPaths];
|
|
1241
|
+
const missing = required.filter(
|
|
1242
|
+
(entry) => !fs.existsSync(path.resolve(cwd, entry)),
|
|
1243
|
+
);
|
|
1244
|
+
return {
|
|
1245
|
+
ok: required.length > 0 && missing.length === 0,
|
|
1246
|
+
required,
|
|
1247
|
+
missing,
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
function releaseTransactionFacts(candidates, version) {
|
|
1252
|
+
const matching = candidates.filter(({ transaction }) => {
|
|
1253
|
+
if (!transaction) return false;
|
|
1254
|
+
return (
|
|
1255
|
+
String(transaction.version || "").replace(/^v/, "") ===
|
|
1256
|
+
String(version || "").replace(/^v/, "")
|
|
1257
|
+
);
|
|
1258
|
+
});
|
|
1259
|
+
const selected =
|
|
1260
|
+
matching[0] ||
|
|
1261
|
+
candidates.find(({ transaction }) => transaction) ||
|
|
1262
|
+
undefined;
|
|
1263
|
+
const publicationState = selected?.transaction
|
|
1264
|
+
? releaseTransactionPublicationState(selected.transaction)
|
|
1265
|
+
: "";
|
|
1266
|
+
return {
|
|
1267
|
+
matching,
|
|
1268
|
+
selected,
|
|
1269
|
+
publicationState,
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
export function collectPaperStatus({ cwd = process.cwd() } = {}) {
|
|
1274
|
+
const resolvedCwd = path.resolve(cwd);
|
|
1275
|
+
const configResult = paperConfig(resolvedCwd);
|
|
1276
|
+
const loaded = configResult.loaded;
|
|
1277
|
+
const publication = loaded?.config?.publication;
|
|
1278
|
+
const publish = loaded?.config?.publish;
|
|
1279
|
+
const packageName = publish?.package || publish?.mainPackage || "";
|
|
1280
|
+
const version = publication?.version || "";
|
|
1281
|
+
const configFact = existingFileFact(
|
|
1282
|
+
resolvedCwd,
|
|
1283
|
+
loaded?.path || PAPER_PATHS.config,
|
|
1284
|
+
);
|
|
1285
|
+
const scaffoldFacts = PAPER_SCAFFOLD_PATHS.map((relativePath) =>
|
|
1286
|
+
existingFileFact(resolvedCwd, relativePath),
|
|
1287
|
+
);
|
|
1288
|
+
const contractLockPath = path.resolve(resolvedCwd, PAPER_PATHS.contractLock);
|
|
1289
|
+
const contractLockJson = readJson(contractLockPath);
|
|
1290
|
+
let contractLock;
|
|
1291
|
+
let contractLockError = contractLockJson.error;
|
|
1292
|
+
if (contractLockJson.exists && !contractLockError) {
|
|
1293
|
+
try {
|
|
1294
|
+
contractLock = readBuildchainContractLock(contractLockPath);
|
|
1295
|
+
} catch (error) {
|
|
1296
|
+
contractLockError = error.message;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
const gitRepo = gitResult(resolvedCwd, [
|
|
1300
|
+
"rev-parse",
|
|
1301
|
+
"--is-inside-work-tree",
|
|
1302
|
+
]);
|
|
1303
|
+
const scaffoldOk = Boolean(
|
|
1304
|
+
!configResult.error && scaffoldFacts.every(Boolean),
|
|
1305
|
+
);
|
|
1306
|
+
const governedOk = Boolean(
|
|
1307
|
+
scaffoldOk &&
|
|
1308
|
+
contractLock &&
|
|
1309
|
+
GIT_SHA_PATTERN.test(contractLock.buildchain?.resolvedSha || "") &&
|
|
1310
|
+
SHA256_PATTERN.test(contractLock.buildchain?.contractDigest || "") &&
|
|
1311
|
+
SHA256_PATTERN.test(contractLock.buildchain?.compatibilityDigest || "") &&
|
|
1312
|
+
gitRepo.ok &&
|
|
1313
|
+
gitRepo.stdout === "true",
|
|
1314
|
+
);
|
|
1315
|
+
|
|
1316
|
+
const admissionInputs = [PAPER_PATHS.capability, PAPER_PATHS.admission].map(
|
|
1317
|
+
(relativePath) => ({
|
|
1318
|
+
relativePath,
|
|
1319
|
+
...readJson(path.resolve(resolvedCwd, relativePath)),
|
|
1320
|
+
}),
|
|
1321
|
+
);
|
|
1322
|
+
const admitted = admissionInputs.find((entry) =>
|
|
1323
|
+
admissionSatisfied(entry.value),
|
|
1324
|
+
);
|
|
1325
|
+
const bootstrap = readJson(
|
|
1326
|
+
path.resolve(resolvedCwd, PAPER_PATHS.npmBootstrap),
|
|
1327
|
+
);
|
|
1328
|
+
const trust = readJson(path.resolve(resolvedCwd, PAPER_PATHS.npmTrust));
|
|
1329
|
+
const bootstrapValue = bootstrap.value;
|
|
1330
|
+
const trustValue = trust.value || bootstrap.value;
|
|
1331
|
+
const content = publication
|
|
1332
|
+
? contentReadiness(resolvedCwd, publication)
|
|
1333
|
+
: { ok: false, required: [], missing: [] };
|
|
1334
|
+
const reproducibility = readJson(
|
|
1335
|
+
path.resolve(resolvedCwd, PAPER_PATHS.reproducibilityReceipt),
|
|
1336
|
+
);
|
|
1337
|
+
const deterministic = Boolean(
|
|
1338
|
+
reproducibility.value?.contract ===
|
|
1339
|
+
PUBLICATION_REPRODUCIBILITY_RECEIPT_CONTRACT &&
|
|
1340
|
+
reproducibility.value?.status === "passed" &&
|
|
1341
|
+
reproducibility.value?.qualifying === true,
|
|
1342
|
+
);
|
|
1343
|
+
const sealed = readJson(path.resolve(resolvedCwd, PAPER_PATHS.sealedBundle));
|
|
1344
|
+
let sealedOk = false;
|
|
1345
|
+
let sealedError = sealed.error;
|
|
1346
|
+
if (sealed.value?.contract === PUBLICATION_SEALED_BUNDLE_CONTRACT) {
|
|
1347
|
+
try {
|
|
1348
|
+
verifyPublicationSealedBundle({
|
|
1349
|
+
bundleRoot: resolvedCwd,
|
|
1350
|
+
manifest: sealed.value,
|
|
1351
|
+
});
|
|
1352
|
+
sealedOk = true;
|
|
1353
|
+
} catch (error) {
|
|
1354
|
+
sealedError = error.message;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
const candidates = transactionCandidates(resolvedCwd, version);
|
|
1358
|
+
const transactionFacts = releaseTransactionFacts(candidates, version);
|
|
1359
|
+
const packagePublished = [
|
|
1360
|
+
"package-published",
|
|
1361
|
+
"alpha-complete",
|
|
1362
|
+
"release-complete",
|
|
1363
|
+
].includes(transactionFacts.publicationState);
|
|
1364
|
+
const alphaComplete = transactionFacts.publicationState === "alpha-complete";
|
|
1365
|
+
const visibility = readJson(
|
|
1366
|
+
path.resolve(resolvedCwd, PAPER_PATHS.visibility),
|
|
1367
|
+
);
|
|
1368
|
+
const stagingVisible = explicitVisibilityState(visibility.value, "staging");
|
|
1369
|
+
const productionVisible = explicitVisibilityState(
|
|
1370
|
+
visibility.value,
|
|
1371
|
+
"production",
|
|
1372
|
+
);
|
|
1373
|
+
|
|
1374
|
+
const states = [
|
|
1375
|
+
state(
|
|
1376
|
+
"scaffolded",
|
|
1377
|
+
scaffoldOk ? "satisfied" : configFact ? "blocked" : "not-reached",
|
|
1378
|
+
scaffoldOk
|
|
1379
|
+
? "The complete managed paper scaffold inventory is present."
|
|
1380
|
+
: configResult.error || "Paper scaffold is incomplete.",
|
|
1381
|
+
scaffoldFacts,
|
|
1382
|
+
),
|
|
1383
|
+
state(
|
|
1384
|
+
"governed",
|
|
1385
|
+
governedOk ? "satisfied" : scaffoldOk ? "blocked" : "not-reached",
|
|
1386
|
+
governedOk
|
|
1387
|
+
? "The repository is Git-governed and carries a valid Buildchain contract lock."
|
|
1388
|
+
: contractLockError ||
|
|
1389
|
+
"Git governance or the Buildchain contract lock is missing.",
|
|
1390
|
+
[existingFileFact(resolvedCwd, PAPER_PATHS.contractLock)],
|
|
1391
|
+
),
|
|
1392
|
+
state(
|
|
1393
|
+
"admitted",
|
|
1394
|
+
admitted ? "satisfied" : "not-reached",
|
|
1395
|
+
admitted
|
|
1396
|
+
? "Explicit publication admission/capability evidence is present."
|
|
1397
|
+
: "No explicit publication admission evidence is present; no admission is inferred.",
|
|
1398
|
+
admitted ? [existingFileFact(resolvedCwd, admitted.relativePath)] : [],
|
|
1399
|
+
),
|
|
1400
|
+
state(
|
|
1401
|
+
"bootstrapped",
|
|
1402
|
+
bootstrapSatisfied(bootstrapValue) ? "satisfied" : "not-reached",
|
|
1403
|
+
bootstrapSatisfied(bootstrapValue)
|
|
1404
|
+
? "A typed npm package bootstrap receipt exists."
|
|
1405
|
+
: "No typed npm bootstrap receipt proves that the public package exists.",
|
|
1406
|
+
[existingFileFact(resolvedCwd, PAPER_PATHS.npmBootstrap)],
|
|
1407
|
+
),
|
|
1408
|
+
state(
|
|
1409
|
+
"trust-bound",
|
|
1410
|
+
trustSatisfied(trustValue) ? "satisfied" : "not-reached",
|
|
1411
|
+
trustSatisfied(trustValue)
|
|
1412
|
+
? "A typed receipt proves npm Trusted Publishing is configured."
|
|
1413
|
+
: "No typed receipt proves the npm Trusted Publisher binding.",
|
|
1414
|
+
[
|
|
1415
|
+
existingFileFact(resolvedCwd, PAPER_PATHS.npmTrust),
|
|
1416
|
+
existingFileFact(resolvedCwd, PAPER_PATHS.npmBootstrap),
|
|
1417
|
+
],
|
|
1418
|
+
),
|
|
1419
|
+
state(
|
|
1420
|
+
"content-ready",
|
|
1421
|
+
content.ok ? "satisfied" : publication ? "blocked" : "not-reached",
|
|
1422
|
+
content.ok
|
|
1423
|
+
? "Every declared source and metadata input exists."
|
|
1424
|
+
: `Declared content is missing: ${content.missing.join(", ") || "publication config unavailable"}.`,
|
|
1425
|
+
content.required.map((entry) => existingFileFact(resolvedCwd, entry)),
|
|
1426
|
+
),
|
|
1427
|
+
state(
|
|
1428
|
+
"artifact-sealed",
|
|
1429
|
+
deterministic && sealedOk ? "satisfied" : "not-reached",
|
|
1430
|
+
deterministic && sealedOk
|
|
1431
|
+
? "A qualifying reproducibility receipt and verified sealed bundle bind exact artifact bytes."
|
|
1432
|
+
: sealedError ||
|
|
1433
|
+
"Artifact sealing requires both qualifying reproducibility and an exact sealed bundle; neither is inferred.",
|
|
1434
|
+
[
|
|
1435
|
+
existingFileFact(resolvedCwd, PAPER_PATHS.reproducibilityReceipt),
|
|
1436
|
+
existingFileFact(resolvedCwd, PAPER_PATHS.sealedBundle),
|
|
1437
|
+
],
|
|
1438
|
+
),
|
|
1439
|
+
state(
|
|
1440
|
+
"package-published",
|
|
1441
|
+
packagePublished ? "satisfied" : "not-reached",
|
|
1442
|
+
packagePublished
|
|
1443
|
+
? `Release transaction explicitly reports ${transactionFacts.publicationState}.`
|
|
1444
|
+
: "No release transaction explicitly proves package publication.",
|
|
1445
|
+
transactionFacts.selected
|
|
1446
|
+
? [existingFileFact(resolvedCwd, transactionFacts.selected.path)]
|
|
1447
|
+
: [],
|
|
1448
|
+
),
|
|
1449
|
+
state(
|
|
1450
|
+
"alpha-complete",
|
|
1451
|
+
alphaComplete ? "satisfied" : "not-reached",
|
|
1452
|
+
alphaComplete
|
|
1453
|
+
? "The alpha release transaction is complete."
|
|
1454
|
+
: "No completed alpha transaction is present.",
|
|
1455
|
+
transactionFacts.selected
|
|
1456
|
+
? [existingFileFact(resolvedCwd, transactionFacts.selected.path)]
|
|
1457
|
+
: [],
|
|
1458
|
+
),
|
|
1459
|
+
state(
|
|
1460
|
+
"staging-visible",
|
|
1461
|
+
stagingVisible ? "satisfied" : "not-reached",
|
|
1462
|
+
stagingVisible
|
|
1463
|
+
? "Explicit digest-bound staging visibility evidence is present."
|
|
1464
|
+
: "No explicit staging visibility evidence is present; visibility is not inferred from publication.",
|
|
1465
|
+
[existingFileFact(resolvedCwd, PAPER_PATHS.visibility)],
|
|
1466
|
+
),
|
|
1467
|
+
state(
|
|
1468
|
+
"production-visible",
|
|
1469
|
+
productionVisible ? "satisfied" : "not-reached",
|
|
1470
|
+
productionVisible
|
|
1471
|
+
? "Explicit digest-bound production visibility evidence is present."
|
|
1472
|
+
: "No explicit production visibility evidence is present; visibility is not inferred from staging.",
|
|
1473
|
+
[existingFileFact(resolvedCwd, PAPER_PATHS.visibility)],
|
|
1474
|
+
),
|
|
1475
|
+
];
|
|
1476
|
+
const achieved = states
|
|
1477
|
+
.filter((entry) => entry.satisfied)
|
|
1478
|
+
.map((entry) => entry.id);
|
|
1479
|
+
const highestEvidenceState =
|
|
1480
|
+
[...states].reverse().find((entry) => entry.satisfied)?.id || "none";
|
|
1481
|
+
const stateMap = Object.fromEntries(states.map((entry) => [entry.id, entry]));
|
|
1482
|
+
const blockingNextActions = [];
|
|
1483
|
+
if (!stateMap.scaffolded.satisfied) {
|
|
1484
|
+
blockingNextActions.push({
|
|
1485
|
+
id: "scaffold-paper",
|
|
1486
|
+
command: "buildchain paper scaffold --help",
|
|
1487
|
+
description: "Plan a no-overwrite Buildchain paper scaffold.",
|
|
1488
|
+
});
|
|
1489
|
+
} else if (!stateMap.governed.satisfied) {
|
|
1490
|
+
blockingNextActions.push({
|
|
1491
|
+
id: "restore-governance",
|
|
1492
|
+
command: "buildchain paper preflight --json",
|
|
1493
|
+
description:
|
|
1494
|
+
"Repair the Git/contract-lock governance checks reported by preflight.",
|
|
1495
|
+
});
|
|
1496
|
+
} else if (!stateMap["content-ready"].satisfied) {
|
|
1497
|
+
blockingNextActions.push({
|
|
1498
|
+
id: "complete-paper-content",
|
|
1499
|
+
command: "make check",
|
|
1500
|
+
description: "Add every declared paper source and metadata input.",
|
|
1501
|
+
});
|
|
1502
|
+
} else if (!deterministic) {
|
|
1503
|
+
blockingNextActions.push({
|
|
1504
|
+
id: "build-reproducible-artifact",
|
|
1505
|
+
command: "buildchain paper build --execute --json",
|
|
1506
|
+
description:
|
|
1507
|
+
"Run the existing two-clean-build reproducibility gate and promote exact bytes.",
|
|
1508
|
+
});
|
|
1509
|
+
} else if (
|
|
1510
|
+
!stateMap.bootstrapped.satisfied ||
|
|
1511
|
+
!stateMap["trust-bound"].satisfied
|
|
1512
|
+
) {
|
|
1513
|
+
blockingNextActions.push({
|
|
1514
|
+
id: "bootstrap-npm",
|
|
1515
|
+
command: "buildchain paper bootstrap npm --json",
|
|
1516
|
+
description:
|
|
1517
|
+
"Inspect the dry-run npm bootstrap and Trusted Publishing handoff.",
|
|
1518
|
+
});
|
|
1519
|
+
} else if (!stateMap["alpha-complete"].satisfied) {
|
|
1520
|
+
blockingNextActions.push({
|
|
1521
|
+
id: transactionFacts.selected ? "resume-alpha" : "start-alpha",
|
|
1522
|
+
command: transactionFacts.selected
|
|
1523
|
+
? "buildchain paper resume --json"
|
|
1524
|
+
: "buildchain paper alpha --json",
|
|
1525
|
+
description: transactionFacts.selected
|
|
1526
|
+
? "Resume the exact sealed release transaction."
|
|
1527
|
+
: "Plan the protected dev-to-alpha publication PR.",
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
return {
|
|
1531
|
+
schemaVersion: 1,
|
|
1532
|
+
contract: PAPER_STATUS_CONTRACT,
|
|
1533
|
+
ok: !configResult.error,
|
|
1534
|
+
cwd: resolvedCwd,
|
|
1535
|
+
identity: {
|
|
1536
|
+
project: loaded?.config?.project?.name || "",
|
|
1537
|
+
package: packageName,
|
|
1538
|
+
version,
|
|
1539
|
+
repository: resolvePaperRepository(resolvedCwd),
|
|
1540
|
+
},
|
|
1541
|
+
states,
|
|
1542
|
+
achieved,
|
|
1543
|
+
highestEvidenceState,
|
|
1544
|
+
deterministicBuild: {
|
|
1545
|
+
status: deterministic
|
|
1546
|
+
? "qualifying"
|
|
1547
|
+
: reproducibility.exists
|
|
1548
|
+
? "non-qualifying"
|
|
1549
|
+
: "not-run",
|
|
1550
|
+
receiptPath: PAPER_PATHS.reproducibilityReceipt,
|
|
1551
|
+
receiptDigest: reproducibility.value?.receiptDigest || "",
|
|
1552
|
+
},
|
|
1553
|
+
transaction: transactionFacts.selected
|
|
1554
|
+
? {
|
|
1555
|
+
path: transactionFacts.selected.path,
|
|
1556
|
+
id: transactionFacts.selected.transaction.id || "",
|
|
1557
|
+
state: transactionFacts.selected.transaction.state || "",
|
|
1558
|
+
publicationState: transactionFacts.publicationState,
|
|
1559
|
+
targetRef: transactionFacts.selected.transaction.target_ref || "",
|
|
1560
|
+
sourceSha: transactionFacts.selected.transaction.source_sha || "",
|
|
1561
|
+
resumeCommand:
|
|
1562
|
+
transactionFacts.selected.transaction.resume_command || "",
|
|
1563
|
+
}
|
|
1564
|
+
: null,
|
|
1565
|
+
conflicts: [
|
|
1566
|
+
...candidates
|
|
1567
|
+
.filter((entry) => entry.error)
|
|
1568
|
+
.map((entry) => ({
|
|
1569
|
+
code: "release-state-invalid",
|
|
1570
|
+
path: entry.path,
|
|
1571
|
+
message: entry.error,
|
|
1572
|
+
})),
|
|
1573
|
+
...(transactionFacts.matching.length > 1
|
|
1574
|
+
? [
|
|
1575
|
+
{
|
|
1576
|
+
code: "multiple-release-transactions-for-version",
|
|
1577
|
+
paths: transactionFacts.matching.map((entry) => entry.path),
|
|
1578
|
+
message:
|
|
1579
|
+
"More than one release transaction claims the configured publication version.",
|
|
1580
|
+
},
|
|
1581
|
+
]
|
|
1582
|
+
: []),
|
|
1583
|
+
],
|
|
1584
|
+
blockingNextActions,
|
|
1585
|
+
nonClaims: [
|
|
1586
|
+
"npm package existence is not inferred without a typed receipt or live preflight observation",
|
|
1587
|
+
"publication admission is not inferred from reproducibility",
|
|
1588
|
+
"package publication is not inferred from a local npm package directory",
|
|
1589
|
+
"staging and production visibility are never inferred from alpha completion",
|
|
1590
|
+
],
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
export function resolvePaperRepository(cwd = process.cwd()) {
|
|
1595
|
+
const packagePath = path.resolve(cwd, "package.json");
|
|
1596
|
+
const sourcePackage = readJson(packagePath).value;
|
|
1597
|
+
const configured =
|
|
1598
|
+
typeof sourcePackage?.repository === "string"
|
|
1599
|
+
? sourcePackage.repository
|
|
1600
|
+
: sourcePackage?.repository?.url;
|
|
1601
|
+
const fromPackage = normalizeRepository(configured);
|
|
1602
|
+
if (fromPackage) return fromPackage;
|
|
1603
|
+
return normalizeRepository(
|
|
1604
|
+
gitValue(cwd, ["config", "--get", "remote.origin.url"]),
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
function validatePaperProvisioningAuthority(cwd) {
|
|
1609
|
+
const authorityPath = path.resolve(cwd, PAPER_PATHS.provisioningAuthority);
|
|
1610
|
+
const source = readJson(authorityPath);
|
|
1611
|
+
if (!source.exists) {
|
|
1612
|
+
return {
|
|
1613
|
+
exists: false,
|
|
1614
|
+
valid: false,
|
|
1615
|
+
value: undefined,
|
|
1616
|
+
errors: ["paper provisioning authority is missing"],
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
if (source.error || !source.value) {
|
|
1620
|
+
return {
|
|
1621
|
+
exists: true,
|
|
1622
|
+
valid: false,
|
|
1623
|
+
value: source.value,
|
|
1624
|
+
errors: [source.error || "paper provisioning authority is invalid"],
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
const value = source.value;
|
|
1628
|
+
const errors = [];
|
|
1629
|
+
if (value.contract !== PAPER_PROVISIONING_CONTRACT) {
|
|
1630
|
+
errors.push("paper provisioning authority contract mismatch");
|
|
1631
|
+
}
|
|
1632
|
+
const { authorityDigest, ...payload } = value;
|
|
1633
|
+
if (
|
|
1634
|
+
!SHA256_PATTERN.test(String(authorityDigest || "")) ||
|
|
1635
|
+
authorityDigest !== sha256Text(stableJson(payload))
|
|
1636
|
+
) {
|
|
1637
|
+
errors.push("paper provisioning authority digest mismatch");
|
|
1638
|
+
}
|
|
1639
|
+
if (
|
|
1640
|
+
!GIT_SHA_PATTERN.test(String(value.runtime?.resolvedSha || "")) ||
|
|
1641
|
+
value.runtime?.ref !== value.runtime?.resolvedSha ||
|
|
1642
|
+
value.admission?.acceptedRef !== value.runtime?.resolvedSha ||
|
|
1643
|
+
value.admission?.acceptedSha !== value.runtime?.resolvedSha
|
|
1644
|
+
) {
|
|
1645
|
+
errors.push("paper runtime and admission are not bound to one exact SHA");
|
|
1646
|
+
}
|
|
1647
|
+
if (
|
|
1648
|
+
value.package?.registry !== NPM_REGISTRY ||
|
|
1649
|
+
value.package?.bootstrapVersion !== DEFAULT_BOOTSTRAP_VERSION
|
|
1650
|
+
) {
|
|
1651
|
+
errors.push(
|
|
1652
|
+
"paper npm bootstrap authority is not fixed to the official registry and bootstrap version",
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
const policy = value.policy || {};
|
|
1656
|
+
const { policyDigest, ...policyPayload } = policy;
|
|
1657
|
+
if (
|
|
1658
|
+
!SHA256_PATTERN.test(String(policyDigest || "")) ||
|
|
1659
|
+
policyDigest !== sha256Text(stableJson(policyPayload))
|
|
1660
|
+
) {
|
|
1661
|
+
errors.push("paper provisioning policy digest mismatch");
|
|
1662
|
+
}
|
|
1663
|
+
if (
|
|
1664
|
+
policy.repositoryActions?.defaultWorkflowPermissions !== "read" ||
|
|
1665
|
+
policy.repositoryActions?.canApprovePullRequestReviews !== false
|
|
1666
|
+
) {
|
|
1667
|
+
errors.push("paper repository Actions policy is not least privilege");
|
|
1668
|
+
}
|
|
1669
|
+
if (
|
|
1670
|
+
policy.generatedWrites?.preferredAuthority !== "github-app" ||
|
|
1671
|
+
policy.generatedWrites?.githubTokenFallback !== false
|
|
1672
|
+
) {
|
|
1673
|
+
errors.push("paper generated-write policy permits an unbounded authority");
|
|
1674
|
+
}
|
|
1675
|
+
if (
|
|
1676
|
+
policy.release?.versionState !== "not-required" ||
|
|
1677
|
+
policy.release?.identityOnlyPullRequests !== false ||
|
|
1678
|
+
policy.release?.manualVersionStateRepairPullRequests !== false
|
|
1679
|
+
) {
|
|
1680
|
+
errors.push(
|
|
1681
|
+
"paper release policy permits avoidable bookkeeping pull requests",
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
for (const workflow of [value.workflows?.build, value.workflows?.release]) {
|
|
1685
|
+
if (!workflow?.path || !workflow?.sourceDigest) {
|
|
1686
|
+
errors.push("paper workflow authority is incomplete");
|
|
1687
|
+
continue;
|
|
1688
|
+
}
|
|
1689
|
+
const absolute = path.resolve(cwd, workflow.path);
|
|
1690
|
+
if (
|
|
1691
|
+
!fs.existsSync(absolute) ||
|
|
1692
|
+
sha256File(absolute) !== workflow.sourceDigest
|
|
1693
|
+
) {
|
|
1694
|
+
errors.push(`paper workflow source digest mismatch: ${workflow.path}`);
|
|
1695
|
+
continue;
|
|
1696
|
+
}
|
|
1697
|
+
const text = fs.readFileSync(absolute, "utf8");
|
|
1698
|
+
const expectedUse = `${value.runtime.repository}/${workflow.reusablePath}@${value.runtime.resolvedSha}`;
|
|
1699
|
+
if (!text.includes(`uses: ${expectedUse}`)) {
|
|
1700
|
+
errors.push(
|
|
1701
|
+
`paper workflow reusable source is not exact: ${workflow.path}`,
|
|
1702
|
+
);
|
|
1703
|
+
}
|
|
1704
|
+
if (!text.includes(`buildchain-ref: ${value.runtime.resolvedSha}`)) {
|
|
1705
|
+
errors.push(
|
|
1706
|
+
`paper workflow runtime input is not exact: ${workflow.path}`,
|
|
1707
|
+
);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
const lockPath = path.resolve(
|
|
1711
|
+
cwd,
|
|
1712
|
+
value.admission?.contractLockPath || PAPER_PATHS.contractLock,
|
|
1713
|
+
);
|
|
1714
|
+
if (
|
|
1715
|
+
!fs.existsSync(lockPath) ||
|
|
1716
|
+
sha256File(lockPath) !== value.admission?.contractLockDigest
|
|
1717
|
+
) {
|
|
1718
|
+
errors.push("paper contract lock bytes differ from provisioning authority");
|
|
1719
|
+
}
|
|
1720
|
+
return {
|
|
1721
|
+
exists: true,
|
|
1722
|
+
valid: errors.length === 0,
|
|
1723
|
+
value,
|
|
1724
|
+
errors,
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
function expectedPaperTrustedPublisher(authority, fallback = {}) {
|
|
1729
|
+
const value = authority?.trustedPublisher || {};
|
|
1730
|
+
return {
|
|
1731
|
+
type: String(value.type || "github").toLowerCase(),
|
|
1732
|
+
repository: normalizeRepository(
|
|
1733
|
+
value.repository || fallback.repository || "",
|
|
1734
|
+
),
|
|
1735
|
+
workflow: toPosix(value.workflow || fallback.workflow || ""),
|
|
1736
|
+
environment: String(value.environment || fallback.environment || ""),
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
function normalizedTrustedPublisher(value) {
|
|
1741
|
+
return {
|
|
1742
|
+
type: String(value?.type || value?.provider || "")
|
|
1743
|
+
.trim()
|
|
1744
|
+
.toLowerCase()
|
|
1745
|
+
.replace(/^github-actions$/, "github"),
|
|
1746
|
+
repository: normalizeRepository(value?.repository || value?.repo || ""),
|
|
1747
|
+
workflow: toPosix(value?.workflow || value?.file || "").replace(/^\/+/, ""),
|
|
1748
|
+
environment: String(value?.environment || value?.env || "").trim(),
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
function trustedPublisherMatches(actual, expected) {
|
|
1753
|
+
const normalized = normalizedTrustedPublisher(actual);
|
|
1754
|
+
return (
|
|
1755
|
+
normalized.type === expected.type &&
|
|
1756
|
+
normalized.repository === expected.repository &&
|
|
1757
|
+
normalized.workflow === expected.workflow &&
|
|
1758
|
+
normalized.environment === expected.environment
|
|
1759
|
+
);
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
function runtimeFacts({
|
|
1763
|
+
buildchainRoot,
|
|
1764
|
+
buildchainVersion,
|
|
1765
|
+
buildchainRef,
|
|
1766
|
+
buildchainSha,
|
|
1767
|
+
}) {
|
|
1768
|
+
const identity = buildchainPackageIdentity(buildchainRoot, buildchainVersion);
|
|
1769
|
+
const contractWorld = runtimeContractWorld(buildchainRoot);
|
|
1770
|
+
return {
|
|
1771
|
+
version: identity.version,
|
|
1772
|
+
ref: buildchainRef,
|
|
1773
|
+
resolvedSha:
|
|
1774
|
+
buildchainSha || runtimeGitSha(buildchainRoot, identity.version),
|
|
1775
|
+
contract: contractWorld.contract,
|
|
1776
|
+
contractDigest: contractWorld.contractDigest,
|
|
1777
|
+
compatibilityDigest: contractWorld.compatibilityDigest,
|
|
1778
|
+
majorLine: contractWorld.majorLine,
|
|
1779
|
+
};
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
function liveNpmPackageObservation(packageName, registry, cwd) {
|
|
1783
|
+
const result = commandResult(
|
|
1784
|
+
"npm",
|
|
1785
|
+
["view", packageName, "version", "--json", `--registry=${registry}`],
|
|
1786
|
+
{ cwd },
|
|
1787
|
+
);
|
|
1788
|
+
if (result.ok) {
|
|
1789
|
+
let version = "";
|
|
1790
|
+
try {
|
|
1791
|
+
const parsed = JSON.parse(result.stdout || '""');
|
|
1792
|
+
version = Array.isArray(parsed)
|
|
1793
|
+
? String(parsed.at(-1) || "")
|
|
1794
|
+
: String(parsed || "");
|
|
1795
|
+
} catch {
|
|
1796
|
+
version = result.stdout.replace(/^"|"$/g, "");
|
|
1797
|
+
}
|
|
1798
|
+
return {
|
|
1799
|
+
status: "observed",
|
|
1800
|
+
exists: true,
|
|
1801
|
+
version,
|
|
1802
|
+
registry,
|
|
1803
|
+
errorCode: "",
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
const output = `${result.stderr}\n${result.stdout}`;
|
|
1807
|
+
if (/\bE404\b|404 Not Found|is not in this registry/i.test(output)) {
|
|
1808
|
+
return {
|
|
1809
|
+
status: "observed",
|
|
1810
|
+
exists: false,
|
|
1811
|
+
version: "",
|
|
1812
|
+
registry,
|
|
1813
|
+
errorCode: "package-not-found",
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
return {
|
|
1817
|
+
status: "unknown",
|
|
1818
|
+
exists: null,
|
|
1819
|
+
version: "",
|
|
1820
|
+
registry,
|
|
1821
|
+
errorCode: result.error ? "npm-unavailable" : "npm-view-failed",
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
function liveNpmAuthObservation(registry, cwd) {
|
|
1826
|
+
const result = commandResult("npm", ["whoami", `--registry=${registry}`], {
|
|
1827
|
+
cwd,
|
|
1828
|
+
});
|
|
1829
|
+
return {
|
|
1830
|
+
status: result.ok ? "authenticated" : "unauthenticated-or-unavailable",
|
|
1831
|
+
authenticated: result.ok,
|
|
1832
|
+
identity: result.ok ? result.stdout : "",
|
|
1833
|
+
errorCode: result.ok
|
|
1834
|
+
? ""
|
|
1835
|
+
: result.error
|
|
1836
|
+
? "npm-unavailable"
|
|
1837
|
+
: "npm-auth-unavailable",
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
function liveNpmTrustObservation(
|
|
1842
|
+
packageName,
|
|
1843
|
+
registry,
|
|
1844
|
+
cwd,
|
|
1845
|
+
expectedPublisher = undefined,
|
|
1846
|
+
) {
|
|
1847
|
+
const result = commandResult(
|
|
1848
|
+
"npm",
|
|
1849
|
+
["trust", "list", packageName, "--json", `--registry=${registry}`],
|
|
1850
|
+
{ cwd },
|
|
1851
|
+
);
|
|
1852
|
+
if (!result.ok) {
|
|
1853
|
+
return {
|
|
1854
|
+
status: "unknown",
|
|
1855
|
+
configured: null,
|
|
1856
|
+
publishers: [],
|
|
1857
|
+
errorCode: result.error
|
|
1858
|
+
? "npm-unavailable"
|
|
1859
|
+
: "npm-trust-query-unavailable",
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
let parsed;
|
|
1863
|
+
try {
|
|
1864
|
+
parsed = JSON.parse(result.stdout || "[]");
|
|
1865
|
+
} catch {
|
|
1866
|
+
return {
|
|
1867
|
+
status: "unknown",
|
|
1868
|
+
configured: null,
|
|
1869
|
+
publishers: [],
|
|
1870
|
+
errorCode: "npm-trust-response-invalid",
|
|
1871
|
+
};
|
|
1872
|
+
}
|
|
1873
|
+
const publishers = Array.isArray(parsed)
|
|
1874
|
+
? parsed
|
|
1875
|
+
: Array.isArray(parsed?.publishers)
|
|
1876
|
+
? parsed.publishers
|
|
1877
|
+
: Array.isArray(parsed?.trustedPublishers)
|
|
1878
|
+
? parsed.trustedPublishers
|
|
1879
|
+
: [];
|
|
1880
|
+
const normalizedPublishers = publishers.map(normalizedTrustedPublisher);
|
|
1881
|
+
const exactMatches = expectedPublisher
|
|
1882
|
+
? normalizedPublishers.filter((entry) =>
|
|
1883
|
+
trustedPublisherMatches(entry, expectedPublisher),
|
|
1884
|
+
)
|
|
1885
|
+
: [];
|
|
1886
|
+
return {
|
|
1887
|
+
status: "observed",
|
|
1888
|
+
configured: expectedPublisher
|
|
1889
|
+
? exactMatches.length === 1
|
|
1890
|
+
: normalizedPublishers.length > 0,
|
|
1891
|
+
exactBinding: expectedPublisher ? exactMatches.length === 1 : null,
|
|
1892
|
+
expectedPublisher: expectedPublisher || null,
|
|
1893
|
+
publishers: normalizedPublishers,
|
|
1894
|
+
errorCode: "",
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
function liveRepositoryPermissionObservation(repository, cwd) {
|
|
1899
|
+
if (!repository) {
|
|
1900
|
+
return {
|
|
1901
|
+
status: "unknown",
|
|
1902
|
+
repository: "",
|
|
1903
|
+
canWrite: null,
|
|
1904
|
+
errorCode: "repository-unresolved",
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1907
|
+
const result = commandResult("gh", ["api", `repos/${repository}`], { cwd });
|
|
1908
|
+
if (!result.ok) {
|
|
1909
|
+
return {
|
|
1910
|
+
status: "unknown",
|
|
1911
|
+
repository,
|
|
1912
|
+
canWrite: null,
|
|
1913
|
+
errorCode: result.error
|
|
1914
|
+
? "gh-unavailable"
|
|
1915
|
+
: "github-permission-query-failed",
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
let parsed;
|
|
1919
|
+
try {
|
|
1920
|
+
parsed = JSON.parse(result.stdout);
|
|
1921
|
+
} catch {
|
|
1922
|
+
return {
|
|
1923
|
+
status: "unknown",
|
|
1924
|
+
repository,
|
|
1925
|
+
canWrite: null,
|
|
1926
|
+
errorCode: "github-response-invalid",
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
const permissions = parsed.permissions || {};
|
|
1930
|
+
return {
|
|
1931
|
+
status: "observed",
|
|
1932
|
+
repository,
|
|
1933
|
+
canWrite: Boolean(
|
|
1934
|
+
permissions.push || permissions.maintain || permissions.admin,
|
|
1935
|
+
),
|
|
1936
|
+
defaultBranch: parsed.default_branch || "",
|
|
1937
|
+
archived: parsed.archived === true,
|
|
1938
|
+
visibility: parsed.visibility || (parsed.private ? "private" : "public"),
|
|
1939
|
+
errorCode: "",
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
function liveRepositoryActionsPolicyObservation(repository, cwd) {
|
|
1944
|
+
if (!repository) {
|
|
1945
|
+
return {
|
|
1946
|
+
status: "unknown",
|
|
1947
|
+
defaultWorkflowPermissions: "",
|
|
1948
|
+
canApprovePullRequestReviews: null,
|
|
1949
|
+
errorCode: "repository-unresolved",
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
const result = commandResult(
|
|
1953
|
+
"gh",
|
|
1954
|
+
["api", `repos/${repository}/actions/permissions/workflow`],
|
|
1955
|
+
{ cwd },
|
|
1956
|
+
);
|
|
1957
|
+
if (!result.ok) {
|
|
1958
|
+
return {
|
|
1959
|
+
status: "unknown",
|
|
1960
|
+
defaultWorkflowPermissions: "",
|
|
1961
|
+
canApprovePullRequestReviews: null,
|
|
1962
|
+
errorCode: result.error
|
|
1963
|
+
? "gh-unavailable"
|
|
1964
|
+
: "github-actions-policy-query-failed",
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
const parsed = safeParseJson(result.stdout);
|
|
1968
|
+
if (!parsed || typeof parsed !== "object") {
|
|
1969
|
+
return {
|
|
1970
|
+
status: "unknown",
|
|
1971
|
+
defaultWorkflowPermissions: "",
|
|
1972
|
+
canApprovePullRequestReviews: null,
|
|
1973
|
+
errorCode: "github-actions-policy-response-invalid",
|
|
1974
|
+
};
|
|
1975
|
+
}
|
|
1976
|
+
return {
|
|
1977
|
+
status: "observed",
|
|
1978
|
+
defaultWorkflowPermissions: String(
|
|
1979
|
+
parsed.default_workflow_permissions || "",
|
|
1980
|
+
),
|
|
1981
|
+
canApprovePullRequestReviews:
|
|
1982
|
+
parsed.can_approve_pull_request_reviews === true,
|
|
1983
|
+
errorCode: "",
|
|
1984
|
+
};
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
function liveGeneratedWriteAuthorityObservation(repository, cwd) {
|
|
1988
|
+
if (!repository) {
|
|
1989
|
+
return {
|
|
1990
|
+
status: "unknown",
|
|
1991
|
+
configured: null,
|
|
1992
|
+
mode: "",
|
|
1993
|
+
errorCode: "repository-unresolved",
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1996
|
+
const result = commandResult(
|
|
1997
|
+
"gh",
|
|
1998
|
+
["secret", "list", "--repo", repository, "--json", "name"],
|
|
1999
|
+
{ cwd },
|
|
2000
|
+
);
|
|
2001
|
+
if (!result.ok) {
|
|
2002
|
+
return {
|
|
2003
|
+
status: "unknown",
|
|
2004
|
+
configured: null,
|
|
2005
|
+
mode: "",
|
|
2006
|
+
errorCode: result.error
|
|
2007
|
+
? "gh-unavailable"
|
|
2008
|
+
: "github-secret-metadata-query-failed",
|
|
2009
|
+
};
|
|
2010
|
+
}
|
|
2011
|
+
const parsed = safeParseJson(result.stdout);
|
|
2012
|
+
const names = new Set(
|
|
2013
|
+
(Array.isArray(parsed) ? parsed : [])
|
|
2014
|
+
.map((entry) => String(entry?.name || ""))
|
|
2015
|
+
.filter(Boolean),
|
|
2016
|
+
);
|
|
2017
|
+
const appConfigured =
|
|
2018
|
+
names.has("BUILDCHAIN_GENERATED_WRITE_APP_CLIENT_ID") &&
|
|
2019
|
+
names.has("BUILDCHAIN_GENERATED_WRITE_APP_PRIVATE_KEY");
|
|
2020
|
+
const narrowTokenConfigured =
|
|
2021
|
+
names.has("BUILDCHAIN_GENERATED_WRITE_TOKEN") ||
|
|
2022
|
+
names.has("BUILDCHAIN_PROMOTION_TOKEN");
|
|
2023
|
+
return {
|
|
2024
|
+
status: "observed",
|
|
2025
|
+
configured: appConfigured || narrowTokenConfigured,
|
|
2026
|
+
mode: appConfigured
|
|
2027
|
+
? "github-app"
|
|
2028
|
+
: narrowTokenConfigured
|
|
2029
|
+
? "narrow-token"
|
|
2030
|
+
: "",
|
|
2031
|
+
errorCode: "",
|
|
2032
|
+
};
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
function localToolchainObservation(cwd, publication) {
|
|
2036
|
+
const toolchain = publication?.toolchain || {};
|
|
2037
|
+
const imageRef =
|
|
2038
|
+
toolchain.image && toolchain.digest
|
|
2039
|
+
? `${toolchain.image}@${toolchain.digest}`
|
|
2040
|
+
: "";
|
|
2041
|
+
const dockerVersion = commandResult(
|
|
2042
|
+
"docker",
|
|
2043
|
+
["version", "--format", "{{.Client.Version}}"],
|
|
2044
|
+
{
|
|
2045
|
+
cwd,
|
|
2046
|
+
timeout: 5000,
|
|
2047
|
+
},
|
|
2048
|
+
);
|
|
2049
|
+
const image = imageRef
|
|
2050
|
+
? commandResult(
|
|
2051
|
+
"docker",
|
|
2052
|
+
["image", "inspect", imageRef, "--format", "{{index .RepoDigests 0}}"],
|
|
2053
|
+
{
|
|
2054
|
+
cwd,
|
|
2055
|
+
timeout: 5000,
|
|
2056
|
+
},
|
|
2057
|
+
)
|
|
2058
|
+
: { ok: false };
|
|
2059
|
+
return {
|
|
2060
|
+
type: toolchain.type || "",
|
|
2061
|
+
image: toolchain.image || "",
|
|
2062
|
+
digest: toolchain.digest || "",
|
|
2063
|
+
command: toolchain.command || "",
|
|
2064
|
+
identityRoot: sha256Text(
|
|
2065
|
+
stableJson({
|
|
2066
|
+
type: toolchain.type || "",
|
|
2067
|
+
image: toolchain.image || "",
|
|
2068
|
+
digest: toolchain.digest || "",
|
|
2069
|
+
command: toolchain.command || "",
|
|
2070
|
+
}),
|
|
2071
|
+
),
|
|
2072
|
+
machineVerifiable: Boolean(
|
|
2073
|
+
toolchain.type === "latex-docker" &&
|
|
2074
|
+
toolchain.image &&
|
|
2075
|
+
SHA256_PATTERN.test(toolchain.digest || "") &&
|
|
2076
|
+
toolchain.command,
|
|
2077
|
+
),
|
|
2078
|
+
dockerAvailable: dockerVersion.ok,
|
|
2079
|
+
imageAvailableLocally: image.ok,
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
export function collectPaperPreflight({
|
|
2084
|
+
cwd = process.cwd(),
|
|
2085
|
+
buildchainRoot = process.cwd(),
|
|
2086
|
+
buildchainVersion = "",
|
|
2087
|
+
buildchainRef = "v3",
|
|
2088
|
+
buildchainSha = "",
|
|
2089
|
+
registry = NPM_REGISTRY,
|
|
2090
|
+
offline = false,
|
|
2091
|
+
} = {}) {
|
|
2092
|
+
const resolvedCwd = path.resolve(cwd);
|
|
2093
|
+
const status = collectPaperStatus({ cwd: resolvedCwd });
|
|
2094
|
+
const provisioning = validatePaperProvisioningAuthority(resolvedCwd);
|
|
2095
|
+
const configResult = paperConfig(resolvedCwd);
|
|
2096
|
+
let validation;
|
|
2097
|
+
let validationError = "";
|
|
2098
|
+
try {
|
|
2099
|
+
validation = validateBuildchainConfig(resolvedCwd, {
|
|
2100
|
+
requireLifecycleStages: ["verify"],
|
|
2101
|
+
});
|
|
2102
|
+
} catch (error) {
|
|
2103
|
+
validationError = error.message;
|
|
2104
|
+
}
|
|
2105
|
+
const runtime = runtimeFacts({
|
|
2106
|
+
buildchainRoot,
|
|
2107
|
+
buildchainVersion,
|
|
2108
|
+
buildchainRef: provisioning.value?.runtime?.ref || buildchainRef,
|
|
2109
|
+
buildchainSha,
|
|
2110
|
+
});
|
|
2111
|
+
const lockPath = path.resolve(resolvedCwd, PAPER_PATHS.contractLock);
|
|
2112
|
+
let lockEvaluation = {
|
|
2113
|
+
status: "missing-lock",
|
|
2114
|
+
compatible: false,
|
|
2115
|
+
drift: false,
|
|
2116
|
+
reasons: ["Buildchain contract lock is missing"],
|
|
2117
|
+
};
|
|
2118
|
+
try {
|
|
2119
|
+
const lock = readBuildchainContractLock(lockPath);
|
|
2120
|
+
if (lock) {
|
|
2121
|
+
const current = runtimeContractWorld(buildchainRoot);
|
|
2122
|
+
lockEvaluation = evaluateBuildchainContractLock({
|
|
2123
|
+
lock,
|
|
2124
|
+
current,
|
|
2125
|
+
runtimeRef: runtime.ref,
|
|
2126
|
+
runtimeSha: runtime.resolvedSha,
|
|
2127
|
+
runtimeClass: /alpha/i.test(runtime.ref) ? "alpha" : "stable",
|
|
2128
|
+
});
|
|
2129
|
+
}
|
|
2130
|
+
} catch (error) {
|
|
2131
|
+
lockEvaluation = {
|
|
2132
|
+
status: "invalid-lock",
|
|
2133
|
+
compatible: false,
|
|
2134
|
+
drift: false,
|
|
2135
|
+
reasons: [error.message],
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2138
|
+
const source = {
|
|
2139
|
+
repositoryRoot: gitValue(resolvedCwd, ["rev-parse", "--show-toplevel"]),
|
|
2140
|
+
head: gitValue(resolvedCwd, ["rev-parse", "HEAD"]),
|
|
2141
|
+
tree: gitValue(resolvedCwd, ["rev-parse", "HEAD^{tree}"]),
|
|
2142
|
+
branch: gitValue(resolvedCwd, ["branch", "--show-current"]),
|
|
2143
|
+
clean: gitResult(resolvedCwd, ["status", "--porcelain"]).stdout === "",
|
|
2144
|
+
};
|
|
2145
|
+
const packageName = status.identity.package;
|
|
2146
|
+
const repository = status.identity.repository;
|
|
2147
|
+
const expectedPublisher = expectedPaperTrustedPublisher(provisioning.value, {
|
|
2148
|
+
repository,
|
|
2149
|
+
workflow: path.posix.basename(PAPER_PATHS.releaseWorkflow),
|
|
2150
|
+
});
|
|
2151
|
+
const npm =
|
|
2152
|
+
offline || !packageName
|
|
2153
|
+
? {
|
|
2154
|
+
package: {
|
|
2155
|
+
status: "unknown",
|
|
2156
|
+
exists: null,
|
|
2157
|
+
version: "",
|
|
2158
|
+
registry,
|
|
2159
|
+
errorCode: offline ? "offline" : "package-unresolved",
|
|
2160
|
+
},
|
|
2161
|
+
auth: {
|
|
2162
|
+
status: "unknown",
|
|
2163
|
+
authenticated: null,
|
|
2164
|
+
identity: "",
|
|
2165
|
+
errorCode: offline ? "offline" : "package-unresolved",
|
|
2166
|
+
},
|
|
2167
|
+
trust: {
|
|
2168
|
+
status: "unknown",
|
|
2169
|
+
configured: null,
|
|
2170
|
+
publishers: [],
|
|
2171
|
+
errorCode: offline ? "offline" : "package-unresolved",
|
|
2172
|
+
},
|
|
2173
|
+
}
|
|
2174
|
+
: {
|
|
2175
|
+
package: liveNpmPackageObservation(
|
|
2176
|
+
packageName,
|
|
2177
|
+
registry,
|
|
2178
|
+
resolvedCwd,
|
|
2179
|
+
),
|
|
2180
|
+
auth: liveNpmAuthObservation(registry, resolvedCwd),
|
|
2181
|
+
trust: liveNpmTrustObservation(
|
|
2182
|
+
packageName,
|
|
2183
|
+
registry,
|
|
2184
|
+
resolvedCwd,
|
|
2185
|
+
expectedPublisher,
|
|
2186
|
+
),
|
|
2187
|
+
};
|
|
2188
|
+
const repositoryPermissions = offline
|
|
2189
|
+
? {
|
|
2190
|
+
status: "unknown",
|
|
2191
|
+
repository,
|
|
2192
|
+
canWrite: null,
|
|
2193
|
+
errorCode: "offline",
|
|
2194
|
+
}
|
|
2195
|
+
: liveRepositoryPermissionObservation(repository, resolvedCwd);
|
|
2196
|
+
const repositoryActions = offline
|
|
2197
|
+
? {
|
|
2198
|
+
status: "unknown",
|
|
2199
|
+
defaultWorkflowPermissions: "",
|
|
2200
|
+
canApprovePullRequestReviews: null,
|
|
2201
|
+
errorCode: "offline",
|
|
2202
|
+
}
|
|
2203
|
+
: liveRepositoryActionsPolicyObservation(repository, resolvedCwd);
|
|
2204
|
+
const generatedWriteAuthority = offline
|
|
2205
|
+
? {
|
|
2206
|
+
status: "unknown",
|
|
2207
|
+
configured: null,
|
|
2208
|
+
mode: "",
|
|
2209
|
+
errorCode: "offline",
|
|
2210
|
+
}
|
|
2211
|
+
: liveGeneratedWriteAuthorityObservation(repository, resolvedCwd);
|
|
2212
|
+
const toolchain = localToolchainObservation(
|
|
2213
|
+
resolvedCwd,
|
|
2214
|
+
configResult.loaded?.config?.publication,
|
|
2215
|
+
);
|
|
2216
|
+
const checks = [
|
|
2217
|
+
{
|
|
2218
|
+
id: "provisioning.authority",
|
|
2219
|
+
status: provisioning.valid ? "pass" : "fail",
|
|
2220
|
+
blocking: true,
|
|
2221
|
+
scope: "local",
|
|
2222
|
+
message: provisioning.valid
|
|
2223
|
+
? "Paper runtime, callers, contract lock, trust target, and policy share one exact authority root."
|
|
2224
|
+
: provisioning.errors.join("; "),
|
|
2225
|
+
},
|
|
2226
|
+
{
|
|
2227
|
+
id: "config.publication",
|
|
2228
|
+
status: validationError ? "fail" : "pass",
|
|
2229
|
+
blocking: true,
|
|
2230
|
+
scope: "local",
|
|
2231
|
+
message:
|
|
2232
|
+
validationError ||
|
|
2233
|
+
"Publication config, digest-pinned toolchain, and verify lifecycle are valid.",
|
|
2234
|
+
},
|
|
2235
|
+
{
|
|
2236
|
+
id: "source.exact-commit",
|
|
2237
|
+
status:
|
|
2238
|
+
GIT_SHA_PATTERN.test(source.head) && source.clean ? "pass" : "fail",
|
|
2239
|
+
blocking: false,
|
|
2240
|
+
scope: "external-mutation",
|
|
2241
|
+
message: !GIT_SHA_PATTERN.test(source.head)
|
|
2242
|
+
? "Repository HEAD is unresolved."
|
|
2243
|
+
: source.clean
|
|
2244
|
+
? "Source is bound to a clean exact commit."
|
|
2245
|
+
: "Working tree is dirty; reproducibility admits committed bytes only.",
|
|
2246
|
+
},
|
|
2247
|
+
{
|
|
2248
|
+
id: "toolchain.pinned",
|
|
2249
|
+
status: toolchain.machineVerifiable ? "pass" : "fail",
|
|
2250
|
+
blocking: true,
|
|
2251
|
+
scope: "local",
|
|
2252
|
+
message: toolchain.machineVerifiable
|
|
2253
|
+
? "Publication toolchain identity is digest-pinned."
|
|
2254
|
+
: "Publication toolchain is not a qualifying digest-pinned latex-docker toolchain.",
|
|
2255
|
+
},
|
|
2256
|
+
{
|
|
2257
|
+
id: "runtime.exact-source",
|
|
2258
|
+
status:
|
|
2259
|
+
runtime.version && GIT_SHA_PATTERN.test(runtime.resolvedSha)
|
|
2260
|
+
? "pass"
|
|
2261
|
+
: "fail",
|
|
2262
|
+
blocking: true,
|
|
2263
|
+
scope: "local",
|
|
2264
|
+
message:
|
|
2265
|
+
runtime.version && GIT_SHA_PATTERN.test(runtime.resolvedSha)
|
|
2266
|
+
? "Buildchain runtime is bound to an exact package version and source SHA."
|
|
2267
|
+
: "Buildchain runtime version or exact source SHA is unresolved.",
|
|
2268
|
+
},
|
|
2269
|
+
{
|
|
2270
|
+
id: "runtime.contract-lock",
|
|
2271
|
+
status: lockEvaluation.compatible ? "pass" : "fail",
|
|
2272
|
+
blocking: true,
|
|
2273
|
+
scope: "local",
|
|
2274
|
+
message: lockEvaluation.compatible
|
|
2275
|
+
? `Buildchain runtime contract is ${lockEvaluation.status}.`
|
|
2276
|
+
: `Buildchain runtime contract is not admitted: ${(lockEvaluation.reasons || []).join("; ")}`,
|
|
2277
|
+
},
|
|
2278
|
+
{
|
|
2279
|
+
id: "repository.write-permission",
|
|
2280
|
+
status:
|
|
2281
|
+
repositoryPermissions.canWrite === true
|
|
2282
|
+
? "pass"
|
|
2283
|
+
: repositoryPermissions.canWrite === false
|
|
2284
|
+
? "fail"
|
|
2285
|
+
: "unknown",
|
|
2286
|
+
blocking: false,
|
|
2287
|
+
scope: "external-mutation",
|
|
2288
|
+
message:
|
|
2289
|
+
repositoryPermissions.canWrite === true
|
|
2290
|
+
? "Current GitHub identity can write the repository."
|
|
2291
|
+
: repositoryPermissions.canWrite === false
|
|
2292
|
+
? "Current GitHub identity cannot write the repository."
|
|
2293
|
+
: `Repository permission is unknown (${repositoryPermissions.errorCode}).`,
|
|
2294
|
+
},
|
|
2295
|
+
{
|
|
2296
|
+
id: "repository.actions-policy",
|
|
2297
|
+
status:
|
|
2298
|
+
repositoryActions.defaultWorkflowPermissions === "read" &&
|
|
2299
|
+
repositoryActions.canApprovePullRequestReviews === false
|
|
2300
|
+
? "pass"
|
|
2301
|
+
: repositoryActions.status === "observed"
|
|
2302
|
+
? "fail"
|
|
2303
|
+
: "unknown",
|
|
2304
|
+
blocking: false,
|
|
2305
|
+
scope: "external-mutation",
|
|
2306
|
+
message:
|
|
2307
|
+
repositoryActions.defaultWorkflowPermissions === "read" &&
|
|
2308
|
+
repositoryActions.canApprovePullRequestReviews === false
|
|
2309
|
+
? "Repository defaults workflow permissions to read and disables Actions pull-request approval."
|
|
2310
|
+
: repositoryActions.status === "observed"
|
|
2311
|
+
? "Repository Actions policy is broader than the paper provisioning authority."
|
|
2312
|
+
: `Repository Actions policy is unknown (${repositoryActions.errorCode}).`,
|
|
2313
|
+
},
|
|
2314
|
+
{
|
|
2315
|
+
id: "repository.generated-write-authority",
|
|
2316
|
+
status:
|
|
2317
|
+
generatedWriteAuthority.configured === true
|
|
2318
|
+
? "pass"
|
|
2319
|
+
: generatedWriteAuthority.configured === false
|
|
2320
|
+
? "pending"
|
|
2321
|
+
: "unknown",
|
|
2322
|
+
blocking: false,
|
|
2323
|
+
scope: "external-mutation",
|
|
2324
|
+
message:
|
|
2325
|
+
generatedWriteAuthority.configured === true
|
|
2326
|
+
? `Generated writes use ${generatedWriteAuthority.mode} metadata; no secret value was read.`
|
|
2327
|
+
: generatedWriteAuthority.configured === false
|
|
2328
|
+
? "No GitHub App or compatible narrow generated-write credential is configured."
|
|
2329
|
+
: `Generated-write authority is unknown (${generatedWriteAuthority.errorCode}).`,
|
|
2330
|
+
},
|
|
2331
|
+
{
|
|
2332
|
+
id: "npm.package",
|
|
2333
|
+
status:
|
|
2334
|
+
npm.package.exists === true
|
|
2335
|
+
? "pass"
|
|
2336
|
+
: npm.package.exists === false
|
|
2337
|
+
? "pending"
|
|
2338
|
+
: "unknown",
|
|
2339
|
+
blocking: false,
|
|
2340
|
+
scope: "external-mutation",
|
|
2341
|
+
message:
|
|
2342
|
+
npm.package.exists === true
|
|
2343
|
+
? `npm package exists at ${npm.package.version}.`
|
|
2344
|
+
: npm.package.exists === false
|
|
2345
|
+
? "npm package does not exist and requires bootstrap."
|
|
2346
|
+
: `npm package existence is unknown (${npm.package.errorCode}).`,
|
|
2347
|
+
},
|
|
2348
|
+
{
|
|
2349
|
+
id: "npm.trusted-publisher",
|
|
2350
|
+
status:
|
|
2351
|
+
npm.trust.configured === true
|
|
2352
|
+
? "pass"
|
|
2353
|
+
: npm.trust.configured === false
|
|
2354
|
+
? "pending"
|
|
2355
|
+
: "unknown",
|
|
2356
|
+
blocking: false,
|
|
2357
|
+
scope: "external-mutation",
|
|
2358
|
+
message:
|
|
2359
|
+
npm.trust.configured === true
|
|
2360
|
+
? "npm reports exactly the expected repository, workflow, and environment Trusted Publisher binding."
|
|
2361
|
+
: npm.trust.configured === false
|
|
2362
|
+
? "npm does not report the exact expected Trusted Publisher binding."
|
|
2363
|
+
: `npm Trusted Publisher status is unknown (${npm.trust.errorCode}).`,
|
|
2364
|
+
},
|
|
2365
|
+
{
|
|
2366
|
+
id: "build.reproducible",
|
|
2367
|
+
status:
|
|
2368
|
+
status.deterministicBuild.status === "qualifying" ? "pass" : "pending",
|
|
2369
|
+
blocking: false,
|
|
2370
|
+
scope: "local",
|
|
2371
|
+
message:
|
|
2372
|
+
status.deterministicBuild.status === "qualifying"
|
|
2373
|
+
? "A qualifying two-clean-build receipt exists."
|
|
2374
|
+
: "No qualifying two-clean-build receipt exists for the exact source.",
|
|
2375
|
+
},
|
|
2376
|
+
{
|
|
2377
|
+
id: "release.state-conflicts",
|
|
2378
|
+
status: status.conflicts.length === 0 ? "pass" : "fail",
|
|
2379
|
+
blocking: true,
|
|
2380
|
+
scope: "local",
|
|
2381
|
+
message:
|
|
2382
|
+
status.conflicts.length === 0
|
|
2383
|
+
? "No local release-state conflict was detected."
|
|
2384
|
+
: `${status.conflicts.length} release-state conflict(s) require repair.`,
|
|
2385
|
+
},
|
|
2386
|
+
];
|
|
2387
|
+
const blockingChecks = checks.filter(
|
|
2388
|
+
(entry) => entry.blocking && ["fail", "unknown"].includes(entry.status),
|
|
2389
|
+
);
|
|
2390
|
+
const externalMutationChecks = checks.filter(
|
|
2391
|
+
(entry) =>
|
|
2392
|
+
entry.scope === "external-mutation" &&
|
|
2393
|
+
["fail", "pending", "unknown"].includes(entry.status),
|
|
2394
|
+
);
|
|
2395
|
+
const nextActions = [];
|
|
2396
|
+
if (validationError) {
|
|
2397
|
+
nextActions.push({
|
|
2398
|
+
id: "repair-config",
|
|
2399
|
+
command: "buildchain validate --require-lifecycle-stages verify",
|
|
2400
|
+
description: validationError,
|
|
2401
|
+
});
|
|
2402
|
+
}
|
|
2403
|
+
if (!source.clean) {
|
|
2404
|
+
nextActions.push({
|
|
2405
|
+
id: "commit-source",
|
|
2406
|
+
command: "git status --short",
|
|
2407
|
+
description:
|
|
2408
|
+
"Review and commit the exact source before deterministic publication.",
|
|
2409
|
+
});
|
|
2410
|
+
}
|
|
2411
|
+
if (!lockEvaluation.compatible) {
|
|
2412
|
+
nextActions.push({
|
|
2413
|
+
id: "refresh-contract-lock",
|
|
2414
|
+
command: "buildchain paper scaffold --json",
|
|
2415
|
+
description:
|
|
2416
|
+
"Review the current runtime contract and resolve the contract-lock difference without overwriting repository files.",
|
|
2417
|
+
});
|
|
2418
|
+
}
|
|
2419
|
+
if (npm.package.exists === false || npm.trust.configured === false) {
|
|
2420
|
+
nextActions.push({
|
|
2421
|
+
id: "bootstrap-npm",
|
|
2422
|
+
command: "buildchain paper bootstrap npm --json",
|
|
2423
|
+
description:
|
|
2424
|
+
"Run the public-package bootstrap and Trusted Publishing dry-run.",
|
|
2425
|
+
});
|
|
2426
|
+
}
|
|
2427
|
+
if (
|
|
2428
|
+
repositoryActions.status === "observed" &&
|
|
2429
|
+
(repositoryActions.defaultWorkflowPermissions !== "read" ||
|
|
2430
|
+
repositoryActions.canApprovePullRequestReviews !== false)
|
|
2431
|
+
) {
|
|
2432
|
+
nextActions.push({
|
|
2433
|
+
id: "constrain-repository-actions",
|
|
2434
|
+
command: "",
|
|
2435
|
+
description:
|
|
2436
|
+
"Set default workflow permissions to read and disable Actions pull-request approval through the repository provisioner.",
|
|
2437
|
+
});
|
|
2438
|
+
}
|
|
2439
|
+
if (generatedWriteAuthority.configured === false) {
|
|
2440
|
+
nextActions.push({
|
|
2441
|
+
id: "configure-generated-write-authority",
|
|
2442
|
+
command: "",
|
|
2443
|
+
description:
|
|
2444
|
+
"Install a least-privilege GitHub App or configure an equivalent narrow generated-write token without exposing its value.",
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2447
|
+
if (status.deterministicBuild.status !== "qualifying") {
|
|
2448
|
+
nextActions.push({
|
|
2449
|
+
id: "build-paper",
|
|
2450
|
+
command: "buildchain paper build --execute --json",
|
|
2451
|
+
description: "Run the existing qualifying two-clean-build gate.",
|
|
2452
|
+
});
|
|
2453
|
+
}
|
|
2454
|
+
return {
|
|
2455
|
+
schemaVersion: 1,
|
|
2456
|
+
contract: PAPER_PREFLIGHT_CONTRACT,
|
|
2457
|
+
ok: blockingChecks.length === 0,
|
|
2458
|
+
localReady: blockingChecks.length === 0,
|
|
2459
|
+
readyForExternalMutation:
|
|
2460
|
+
blockingChecks.length === 0 && externalMutationChecks.length === 0,
|
|
2461
|
+
cwd: resolvedCwd,
|
|
2462
|
+
offline,
|
|
2463
|
+
identity: status.identity,
|
|
2464
|
+
source,
|
|
2465
|
+
toolchain,
|
|
2466
|
+
runtime: {
|
|
2467
|
+
...runtime,
|
|
2468
|
+
admission: {
|
|
2469
|
+
contractLockPath: PAPER_PATHS.contractLock,
|
|
2470
|
+
status: lockEvaluation.status,
|
|
2471
|
+
compatible: lockEvaluation.compatible === true,
|
|
2472
|
+
drift: lockEvaluation.drift === true,
|
|
2473
|
+
reasons: lockEvaluation.reasons || [],
|
|
2474
|
+
},
|
|
2475
|
+
},
|
|
2476
|
+
provisioning: {
|
|
2477
|
+
path: PAPER_PATHS.provisioningAuthority,
|
|
2478
|
+
valid: provisioning.valid,
|
|
2479
|
+
authorityDigest: provisioning.value?.authorityDigest || "",
|
|
2480
|
+
policyDigest: provisioning.value?.policy?.policyDigest || "",
|
|
2481
|
+
errors: provisioning.errors,
|
|
2482
|
+
},
|
|
2483
|
+
repositoryPermissions,
|
|
2484
|
+
repositoryActions,
|
|
2485
|
+
generatedWriteAuthority,
|
|
2486
|
+
npm,
|
|
2487
|
+
deterministicBuild: status.deterministicBuild,
|
|
2488
|
+
releaseState: {
|
|
2489
|
+
transaction: status.transaction,
|
|
2490
|
+
conflicts: status.conflicts,
|
|
2491
|
+
},
|
|
2492
|
+
checks,
|
|
2493
|
+
blockingNextActions: nextActions,
|
|
2494
|
+
status,
|
|
2495
|
+
};
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
export function createPaperBuildPlan({
|
|
2499
|
+
cwd = process.cwd(),
|
|
2500
|
+
sourceSha = "",
|
|
2501
|
+
pullToolchain = true,
|
|
2502
|
+
} = {}) {
|
|
2503
|
+
const resolvedCwd = path.resolve(cwd);
|
|
2504
|
+
const configResult = paperConfig(resolvedCwd);
|
|
2505
|
+
if (configResult.error) throw new Error(configResult.error);
|
|
2506
|
+
const resolvedSha = sourceSha || gitValue(resolvedCwd, ["rev-parse", "HEAD"]);
|
|
2507
|
+
return {
|
|
2508
|
+
schemaVersion: 1,
|
|
2509
|
+
contract: PAPER_BUILD_PLAN_CONTRACT,
|
|
2510
|
+
ok: GIT_SHA_PATTERN.test(resolvedSha),
|
|
2511
|
+
cwd: resolvedCwd,
|
|
2512
|
+
dryRun: true,
|
|
2513
|
+
sourceSha: resolvedSha,
|
|
2514
|
+
toolchain: configResult.loaded.config.publication.toolchain,
|
|
2515
|
+
pullToolchain,
|
|
2516
|
+
output: PAPER_PATHS.reproducibilityReceipt,
|
|
2517
|
+
promotion: "first-byte-identical-clean-build",
|
|
2518
|
+
delegatesTo: "buildchain publication-artifact reproducibility --promote",
|
|
2519
|
+
nextActions: [
|
|
2520
|
+
{
|
|
2521
|
+
id: "execute-build",
|
|
2522
|
+
command: "buildchain paper build --execute --json",
|
|
2523
|
+
description:
|
|
2524
|
+
"Run two independent clean builds and promote only byte-identical qualifying outputs.",
|
|
2525
|
+
},
|
|
2526
|
+
],
|
|
2527
|
+
};
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
function parsePaperVersion(version) {
|
|
2531
|
+
const normalized = String(version || "")
|
|
2532
|
+
.trim()
|
|
2533
|
+
.replace(/^v/, "");
|
|
2534
|
+
const match = normalized.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
|
|
2535
|
+
if (!match) {
|
|
2536
|
+
throw new Error("publication.version must be semver before planning Alpha");
|
|
2537
|
+
}
|
|
2538
|
+
return {
|
|
2539
|
+
version: normalized,
|
|
2540
|
+
major: Number(match[1]),
|
|
2541
|
+
minor: Number(match[2]),
|
|
2542
|
+
patch: Number(match[3]),
|
|
2543
|
+
prerelease: match[4] || "",
|
|
2544
|
+
};
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
function resolvePaperChannelRef(cwd, ref) {
|
|
2548
|
+
const candidates = [
|
|
2549
|
+
{
|
|
2550
|
+
observedRef: `refs/remotes/origin/${ref}`,
|
|
2551
|
+
observation: "origin-tracking-ref",
|
|
2552
|
+
},
|
|
2553
|
+
{
|
|
2554
|
+
observedRef: `refs/heads/${ref}`,
|
|
2555
|
+
observation: "local-branch-ref",
|
|
2556
|
+
},
|
|
2557
|
+
];
|
|
2558
|
+
for (const candidate of candidates) {
|
|
2559
|
+
const sha = gitValue(cwd, [
|
|
2560
|
+
"rev-parse",
|
|
2561
|
+
"--verify",
|
|
2562
|
+
`${candidate.observedRef}^{commit}`,
|
|
2563
|
+
]);
|
|
2564
|
+
if (GIT_SHA_PATTERN.test(sha)) {
|
|
2565
|
+
return {
|
|
2566
|
+
sha,
|
|
2567
|
+
...candidate,
|
|
2568
|
+
};
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
return {
|
|
2572
|
+
sha: "",
|
|
2573
|
+
observedRef: "",
|
|
2574
|
+
observation: "unresolved",
|
|
2575
|
+
};
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
export function createPaperAlphaPlan({
|
|
2579
|
+
cwd = process.cwd(),
|
|
2580
|
+
sourceRef = "",
|
|
2581
|
+
targetRef = "",
|
|
2582
|
+
} = {}) {
|
|
2583
|
+
const resolvedCwd = path.resolve(cwd);
|
|
2584
|
+
const configResult = paperConfig(resolvedCwd);
|
|
2585
|
+
if (configResult.error) throw new Error(configResult.error);
|
|
2586
|
+
const publication = configResult.loaded.config.publication;
|
|
2587
|
+
const parsed = parsePaperVersion(publication.version);
|
|
2588
|
+
const line = `v${parsed.major}.${parsed.minor}`;
|
|
2589
|
+
const source = sourceRef || `dev/v${parsed.major}/${line}`;
|
|
2590
|
+
const target = targetRef || `alpha/v${parsed.major}/${line}`;
|
|
2591
|
+
const repository = resolvePaperRepository(resolvedCwd);
|
|
2592
|
+
const sourceObservation = resolvePaperChannelRef(resolvedCwd, source);
|
|
2593
|
+
const targetObservation = resolvePaperChannelRef(resolvedCwd, target);
|
|
2594
|
+
const currentBranch = gitValue(resolvedCwd, ["branch", "--show-current"]);
|
|
2595
|
+
return {
|
|
2596
|
+
schemaVersion: 1,
|
|
2597
|
+
contract: PAPER_ALPHA_PLAN_CONTRACT,
|
|
2598
|
+
ok: Boolean(repository && source && target && source !== target),
|
|
2599
|
+
cwd: resolvedCwd,
|
|
2600
|
+
dryRun: true,
|
|
2601
|
+
repository,
|
|
2602
|
+
package: configResult.loaded.config.publish?.package || "",
|
|
2603
|
+
publicationVersion: parsed.version,
|
|
2604
|
+
channel: "alpha",
|
|
2605
|
+
source: {
|
|
2606
|
+
ref: source,
|
|
2607
|
+
...sourceObservation,
|
|
2608
|
+
},
|
|
2609
|
+
target: {
|
|
2610
|
+
ref: target,
|
|
2611
|
+
...targetObservation,
|
|
2612
|
+
},
|
|
2613
|
+
currentBranch,
|
|
2614
|
+
mutation: {
|
|
2615
|
+
kind: "github-protected-channel-pr",
|
|
2616
|
+
directPublish: false,
|
|
2617
|
+
directMerge: false,
|
|
2618
|
+
command: `gh pr create --repo ${repository || "<owner/repo>"} --base ${target} --head ${source}`,
|
|
2619
|
+
},
|
|
2620
|
+
gates: [
|
|
2621
|
+
"source is the protected dev channel for the configured semver line",
|
|
2622
|
+
"target is the protected alpha channel",
|
|
2623
|
+
"the paper release workflow seals exact bytes and uses npm OIDC",
|
|
2624
|
+
"this command opens a PR but never merges it or publishes directly",
|
|
2625
|
+
],
|
|
2626
|
+
nextActions: [
|
|
2627
|
+
{
|
|
2628
|
+
id: "open-alpha-pr",
|
|
2629
|
+
command: "buildchain paper alpha --execute --json",
|
|
2630
|
+
description: "Open or reuse the protected dev-to-alpha publication PR.",
|
|
2631
|
+
},
|
|
2632
|
+
],
|
|
2633
|
+
};
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
export function createPaperResumePlan({
|
|
2637
|
+
cwd = process.cwd(),
|
|
2638
|
+
buildchainRef = "",
|
|
2639
|
+
} = {}) {
|
|
2640
|
+
const resolvedCwd = path.resolve(cwd);
|
|
2641
|
+
const status = collectPaperStatus({ cwd: resolvedCwd });
|
|
2642
|
+
const transaction = status.transaction;
|
|
2643
|
+
if (!transaction) {
|
|
2644
|
+
return {
|
|
2645
|
+
schemaVersion: 1,
|
|
2646
|
+
contract: PAPER_RESUME_PLAN_CONTRACT,
|
|
2647
|
+
ok: false,
|
|
2648
|
+
cwd: resolvedCwd,
|
|
2649
|
+
dryRun: true,
|
|
2650
|
+
resumable: false,
|
|
2651
|
+
reason: "no-release-transaction",
|
|
2652
|
+
transaction: null,
|
|
2653
|
+
nextActions: [
|
|
2654
|
+
{
|
|
2655
|
+
id: "start-alpha",
|
|
2656
|
+
command: "buildchain paper alpha --json",
|
|
2657
|
+
description:
|
|
2658
|
+
"No transaction exists; plan the protected Alpha publication first.",
|
|
2659
|
+
},
|
|
2660
|
+
],
|
|
2661
|
+
};
|
|
2662
|
+
}
|
|
2663
|
+
if (
|
|
2664
|
+
["alpha-complete", "release-complete"].includes(
|
|
2665
|
+
transaction.publicationState,
|
|
2666
|
+
)
|
|
2667
|
+
) {
|
|
2668
|
+
return {
|
|
2669
|
+
schemaVersion: 1,
|
|
2670
|
+
contract: PAPER_RESUME_PLAN_CONTRACT,
|
|
2671
|
+
ok: true,
|
|
2672
|
+
cwd: resolvedCwd,
|
|
2673
|
+
dryRun: true,
|
|
2674
|
+
resumable: false,
|
|
2675
|
+
reason: "transaction-complete",
|
|
2676
|
+
transaction,
|
|
2677
|
+
nextActions: [],
|
|
2678
|
+
};
|
|
2679
|
+
}
|
|
2680
|
+
const targetRef = transaction.targetRef;
|
|
2681
|
+
const command = [
|
|
2682
|
+
"gh workflow run .github/workflows/paper-release.yml",
|
|
2683
|
+
targetRef ? `--ref ${targetRef}` : "",
|
|
2684
|
+
buildchainRef ? `-f buildchain-ref=${buildchainRef}` : "",
|
|
2685
|
+
]
|
|
2686
|
+
.filter(Boolean)
|
|
2687
|
+
.join(" ");
|
|
2688
|
+
return {
|
|
2689
|
+
schemaVersion: 1,
|
|
2690
|
+
contract: PAPER_RESUME_PLAN_CONTRACT,
|
|
2691
|
+
ok: Boolean(targetRef),
|
|
2692
|
+
cwd: resolvedCwd,
|
|
2693
|
+
dryRun: true,
|
|
2694
|
+
resumable: Boolean(targetRef),
|
|
2695
|
+
reason: targetRef
|
|
2696
|
+
? "rerun-sealed-paper-release"
|
|
2697
|
+
: "transaction-target-ref-missing",
|
|
2698
|
+
transaction,
|
|
2699
|
+
mutation: {
|
|
2700
|
+
kind: "github-workflow-dispatch",
|
|
2701
|
+
directPublish: false,
|
|
2702
|
+
command,
|
|
2703
|
+
},
|
|
2704
|
+
nextActions: targetRef
|
|
2705
|
+
? [
|
|
2706
|
+
{
|
|
2707
|
+
id: "dispatch-resume",
|
|
2708
|
+
command: "buildchain paper resume --execute --json",
|
|
2709
|
+
description:
|
|
2710
|
+
"Dispatch the thin repository workflow against the exact transaction target ref.",
|
|
2711
|
+
},
|
|
2712
|
+
]
|
|
2713
|
+
: [],
|
|
2714
|
+
};
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
function extractUrls(value, output = new Set()) {
|
|
2718
|
+
if (typeof value === "string") {
|
|
2719
|
+
for (const match of value.matchAll(/https?:\/\/[^\s"'<>]+/g)) {
|
|
2720
|
+
output.add(match[0].replace(/[),.;]+$/, ""));
|
|
2721
|
+
}
|
|
2722
|
+
return output;
|
|
2723
|
+
}
|
|
2724
|
+
if (Array.isArray(value)) {
|
|
2725
|
+
for (const entry of value) extractUrls(entry, output);
|
|
2726
|
+
return output;
|
|
2727
|
+
}
|
|
2728
|
+
if (value && typeof value === "object") {
|
|
2729
|
+
for (const entry of Object.values(value)) extractUrls(entry, output);
|
|
2730
|
+
}
|
|
2731
|
+
return output;
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
function safeParseJson(value) {
|
|
2735
|
+
try {
|
|
2736
|
+
return JSON.parse(value);
|
|
2737
|
+
} catch {
|
|
2738
|
+
return undefined;
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2742
|
+
function writePaperReceipt(cwd, relativePath, value) {
|
|
2743
|
+
const target = path.resolve(cwd, relativePath);
|
|
2744
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
2745
|
+
fs.writeFileSync(target, jsonText(value));
|
|
2746
|
+
return relativePath;
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
function bootstrapPackageShape({ packageName, version }) {
|
|
2750
|
+
return {
|
|
2751
|
+
name: packageName,
|
|
2752
|
+
version,
|
|
2753
|
+
private: false,
|
|
2754
|
+
license: "Apache-2.0",
|
|
2755
|
+
description: "Bootstrap package for npm Trusted Publishing setup.",
|
|
2756
|
+
publishConfig: {
|
|
2757
|
+
access: "public",
|
|
2758
|
+
registry: NPM_REGISTRY,
|
|
2759
|
+
},
|
|
2760
|
+
};
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
export function executePaperNpmBootstrap({
|
|
2764
|
+
cwd = process.cwd(),
|
|
2765
|
+
packageName = "",
|
|
2766
|
+
bootstrapVersion = DEFAULT_BOOTSTRAP_VERSION,
|
|
2767
|
+
registry = NPM_REGISTRY,
|
|
2768
|
+
repository = "",
|
|
2769
|
+
workflow = "paper-release.yml",
|
|
2770
|
+
environment = "",
|
|
2771
|
+
execute = false,
|
|
2772
|
+
confirmedPackage = "",
|
|
2773
|
+
userconfig = "",
|
|
2774
|
+
offline = false,
|
|
2775
|
+
} = {}) {
|
|
2776
|
+
const resolvedCwd = path.resolve(cwd);
|
|
2777
|
+
if (registry !== NPM_REGISTRY) {
|
|
2778
|
+
throw new Error(
|
|
2779
|
+
`paper npm bootstrap requires the official registry ${NPM_REGISTRY}`,
|
|
2780
|
+
);
|
|
2781
|
+
}
|
|
2782
|
+
if (bootstrapVersion !== DEFAULT_BOOTSTRAP_VERSION) {
|
|
2783
|
+
throw new Error(
|
|
2784
|
+
`paper npm bootstrap version is fixed at ${DEFAULT_BOOTSTRAP_VERSION}`,
|
|
2785
|
+
);
|
|
2786
|
+
}
|
|
2787
|
+
const configResult = paperConfig(resolvedCwd);
|
|
2788
|
+
if (configResult.error) throw new Error(configResult.error);
|
|
2789
|
+
const name = normalizePackageName(
|
|
2790
|
+
packageName ||
|
|
2791
|
+
configResult.loaded.config.publish?.package ||
|
|
2792
|
+
configResult.loaded.config.publish?.mainPackage,
|
|
2793
|
+
);
|
|
2794
|
+
const repo = normalizeRepository(
|
|
2795
|
+
repository || resolvePaperRepository(resolvedCwd),
|
|
2796
|
+
);
|
|
2797
|
+
const provisioning = validatePaperProvisioningAuthority(resolvedCwd);
|
|
2798
|
+
if (!provisioning.valid) {
|
|
2799
|
+
throw new Error(
|
|
2800
|
+
`paper npm bootstrap requires a valid provisioning authority: ${provisioning.errors.join("; ")}`,
|
|
2801
|
+
);
|
|
2802
|
+
}
|
|
2803
|
+
const expectedPublisher = expectedPaperTrustedPublisher(provisioning.value);
|
|
2804
|
+
if (
|
|
2805
|
+
provisioning.value.package?.name !== name ||
|
|
2806
|
+
expectedPublisher.repository !== repo ||
|
|
2807
|
+
expectedPublisher.workflow !== toPosix(workflow).replace(/^\/+/, "") ||
|
|
2808
|
+
expectedPublisher.environment !== String(environment || "")
|
|
2809
|
+
) {
|
|
2810
|
+
throw new Error(
|
|
2811
|
+
"paper npm bootstrap coordinates differ from the exact provisioning authority",
|
|
2812
|
+
);
|
|
2813
|
+
}
|
|
2814
|
+
if (execute && confirmedPackage !== name) {
|
|
2815
|
+
throw new Error(
|
|
2816
|
+
`real npm bootstrap requires --confirm-public-package ${name}`,
|
|
2817
|
+
);
|
|
2818
|
+
}
|
|
2819
|
+
if (execute && offline) {
|
|
2820
|
+
throw new Error("real npm bootstrap cannot run with --offline");
|
|
2821
|
+
}
|
|
2822
|
+
if (execute && (!repo || !workflow)) {
|
|
2823
|
+
throw new Error(
|
|
2824
|
+
"real npm bootstrap requires GitHub repository and workflow coordinates",
|
|
2825
|
+
);
|
|
2826
|
+
}
|
|
2827
|
+
const packageObservation = offline
|
|
2828
|
+
? {
|
|
2829
|
+
status: "unknown",
|
|
2830
|
+
exists: null,
|
|
2831
|
+
version: "",
|
|
2832
|
+
registry,
|
|
2833
|
+
errorCode: "offline",
|
|
2834
|
+
}
|
|
2835
|
+
: liveNpmPackageObservation(name, registry, resolvedCwd);
|
|
2836
|
+
const auth = offline
|
|
2837
|
+
? {
|
|
2838
|
+
status: "unknown",
|
|
2839
|
+
authenticated: null,
|
|
2840
|
+
identity: "",
|
|
2841
|
+
errorCode: "offline",
|
|
2842
|
+
}
|
|
2843
|
+
: liveNpmAuthObservation(registry, resolvedCwd);
|
|
2844
|
+
const plan = {
|
|
2845
|
+
schemaVersion: 1,
|
|
2846
|
+
contract: PAPER_NPM_BOOTSTRAP_CONTRACT,
|
|
2847
|
+
ok: true,
|
|
2848
|
+
dryRun: !execute,
|
|
2849
|
+
externalMutation: execute,
|
|
2850
|
+
package: {
|
|
2851
|
+
name,
|
|
2852
|
+
bootstrapVersion,
|
|
2853
|
+
registry,
|
|
2854
|
+
existsBefore: packageObservation.exists,
|
|
2855
|
+
observedVersion: packageObservation.version,
|
|
2856
|
+
},
|
|
2857
|
+
repository: {
|
|
2858
|
+
github: repo,
|
|
2859
|
+
workflow,
|
|
2860
|
+
environment,
|
|
2861
|
+
},
|
|
2862
|
+
authority: {
|
|
2863
|
+
digest: provisioning.value.authorityDigest,
|
|
2864
|
+
policyDigest: provisioning.value.policy.policyDigest,
|
|
2865
|
+
},
|
|
2866
|
+
auth: {
|
|
2867
|
+
authenticated: auth.authenticated,
|
|
2868
|
+
identity: auth.identity,
|
|
2869
|
+
errorCode: auth.errorCode,
|
|
2870
|
+
},
|
|
2871
|
+
publish: {
|
|
2872
|
+
status: packageObservation.exists === true ? "existing" : "planned",
|
|
2873
|
+
distTag: "bootstrap",
|
|
2874
|
+
access: "public",
|
|
2875
|
+
},
|
|
2876
|
+
trust: {
|
|
2877
|
+
status: "planned",
|
|
2878
|
+
urls: [],
|
|
2879
|
+
},
|
|
2880
|
+
receipts: [],
|
|
2881
|
+
nextActions: execute
|
|
2882
|
+
? []
|
|
2883
|
+
: [
|
|
2884
|
+
{
|
|
2885
|
+
id: "confirm-public-bootstrap",
|
|
2886
|
+
command: `buildchain paper bootstrap npm --execute --confirm-public-package ${name} --json`,
|
|
2887
|
+
description:
|
|
2888
|
+
"After reviewing this dry-run, bootstrap the exact public package and configure GitHub Trusted Publishing.",
|
|
2889
|
+
},
|
|
2890
|
+
],
|
|
2891
|
+
};
|
|
2892
|
+
if (!execute) {
|
|
2893
|
+
const dryRunRoot = fs.mkdtempSync(
|
|
2894
|
+
path.join(os.tmpdir(), "buildchain-paper-bootstrap-dry-run-"),
|
|
2895
|
+
);
|
|
2896
|
+
try {
|
|
2897
|
+
fs.writeFileSync(
|
|
2898
|
+
path.join(dryRunRoot, "package.json"),
|
|
2899
|
+
jsonText(
|
|
2900
|
+
bootstrapPackageShape({
|
|
2901
|
+
packageName: name,
|
|
2902
|
+
version: bootstrapVersion,
|
|
2903
|
+
}),
|
|
2904
|
+
),
|
|
2905
|
+
);
|
|
2906
|
+
const configArgs = userconfig ? ["--userconfig", userconfig] : [];
|
|
2907
|
+
const pack = commandResult(
|
|
2908
|
+
"npm",
|
|
2909
|
+
[
|
|
2910
|
+
"pack",
|
|
2911
|
+
"--dry-run",
|
|
2912
|
+
"--json",
|
|
2913
|
+
"--ignore-scripts",
|
|
2914
|
+
`--registry=${registry}`,
|
|
2915
|
+
...configArgs,
|
|
2916
|
+
],
|
|
2917
|
+
{ cwd: dryRunRoot },
|
|
2918
|
+
);
|
|
2919
|
+
const publish = commandResult(
|
|
2920
|
+
"npm",
|
|
2921
|
+
[
|
|
2922
|
+
"publish",
|
|
2923
|
+
"--dry-run",
|
|
2924
|
+
"--ignore-scripts",
|
|
2925
|
+
"--access",
|
|
2926
|
+
"public",
|
|
2927
|
+
"--tag",
|
|
2928
|
+
"bootstrap",
|
|
2929
|
+
`--registry=${registry}`,
|
|
2930
|
+
...configArgs,
|
|
2931
|
+
],
|
|
2932
|
+
{ cwd: dryRunRoot },
|
|
2933
|
+
);
|
|
2934
|
+
return {
|
|
2935
|
+
...plan,
|
|
2936
|
+
ok: pack.ok && publish.ok,
|
|
2937
|
+
errorCode: !pack.ok
|
|
2938
|
+
? "npm-pack-dry-run-failed"
|
|
2939
|
+
: !publish.ok
|
|
2940
|
+
? "npm-publish-dry-run-failed"
|
|
2941
|
+
: "",
|
|
2942
|
+
dryRunChecks: {
|
|
2943
|
+
minimalPackageOnly: true,
|
|
2944
|
+
pack: {
|
|
2945
|
+
status: pack.ok ? "pass" : "fail",
|
|
2946
|
+
entryCount: (() => {
|
|
2947
|
+
const parsed = safeParseJson(pack.stdout);
|
|
2948
|
+
const value = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
2949
|
+
return Array.isArray(value?.files) ? value.files.length : 0;
|
|
2950
|
+
})(),
|
|
2951
|
+
},
|
|
2952
|
+
publish: {
|
|
2953
|
+
status: publish.ok ? "pass" : "fail",
|
|
2954
|
+
registry,
|
|
2955
|
+
access: "public",
|
|
2956
|
+
distTag: "bootstrap",
|
|
2957
|
+
},
|
|
2958
|
+
},
|
|
2959
|
+
};
|
|
2960
|
+
} finally {
|
|
2961
|
+
fs.rmSync(dryRunRoot, { recursive: true, force: true });
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
if (packageObservation.exists === null) {
|
|
2965
|
+
return {
|
|
2966
|
+
...plan,
|
|
2967
|
+
ok: false,
|
|
2968
|
+
errorCode: "npm-package-status-unknown",
|
|
2969
|
+
publish: {
|
|
2970
|
+
...plan.publish,
|
|
2971
|
+
status: "blocked",
|
|
2972
|
+
},
|
|
2973
|
+
trust: {
|
|
2974
|
+
status: "blocked",
|
|
2975
|
+
urls: [],
|
|
2976
|
+
},
|
|
2977
|
+
nextActions: [
|
|
2978
|
+
{
|
|
2979
|
+
id: "verify-npm-package",
|
|
2980
|
+
command: `npm view ${name} version --json --registry=${registry}`,
|
|
2981
|
+
description:
|
|
2982
|
+
"Resolve whether the exact public package exists before any publish mutation.",
|
|
2983
|
+
},
|
|
2984
|
+
],
|
|
2985
|
+
};
|
|
2986
|
+
}
|
|
2987
|
+
if (auth.authenticated !== true) {
|
|
2988
|
+
return {
|
|
2989
|
+
...plan,
|
|
2990
|
+
ok: false,
|
|
2991
|
+
errorCode: "npm-auth-required",
|
|
2992
|
+
publish: {
|
|
2993
|
+
...plan.publish,
|
|
2994
|
+
status: "blocked",
|
|
2995
|
+
},
|
|
2996
|
+
trust: {
|
|
2997
|
+
status: "blocked",
|
|
2998
|
+
urls: [],
|
|
2999
|
+
},
|
|
3000
|
+
nextActions: [
|
|
3001
|
+
{
|
|
3002
|
+
id: "authenticate-npm",
|
|
3003
|
+
command: `npm whoami --registry=${registry}`,
|
|
3004
|
+
description:
|
|
3005
|
+
"Authenticate npm without sharing token contents, then rerun the exact bootstrap command.",
|
|
3006
|
+
},
|
|
3007
|
+
],
|
|
3008
|
+
};
|
|
3009
|
+
}
|
|
3010
|
+
const tempRoot = fs.mkdtempSync(
|
|
3011
|
+
path.join(os.tmpdir(), "buildchain-paper-bootstrap-"),
|
|
3012
|
+
);
|
|
3013
|
+
try {
|
|
3014
|
+
fs.writeFileSync(
|
|
3015
|
+
path.join(tempRoot, "package.json"),
|
|
3016
|
+
jsonText(
|
|
3017
|
+
bootstrapPackageShape({ packageName: name, version: bootstrapVersion }),
|
|
3018
|
+
),
|
|
3019
|
+
);
|
|
3020
|
+
const configArgs = userconfig ? ["--userconfig", userconfig] : [];
|
|
3021
|
+
const pack = commandResult(
|
|
3022
|
+
"npm",
|
|
3023
|
+
[
|
|
3024
|
+
"pack",
|
|
3025
|
+
"--dry-run",
|
|
3026
|
+
"--json",
|
|
3027
|
+
"--ignore-scripts",
|
|
3028
|
+
`--registry=${registry}`,
|
|
3029
|
+
...configArgs,
|
|
3030
|
+
],
|
|
3031
|
+
{ cwd: tempRoot },
|
|
3032
|
+
);
|
|
3033
|
+
if (!pack.ok) {
|
|
3034
|
+
return {
|
|
3035
|
+
...plan,
|
|
3036
|
+
ok: false,
|
|
3037
|
+
errorCode: "npm-pack-dry-run-failed",
|
|
3038
|
+
publish: { ...plan.publish, status: "blocked" },
|
|
3039
|
+
trust: { status: "blocked", urls: [] },
|
|
3040
|
+
};
|
|
3041
|
+
}
|
|
3042
|
+
const publishDryRun = commandResult(
|
|
3043
|
+
"npm",
|
|
3044
|
+
[
|
|
3045
|
+
"publish",
|
|
3046
|
+
"--dry-run",
|
|
3047
|
+
"--ignore-scripts",
|
|
3048
|
+
"--access",
|
|
3049
|
+
"public",
|
|
3050
|
+
"--tag",
|
|
3051
|
+
"bootstrap",
|
|
3052
|
+
`--registry=${registry}`,
|
|
3053
|
+
...configArgs,
|
|
3054
|
+
],
|
|
3055
|
+
{ cwd: tempRoot },
|
|
3056
|
+
);
|
|
3057
|
+
if (!publishDryRun.ok) {
|
|
3058
|
+
return {
|
|
3059
|
+
...plan,
|
|
3060
|
+
ok: false,
|
|
3061
|
+
errorCode: "npm-publish-dry-run-failed",
|
|
3062
|
+
publish: { ...plan.publish, status: "blocked" },
|
|
3063
|
+
trust: { status: "blocked", urls: [] },
|
|
3064
|
+
};
|
|
3065
|
+
}
|
|
3066
|
+
let publishStatus =
|
|
3067
|
+
packageObservation.exists === true ? "existing" : "published";
|
|
3068
|
+
if (packageObservation.exists !== true) {
|
|
3069
|
+
const published = commandResult(
|
|
3070
|
+
"npm",
|
|
3071
|
+
[
|
|
3072
|
+
"publish",
|
|
3073
|
+
"--ignore-scripts",
|
|
3074
|
+
"--access",
|
|
3075
|
+
"public",
|
|
3076
|
+
"--tag",
|
|
3077
|
+
"bootstrap",
|
|
3078
|
+
`--registry=${registry}`,
|
|
3079
|
+
...configArgs,
|
|
3080
|
+
],
|
|
3081
|
+
{ cwd: tempRoot, timeout: 60000 },
|
|
3082
|
+
);
|
|
3083
|
+
if (!published.ok) {
|
|
3084
|
+
const failure = {
|
|
3085
|
+
...plan,
|
|
3086
|
+
ok: false,
|
|
3087
|
+
errorCode: "npm-publish-failed",
|
|
3088
|
+
publish: { ...plan.publish, status: "failed" },
|
|
3089
|
+
trust: { status: "blocked", urls: [] },
|
|
3090
|
+
};
|
|
3091
|
+
failure.receipts = [
|
|
3092
|
+
writePaperReceipt(resolvedCwd, PAPER_PATHS.npmBootstrap, failure),
|
|
3093
|
+
];
|
|
3094
|
+
return failure;
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
const trustArgs = [
|
|
3098
|
+
"trust",
|
|
3099
|
+
"github",
|
|
3100
|
+
name,
|
|
3101
|
+
"--repo",
|
|
3102
|
+
repo,
|
|
3103
|
+
"--file",
|
|
3104
|
+
workflow,
|
|
3105
|
+
...(environment ? ["--env", environment] : []),
|
|
3106
|
+
"--allow-publish",
|
|
3107
|
+
"--yes",
|
|
3108
|
+
"--json",
|
|
3109
|
+
`--registry=${registry}`,
|
|
3110
|
+
...configArgs,
|
|
3111
|
+
];
|
|
3112
|
+
const trustCommand = commandResult("npm", trustArgs, {
|
|
3113
|
+
cwd: resolvedCwd,
|
|
3114
|
+
timeout: 60000,
|
|
3115
|
+
});
|
|
3116
|
+
const parsedTrust = safeParseJson(trustCommand.stdout);
|
|
3117
|
+
const urls = [
|
|
3118
|
+
...extractUrls(
|
|
3119
|
+
parsedTrust || `${trustCommand.stdout}\n${trustCommand.stderr}`,
|
|
3120
|
+
),
|
|
3121
|
+
];
|
|
3122
|
+
const trustList =
|
|
3123
|
+
trustCommand.ok && urls.length === 0
|
|
3124
|
+
? liveNpmTrustObservation(
|
|
3125
|
+
name,
|
|
3126
|
+
registry,
|
|
3127
|
+
resolvedCwd,
|
|
3128
|
+
expectedPublisher,
|
|
3129
|
+
)
|
|
3130
|
+
: {
|
|
3131
|
+
status: "unknown",
|
|
3132
|
+
configured: null,
|
|
3133
|
+
publishers: [],
|
|
3134
|
+
errorCode:
|
|
3135
|
+
urls.length > 0 ? "web-action-required" : "npm-trust-failed",
|
|
3136
|
+
};
|
|
3137
|
+
const trustStatus =
|
|
3138
|
+
trustCommand.ok && trustList.configured === true
|
|
3139
|
+
? "configured"
|
|
3140
|
+
: urls.length > 0
|
|
3141
|
+
? "action-required"
|
|
3142
|
+
: "failed";
|
|
3143
|
+
const packageAfter = liveNpmPackageObservation(name, registry, resolvedCwd);
|
|
3144
|
+
const packageVerified = packageAfter.exists === true;
|
|
3145
|
+
const receipt = {
|
|
3146
|
+
...plan,
|
|
3147
|
+
ok: trustStatus !== "failed" && packageVerified,
|
|
3148
|
+
dryRun: false,
|
|
3149
|
+
externalMutation: true,
|
|
3150
|
+
errorCode:
|
|
3151
|
+
trustStatus === "failed"
|
|
3152
|
+
? "npm-trust-failed"
|
|
3153
|
+
: packageVerified
|
|
3154
|
+
? ""
|
|
3155
|
+
: "npm-package-readback-failed",
|
|
3156
|
+
package: {
|
|
3157
|
+
...plan.package,
|
|
3158
|
+
existsAfter: packageAfter.exists,
|
|
3159
|
+
observedVersionAfter: packageAfter.version,
|
|
3160
|
+
readbackStatus: packageAfter.status,
|
|
3161
|
+
},
|
|
3162
|
+
publish: {
|
|
3163
|
+
...plan.publish,
|
|
3164
|
+
status: publishStatus,
|
|
3165
|
+
},
|
|
3166
|
+
trust: {
|
|
3167
|
+
status: trustStatus,
|
|
3168
|
+
urls,
|
|
3169
|
+
expectedPublisher,
|
|
3170
|
+
exactBinding: trustList.exactBinding === true,
|
|
3171
|
+
publishers: trustList.publishers,
|
|
3172
|
+
},
|
|
3173
|
+
nextActions:
|
|
3174
|
+
trustStatus === "configured" && packageVerified
|
|
3175
|
+
? [
|
|
3176
|
+
{
|
|
3177
|
+
id: "paper-preflight",
|
|
3178
|
+
command: "buildchain paper preflight --json",
|
|
3179
|
+
description:
|
|
3180
|
+
"Verify package existence and Trusted Publisher binding from live read-only sources.",
|
|
3181
|
+
},
|
|
3182
|
+
]
|
|
3183
|
+
: trustStatus === "action-required" && packageVerified
|
|
3184
|
+
? [
|
|
3185
|
+
{
|
|
3186
|
+
id: "complete-npm-web-action",
|
|
3187
|
+
command: "",
|
|
3188
|
+
description:
|
|
3189
|
+
"Open the exact npm URL returned below, complete the web step, then rerun preflight.",
|
|
3190
|
+
urls,
|
|
3191
|
+
},
|
|
3192
|
+
]
|
|
3193
|
+
: !packageVerified
|
|
3194
|
+
? [
|
|
3195
|
+
{
|
|
3196
|
+
id: "verify-npm-package-readback",
|
|
3197
|
+
command: `npm view ${name} version --json --registry=${registry}`,
|
|
3198
|
+
description:
|
|
3199
|
+
"The mutation returned but the official registry did not prove package existence; do not infer success.",
|
|
3200
|
+
},
|
|
3201
|
+
]
|
|
3202
|
+
: [
|
|
3203
|
+
{
|
|
3204
|
+
id: "retry-npm-trust",
|
|
3205
|
+
command: `buildchain paper bootstrap npm --execute --confirm-public-package ${name} --json`,
|
|
3206
|
+
description:
|
|
3207
|
+
"The package exists; retry only the idempotent Trusted Publisher configuration path.",
|
|
3208
|
+
},
|
|
3209
|
+
],
|
|
3210
|
+
};
|
|
3211
|
+
receipt.receipts = [
|
|
3212
|
+
writePaperReceipt(resolvedCwd, PAPER_PATHS.npmBootstrap, receipt),
|
|
3213
|
+
];
|
|
3214
|
+
if (
|
|
3215
|
+
packageVerified &&
|
|
3216
|
+
(trustStatus === "configured" || trustStatus === "action-required")
|
|
3217
|
+
) {
|
|
3218
|
+
receipt.receipts.push(
|
|
3219
|
+
writePaperReceipt(resolvedCwd, PAPER_PATHS.npmTrust, receipt),
|
|
3220
|
+
);
|
|
3221
|
+
}
|
|
3222
|
+
return receipt;
|
|
3223
|
+
} finally {
|
|
3224
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
3225
|
+
}
|
|
3226
|
+
}
|