@kungfu-tech/buildchain 2.5.7 → 2.6.0-alpha.0
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/bin/buildchain.mjs +85 -0
- package/dist/site/release-provenance.json +1 -0
- package/docs/cli.md +52 -0
- package/docs/release-governance.md +24 -8
- package/docs/release-passport.md +28 -0
- package/package.json +2 -1
- package/packages/core/artifact-passport.js +748 -0
- package/packages/core/index.js +12 -0
- package/packages/core/release-line-dry-run.js +1 -1
- package/scripts/buildchain-patrol.mjs +3 -3
- package/scripts/check-inventory.mjs +1 -0
- package/scripts/dev-pr-auto-merge.mjs +1 -1
package/bin/buildchain.mjs
CHANGED
|
@@ -25,6 +25,10 @@ import {
|
|
|
25
25
|
explainReleasePassport,
|
|
26
26
|
verifyReleasePassport,
|
|
27
27
|
} from "../packages/core/release-passport.js";
|
|
28
|
+
import {
|
|
29
|
+
explainArtifactPassport,
|
|
30
|
+
verifyArtifactPassport,
|
|
31
|
+
} from "../packages/core/artifact-passport.js";
|
|
28
32
|
import {
|
|
29
33
|
BUILDCHAIN_PROCESS_SAMPLE_REPORT_CONTRACT,
|
|
30
34
|
formatDiagnosticsSummaryTable,
|
|
@@ -74,13 +78,18 @@ function usage() {
|
|
|
74
78
|
[--release-extra-json <json-or-path>]
|
|
75
79
|
[--publish-json <json-or-path>] [--output-dir <dir>] [--json]
|
|
76
80
|
buildchain verify release-passport <file-or-url> [--json]
|
|
81
|
+
buildchain verify artifact <file|dir|url|npm:...|oci:...|github-release:...>
|
|
82
|
+
[--passport <file-or-url>] [--locator-config <json>]
|
|
83
|
+
[--repository <owner/repo>] [--tag <tag>] [--json]
|
|
77
84
|
buildchain verify infra-contract-evidence-bundle <file> [--json]
|
|
78
85
|
buildchain verify observability-log <jsonl> [--min-events <n>]
|
|
79
86
|
[--require-phase <csv>]
|
|
80
87
|
[--require-component <csv>]
|
|
81
88
|
[--require-event <csv>] [--allow-errors] [--json]
|
|
82
89
|
buildchain explain release --passport <file-or-url> [--for human|agent] [--json]
|
|
90
|
+
buildchain explain artifact <subject> [--passport <file-or-url>] [--for human|agent] [--json]
|
|
83
91
|
buildchain inspect release --passport <file-or-url> [--json]
|
|
92
|
+
buildchain inspect artifact <subject> [--passport <file-or-url>] [--json]
|
|
84
93
|
buildchain doctor [--cwd <dir>] [--require-publish-source-lock] [--json]
|
|
85
94
|
buildchain log <info|warn|error> --event <name> [--phase <phase>]
|
|
86
95
|
[--component <name>] [--source <name>] [--attribute key=value]...
|
|
@@ -110,6 +119,7 @@ Examples:
|
|
|
110
119
|
buildchain span --event native.build -- cmake --build build
|
|
111
120
|
buildchain collect github-release --tag v2.2.0 --assets-dir dist --output-dir .buildchain/release-passport
|
|
112
121
|
buildchain verify release-passport .buildchain/release-passport/buildchain.release.json
|
|
122
|
+
buildchain verify artifact ./dist/buildchain-x86_64-unknown-linux-gnu.tar.gz --passport .buildchain/release-passport/buildchain.release.json
|
|
113
123
|
buildchain verify infra-contract-evidence-bundle .buildchain/infra-contract-evidence-bundle.json
|
|
114
124
|
buildchain verify observability-log .buildchain/logs/events.jsonl --min-events 4 --require-phase build
|
|
115
125
|
buildchain infra-contract --mode plan --source-sha <sha>
|
|
@@ -736,6 +746,34 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
736
746
|
|
|
737
747
|
if (command === "verify") {
|
|
738
748
|
const [subcommand = "", location = "", ...verifyArgs] = args;
|
|
749
|
+
if (subcommand === "artifact") {
|
|
750
|
+
if (!location) {
|
|
751
|
+
throw new Error("usage: buildchain verify artifact <subject>");
|
|
752
|
+
}
|
|
753
|
+
const report = await verifyArtifactPassport({
|
|
754
|
+
subject: location,
|
|
755
|
+
cwd: process.cwd(),
|
|
756
|
+
passportLocation: readFlag(verifyArgs, "passport", ""),
|
|
757
|
+
locatorConfig: readFlag(verifyArgs, "locator-config", ""),
|
|
758
|
+
repository: readFlag(verifyArgs, "repository", ""),
|
|
759
|
+
tag: readFlag(verifyArgs, "tag", ""),
|
|
760
|
+
githubReleaseBaseUrl: readFlag(verifyArgs, "github-release-base-url", ""),
|
|
761
|
+
subjectDigest: readFlag(verifyArgs, "subject-digest", ""),
|
|
762
|
+
subjectKind: readFlag(verifyArgs, "subject-kind", ""),
|
|
763
|
+
});
|
|
764
|
+
if (readBooleanFlag(verifyArgs, "json")) {
|
|
765
|
+
printJson(report);
|
|
766
|
+
} else {
|
|
767
|
+
process.stdout.write(`artifact: ${report.outcome}\n`);
|
|
768
|
+
process.stdout.write(`subject: ${report.subject?.name || location}\n`);
|
|
769
|
+
process.stdout.write(`passport: ${report.passport?.location || report.discovery?.passportLocation || "unresolved"}\n`);
|
|
770
|
+
for (const entry of report.issues) {
|
|
771
|
+
process.stdout.write(`- ${entry.level}: ${entry.code}: ${entry.message}\n`);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
process.exitCode = report.ok ? 0 : 1;
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
739
777
|
if (subcommand === "observability-log") {
|
|
740
778
|
if (!location) {
|
|
741
779
|
throw new Error("usage: buildchain verify observability-log <jsonl>");
|
|
@@ -797,6 +835,33 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
797
835
|
|
|
798
836
|
if (command === "explain") {
|
|
799
837
|
const [subcommand = "", ...explainArgs] = args;
|
|
838
|
+
if (subcommand === "artifact") {
|
|
839
|
+
const subject = explainArgs[0] || "";
|
|
840
|
+
if (!subject) {
|
|
841
|
+
throw new Error("usage: buildchain explain artifact <subject>");
|
|
842
|
+
}
|
|
843
|
+
const explanation = await explainArtifactPassport({
|
|
844
|
+
subject,
|
|
845
|
+
cwd: process.cwd(),
|
|
846
|
+
passportLocation: readFlag(explainArgs, "passport", ""),
|
|
847
|
+
locatorConfig: readFlag(explainArgs, "locator-config", ""),
|
|
848
|
+
repository: readFlag(explainArgs, "repository", ""),
|
|
849
|
+
tag: readFlag(explainArgs, "tag", ""),
|
|
850
|
+
githubReleaseBaseUrl: readFlag(explainArgs, "github-release-base-url", ""),
|
|
851
|
+
subjectDigest: readFlag(explainArgs, "subject-digest", ""),
|
|
852
|
+
subjectKind: readFlag(explainArgs, "subject-kind", ""),
|
|
853
|
+
forAudience: readFlag(explainArgs, "for", "human"),
|
|
854
|
+
});
|
|
855
|
+
if (readBooleanFlag(explainArgs, "json")) {
|
|
856
|
+
printJson(explanation);
|
|
857
|
+
} else {
|
|
858
|
+
process.stdout.write(`artifact: ${explanation.subject?.name || subject}\n`);
|
|
859
|
+
process.stdout.write(`trust: ${explanation.trust}\n`);
|
|
860
|
+
process.stdout.write(`next action: ${explanation.nextAction}\n`);
|
|
861
|
+
}
|
|
862
|
+
process.exitCode = explanation.trust === "pass" ? 0 : 1;
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
800
865
|
if (subcommand !== "release") {
|
|
801
866
|
throw new Error("usage: buildchain explain release --passport <file-or-url>");
|
|
802
867
|
}
|
|
@@ -820,6 +885,26 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
820
885
|
|
|
821
886
|
if (command === "inspect") {
|
|
822
887
|
const [subcommand = "", ...inspectArgs] = args;
|
|
888
|
+
if (subcommand === "artifact") {
|
|
889
|
+
const subject = inspectArgs[0] || "";
|
|
890
|
+
if (!subject) {
|
|
891
|
+
throw new Error("usage: buildchain inspect artifact <subject>");
|
|
892
|
+
}
|
|
893
|
+
const report = await verifyArtifactPassport({
|
|
894
|
+
subject,
|
|
895
|
+
cwd: process.cwd(),
|
|
896
|
+
passportLocation: readFlag(inspectArgs, "passport", ""),
|
|
897
|
+
locatorConfig: readFlag(inspectArgs, "locator-config", ""),
|
|
898
|
+
repository: readFlag(inspectArgs, "repository", ""),
|
|
899
|
+
tag: readFlag(inspectArgs, "tag", ""),
|
|
900
|
+
githubReleaseBaseUrl: readFlag(inspectArgs, "github-release-base-url", ""),
|
|
901
|
+
subjectDigest: readFlag(inspectArgs, "subject-digest", ""),
|
|
902
|
+
subjectKind: readFlag(inspectArgs, "subject-kind", ""),
|
|
903
|
+
});
|
|
904
|
+
printJson(report);
|
|
905
|
+
process.exitCode = report.ok ? 0 : 1;
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
823
908
|
if (subcommand !== "release") {
|
|
824
909
|
throw new Error("usage: buildchain inspect release --passport <file-or-url>");
|
|
825
910
|
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"exports": {
|
|
11
11
|
".": "./packages/core/index.js",
|
|
12
12
|
"./core": "./packages/core/index.js",
|
|
13
|
+
"./artifact-passport": "./packages/core/artifact-passport.js",
|
|
13
14
|
"./diagnostics": "./packages/core/diagnostics.js",
|
|
14
15
|
"./issue-reporting": "./packages/core/issue-reporting.js",
|
|
15
16
|
"./logging": "./packages/core/logging.js",
|
package/docs/cli.md
CHANGED
|
@@ -304,6 +304,58 @@ The verifier fails closed when required protocol files are absent, artifacts are
|
|
|
304
304
|
not covered by evidence, or digests disagree. The explanation output is shaped
|
|
305
305
|
for agents: trust, completeness, impact, recovery route, and next action.
|
|
306
306
|
|
|
307
|
+
Verify a published artifact by subject:
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
buildchain verify artifact ./Kungfu-2.8.0-windows-x64.exe
|
|
311
|
+
buildchain inspect artifact ./Kungfu-2.8.0-windows-x64.exe --json
|
|
312
|
+
buildchain explain artifact ./Kungfu-2.8.0-windows-x64.exe --for agent --json
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
`verify artifact` computes or obtains the subject digest, discovers the
|
|
316
|
+
detached release passport, verifies the passport, then requires that the
|
|
317
|
+
subject digest appears in the passport's release assets, package set, publish
|
|
318
|
+
evidence, or artifact evidence. Outcomes are explicit: `pass`, `fail`, or
|
|
319
|
+
`unverifiable`. A filename is only a hint; trust comes from digest equality.
|
|
320
|
+
|
|
321
|
+
Discovery is fail-closed and ordered:
|
|
322
|
+
|
|
323
|
+
1. `--passport <file-or-url>`.
|
|
324
|
+
2. Sidecar pointer, such as `<artifact>.buildchain-passport.json`.
|
|
325
|
+
3. Embedded/package pointer, such as `package.json` `buildchain.releasePassport`.
|
|
326
|
+
4. Local config or org index, such as `.buildchain/artifact-passport-locators.json`.
|
|
327
|
+
5. GitHub Release default discovery from `github-release:` subjects, GitHub
|
|
328
|
+
Release asset URLs, or `--repository <owner/repo> --tag <tag>`.
|
|
329
|
+
6. Custom `--locator-config <json-or-url>`.
|
|
330
|
+
7. `unverifiable` with retry guidance.
|
|
331
|
+
|
|
332
|
+
Locator files are policy, not protocol. They map subject fields such as
|
|
333
|
+
`name`, `kind`, `version`, `digest`, `repository`, or `tag` to a detached
|
|
334
|
+
passport location:
|
|
335
|
+
|
|
336
|
+
```json
|
|
337
|
+
{
|
|
338
|
+
"schemaVersion": 1,
|
|
339
|
+
"contract": "kungfu-buildchain-artifact-passport-locator",
|
|
340
|
+
"locators": [
|
|
341
|
+
{
|
|
342
|
+
"match": {
|
|
343
|
+
"name": "Kungfu-2.8.0-windows-x64.exe",
|
|
344
|
+
"digest": "sha256:..."
|
|
345
|
+
},
|
|
346
|
+
"passport": "../release-passport/buildchain.release.json"
|
|
347
|
+
}
|
|
348
|
+
]
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Supported subject shapes include local files and directories, URLs,
|
|
353
|
+
`npm:<name>@<version>`, `oci:...`, `s3:...`,
|
|
354
|
+
`github-release:<owner/repo>@<tag>/<asset>`, and deployment endpoints. Local
|
|
355
|
+
files, directories, and URLs are digestable directly; remote package, OCI,
|
|
356
|
+
object storage, and deployment subjects should provide a digest or resolve to a
|
|
357
|
+
locator that records one.
|
|
358
|
+
|
|
307
359
|
Verify infra-contract lifecycle evidence bundles:
|
|
308
360
|
|
|
309
361
|
```bash
|
|
@@ -250,6 +250,23 @@ After each merge, the next PR is re-evaluated before it can move the protected
|
|
|
250
250
|
dev branch. This prevents one merge from silently making the next candidate
|
|
251
251
|
stale or conflicting.
|
|
252
252
|
|
|
253
|
+
The required check should be the `check` job. Repositories can keep that job
|
|
254
|
+
name stable while changing the actual verification command declaratively in
|
|
255
|
+
`buildchain.toml`:
|
|
256
|
+
|
|
257
|
+
```toml
|
|
258
|
+
[lifecycle.install]
|
|
259
|
+
command = "cargo fetch --locked"
|
|
260
|
+
|
|
261
|
+
[lifecycle.verify]
|
|
262
|
+
command = "cargo test --workspace --locked"
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Consumers that want Buildchain to own the check wrapper can call
|
|
266
|
+
`.github/workflows/check.yml@v2`. The wrapper runs the declared
|
|
267
|
+
`lifecycle.install` and `lifecycle.verify` stages and fails the `check` job when
|
|
268
|
+
either declaration is missing or the command exits non-zero.
|
|
269
|
+
|
|
253
270
|
Typical consumer wrapper:
|
|
254
271
|
|
|
255
272
|
```yaml
|
|
@@ -273,8 +290,8 @@ jobs:
|
|
|
273
290
|
checks: read
|
|
274
291
|
statuses: read
|
|
275
292
|
with:
|
|
276
|
-
target-branch: dev/v2/v2.
|
|
277
|
-
required-status-checks:
|
|
293
|
+
target-branch: dev/v2/v2.6
|
|
294
|
+
required-status-checks: check
|
|
278
295
|
ready-label: ready
|
|
279
296
|
block-labels: blocked,do-not-merge
|
|
280
297
|
max-merges: 1
|
|
@@ -315,7 +332,6 @@ jobs:
|
|
|
315
332
|
patrol:
|
|
316
333
|
uses: kungfu-systems/buildchain/.github/workflows/patrol-daily.yml@v2
|
|
317
334
|
with:
|
|
318
|
-
target-branch: dev/v2/v2.5
|
|
319
335
|
dry-run: false
|
|
320
336
|
max-actions: 1
|
|
321
337
|
```
|
|
@@ -327,14 +343,14 @@ jobs:
|
|
|
327
343
|
patrol:
|
|
328
344
|
uses: kungfu-systems/buildchain/.github/workflows/patrol-weekly.yml@v2
|
|
329
345
|
with:
|
|
330
|
-
target-branch: dev/v2/v2.5
|
|
331
346
|
dry-run: true
|
|
332
347
|
```
|
|
333
348
|
|
|
334
|
-
All three wrappers
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
349
|
+
All three wrappers default to the `v2` floating Buildchain runtime. When
|
|
350
|
+
`target-branch` is omitted, the caller's current/default branch selects the
|
|
351
|
+
active semver dev line, so consumers do not pin patrol to a stale minor branch.
|
|
352
|
+
The separate workflow names keep consumer schedules readable and stable while
|
|
353
|
+
Buildchain adds new checks behind the cadence wrappers.
|
|
338
354
|
|
|
339
355
|
## Package-Manager Adapters
|
|
340
356
|
|
package/docs/release-passport.md
CHANGED
|
@@ -122,6 +122,34 @@ Verify a release passport:
|
|
|
122
122
|
buildchain verify release-passport .buildchain/release-passport/buildchain.release.json
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
+
Verify a specific artifact by discovering its detached passport:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
buildchain verify artifact ./Kungfu-2.8.0-windows-x64.exe
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Artifact verification is subject-centric. Buildchain identifies the subject,
|
|
132
|
+
computes or obtains its digest, discovers a detached `buildchain.release.json`,
|
|
133
|
+
verifies that release passport and its evidence, then proves the subject digest
|
|
134
|
+
appears in the passport's artifacts, package set, publish evidence, or artifact
|
|
135
|
+
evidence. The command returns `pass`, `fail`, or `unverifiable`; missing
|
|
136
|
+
passports and digest mismatches fail closed.
|
|
137
|
+
|
|
138
|
+
Discovery is ordered and auditable:
|
|
139
|
+
|
|
140
|
+
1. explicit `--passport`;
|
|
141
|
+
2. sidecar pointer;
|
|
142
|
+
3. embedded/package pointer;
|
|
143
|
+
4. local config or org index;
|
|
144
|
+
5. GitHub Release default from artifact naming/repository/tag hints;
|
|
145
|
+
6. custom locator;
|
|
146
|
+
7. unverifiable with retry guidance.
|
|
147
|
+
|
|
148
|
+
For Buildchain-managed GitHub Release lanes, release passport files are
|
|
149
|
+
published as release assets by default when the upload backend is enabled, so a
|
|
150
|
+
GitHub Release asset URL can discover the sibling `buildchain.release.json`
|
|
151
|
+
without a consumer copying YAML resolver logic.
|
|
152
|
+
|
|
125
153
|
Explain a release to an agent:
|
|
126
154
|
|
|
127
155
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kungfu-tech/buildchain",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0-alpha.0",
|
|
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",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"exports": {
|
|
14
14
|
".": "./packages/core/index.js",
|
|
15
15
|
"./core": "./packages/core/index.js",
|
|
16
|
+
"./artifact-passport": "./packages/core/artifact-passport.js",
|
|
16
17
|
"./diagnostics": "./packages/core/diagnostics.js",
|
|
17
18
|
"./issue-reporting": "./packages/core/issue-reporting.js",
|
|
18
19
|
"./logging": "./packages/core/logging.js",
|
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import http from "node:http";
|
|
4
|
+
import https from "node:https";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { readJsonFromLocation, sha256File, verifyReleasePassport } from "./release-passport.js";
|
|
7
|
+
|
|
8
|
+
export const ARTIFACT_VERIFICATION_CONTRACT = "kungfu-buildchain-artifact-verification";
|
|
9
|
+
export const ARTIFACT_PASSPORT_POINTER_CONTRACT = "kungfu-buildchain-artifact-passport-pointer";
|
|
10
|
+
export const ARTIFACT_PASSPORT_LOCATOR_CONTRACT = "kungfu-buildchain-artifact-passport-locator";
|
|
11
|
+
|
|
12
|
+
function optionalString(value) {
|
|
13
|
+
return value === undefined || value === null ? "" : String(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isHttpLocation(value = "") {
|
|
17
|
+
return /^https?:\/\//i.test(String(value));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isRemoteSubject(value = "") {
|
|
21
|
+
return isHttpLocation(value) || /^(npm|oci|s3|deployment|github-release):/i.test(String(value));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function issue(level, code, message, details = {}) {
|
|
25
|
+
return { level, code, message, details };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sha256Buffer(buffer) {
|
|
29
|
+
return crypto.createHash("sha256").update(buffer).digest("hex");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function sha512IntegrityBuffer(buffer) {
|
|
33
|
+
return `sha512-${crypto.createHash("sha512").update(buffer).digest("base64")}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function sha512IntegrityFile(filePath) {
|
|
37
|
+
return sha512IntegrityBuffer(fs.readFileSync(filePath));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function readBufferFromHttp(location) {
|
|
41
|
+
const client = location.startsWith("https:") ? https : http;
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
client
|
|
44
|
+
.get(location, (response) => {
|
|
45
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
46
|
+
reject(new Error(`HTTP ${response.statusCode} while reading ${location}`));
|
|
47
|
+
response.resume();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const chunks = [];
|
|
51
|
+
response.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
52
|
+
response.on("end", () => resolve(Buffer.concat(chunks)));
|
|
53
|
+
})
|
|
54
|
+
.on("error", reject);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function readTextMaybe(location) {
|
|
59
|
+
if (isHttpLocation(location)) {
|
|
60
|
+
return (await readBufferFromHttp(location)).toString("utf8");
|
|
61
|
+
}
|
|
62
|
+
return fs.readFileSync(location, "utf8");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readJsonMaybe(filePath) {
|
|
66
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function digestDirectory(dir) {
|
|
70
|
+
const files = [];
|
|
71
|
+
const visit = (current) => {
|
|
72
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
73
|
+
if (entry.name === ".git") {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const fullPath = path.join(current, entry.name);
|
|
77
|
+
if (entry.isDirectory()) {
|
|
78
|
+
visit(fullPath);
|
|
79
|
+
} else if (entry.isFile()) {
|
|
80
|
+
const relativePath = path.relative(dir, fullPath).split(path.sep).join("/");
|
|
81
|
+
files.push({
|
|
82
|
+
path: relativePath,
|
|
83
|
+
size: fs.statSync(fullPath).size,
|
|
84
|
+
sha256: sha256File(fullPath),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
visit(dir);
|
|
90
|
+
files.sort((left, right) => left.path.localeCompare(right.path));
|
|
91
|
+
const manifest = files.map((file) => `${file.path}\0${file.size}\0${file.sha256}`).join("\n");
|
|
92
|
+
return {
|
|
93
|
+
sha256: sha256Buffer(Buffer.from(manifest, "utf8")),
|
|
94
|
+
fileCount: files.length,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function inferKindFromName(name = "") {
|
|
99
|
+
const lower = name.toLowerCase();
|
|
100
|
+
if (lower.endsWith(".exe") || lower.endsWith(".msi") || lower.endsWith(".dmg") || lower.endsWith(".pkg")) {
|
|
101
|
+
return "native-installer";
|
|
102
|
+
}
|
|
103
|
+
if (lower.endsWith(".tgz")) {
|
|
104
|
+
return "npm-package";
|
|
105
|
+
}
|
|
106
|
+
if (lower.endsWith(".zip") || lower.endsWith(".tar.gz") || lower.endsWith(".tar.xz") || lower.endsWith(".tar")) {
|
|
107
|
+
return "archive";
|
|
108
|
+
}
|
|
109
|
+
return "artifact";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function parseNpmSubject(subject) {
|
|
113
|
+
const spec = subject.replace(/^npm:/, "");
|
|
114
|
+
const atIndex = spec.startsWith("@") ? spec.indexOf("@", 1) : spec.lastIndexOf("@");
|
|
115
|
+
if (atIndex <= 0) {
|
|
116
|
+
return { name: spec, version: "" };
|
|
117
|
+
}
|
|
118
|
+
return { name: spec.slice(0, atIndex), version: spec.slice(atIndex + 1) };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function parseGitHubReleaseSubject(subject) {
|
|
122
|
+
const value = subject.replace(/^github-release:/, "");
|
|
123
|
+
const match = value.match(/^([^/]+\/[^@/]+)@([^/]+)\/(.+)$/);
|
|
124
|
+
if (!match) {
|
|
125
|
+
return {};
|
|
126
|
+
}
|
|
127
|
+
return { repository: match[1], tag: match[2], name: match[3] };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function parseGitHubReleaseUrl(location) {
|
|
131
|
+
try {
|
|
132
|
+
const parsed = new URL(location);
|
|
133
|
+
const match = parsed.pathname.match(/^\/([^/]+)\/([^/]+)\/releases\/download\/([^/]+)\/(.+)$/);
|
|
134
|
+
if (!match) {
|
|
135
|
+
return {};
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
repository: `${match[1]}/${match[2]}`,
|
|
139
|
+
tag: decodeURIComponent(match[3]),
|
|
140
|
+
name: decodeURIComponent(match[4]),
|
|
141
|
+
};
|
|
142
|
+
} catch {
|
|
143
|
+
return {};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function resolveLocation(baseLocation, relativeLocation) {
|
|
148
|
+
const relative = optionalString(relativeLocation).trim();
|
|
149
|
+
if (!relative) {
|
|
150
|
+
return "";
|
|
151
|
+
}
|
|
152
|
+
if (isHttpLocation(relative) || isRemoteSubject(relative)) {
|
|
153
|
+
return relative;
|
|
154
|
+
}
|
|
155
|
+
if (isHttpLocation(baseLocation)) {
|
|
156
|
+
return new URL(relative, baseLocation).toString();
|
|
157
|
+
}
|
|
158
|
+
return path.resolve(path.dirname(baseLocation), relative);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function readOptionalJson(location) {
|
|
162
|
+
if (!location) {
|
|
163
|
+
return {};
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
return await readJsonFromLocation(location);
|
|
167
|
+
} catch {
|
|
168
|
+
return {};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function relativeTo(baseDir, maybeRelative) {
|
|
173
|
+
if (!maybeRelative) {
|
|
174
|
+
return "";
|
|
175
|
+
}
|
|
176
|
+
if (isHttpLocation(maybeRelative) || isRemoteSubject(maybeRelative) || path.isAbsolute(maybeRelative)) {
|
|
177
|
+
return maybeRelative;
|
|
178
|
+
}
|
|
179
|
+
return path.resolve(baseDir, maybeRelative);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function readPassportPointer(pointerLocation) {
|
|
183
|
+
const pointer = JSON.parse(await readTextMaybe(pointerLocation));
|
|
184
|
+
const passport = pointer.passport || pointer.passportLocation || pointer.releasePassport || pointer.url || "";
|
|
185
|
+
if (!passport) {
|
|
186
|
+
throw new Error(`passport pointer ${pointerLocation} does not include passport`);
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
pointer,
|
|
190
|
+
passportLocation: resolveLocation(pointerLocation, passport),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function localConfigCandidates({ cwd, subject }) {
|
|
195
|
+
const starts = [cwd];
|
|
196
|
+
if (subject.localPath) {
|
|
197
|
+
starts.push(subject.kind === "directory" || subject.kind === "npm-package" ? subject.localPath : path.dirname(subject.localPath));
|
|
198
|
+
}
|
|
199
|
+
const seen = new Set();
|
|
200
|
+
const result = [];
|
|
201
|
+
for (const start of starts) {
|
|
202
|
+
let current = path.resolve(start || cwd);
|
|
203
|
+
while (!seen.has(current)) {
|
|
204
|
+
seen.add(current);
|
|
205
|
+
for (const name of [
|
|
206
|
+
".buildchain/artifact-passport-locators.json",
|
|
207
|
+
"buildchain.artifact-passport.json",
|
|
208
|
+
]) {
|
|
209
|
+
result.push(path.join(current, name));
|
|
210
|
+
}
|
|
211
|
+
const parent = path.dirname(current);
|
|
212
|
+
if (parent === current) {
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
current = parent;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function locatorMatchesSubject(locator, subject) {
|
|
222
|
+
const match = locator.match || locator.subject || {};
|
|
223
|
+
const digest = optionalString(match.digest || match.sha256 || match.integrity);
|
|
224
|
+
if (digest && !subjectDigestMatches(subject, digest)) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
for (const [left, right] of [
|
|
228
|
+
[match.name, subject.name],
|
|
229
|
+
[match.kind, subject.kind],
|
|
230
|
+
[match.version || match.ref, subject.version],
|
|
231
|
+
[match.repository, subject.githubRelease?.repository],
|
|
232
|
+
[match.tag, subject.githubRelease?.tag],
|
|
233
|
+
]) {
|
|
234
|
+
if (left && right && String(left) !== String(right)) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
if (left && !right) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function readLocatorConfig(location, baseDir) {
|
|
245
|
+
const resolved = relativeTo(baseDir, location);
|
|
246
|
+
const value = await readJsonFromLocation(resolved);
|
|
247
|
+
const locators = Array.isArray(value) ? value : value.locators || value.passports || [];
|
|
248
|
+
return { location: resolved, locators };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function findPassportInLocator(location, subject, baseDir) {
|
|
252
|
+
const config = await readLocatorConfig(location, baseDir);
|
|
253
|
+
for (const locator of config.locators) {
|
|
254
|
+
if (!locatorMatchesSubject(locator, subject)) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const passportLocation = locator.passport || locator.passportLocation || locator.releasePassport || locator.url || "";
|
|
258
|
+
if (passportLocation) {
|
|
259
|
+
return {
|
|
260
|
+
passportLocation: resolveLocation(config.location, passportLocation),
|
|
261
|
+
locator: config.location,
|
|
262
|
+
entry: locator,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function sidecarPointerCandidates(subject) {
|
|
270
|
+
if (!subject.localPath) {
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
const candidates = [];
|
|
274
|
+
if (subject.kind === "directory" || subject.kind === "npm-package") {
|
|
275
|
+
candidates.push(path.join(subject.localPath, ".buildchain-passport.json"));
|
|
276
|
+
candidates.push(path.join(subject.localPath, ".buildchain", "artifact-passport.json"));
|
|
277
|
+
} else {
|
|
278
|
+
candidates.push(`${subject.localPath}.buildchain-passport.json`);
|
|
279
|
+
candidates.push(`${subject.localPath}.passport.json`);
|
|
280
|
+
candidates.push(path.join(path.dirname(subject.localPath), ".buildchain-passport.json"));
|
|
281
|
+
}
|
|
282
|
+
return candidates;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function embeddedPointerCandidates(subject) {
|
|
286
|
+
if (!subject.packageJsonPath || !fs.existsSync(subject.packageJsonPath)) {
|
|
287
|
+
return [];
|
|
288
|
+
}
|
|
289
|
+
const packageJson = readJsonMaybe(subject.packageJsonPath);
|
|
290
|
+
const passport =
|
|
291
|
+
packageJson.buildchain?.releasePassport ||
|
|
292
|
+
packageJson.buildchain?.passport ||
|
|
293
|
+
packageJson.releasePassport ||
|
|
294
|
+
"";
|
|
295
|
+
return passport
|
|
296
|
+
? [{ passport, packageJsonPath: subject.packageJsonPath }]
|
|
297
|
+
: [];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function githubReleasePassportLocation(subject, options) {
|
|
301
|
+
const repository = options.repository || subject.githubRelease?.repository || "";
|
|
302
|
+
const tag = options.tag || subject.githubRelease?.tag || "";
|
|
303
|
+
if (!repository || !tag) {
|
|
304
|
+
return "";
|
|
305
|
+
}
|
|
306
|
+
const baseUrl = (options.githubReleaseBaseUrl || "https://github.com").replace(/\/$/, "");
|
|
307
|
+
return `${baseUrl}/${repository}/releases/download/${encodeURIComponent(tag)}/buildchain.release.json`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export async function resolveArtifactSubject(subject, {
|
|
311
|
+
cwd = process.cwd(),
|
|
312
|
+
subjectDigest = "",
|
|
313
|
+
subjectKind = "",
|
|
314
|
+
} = {}) {
|
|
315
|
+
const input = optionalString(subject).trim();
|
|
316
|
+
if (!input) {
|
|
317
|
+
throw new Error("artifact subject must be a non-empty string");
|
|
318
|
+
}
|
|
319
|
+
if (/^npm:/i.test(input)) {
|
|
320
|
+
const parsed = parseNpmSubject(input);
|
|
321
|
+
return {
|
|
322
|
+
input,
|
|
323
|
+
kind: subjectKind || "npm-package",
|
|
324
|
+
name: parsed.name,
|
|
325
|
+
version: parsed.version,
|
|
326
|
+
digest: subjectDigest,
|
|
327
|
+
localPath: "",
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
if (/^github-release:/i.test(input)) {
|
|
331
|
+
const parsed = parseGitHubReleaseSubject(input);
|
|
332
|
+
return {
|
|
333
|
+
input,
|
|
334
|
+
kind: subjectKind || inferKindFromName(parsed.name || input),
|
|
335
|
+
name: parsed.name || input,
|
|
336
|
+
version: "",
|
|
337
|
+
digest: subjectDigest,
|
|
338
|
+
localPath: "",
|
|
339
|
+
githubRelease: parsed,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
if (/^(oci|s3|deployment):/i.test(input)) {
|
|
343
|
+
return {
|
|
344
|
+
input,
|
|
345
|
+
kind: subjectKind || input.split(":", 1)[0],
|
|
346
|
+
name: input,
|
|
347
|
+
version: "",
|
|
348
|
+
digest: subjectDigest,
|
|
349
|
+
localPath: "",
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
if (isHttpLocation(input)) {
|
|
353
|
+
const buffer = await readBufferFromHttp(input);
|
|
354
|
+
const githubRelease = parseGitHubReleaseUrl(input);
|
|
355
|
+
return {
|
|
356
|
+
input,
|
|
357
|
+
kind: subjectKind || inferKindFromName(githubRelease.name || path.basename(new URL(input).pathname)),
|
|
358
|
+
name: githubRelease.name || path.basename(new URL(input).pathname),
|
|
359
|
+
version: "",
|
|
360
|
+
digest: subjectDigest || `sha256:${sha256Buffer(buffer)}`,
|
|
361
|
+
sha256: sha256Buffer(buffer),
|
|
362
|
+
integrity: sha512IntegrityBuffer(buffer),
|
|
363
|
+
size: buffer.length,
|
|
364
|
+
localPath: "",
|
|
365
|
+
githubRelease,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
const localPath = path.resolve(cwd, input);
|
|
369
|
+
if (!fs.existsSync(localPath)) {
|
|
370
|
+
return {
|
|
371
|
+
input,
|
|
372
|
+
kind: subjectKind || inferKindFromName(path.basename(input)),
|
|
373
|
+
name: path.basename(input),
|
|
374
|
+
version: "",
|
|
375
|
+
digest: subjectDigest,
|
|
376
|
+
localPath,
|
|
377
|
+
missing: true,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
const stat = fs.statSync(localPath);
|
|
381
|
+
if (stat.isDirectory()) {
|
|
382
|
+
const packageJsonPath = path.join(localPath, "package.json");
|
|
383
|
+
const packageJson = fs.existsSync(packageJsonPath) ? readJsonMaybe(packageJsonPath) : {};
|
|
384
|
+
const directoryDigest = digestDirectory(localPath);
|
|
385
|
+
return {
|
|
386
|
+
input,
|
|
387
|
+
kind: subjectKind || (packageJson.name ? "npm-package" : "directory"),
|
|
388
|
+
name: packageJson.name || path.basename(localPath),
|
|
389
|
+
version: packageJson.version || "",
|
|
390
|
+
digest: subjectDigest || `sha256:${directoryDigest.sha256}`,
|
|
391
|
+
sha256: directoryDigest.sha256,
|
|
392
|
+
integrity: "",
|
|
393
|
+
size: stat.size,
|
|
394
|
+
fileCount: directoryDigest.fileCount,
|
|
395
|
+
localPath,
|
|
396
|
+
packageJsonPath: fs.existsSync(packageJsonPath) ? packageJsonPath : "",
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
const buffer = fs.readFileSync(localPath);
|
|
400
|
+
const sha256 = sha256Buffer(buffer);
|
|
401
|
+
return {
|
|
402
|
+
input,
|
|
403
|
+
kind: subjectKind || inferKindFromName(path.basename(localPath)),
|
|
404
|
+
name: path.basename(localPath),
|
|
405
|
+
version: "",
|
|
406
|
+
digest: subjectDigest || `sha256:${sha256}`,
|
|
407
|
+
sha256,
|
|
408
|
+
integrity: sha512IntegrityBuffer(buffer),
|
|
409
|
+
size: stat.size,
|
|
410
|
+
localPath,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export async function discoverArtifactPassport({
|
|
415
|
+
subject,
|
|
416
|
+
cwd = process.cwd(),
|
|
417
|
+
passportLocation = "",
|
|
418
|
+
locatorConfig = "",
|
|
419
|
+
repository = "",
|
|
420
|
+
tag = "",
|
|
421
|
+
githubReleaseBaseUrl = "",
|
|
422
|
+
} = {}) {
|
|
423
|
+
const attempts = [];
|
|
424
|
+
const found = (method, location, details = {}) => ({
|
|
425
|
+
status: "found",
|
|
426
|
+
method,
|
|
427
|
+
passportLocation: location,
|
|
428
|
+
attempts: attempts.concat({ method, status: "found", location, details }),
|
|
429
|
+
details,
|
|
430
|
+
});
|
|
431
|
+
if (passportLocation) {
|
|
432
|
+
const resolved = relativeTo(cwd, passportLocation);
|
|
433
|
+
return found("explicit-passport", resolved);
|
|
434
|
+
}
|
|
435
|
+
for (const candidate of sidecarPointerCandidates(subject)) {
|
|
436
|
+
attempts.push({ method: "sidecar-pointer", location: candidate, status: fs.existsSync(candidate) ? "found" : "miss" });
|
|
437
|
+
if (fs.existsSync(candidate)) {
|
|
438
|
+
const pointer = await readPassportPointer(candidate);
|
|
439
|
+
return found("sidecar-pointer", pointer.passportLocation, { pointer: candidate });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
for (const pointer of embeddedPointerCandidates(subject)) {
|
|
443
|
+
attempts.push({ method: "embedded-package-pointer", location: pointer.packageJsonPath, status: "found" });
|
|
444
|
+
return found("embedded-package-pointer", resolveLocation(pointer.packageJsonPath, pointer.passport), {
|
|
445
|
+
packageJson: pointer.packageJsonPath,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
for (const candidate of localConfigCandidates({ cwd, subject })) {
|
|
449
|
+
attempts.push({ method: "local-config-index", location: candidate, status: fs.existsSync(candidate) ? "checked" : "miss" });
|
|
450
|
+
if (!fs.existsSync(candidate)) {
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
const matched = await findPassportInLocator(candidate, subject, cwd);
|
|
454
|
+
if (matched) {
|
|
455
|
+
return found("local-config-index", matched.passportLocation, { locator: matched.locator });
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const githubLocation = githubReleasePassportLocation(subject, { repository, tag, githubReleaseBaseUrl });
|
|
459
|
+
if (githubLocation) {
|
|
460
|
+
try {
|
|
461
|
+
await readJsonFromLocation(githubLocation);
|
|
462
|
+
attempts.push({ method: "github-release-default", location: githubLocation, status: "found" });
|
|
463
|
+
return found("github-release-default", githubLocation, { repository: repository || subject.githubRelease?.repository || "", tag: tag || subject.githubRelease?.tag || "" });
|
|
464
|
+
} catch (error) {
|
|
465
|
+
attempts.push({
|
|
466
|
+
method: "github-release-default",
|
|
467
|
+
location: githubLocation,
|
|
468
|
+
status: "miss",
|
|
469
|
+
error: error.message,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (locatorConfig) {
|
|
474
|
+
const resolved = relativeTo(cwd, locatorConfig);
|
|
475
|
+
attempts.push({ method: "custom-locator", location: resolved, status: fs.existsSync(resolved) || isHttpLocation(resolved) ? "checked" : "miss" });
|
|
476
|
+
let matched;
|
|
477
|
+
try {
|
|
478
|
+
matched = await findPassportInLocator(resolved, subject, cwd);
|
|
479
|
+
} catch (error) {
|
|
480
|
+
attempts.push({ method: "custom-locator", location: resolved, status: "error", error: error.message });
|
|
481
|
+
}
|
|
482
|
+
if (matched) {
|
|
483
|
+
return found("custom-locator", matched.passportLocation, { locator: matched.locator });
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
status: "unverifiable",
|
|
488
|
+
method: "unresolved",
|
|
489
|
+
passportLocation: "",
|
|
490
|
+
attempts: attempts.concat({
|
|
491
|
+
method: "unverifiable",
|
|
492
|
+
status: "unverifiable",
|
|
493
|
+
guidance: "Provide --passport, add a sidecar pointer, publish buildchain.release.json to the GitHub Release, or configure a locator.",
|
|
494
|
+
}),
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function digestValues(value = {}) {
|
|
499
|
+
return [
|
|
500
|
+
value.digest,
|
|
501
|
+
value.sha256 ? `sha256:${value.sha256}` : "",
|
|
502
|
+
value.integrity,
|
|
503
|
+
value.shasum,
|
|
504
|
+
].map(optionalString).filter(Boolean);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function normalizeDigest(value = "") {
|
|
508
|
+
return optionalString(value).trim();
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function digestEquivalent(left = "", right = "") {
|
|
512
|
+
const a = normalizeDigest(left);
|
|
513
|
+
const b = normalizeDigest(right);
|
|
514
|
+
if (!a || !b) {
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
return a === b || a === `sha256:${b}` || `sha256:${a}` === b;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function subjectDigestMatches(subject, digest) {
|
|
521
|
+
return digestValues(subject).some((value) => digestEquivalent(value, digest));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function packageSetArtifacts(passport = {}) {
|
|
525
|
+
const packageSet = passport.packageSet || {};
|
|
526
|
+
return [
|
|
527
|
+
...(packageSet.main?.name ? [{ role: "main", ...packageSet.main }] : []),
|
|
528
|
+
...((packageSet.platforms || []).map((entry) => ({ role: "platform", ...entry }))),
|
|
529
|
+
].map((entry) => ({
|
|
530
|
+
group: "node",
|
|
531
|
+
kind: "npm",
|
|
532
|
+
name: entry.name,
|
|
533
|
+
ref: entry.version,
|
|
534
|
+
digest: entry.digest,
|
|
535
|
+
role: entry.role,
|
|
536
|
+
platform: entry.platform || "",
|
|
537
|
+
source: "packageSet",
|
|
538
|
+
}));
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function publishSummaryArtifacts(passport = {}) {
|
|
542
|
+
return (passport.publish?.packages || []).map((entry) => ({
|
|
543
|
+
group: "node",
|
|
544
|
+
kind: "npm",
|
|
545
|
+
name: entry.name,
|
|
546
|
+
ref: entry.publishedVersion || entry.version,
|
|
547
|
+
digest: entry.digest,
|
|
548
|
+
role: entry.role,
|
|
549
|
+
platform: entry.platform || "",
|
|
550
|
+
source: "publish.packages",
|
|
551
|
+
}));
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function passportArtifacts({ passport = {}, artifactEvidence = {}, publishEvidence = {} } = {}) {
|
|
555
|
+
return [
|
|
556
|
+
...(passport.artifacts || []).map((entry) => ({ ...entry, source: "passport.artifacts" })),
|
|
557
|
+
...(artifactEvidence.artifacts || []).map((entry) => ({ ...entry, source: "artifact-evidence.artifacts" })),
|
|
558
|
+
...(publishEvidence.artifacts || []).map((entry) => ({ ...entry, source: "publish-evidence.artifacts" })),
|
|
559
|
+
...packageSetArtifacts(passport),
|
|
560
|
+
...publishSummaryArtifacts(passport),
|
|
561
|
+
];
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function artifactMatchesSubject(artifact, subject) {
|
|
565
|
+
const artifactDigests = digestValues(artifact);
|
|
566
|
+
const digestMatch = artifactDigests.some((digest) => subjectDigestMatches(subject, digest));
|
|
567
|
+
const nameMatch = !artifact.name || !subject.name || artifact.name === subject.name;
|
|
568
|
+
const refMatch = !artifact.ref || !subject.version || artifact.ref === subject.version;
|
|
569
|
+
const packageIdentityMatch =
|
|
570
|
+
subject.kind === "npm-package" &&
|
|
571
|
+
artifact.name === subject.name &&
|
|
572
|
+
(!artifact.ref || !subject.version || artifact.ref === subject.version);
|
|
573
|
+
if (digestMatch && (nameMatch || packageIdentityMatch)) {
|
|
574
|
+
return { matched: true, reason: "digest" };
|
|
575
|
+
}
|
|
576
|
+
if (digestMatch) {
|
|
577
|
+
return { matched: true, reason: "digest-only" };
|
|
578
|
+
}
|
|
579
|
+
return { matched: false, reason: packageIdentityMatch ? "package-identity-without-digest" : "no-match" };
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
async function loadPassportBundle(passportLocation) {
|
|
583
|
+
const passport = await readJsonFromLocation(passportLocation);
|
|
584
|
+
const artifactEvidenceLocation = resolveLocation(passportLocation, passport.evidence?.artifactEvidence);
|
|
585
|
+
const publishEvidenceLocation = resolveLocation(passportLocation, passport.evidence?.publishEvidence);
|
|
586
|
+
const impactLocation = resolveLocation(passportLocation, passport.evidence?.impact);
|
|
587
|
+
const agentIndexLocation = resolveLocation(passportLocation, passport.evidence?.agentIndex);
|
|
588
|
+
const productMechanismLocation = resolveLocation(passportLocation, passport.product?.mechanism);
|
|
589
|
+
const [artifactEvidence, publishEvidence] = await Promise.all([
|
|
590
|
+
readOptionalJson(artifactEvidenceLocation),
|
|
591
|
+
readOptionalJson(publishEvidenceLocation),
|
|
592
|
+
]);
|
|
593
|
+
return {
|
|
594
|
+
passport,
|
|
595
|
+
artifactEvidence,
|
|
596
|
+
publishEvidence,
|
|
597
|
+
locations: {
|
|
598
|
+
artifactEvidenceLocation,
|
|
599
|
+
publishEvidenceLocation,
|
|
600
|
+
impactLocation,
|
|
601
|
+
agentIndexLocation,
|
|
602
|
+
productMechanismLocation,
|
|
603
|
+
},
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
export async function verifyArtifactPassport({
|
|
608
|
+
subject,
|
|
609
|
+
cwd = process.cwd(),
|
|
610
|
+
passportLocation = "",
|
|
611
|
+
locatorConfig = "",
|
|
612
|
+
repository = "",
|
|
613
|
+
tag = "",
|
|
614
|
+
githubReleaseBaseUrl = "",
|
|
615
|
+
subjectDigest = "",
|
|
616
|
+
subjectKind = "",
|
|
617
|
+
} = {}) {
|
|
618
|
+
const resolvedSubject = await resolveArtifactSubject(subject, { cwd, subjectDigest, subjectKind });
|
|
619
|
+
const issues = [];
|
|
620
|
+
if (resolvedSubject.missing) {
|
|
621
|
+
issues.push(issue("error", "subject.missing", "artifact subject does not exist locally", { subject: resolvedSubject.input }));
|
|
622
|
+
return {
|
|
623
|
+
schemaVersion: 1,
|
|
624
|
+
contract: ARTIFACT_VERIFICATION_CONTRACT,
|
|
625
|
+
outcome: "unverifiable",
|
|
626
|
+
ok: false,
|
|
627
|
+
trust: "unverifiable",
|
|
628
|
+
subject: resolvedSubject,
|
|
629
|
+
discovery: { status: "unverifiable", method: "subject-missing", attempts: [] },
|
|
630
|
+
issues,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
const discovery = await discoverArtifactPassport({
|
|
634
|
+
subject: resolvedSubject,
|
|
635
|
+
cwd,
|
|
636
|
+
passportLocation,
|
|
637
|
+
locatorConfig,
|
|
638
|
+
repository,
|
|
639
|
+
tag,
|
|
640
|
+
githubReleaseBaseUrl,
|
|
641
|
+
});
|
|
642
|
+
if (!discovery.passportLocation) {
|
|
643
|
+
issues.push(issue("error", "passport.unavailable", "no release passport could be discovered for artifact subject"));
|
|
644
|
+
return {
|
|
645
|
+
schemaVersion: 1,
|
|
646
|
+
contract: ARTIFACT_VERIFICATION_CONTRACT,
|
|
647
|
+
outcome: "unverifiable",
|
|
648
|
+
ok: false,
|
|
649
|
+
trust: "unverifiable",
|
|
650
|
+
subject: resolvedSubject,
|
|
651
|
+
discovery,
|
|
652
|
+
issues,
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
let bundle;
|
|
656
|
+
let passportReport;
|
|
657
|
+
try {
|
|
658
|
+
bundle = await loadPassportBundle(discovery.passportLocation);
|
|
659
|
+
passportReport = await verifyReleasePassport({
|
|
660
|
+
passportLocation: discovery.passportLocation,
|
|
661
|
+
artifactEvidenceLocation: bundle.locations.artifactEvidenceLocation,
|
|
662
|
+
publishEvidenceLocation: bundle.locations.publishEvidenceLocation,
|
|
663
|
+
impactLocation: bundle.locations.impactLocation,
|
|
664
|
+
agentIndexLocation: bundle.locations.agentIndexLocation,
|
|
665
|
+
productMechanismLocation: bundle.locations.productMechanismLocation,
|
|
666
|
+
});
|
|
667
|
+
} catch (error) {
|
|
668
|
+
issues.push(issue("error", "passport.read", "release passport could not be read or parsed", {
|
|
669
|
+
passportLocation: discovery.passportLocation,
|
|
670
|
+
error: error.message,
|
|
671
|
+
}));
|
|
672
|
+
return {
|
|
673
|
+
schemaVersion: 1,
|
|
674
|
+
contract: ARTIFACT_VERIFICATION_CONTRACT,
|
|
675
|
+
outcome: "fail",
|
|
676
|
+
ok: false,
|
|
677
|
+
trust: "fail",
|
|
678
|
+
subject: resolvedSubject,
|
|
679
|
+
discovery,
|
|
680
|
+
issues,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
if (!passportReport.ok) {
|
|
684
|
+
issues.push(issue("error", "passport.verification", "release passport verification failed"));
|
|
685
|
+
}
|
|
686
|
+
const candidates = passportArtifacts(bundle);
|
|
687
|
+
const match = candidates.map((artifact) => ({ artifact, result: artifactMatchesSubject(artifact, resolvedSubject) }))
|
|
688
|
+
.find((entry) => entry.result.matched);
|
|
689
|
+
if (!match) {
|
|
690
|
+
issues.push(issue("error", "subject.digest.missing", "artifact subject digest was not found in the release passport evidence", {
|
|
691
|
+
digest: resolvedSubject.digest || resolvedSubject.integrity || "",
|
|
692
|
+
name: resolvedSubject.name,
|
|
693
|
+
kind: resolvedSubject.kind,
|
|
694
|
+
}));
|
|
695
|
+
}
|
|
696
|
+
const ok = passportReport.ok && Boolean(match);
|
|
697
|
+
const outcome = ok ? "pass" : "fail";
|
|
698
|
+
return {
|
|
699
|
+
schemaVersion: 1,
|
|
700
|
+
contract: ARTIFACT_VERIFICATION_CONTRACT,
|
|
701
|
+
outcome,
|
|
702
|
+
ok,
|
|
703
|
+
trust: outcome,
|
|
704
|
+
subject: resolvedSubject,
|
|
705
|
+
discovery,
|
|
706
|
+
passport: {
|
|
707
|
+
location: discovery.passportLocation,
|
|
708
|
+
product: bundle.passport.product,
|
|
709
|
+
release: bundle.passport.release,
|
|
710
|
+
verification: {
|
|
711
|
+
ok: passportReport.ok,
|
|
712
|
+
trust: passportReport.trust,
|
|
713
|
+
completeness: passportReport.completeness,
|
|
714
|
+
issues: passportReport.issues,
|
|
715
|
+
},
|
|
716
|
+
},
|
|
717
|
+
match: match
|
|
718
|
+
? {
|
|
719
|
+
source: match.artifact.source,
|
|
720
|
+
reason: match.result.reason,
|
|
721
|
+
artifact: match.artifact,
|
|
722
|
+
}
|
|
723
|
+
: undefined,
|
|
724
|
+
issues,
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
export async function explainArtifactPassport(options = {}) {
|
|
729
|
+
const report = await verifyArtifactPassport(options);
|
|
730
|
+
const nextAction = report.ok
|
|
731
|
+
? "use-artifact-after-policy-review"
|
|
732
|
+
: report.outcome === "unverifiable"
|
|
733
|
+
? "locate-passport-or-add-artifact-passport-pointer"
|
|
734
|
+
: "block-artifact-and-report-verification-failure";
|
|
735
|
+
return {
|
|
736
|
+
schemaVersion: 1,
|
|
737
|
+
contract: "kungfu-buildchain-artifact-explanation",
|
|
738
|
+
audience: options.forAudience || "human",
|
|
739
|
+
subject: report.subject,
|
|
740
|
+
outcome: report.outcome,
|
|
741
|
+
trust: report.trust,
|
|
742
|
+
passport: report.passport,
|
|
743
|
+
discovery: report.discovery,
|
|
744
|
+
match: report.match,
|
|
745
|
+
nextAction,
|
|
746
|
+
issues: report.issues,
|
|
747
|
+
};
|
|
748
|
+
}
|
package/packages/core/index.js
CHANGED
|
@@ -92,6 +92,18 @@ export {
|
|
|
92
92
|
validateReleaseCandidatePassport,
|
|
93
93
|
} from "./release-candidate.js";
|
|
94
94
|
|
|
95
|
+
export {
|
|
96
|
+
ARTIFACT_PASSPORT_LOCATOR_CONTRACT,
|
|
97
|
+
ARTIFACT_PASSPORT_POINTER_CONTRACT,
|
|
98
|
+
ARTIFACT_VERIFICATION_CONTRACT,
|
|
99
|
+
discoverArtifactPassport,
|
|
100
|
+
explainArtifactPassport,
|
|
101
|
+
resolveArtifactSubject,
|
|
102
|
+
sha512IntegrityBuffer,
|
|
103
|
+
sha512IntegrityFile,
|
|
104
|
+
verifyArtifactPassport,
|
|
105
|
+
} from "./artifact-passport.js";
|
|
106
|
+
|
|
95
107
|
export {
|
|
96
108
|
AGENT_INDEX_CONTRACT,
|
|
97
109
|
ARTIFACT_EVIDENCE_CONTRACT,
|
|
@@ -173,7 +173,7 @@ function explainReleaseLineDryRun({
|
|
|
173
173
|
"target branch protection must be readable",
|
|
174
174
|
"branch protection must enforce administrators",
|
|
175
175
|
"required pull request review must be enabled",
|
|
176
|
-
"strict required status check must include the
|
|
176
|
+
"strict required status check must include the check job",
|
|
177
177
|
`source PR must be a merged same-repository PR from ${rule.sourceRef} to ${targetRef}`,
|
|
178
178
|
],
|
|
179
179
|
publishTransaction: {
|
|
@@ -3,7 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { runDevPrAutoMerge } from "./dev-pr-auto-merge.mjs";
|
|
5
5
|
|
|
6
|
-
const DEFAULT_TARGET_BRANCH = "
|
|
6
|
+
const DEFAULT_TARGET_BRANCH = "";
|
|
7
7
|
const DEFAULT_OUTPUT_PATH = ".buildchain/patrol/result.json";
|
|
8
8
|
const VALID_CADENCES = new Set(["daily", "weekly", "monthly"]);
|
|
9
9
|
const VALID_MODES = new Set(["cadence-default", "inspect", "merge-ready-dev-prs", "cleanup-safe"]);
|
|
@@ -47,9 +47,9 @@ function normalizeRepository(value) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
function normalizeTargetBranch(value) {
|
|
50
|
-
const branch = String(value || DEFAULT_TARGET_BRANCH).replace(/^refs\/heads\//, "");
|
|
50
|
+
const branch = String(value || process.env.GITHUB_REF_NAME || DEFAULT_TARGET_BRANCH).replace(/^refs\/heads\//, "");
|
|
51
51
|
if (!/^dev\/v\d+\/v\d+\.\d+$/.test(branch)) {
|
|
52
|
-
throw new Error(`target-branch must be a semver dev branch such as dev/v2/v2.
|
|
52
|
+
throw new Error(`target-branch must be a semver dev branch such as dev/v2/v2.N, got: ${branch || "<empty>"}`);
|
|
53
53
|
}
|
|
54
54
|
return branch;
|
|
55
55
|
}
|
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
|
|
5
5
|
const DEFAULT_BLOCK_LABELS = ["blocked", "do-not-merge", "work-in-progress"];
|
|
6
6
|
const DEFAULT_ALLOWED_HEAD_PREFIXES = ["feature/", "fix/", "chore/", "docs/", "ci/", "refactor/"];
|
|
7
|
-
const DEFAULT_REQUIRED_CHECKS = ["
|
|
7
|
+
const DEFAULT_REQUIRED_CHECKS = ["check"];
|
|
8
8
|
const SUCCESS_STATES = new Set(["success"]);
|
|
9
9
|
const SUCCESS_CONCLUSIONS = new Set(["success", "neutral", "skipped"]);
|
|
10
10
|
|