@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.
@@ -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.5
277
- required-status-checks: Verify
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 call the same underlying Buildchain patrol protocol, but the
335
- separate workflow names keep consumer schedules readable and stable. Buildchain
336
- can add new checks behind the cadence wrappers without forcing every consumer
337
- repository to rewrite its schedule YAML.
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
 
@@ -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.5.7",
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
+ }
@@ -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 Verify check",
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 = "dev/v2/v2.5";
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.5, got: ${branch}`);
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
  }
@@ -271,6 +271,7 @@ for (const requiredSnippet of [
271
271
  "bin/buildchain.mjs log summary",
272
272
  "collect github-release",
273
273
  "verify release-passport",
274
+ "verify artifact",
274
275
  "scripts/create-release-bundle.mjs",
275
276
  "buildchain-release-bundle",
276
277
  "gh release upload",
@@ -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 = ["Verify"];
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