@kungfu-tech/buildchain 3.0.2-alpha.0 → 3.0.2-alpha.10

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.
Files changed (83) hide show
  1. package/README.md +4 -2
  2. package/actions/github-artifact-attestation/README.md +10 -0
  3. package/actions/promote-buildchain-ref/README.md +7 -0
  4. package/bin/buildchain.mjs +5 -0
  5. package/bin/internal/trust-release-cli.mjs +74 -3
  6. package/contracts/auditable-demo-media-profiles-v1.json +168 -0
  7. package/contracts/evidence/auditable-demo-web-delivery-v1.json +103 -0
  8. package/contracts/fixtures/auditable-demo-web-delivery-v1/complete-transcript.txt +2 -0
  9. package/contracts/fixtures/auditable-demo-web-delivery-v1/public-projection.json +16 -0
  10. package/contracts/fixtures/auditable-demo-web-delivery-v1/scene.json +12 -0
  11. package/dist/site/artifact-schemas.json +5 -1
  12. package/dist/site/buildchain-contract.json +133 -34
  13. package/dist/site/buildchain-site.json +159 -40
  14. package/dist/site/capability-registry.json +14 -13
  15. package/dist/site/cli-registry.json +12 -0
  16. package/dist/site/controller-registry.json +72 -4
  17. package/dist/site/kfd-claims.json +338 -20
  18. package/dist/site/kfd-upstream-aggregate.json +1 -1
  19. package/dist/site/manual-registry.json +24 -9
  20. package/dist/site/node-api-registry.json +89 -11
  21. package/dist/site/page-registry.json +135 -25
  22. package/dist/site/public-surface-audit.json +203 -21
  23. package/dist/site/publication-authority-registry.json +72 -2
  24. package/dist/site/publication-registry.json +4 -4
  25. package/dist/site/release-model.json +2 -1
  26. package/dist/site/release-passport-check-manifest.json +1 -0
  27. package/dist/site/release-provenance.json +6 -0
  28. package/dist/site/schemas/release-passport-v1.schema.json +6 -0
  29. package/dist/site/site-manifest.json +21 -13
  30. package/dist/site/workflow-registry.json +167 -13
  31. package/docs/MAP.md +5 -1
  32. package/docs/auditable-demo.md +55 -3
  33. package/docs/binary-distribution.md +7 -0
  34. package/docs/cli.md +9 -0
  35. package/docs/dev-alpha-candidate-patrol.md +111 -0
  36. package/docs/github-artifact-attestation.md +219 -0
  37. package/docs/release-governance.md +32 -0
  38. package/docs/release-passport.md +13 -0
  39. package/docs/reusable-build-surface.md +83 -61
  40. package/docs/runtime-train-validation.md +21 -0
  41. package/docs/versioning.md +1 -0
  42. package/package.json +8 -1
  43. package/packages/core/artifact-signing-result.js +228 -0
  44. package/packages/core/artifact-signing.js +412 -0
  45. package/packages/core/buildchain-config.js +58 -0
  46. package/packages/core/buildchain-contract.js +14 -0
  47. package/packages/core/buildchain-kfd-claims.js +5 -0
  48. package/packages/core/buildchain-publication-authority.js +3 -0
  49. package/packages/core/cache-evidence.js +288 -0
  50. package/packages/core/channel-candidate.js +186 -0
  51. package/packages/core/detached-artifact-signature.js +121 -0
  52. package/packages/core/diagnostics.js +276 -10
  53. package/packages/core/github-artifact-attestation.js +642 -0
  54. package/packages/core/github-governance-authority.js +77 -16
  55. package/packages/core/index.js +62 -0
  56. package/packages/core/publication-authority.js +1 -1
  57. package/packages/core/release-passport-contract.js +2 -0
  58. package/packages/core/release-passport.js +60 -3
  59. package/scripts/auditable-demo.mjs +520 -28
  60. package/scripts/build-contract-core.mjs +31 -0
  61. package/scripts/buildchain-channel-router.mjs +8 -2
  62. package/scripts/check-inventory.mjs +9 -0
  63. package/scripts/create-github-artifact-attestation-policy.mjs +62 -0
  64. package/scripts/dev-alpha-candidate-patrol.mjs +864 -0
  65. package/scripts/dispatch-artifact-signing-authority.mjs +152 -0
  66. package/scripts/finalize-native-artifact-signing-result.mjs +96 -0
  67. package/scripts/generate-channel-promotion-workflow.mjs +6 -0
  68. package/scripts/generate-site-bundle.mjs +20 -0
  69. package/scripts/import-artifact-signing-results.mjs +76 -0
  70. package/scripts/inspect-artifact-signing-requests.mjs +101 -0
  71. package/scripts/locked-source-checkout.mjs +48 -0
  72. package/scripts/materialize-artifact-signing-request.mjs +66 -0
  73. package/scripts/merge-artifact-signing-results.mjs +76 -0
  74. package/scripts/publish-github-artifact-attestation-evidence.mjs +201 -0
  75. package/scripts/reconcile-github-governance.mjs +158 -25
  76. package/scripts/release-candidate-resolver.mjs +12 -0
  77. package/scripts/release-line-policy.mjs +27 -0
  78. package/scripts/runtime-ref-core.mjs +10 -6
  79. package/scripts/seal-artifact-signing-requests.mjs +368 -0
  80. package/scripts/shifu-gate-profile.mjs +26 -20
  81. package/scripts/sign-detached-artifact-requests.mjs +237 -0
  82. package/scripts/stage-github-artifact-attestation-inputs.mjs +65 -0
  83. package/scripts/verify-artifact-signing-results.mjs +99 -0
@@ -90,6 +90,12 @@ function readJson(filePath, label) {
90
90
  }
91
91
  }
92
92
 
93
+ function writeJson(filePath, value) {
94
+ const target = path.resolve(filePath);
95
+ fs.mkdirSync(path.dirname(target), { recursive: true });
96
+ fs.writeFileSync(target, `${JSON.stringify(value, null, 2)}\n`);
97
+ }
98
+
93
99
  function githubApi(route, { method = "GET", body } = {}) {
94
100
  const args = [
95
101
  "api",
@@ -142,6 +148,42 @@ function readRuleset(repository, rulesetId) {
142
148
  return normalizeGithubRulesetSnapshot(response.data);
143
149
  }
144
150
 
151
+ function readMatchingRulesetEntries(repository, branch) {
152
+ const response = githubApi(
153
+ `repos/${repository}/rulesets?includes_parents=false&per_page=100`,
154
+ );
155
+ const include = `refs/heads/${branch}`;
156
+ return (response.data || [])
157
+ .map((entry) => ({
158
+ id: Number(entry.id),
159
+ ruleset: readRuleset(repository, entry.id),
160
+ }))
161
+ .filter(({ ruleset }) => {
162
+ const refName = ruleset.conditions?.ref_name || {};
163
+ return (
164
+ ruleset.target === "branch" &&
165
+ (refName.include || []).length === 1 &&
166
+ refName.include[0] === include &&
167
+ (refName.exclude || []).length === 0
168
+ );
169
+ })
170
+ .sort((left, right) => left.ruleset.name.localeCompare(right.ruleset.name));
171
+ }
172
+
173
+ function readMatchingRulesets(repository, branch) {
174
+ return readMatchingRulesetEntries(repository, branch).map(
175
+ ({ ruleset }) => ruleset,
176
+ );
177
+ }
178
+
179
+ function rulesetInventory(repository, branch) {
180
+ return {
181
+ repository,
182
+ targetRef: branch,
183
+ matchingRulesets: readMatchingRulesets(repository, branch),
184
+ };
185
+ }
186
+
145
187
  function snapshotCore(repository, branch, protection) {
146
188
  return {
147
189
  schemaVersion: 1,
@@ -200,8 +242,8 @@ function rulesetPlan(args) {
200
242
  ...planCore,
201
243
  planRoot: githubGovernanceDigest(planCore),
202
244
  };
203
- fs.writeFileSync(path.resolve(snapshotOutput), `${JSON.stringify(snapshot, null, 2)}\n`);
204
- fs.writeFileSync(path.resolve(planOutput), `${JSON.stringify(finalPlan, null, 2)}\n`);
245
+ writeJson(snapshotOutput, snapshot);
246
+ writeJson(planOutput, finalPlan);
205
247
  return finalPlan;
206
248
  }
207
249
 
@@ -209,13 +251,45 @@ function rulesetPolicyPlan(args) {
209
251
  const repository = required(flag(args, "repository"), "--repository");
210
252
  const branch = required(flag(args, "branch"), "--branch")
211
253
  .replace(/^refs\/heads\//, "");
212
- const rulesetId = Number(required(flag(args, "ruleset-id"), "--ruleset-id"));
213
- if (!Number.isInteger(rulesetId) || rulesetId <= 0) {
214
- throw new Error("--ruleset-id must be a positive integer");
215
- }
254
+ const requestedRulesetId = flag(args, "ruleset-id");
255
+ const rulesetName = required(flag(args, "ruleset-name"), "--ruleset-name");
216
256
  const snapshotOutput = required(flag(args, "snapshot-output"), "--snapshot-output");
217
257
  const planOutput = required(flag(args, "plan-output"), "--plan-output");
218
- const before = readRuleset(repository, rulesetId);
258
+ const matchingEntries = readMatchingRulesetEntries(repository, branch);
259
+ const inventory = {
260
+ repository,
261
+ targetRef: branch,
262
+ matchingRulesets: matchingEntries.map(({ ruleset }) => ruleset),
263
+ };
264
+ let rulesetId = null;
265
+ let before = null;
266
+ if (requestedRulesetId) {
267
+ rulesetId = Number(requestedRulesetId);
268
+ if (!Number.isInteger(rulesetId) || rulesetId <= 0) {
269
+ throw new Error("--ruleset-id must be a positive integer");
270
+ }
271
+ before = readRuleset(repository, rulesetId);
272
+ if (
273
+ !inventory.matchingRulesets.some(
274
+ (ruleset) =>
275
+ githubGovernanceDigest(ruleset) === githubGovernanceDigest(before),
276
+ )
277
+ ) {
278
+ throw new Error("--ruleset-id does not target the exact requested branch");
279
+ }
280
+ } else if (matchingEntries.length === 1) {
281
+ before = matchingEntries[0].ruleset;
282
+ rulesetId = matchingEntries[0].id;
283
+ if (!Number.isInteger(rulesetId) || rulesetId <= 0) {
284
+ throw new Error("matching ruleset id could not be resolved");
285
+ }
286
+ } else if (inventory.matchingRulesets.length > 1) {
287
+ throw new Error("multiple exact-target rulesets are ambiguous");
288
+ } else if (!hasFlag(args, "create-if-missing")) {
289
+ throw new Error(
290
+ "exact-target ruleset is absent; pass --create-if-missing to plan creation",
291
+ );
292
+ }
219
293
  const targetPolicy = resolveGithubGovernanceTargetPolicy({
220
294
  repository,
221
295
  targetRef: branch,
@@ -225,6 +299,7 @@ function rulesetPolicyPlan(args) {
225
299
  contract: "kungfu-buildchain-github-governance-ruleset-rollback-snapshot",
226
300
  repository,
227
301
  targetRef: branch,
302
+ rulesetExists: Boolean(before),
228
303
  rulesetId,
229
304
  ruleset: before,
230
305
  };
@@ -236,13 +311,16 @@ function rulesetPolicyPlan(args) {
236
311
  repository,
237
312
  targetRef: branch,
238
313
  rulesetId,
239
- inventory: before,
314
+ rulesetName,
315
+ inventory,
240
316
  rollbackSnapshot: before,
241
317
  desiredProtection: {
242
318
  strictRequiredChecks: targetPolicy.strictRequiredChecks,
243
319
  requiredCheckBindings: targetPolicy.requiredCheckBindings,
244
320
  requiredApprovals: targetPolicy.requiredApprovals,
245
321
  rulesetBypassActors: [],
322
+ blockDeletions: hasFlag(args, "block-deletions"),
323
+ blockNonFastForward: hasFlag(args, "block-non-fast-forward"),
246
324
  },
247
325
  });
248
326
  const boundCore = {
@@ -255,8 +333,8 @@ function rulesetPolicyPlan(args) {
255
333
  ...planCore,
256
334
  planRoot: githubGovernanceDigest(planCore),
257
335
  };
258
- fs.writeFileSync(path.resolve(snapshotOutput), `${JSON.stringify(snapshot, null, 2)}\n`);
259
- fs.writeFileSync(path.resolve(planOutput), `${JSON.stringify(finalPlan, null, 2)}\n`);
336
+ writeJson(snapshotOutput, snapshot);
337
+ writeJson(planOutput, finalPlan);
260
338
  return finalPlan;
261
339
  }
262
340
 
@@ -276,20 +354,29 @@ function rulesetApply(args) {
276
354
  snapshotRoot !== githubGovernanceDigest(snapshotCore)) {
277
355
  throw new Error("ruleset rollback snapshot root mismatch");
278
356
  }
279
- const current = readRuleset(rollout.repository, rollout.rulesetId);
280
- if (githubGovernanceDigest(current) !== rollout.inventoryRoot) {
357
+ const currentInventory = rulesetInventory(
358
+ rollout.repository,
359
+ rollout.targetRef,
360
+ );
361
+ if (githubGovernanceDigest(currentInventory) !== rollout.inventoryRoot) {
281
362
  throw new Error("live GitHub ruleset drifted after planning; apply stopped");
282
363
  }
283
- githubApi(rollout.operations[0].endpoint, {
364
+ const result = githubApi(rollout.operations[0].endpoint, {
284
365
  method: rollout.operations[0].method,
285
366
  body: rollout.operations[0].body,
286
367
  });
287
- const after = readRuleset(rollout.repository, rollout.rulesetId);
368
+ const rulesetId =
369
+ rollout.rulesetId ||
370
+ Number(result.data?.id || 0);
371
+ if (!Number.isInteger(rulesetId) || rulesetId <= 0) {
372
+ throw new Error("GitHub did not return the created ruleset id");
373
+ }
374
+ const after = readRuleset(rollout.repository, rulesetId);
288
375
  const afterRoot = githubGovernanceDigest(after);
289
376
  if (afterRoot !== rollout.expectedObservation.rulesetRoot) {
290
377
  throw new Error("post-change GitHub ruleset read-back does not match the rollout plan");
291
378
  }
292
- return {
379
+ const receiptCore = {
293
380
  schemaVersion: 1,
294
381
  contract: "kungfu-buildchain-github-governance-ruleset-rollout-receipt",
295
382
  status: "applied",
@@ -297,7 +384,21 @@ function rulesetApply(args) {
297
384
  snapshotRoot: rollout.snapshotRoot,
298
385
  afterRoot,
299
386
  repository: rollout.repository,
300
- rulesetId: rollout.rulesetId,
387
+ targetRef: rollout.targetRef,
388
+ rulesetId,
389
+ rollback:
390
+ rollout.action === "create"
391
+ ? {
392
+ method: "DELETE",
393
+ endpoint: rulesetEndpoint(rollout.repository, rulesetId),
394
+ body: null,
395
+ preconditionRoot: afterRoot,
396
+ }
397
+ : rollout.rollback[0],
398
+ };
399
+ return {
400
+ ...receiptCore,
401
+ receiptRoot: githubGovernanceDigest(receiptCore),
301
402
  };
302
403
  }
303
404
 
@@ -311,14 +412,46 @@ function rulesetRollback(args) {
311
412
  if (rollout.snapshotRoot !== confirmed) {
312
413
  throw new Error("--confirm-rollback-root does not match the frozen ruleset snapshot");
313
414
  }
314
- const operation = rollout.rollback[0];
415
+ let rulesetId = rollout.rulesetId;
416
+ let operation = rollout.rollback[0];
417
+ if (operation.requiresApplyReceipt) {
418
+ const receipt = readJson(
419
+ required(flag(args, "apply-receipt"), "--apply-receipt"),
420
+ "ruleset apply receipt",
421
+ );
422
+ const { receiptRoot, ...receiptCore } = receipt;
423
+ if (
424
+ receiptRoot !== githubGovernanceDigest(receiptCore) ||
425
+ receipt.planRoot !== rollout.planRoot ||
426
+ receipt.repository !== rollout.repository ||
427
+ receipt.targetRef !== rollout.targetRef
428
+ ) {
429
+ throw new Error("ruleset apply receipt does not match the rollout plan");
430
+ }
431
+ rulesetId = Number(receipt.rulesetId);
432
+ operation = receipt.rollback;
433
+ const current = readRuleset(rollout.repository, rulesetId);
434
+ if (githubGovernanceDigest(current) !== operation.preconditionRoot) {
435
+ throw new Error("created ruleset drifted after apply; rollback stopped");
436
+ }
437
+ }
315
438
  githubApi(operation.endpoint, {
316
439
  method: operation.method,
317
440
  body: operation.body,
318
441
  });
319
- const restored = readRuleset(rollout.repository, rollout.rulesetId);
320
- if (githubGovernanceDigest(restored) !== operation.preconditionRoot) {
321
- throw new Error("ruleset rollback read-back does not match the frozen snapshot");
442
+ if (operation.method === "DELETE") {
443
+ const remaining = readMatchingRulesets(
444
+ rollout.repository,
445
+ rollout.targetRef,
446
+ );
447
+ if (remaining.length !== 0) {
448
+ throw new Error("created ruleset remains after rollback");
449
+ }
450
+ } else {
451
+ const restored = readRuleset(rollout.repository, rulesetId);
452
+ if (githubGovernanceDigest(restored) !== operation.preconditionRoot) {
453
+ throw new Error("ruleset rollback read-back does not match the frozen snapshot");
454
+ }
322
455
  }
323
456
  return {
324
457
  schemaVersion: 1,
@@ -327,7 +460,7 @@ function rulesetRollback(args) {
327
460
  planRoot: rollout.planRoot,
328
461
  snapshotRoot: rollout.snapshotRoot,
329
462
  repository: rollout.repository,
330
- rulesetId: rollout.rulesetId,
463
+ rulesetId,
331
464
  };
332
465
  }
333
466
 
@@ -368,8 +501,8 @@ function protectionPolicyPlan(args) {
368
501
  ...boundCore,
369
502
  planRoot: githubGovernanceDigest(boundCore),
370
503
  };
371
- fs.writeFileSync(path.resolve(snapshotOutput), `${JSON.stringify(snapshotWithRoot, null, 2)}\n`);
372
- fs.writeFileSync(path.resolve(planOutput), `${JSON.stringify(finalPlan, null, 2)}\n`);
504
+ writeJson(snapshotOutput, snapshotWithRoot);
505
+ writeJson(planOutput, finalPlan);
373
506
  return finalPlan;
374
507
  }
375
508
 
@@ -419,8 +552,8 @@ function plan(args) {
419
552
  ...boundCore,
420
553
  planRoot: githubGovernanceDigest(boundCore),
421
554
  };
422
- fs.writeFileSync(path.resolve(snapshotOutput), `${JSON.stringify(snapshotWithRoot, null, 2)}\n`);
423
- fs.writeFileSync(path.resolve(planOutput), `${JSON.stringify(finalPlan, null, 2)}\n`);
555
+ writeJson(snapshotOutput, snapshotWithRoot);
556
+ writeJson(planOutput, finalPlan);
424
557
  return finalPlan;
425
558
  }
426
559
 
@@ -608,6 +608,10 @@ export async function resolveReleaseCandidateArtifacts({
608
608
  }
609
609
  const passport = JSON.parse(fs.readFileSync(passportPath, "utf8"));
610
610
  const platformManifestPaths = findDownloadedFiles(payloadDir, "manifest.json");
611
+ const githubArtifactAttestationPolicyPaths = findDownloadedFiles(
612
+ payloadDir,
613
+ "github-artifact-attestation-policy.json",
614
+ );
611
615
  const npmTarballPaths = publishArtifactKind === "npm"
612
616
  ? findDownloadedFilesByExtension(payloadDir, [".tgz"])
613
617
  : [];
@@ -639,6 +643,7 @@ export async function resolveReleaseCandidateArtifacts({
639
643
  buildSummary: outputPath(buildSummaryPath),
640
644
  payloads: outputPath(payloadDir),
641
645
  platformManifests: platformManifestPaths.map(outputPath),
646
+ githubArtifactAttestationPolicies: githubArtifactAttestationPolicyPaths.map(outputPath),
642
647
  npmTarballs: npmTarballPaths.map(outputPath),
643
648
  releaseAssets: releaseAssetPaths.map(outputPath),
644
649
  publishRequiredArtifacts: outputPath(requiredArtifactsPath),
@@ -647,6 +652,7 @@ export async function resolveReleaseCandidateArtifacts({
647
652
  candidateHash: passport.candidateHash || "",
648
653
  payloadCount: payloadArtifacts.length,
649
654
  platformManifestCount: platformManifestPaths.length,
655
+ githubArtifactAttestationPolicyCount: githubArtifactAttestationPolicyPaths.length,
650
656
  npmTarballCount: npmTarballPaths.length,
651
657
  publishRequiredArtifacts: generatedRequiredArtifacts,
652
658
  };
@@ -684,6 +690,12 @@ export async function resolveReleaseCandidateArtifactsCli() {
684
690
  "release-candidate-payload-dir": result.paths?.payloads || "",
685
691
  "release-candidate-platform-manifest-paths": (result.paths?.platformManifests || []).join(","),
686
692
  "release-candidate-platform-manifest-count": String(result.platformManifestCount || 0),
693
+ "release-candidate-github-artifact-attestation-policy-paths": (
694
+ result.paths?.githubArtifactAttestationPolicies || []
695
+ ).join(","),
696
+ "release-candidate-github-artifact-attestation-policy-count": String(
697
+ result.githubArtifactAttestationPolicyCount || 0,
698
+ ),
687
699
  "release-candidate-npm-tarball-paths": (result.paths?.npmTarballs || []).join(","),
688
700
  "release-candidate-npm-tarball-count": String(result.npmTarballCount || 0),
689
701
  "release-candidate-github-release-artifact-paths": (
@@ -70,6 +70,18 @@ export function parsePublishGateChannelRef(ref) {
70
70
  };
71
71
  }
72
72
 
73
+ export function parseAuthorityRef(ref) {
74
+ const normalizedRef = normalizeRef(ref);
75
+ const match = normalizedRef.match(/^authority\/v(\d+)\/v(\d+\.\d+)\/[A-Za-z0-9._/-]+$/);
76
+ if (!match) return undefined;
77
+ return {
78
+ major: Number(match[1]),
79
+ loose: Number(match[2]),
80
+ normalizedRef,
81
+ lineSuffix: `/v${match[1]}/v${match[2]}`,
82
+ };
83
+ }
84
+
73
85
  export function getChannel(ref) {
74
86
  const versionStateTarget = parseVersionStateRef(ref);
75
87
  if (versionStateTarget) return versionStateTarget.channel;
@@ -116,6 +128,7 @@ export function getBumpKeyword({ cwd = process.cwd(), headRef, baseRef, loose =
116
128
  const versionStateTarget = parseVersionStateRef(headRef);
117
129
  const releaseLineRecoveryTarget = parseReleaseLineRecoveryRef(headRef);
118
130
  const publishGateTarget = parsePublishGateChannelRef(headRef);
131
+ const authorityTarget = parseAuthorityRef(baseRef);
119
132
  const headChannel = getChannel(headRef);
120
133
  const baseChannel = getChannel(baseRef);
121
134
  const key = `${headChannel}->${baseChannel}`;
@@ -130,6 +143,20 @@ export function getBumpKeyword({ cwd = process.cwd(), headRef, baseRef, loose =
130
143
  const preminor = headChannel === "release" && lts;
131
144
  const majorGate = headChannel === "release" && baseChannel === MAJOR_GATE_CHANNEL;
132
145
 
146
+ if (authorityTarget) {
147
+ const headMatch = normalizeRef(headRef).match(/^dev\/v(\d+)\/v(\d+\.\d+)$/);
148
+ if (
149
+ !headMatch ||
150
+ Number(headMatch[1]) !== authorityTarget.major ||
151
+ Number(headMatch[2]) !== authorityTarget.loose ||
152
+ version.major !== authorityTarget.major ||
153
+ looseVersionNumber !== authorityTarget.loose
154
+ ) {
155
+ throw new Error(`Versions not match for head/base refs: ${headRef} -> ${baseRef}`);
156
+ }
157
+ return "none";
158
+ }
159
+
133
160
  if (releaseLineRecoveryTarget) {
134
161
  if (baseChannel !== "release" || releaseLineRecoveryTarget.normalizedRef !== normalizeRef(baseRef)) {
135
162
  throw new Error(`Versions not match for head/base refs: ${headRef} -> ${baseRef}`);
@@ -1,5 +1,6 @@
1
1
  const EXACT_SHA_RE = /^[0-9a-f]{40}$/i;
2
2
  const TRAIN_REF_RE = /^train\/v\d+\/v\d+\.\d+\/[A-Za-z0-9._/-]+$/;
3
+ const AUTHORITY_REF_RE = /^authority\/v\d+\/v\d+\.\d+\/[A-Za-z0-9._/-]+$/;
3
4
  const OFFICIAL_CHANNEL_REF_RE = /^v\d+(?:\.\d+)?(?:-alpha)?$/;
4
5
 
5
6
  export function parseWorkflowShellRef(workflowRef = "", fallback = "v2", buildchainRepository = "kungfu-systems/buildchain") {
@@ -20,6 +21,9 @@ export function classifyBuildchainRuntimeRef(ref = "") {
20
21
  if (TRAIN_REF_RE.test(value)) {
21
22
  return "train";
22
23
  }
24
+ if (AUTHORITY_REF_RE.test(value)) {
25
+ return "authority";
26
+ }
23
27
  if (/^v\d+(?:\.\d+)?$/.test(value) || /^v\d+\.\d+\.\d+$/.test(value)) {
24
28
  return "stable";
25
29
  }
@@ -58,16 +62,16 @@ export function normalizeRequestedRuntimeRef(requestedRef = "") {
58
62
  officialChannel: false,
59
63
  };
60
64
  }
61
- const trainRef = requested.replace(/^refs\/heads\//, "");
62
- if (!TRAIN_REF_RE.test(trainRef)) {
65
+ const protectedRef = requested.replace(/^refs\/heads\//, "");
66
+ if (!TRAIN_REF_RE.test(protectedRef) && !AUTHORITY_REF_RE.test(protectedRef)) {
63
67
  throw new Error(
64
- "buildchain-ref override must be train/vN/vN.M/<capability>, refs/heads/train/vN/vN.M/<capability>, or an exact 40-character SHA",
68
+ "buildchain-ref override must be train/vN/vN.M/<capability>, refs/heads/train/vN/vN.M/<capability>, authority/vN/vN.M/<capability>, refs/heads/authority/vN/vN.M/<capability>, or an exact 40-character SHA",
65
69
  );
66
70
  }
67
71
  return {
68
- ref: trainRef,
69
- fullRef: `refs/heads/${trainRef}`,
70
- class: "train",
72
+ ref: protectedRef,
73
+ fullRef: `refs/heads/${protectedRef}`,
74
+ class: AUTHORITY_REF_RE.test(protectedRef) ? "authority" : "train",
71
75
  exactSha: false,
72
76
  officialChannel: false,
73
77
  };