@kungfu-tech/buildchain 2.12.7-alpha.8 → 2.12.7
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/actions/promote-buildchain-ref/README.md +5 -1
- package/dist/site/buildchain-contract.json +25 -20
- package/dist/site/buildchain-site.json +14 -14
- package/dist/site/controller-registry.json +6 -2
- package/dist/site/kfd-claims.json +6 -4
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +2 -2
- package/dist/site/node-api-registry.json +2 -2
- package/dist/site/page-registry.json +8 -8
- package/dist/site/public-surface-audit.json +10 -6
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +6 -6
- package/dist/site/workflow-registry.json +8 -4
- package/docs/publication-artifacts.md +13 -8
- package/docs/publication-authority.md +10 -7
- package/docs/release-candidate.md +19 -0
- package/package.json +1 -1
- package/packages/core/index.js +1 -0
- package/packages/core/publication-artifact-candidate.js +12 -0
- package/scripts/assemble-self-publication-admission.mjs +17 -8
- package/scripts/locked-source-checkout.mjs +50 -13
- package/scripts/publication-artifact-candidate.mjs +22 -25
|
@@ -196,16 +196,21 @@ jobs:
|
|
|
196
196
|
verify-command: make check
|
|
197
197
|
artifact-paths: _build/paper-name.pdf
|
|
198
198
|
buildchain-contract-lock-path: .buildchain/contract-lock.json
|
|
199
|
+
secrets:
|
|
200
|
+
BUILDCHAIN_PROMOTION_TOKEN: ${{ secrets.BUILDCHAIN_PROMOTION_TOKEN }}
|
|
199
201
|
```
|
|
200
202
|
|
|
201
|
-
The sealed preset does not
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
203
|
+
The sealed preset does not use a long-lived token for npm publication. It may
|
|
204
|
+
accept an optional `BUILDCHAIN_PROMOTION_TOKEN` only for machine-generated
|
|
205
|
+
version-state updates on protected channel branches; npm publication remains
|
|
206
|
+
bound to GitHub OIDC trusted publishing. The preset builds and packages the
|
|
207
|
+
paper in a read-only job, then a credential-free authority job downloads that
|
|
208
|
+
exact candidate, audits the external control plane, and seals a capability over
|
|
209
|
+
the source tree, Buildchain runtime, controller receipt, PDF, and npm package
|
|
210
|
+
bytes. Only the final job receives write and OIDC permissions; it downloads the
|
|
211
|
+
admitted candidate, recomputes the capability binding, and publishes without
|
|
212
|
+
executing consumer build commands. npm binds the OIDC identity to the consumer
|
|
213
|
+
workflow named by `publisher-workflow-path`.
|
|
209
214
|
|
|
210
215
|
The preset:
|
|
211
216
|
|
|
@@ -109,17 +109,20 @@ artifacts. The denial explicitly records that npm Trusted Publishing and OIDC
|
|
|
109
109
|
were not evaluated, so downstream diagnostics cannot misclassify an admission
|
|
110
110
|
assembly failure as an npm authentication failure.
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
An explicitly opted-in managed `workflow_run` promotion lane may assemble those
|
|
113
|
+
inputs from evidence owned by its caller repository. It downloads the exact prior RC passport,
|
|
114
114
|
summary, referenced controller receipt, manifests, and product payloads; proves
|
|
115
115
|
the admitted channel commit has the same Git tree as the RC; performs the live
|
|
116
116
|
read-only control-plane audit; records the GitHub-hosted job as ephemeral runner
|
|
117
|
-
provenance; and
|
|
117
|
+
provenance; and consumes either a caller-supplied Gate aggregate or an explicit
|
|
118
|
+
caller no-Gate decision. The
|
|
118
119
|
independent verifier then recomputes every receipt and payload digest exactly as
|
|
119
|
-
it does for externally supplied admission.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
it does for externally supplied admission. Automatic assembly keeps Buildchain
|
|
121
|
+
as the canonical authority repository, requires evidence to belong to the
|
|
122
|
+
caller, binds a repository-local publisher workflow, and rejects unknown refs,
|
|
123
|
+
non-exact source SHAs, package/target mismatches, or an undeclared Gate policy.
|
|
124
|
+
Manual apply and consumers that do not opt in still require their own explicit
|
|
125
|
+
admission inputs.
|
|
123
126
|
|
|
124
127
|
Before authority verification, the reusable promotion controller runs the same
|
|
125
128
|
release transaction selector in read-only mode. That plan supplies one exact
|
|
@@ -116,3 +116,22 @@ KFD-1 source/artifact contract surfaces, audits KFD-2 public release claims, and
|
|
|
116
116
|
compares KFD-3 declared shipped public surfaces with artifact-exposed public
|
|
117
117
|
surfaces. The release passport records the results under `kfd-1`, `kfd-2`, and
|
|
118
118
|
the KFD-provided `kfd-3` section.
|
|
119
|
+
|
|
120
|
+
Managed consumers may also ask the promotion wrapper to assemble sealed
|
|
121
|
+
publication evidence from the exact release candidate instead of producing
|
|
122
|
+
short-lived admission JSON in repository-specific workflow code:
|
|
123
|
+
|
|
124
|
+
```yaml
|
|
125
|
+
publication-auto-admission: true
|
|
126
|
+
publication-auto-no-gate: true
|
|
127
|
+
publication-publisher-workflow-path: .github/workflows/buildchain-ref-promotion.yml
|
|
128
|
+
publication-product: Example Product
|
|
129
|
+
publication-target: npm:@example/product
|
|
130
|
+
publication-package-name: "@example/product"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`publication-auto-no-gate` is an explicit consumer decision, not a default. A
|
|
134
|
+
consumer with a Shifu Gate registry supplies `publication-gate-aggregate-json`
|
|
135
|
+
instead. Buildchain still requires caller-owned RC evidence, an exact authority
|
|
136
|
+
runtime and source SHA, a repository-local publisher workflow, matching npm
|
|
137
|
+
target/package identity, and a qualifying control-plane audit.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kungfu-tech/buildchain",
|
|
3
|
-
"version": "2.12.7
|
|
3
|
+
"version": "2.12.7",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Buildchain Release Passport, release governance, CLI toolkit, and site facts.",
|
|
6
6
|
"repository": "https://github.com/kungfu-systems/buildchain",
|
package/packages/core/index.js
CHANGED
|
@@ -20,6 +20,18 @@ export function publicationArtifactCandidateDigest(value) {
|
|
|
20
20
|
return crypto.createHash("sha256").update(stableJson(value)).digest("hex");
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
export function resolvePublicationCandidateFile(files = [], candidatePath) {
|
|
24
|
+
const normalizedPath = requiredString(candidatePath, "candidatePath").replaceAll("\\", "/");
|
|
25
|
+
if (normalizedPath.startsWith("/") || normalizedPath.split("/").includes("..")) {
|
|
26
|
+
throw new Error(`publication artifact candidate contains an unsafe path: ${normalizedPath}`);
|
|
27
|
+
}
|
|
28
|
+
const matches = files.filter((entry) => entry.path === normalizedPath);
|
|
29
|
+
if (matches.length !== 1) {
|
|
30
|
+
throw new Error(`expected exactly one publication candidate file at ${normalizedPath}, found ${matches.length}`);
|
|
31
|
+
}
|
|
32
|
+
return matches[0].path;
|
|
33
|
+
}
|
|
34
|
+
|
|
23
35
|
function requiredString(value, label) {
|
|
24
36
|
const normalized = String(value || "").trim();
|
|
25
37
|
if (!normalized) throw new Error(`${label} must be a non-empty string`);
|
|
@@ -115,13 +115,22 @@ async function main() {
|
|
|
115
115
|
manifests,
|
|
116
116
|
payloads: manifests.map((manifest) => payloadFor(manifest, path.join(evidenceRoot, "payloads"))),
|
|
117
117
|
});
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
118
|
+
const suppliedGateAggregate = String(process.env.BUILDCHAIN_GATE_AGGREGATE_JSON || "").trim();
|
|
119
|
+
let gateAggregate;
|
|
120
|
+
if (suppliedGateAggregate) {
|
|
121
|
+
gateAggregate = JSON.parse(suppliedGateAggregate);
|
|
122
|
+
} else {
|
|
123
|
+
if (process.env.BUILDCHAIN_ALLOW_NO_GATE !== "true") {
|
|
124
|
+
throw new Error("managed release-candidate admission requires a Gate aggregate or explicit no-Gate decision");
|
|
125
|
+
}
|
|
126
|
+
gateAggregate = createPublicationGateDecision({
|
|
127
|
+
sourceSha,
|
|
128
|
+
profile: process.env.BUILDCHAIN_GATE_PROFILE || "managed-release-candidate-no-gate",
|
|
129
|
+
required: false,
|
|
130
|
+
rationale: process.env.BUILDCHAIN_GATE_RATIONALE || "The consumer explicitly declared no Shifu Gate registry for this publication transaction.",
|
|
131
|
+
policy: { scope: "managed-release-candidate", repository },
|
|
132
|
+
});
|
|
133
|
+
}
|
|
125
134
|
const runnerProvenance = createRunnerProvenance({
|
|
126
135
|
runnerClass: "ephemeral",
|
|
127
136
|
os: required("RUNNER_OS"),
|
|
@@ -178,6 +187,6 @@ async function main() {
|
|
|
178
187
|
}
|
|
179
188
|
|
|
180
189
|
main().catch((error) => {
|
|
181
|
-
console.error(`assemble
|
|
190
|
+
console.error(`assemble release-candidate admission: ${error.message}`);
|
|
182
191
|
process.exitCode = 1;
|
|
183
192
|
});
|
|
@@ -6,6 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
7
|
|
|
8
8
|
export const LOCKED_SOURCE_CHECKOUT_CONTRACT = "kungfu-buildchain-locked-source-checkout-cache";
|
|
9
|
+
export const ISOLATED_GIT_GLOBAL_CONFIG = process.platform === "win32" ? "NUL" : "/dev/null";
|
|
9
10
|
|
|
10
11
|
const GIT_SHA_PATTERN = /^[0-9a-f]{40}$/i;
|
|
11
12
|
|
|
@@ -133,6 +134,24 @@ function githubAuthEnv(token = "") {
|
|
|
133
134
|
};
|
|
134
135
|
}
|
|
135
136
|
|
|
137
|
+
function isolatedGitFetchEnv(env = {}, targetPath) {
|
|
138
|
+
const configuredCount = Number.parseInt(env.GIT_CONFIG_COUNT || "0", 10);
|
|
139
|
+
const safeDirectoryIndex = Number.isInteger(configuredCount) && configuredCount >= 0
|
|
140
|
+
? configuredCount
|
|
141
|
+
: 0;
|
|
142
|
+
return {
|
|
143
|
+
...env,
|
|
144
|
+
// Runner-global URL rewrites are shared mutable state. A concurrent job
|
|
145
|
+
// may point the same repository URL at a different single-SHA bundle, so
|
|
146
|
+
// network fetches must not consult the account-level Git config.
|
|
147
|
+
GIT_CONFIG_GLOBAL: ISOLATED_GIT_GLOBAL_CONFIG,
|
|
148
|
+
GIT_CONFIG_NOSYSTEM: "1",
|
|
149
|
+
GIT_CONFIG_COUNT: String(safeDirectoryIndex + 1),
|
|
150
|
+
[`GIT_CONFIG_KEY_${safeDirectoryIndex}`]: "safe.directory",
|
|
151
|
+
[`GIT_CONFIG_VALUE_${safeDirectoryIndex}`]: path.resolve(targetPath),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
136
155
|
function markSafeDirectory(targetPath, timeoutMs) {
|
|
137
156
|
try {
|
|
138
157
|
git(["config", "--global", "--add", "safe.directory", path.resolve(targetPath)], {
|
|
@@ -203,9 +222,32 @@ export function fetchSourceCommit({
|
|
|
203
222
|
sourceTreeSha = "",
|
|
204
223
|
timeoutMs,
|
|
205
224
|
env = {},
|
|
225
|
+
allowFullFetchRetry = false,
|
|
206
226
|
runGit = git,
|
|
207
227
|
containsCommit = hasCommit,
|
|
208
228
|
}) {
|
|
229
|
+
const fetchEnv = isolatedGitFetchEnv(env, targetPath);
|
|
230
|
+
const fetch = (refspec) => {
|
|
231
|
+
const options = { cwd: targetPath, timeoutMs, env: fetchEnv };
|
|
232
|
+
try {
|
|
233
|
+
runGit(["fetch", "--no-tags", "--depth=1", remoteName, refspec], options);
|
|
234
|
+
return "shallow";
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (
|
|
237
|
+
!allowFullFetchRetry
|
|
238
|
+
|| !/dumb http transport does not support shallow capabilities/i.test(
|
|
239
|
+
String(error?.message || error || ""),
|
|
240
|
+
)
|
|
241
|
+
) {
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
// Dumb HTTP mirrors are intentionally simple static cache endpoints.
|
|
245
|
+
// Retry only that explicit capability mismatch without --depth; all
|
|
246
|
+
// network fallbacks remain shallow and bounded by their own policy.
|
|
247
|
+
runGit(["fetch", "--no-tags", remoteName, refspec], options);
|
|
248
|
+
return "full";
|
|
249
|
+
}
|
|
250
|
+
};
|
|
209
251
|
try {
|
|
210
252
|
runGit(["remote", "remove", remoteName], { cwd: targetPath, timeoutMs, stdio: "ignore" });
|
|
211
253
|
} catch {
|
|
@@ -219,13 +261,9 @@ export function fetchSourceCommit({
|
|
|
219
261
|
|
|
220
262
|
if (fetchRef) {
|
|
221
263
|
try {
|
|
222
|
-
|
|
223
|
-
cwd: targetPath,
|
|
224
|
-
timeoutMs,
|
|
225
|
-
env,
|
|
226
|
-
});
|
|
264
|
+
const fetchMode = fetch(`+${fetchRef}:refs/buildchain/source-ref`);
|
|
227
265
|
if (containsCommit(targetPath, sha, timeoutMs)) {
|
|
228
|
-
return { selector: "ref", checkoutSha: sha };
|
|
266
|
+
return { selector: "ref", checkoutSha: sha, fetchMode };
|
|
229
267
|
}
|
|
230
268
|
if (/^refs\/pull\/\d+\/merge$/.test(fetchRef) && sourceTreeSha) {
|
|
231
269
|
const fetchedSha = runGit(["rev-parse", "refs/buildchain/source-ref^{commit}"], {
|
|
@@ -237,7 +275,7 @@ export function fetchSourceCommit({
|
|
|
237
275
|
timeoutMs,
|
|
238
276
|
});
|
|
239
277
|
if (fetchedTree === sourceTreeSha) {
|
|
240
|
-
return { selector: "ref-tree", checkoutSha: fetchedSha };
|
|
278
|
+
return { selector: "ref-tree", checkoutSha: fetchedSha, fetchMode };
|
|
241
279
|
}
|
|
242
280
|
}
|
|
243
281
|
} catch (error) {
|
|
@@ -249,15 +287,11 @@ export function fetchSourceCommit({
|
|
|
249
287
|
}
|
|
250
288
|
}
|
|
251
289
|
|
|
252
|
-
|
|
253
|
-
cwd: targetPath,
|
|
254
|
-
timeoutMs,
|
|
255
|
-
env,
|
|
256
|
-
});
|
|
290
|
+
const fetchMode = fetch(`+${sha}:refs/buildchain/source`);
|
|
257
291
|
if (!containsCommit(targetPath, sha, timeoutMs)) {
|
|
258
292
|
throw new Error(`fetched ${fetchRef || sha}, but ${sha} is not available`);
|
|
259
293
|
}
|
|
260
|
-
return { selector: "sha", checkoutSha: sha };
|
|
294
|
+
return { selector: "sha", checkoutSha: sha, fetchMode };
|
|
261
295
|
}
|
|
262
296
|
|
|
263
297
|
export function runBoundedFetch({ attempts = 1, fetch, onAttempt = () => {}, onRetry = () => {}, shouldRetry = () => true }) {
|
|
@@ -413,7 +447,9 @@ export function lockedSourceCheckout({
|
|
|
413
447
|
fetchRef,
|
|
414
448
|
sourceTreeSha: treeSha,
|
|
415
449
|
timeoutMs,
|
|
450
|
+
allowFullFetchRetry: true,
|
|
416
451
|
});
|
|
452
|
+
evidence.cache.fetchMode = fetchResult.fetchMode;
|
|
417
453
|
checkoutSha = fetchResult.checkoutSha || sha;
|
|
418
454
|
checkoutFetchedCommit(targetPath, checkoutSha, timeoutMs);
|
|
419
455
|
} else {
|
|
@@ -458,6 +494,7 @@ export function lockedSourceCheckout({
|
|
|
458
494
|
shouldRetry: retryableGitFetchError,
|
|
459
495
|
});
|
|
460
496
|
evidence.cache.githubFetchAttempts = fetchResult.attempts;
|
|
497
|
+
evidence.cache.fetchMode = fetchResult.value.fetchMode;
|
|
461
498
|
checkoutSha = fetchResult.value.checkoutSha || sha;
|
|
462
499
|
} catch (error) {
|
|
463
500
|
evidence.durationMs = Date.now() - startedAt;
|
|
@@ -4,7 +4,12 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
createPublicationArtifactCandidate,
|
|
9
|
+
resolvePublicationCandidateFile,
|
|
10
|
+
} from "../packages/core/publication-artifact-candidate.js";
|
|
11
|
+
|
|
12
|
+
export { resolvePublicationCandidateFile };
|
|
8
13
|
|
|
9
14
|
function flag(name, fallback = "") {
|
|
10
15
|
const index = process.argv.indexOf(`--${name}`);
|
|
@@ -17,27 +22,16 @@ function requiredFlag(name) {
|
|
|
17
22
|
return value;
|
|
18
23
|
}
|
|
19
24
|
|
|
20
|
-
function
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
26
|
-
const full = path.join(current, entry.name);
|
|
27
|
-
if (entry.isDirectory()) pending.push(full);
|
|
28
|
-
else if (entry.isFile() && entry.name === name) matches.push(full);
|
|
29
|
-
}
|
|
25
|
+
function exactJson(root, relativePath) {
|
|
26
|
+
const absoluteRoot = path.resolve(root);
|
|
27
|
+
const absolutePath = path.resolve(absoluteRoot, relativePath);
|
|
28
|
+
if (!absolutePath.startsWith(`${absoluteRoot}${path.sep}`)) {
|
|
29
|
+
throw new Error(`publication evidence path escapes artifact root: ${relativePath}`);
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const matches = filesNamed(root, name);
|
|
36
|
-
if (matches.length !== 1)
|
|
37
|
-
throw new Error(
|
|
38
|
-
`expected exactly one ${name} under ${root}, found ${matches.length}`,
|
|
39
|
-
);
|
|
40
|
-
return JSON.parse(fs.readFileSync(matches[0], "utf8"));
|
|
31
|
+
if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) {
|
|
32
|
+
throw new Error(`expected publication evidence at ${relativePath}`);
|
|
33
|
+
}
|
|
34
|
+
return JSON.parse(fs.readFileSync(absolutePath, "utf8"));
|
|
41
35
|
}
|
|
42
36
|
|
|
43
37
|
function collectFiles(root) {
|
|
@@ -79,12 +73,15 @@ export function buildPublicationArtifactCandidate({
|
|
|
79
73
|
sourceSha,
|
|
80
74
|
sourceTreeSha,
|
|
81
75
|
runtimeSha,
|
|
82
|
-
manifest:
|
|
83
|
-
|
|
76
|
+
manifest: exactJson(
|
|
77
|
+
resolvedArtifactRoot,
|
|
78
|
+
".buildchain/publication/publication-artifact.json",
|
|
79
|
+
),
|
|
80
|
+
passport: exactJson(
|
|
84
81
|
resolvedArtifactRoot,
|
|
85
|
-
"publication-artifact-passport.json",
|
|
82
|
+
".buildchain/publication/publication-artifact-passport.json",
|
|
86
83
|
),
|
|
87
|
-
controllerReceipt:
|
|
84
|
+
controllerReceipt: exactJson(resolvedControllerRoot, "receipt.json"),
|
|
88
85
|
files: collectFiles(resolvedArtifactRoot),
|
|
89
86
|
};
|
|
90
87
|
const candidate = createPublicationArtifactCandidate(evidence);
|