@kungfu-tech/buildchain 3.0.2-alpha.1 → 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 (78) 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 +103 -34
  13. package/dist/site/buildchain-site.json +159 -40
  14. package/dist/site/capability-registry.json +13 -12
  15. package/dist/site/cli-registry.json +12 -0
  16. package/dist/site/controller-registry.json +48 -4
  17. package/dist/site/kfd-claims.json +320 -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 +75 -10
  21. package/dist/site/page-registry.json +135 -25
  22. package/dist/site/public-surface-audit.json +197 -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 +5 -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 +161 -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 +7 -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/channel-candidate.js +186 -0
  50. package/packages/core/detached-artifact-signature.js +121 -0
  51. package/packages/core/github-artifact-attestation.js +642 -0
  52. package/packages/core/github-governance-authority.js +6 -0
  53. package/packages/core/index.js +52 -0
  54. package/packages/core/publication-authority.js +1 -1
  55. package/packages/core/release-passport-contract.js +2 -0
  56. package/packages/core/release-passport.js +60 -3
  57. package/scripts/auditable-demo.mjs +520 -28
  58. package/scripts/build-contract-core.mjs +31 -0
  59. package/scripts/buildchain-channel-router.mjs +8 -2
  60. package/scripts/check-inventory.mjs +9 -0
  61. package/scripts/create-github-artifact-attestation-policy.mjs +62 -0
  62. package/scripts/dev-alpha-candidate-patrol.mjs +864 -0
  63. package/scripts/dispatch-artifact-signing-authority.mjs +152 -0
  64. package/scripts/finalize-native-artifact-signing-result.mjs +96 -0
  65. package/scripts/generate-channel-promotion-workflow.mjs +6 -0
  66. package/scripts/generate-site-bundle.mjs +19 -0
  67. package/scripts/import-artifact-signing-results.mjs +76 -0
  68. package/scripts/inspect-artifact-signing-requests.mjs +101 -0
  69. package/scripts/materialize-artifact-signing-request.mjs +66 -0
  70. package/scripts/merge-artifact-signing-results.mjs +76 -0
  71. package/scripts/publish-github-artifact-attestation-evidence.mjs +201 -0
  72. package/scripts/release-candidate-resolver.mjs +12 -0
  73. package/scripts/release-line-policy.mjs +27 -0
  74. package/scripts/runtime-ref-core.mjs +10 -6
  75. package/scripts/seal-artifact-signing-requests.mjs +368 -0
  76. package/scripts/sign-detached-artifact-requests.mjs +237 -0
  77. package/scripts/stage-github-artifact-attestation-inputs.mjs +65 -0
  78. package/scripts/verify-artifact-signing-results.mjs +99 -0
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+
6
+ import { writeGitHubOutputs } from "./build-contract-core.mjs";
7
+
8
+ function required(value, label) {
9
+ const normalized = String(value || "").trim();
10
+ if (!normalized) throw new Error(`${label} is required`);
11
+ return normalized;
12
+ }
13
+
14
+ function safeId(value) {
15
+ const normalized = String(value || "").replace(/[^A-Za-z0-9._-]+/gu, "-").replace(/^-+|-+$/gu, "");
16
+ if (!normalized || normalized === "." || normalized === "..") throw new Error("unsafe signing result id");
17
+ return normalized;
18
+ }
19
+
20
+ function walk(root, output = []) {
21
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
22
+ const child = path.join(root, entry.name);
23
+ if (entry.isDirectory()) walk(child, output);
24
+ else if (entry.isFile() && entry.name === "index.json") output.push(child);
25
+ }
26
+ return output;
27
+ }
28
+
29
+ export function mergeArtifactSigningResults({
30
+ inputRoot = process.env.BUILDCHAIN_SIGNING_RESULT_INPUT_ROOT,
31
+ outputRoot = process.env.BUILDCHAIN_SIGNING_RESULT_ROOT,
32
+ } = {}) {
33
+ const input = path.resolve(required(inputRoot, "signing result input root"));
34
+ const output = path.resolve(required(outputRoot, "signing result output root"));
35
+ fs.mkdirSync(output, { recursive: true });
36
+ const merged = [];
37
+ const seen = new Set();
38
+ for (const indexPath of walk(input)) {
39
+ const index = JSON.parse(fs.readFileSync(indexPath, "utf8"));
40
+ if (index.contract !== "kungfu-buildchain-artifact-signing-result-index/v1") continue;
41
+ for (const entry of index.results || []) {
42
+ if (seen.has(entry.id)) throw new Error(`duplicate signing result: ${entry.id}`);
43
+ seen.add(entry.id);
44
+ const sourceResult = path.resolve(path.dirname(indexPath), entry.result);
45
+ const sourceDirectory = path.dirname(sourceResult);
46
+ const relative = path.relative(path.dirname(indexPath), sourceDirectory);
47
+ if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error("signing result escapes provider root");
48
+ const destinationName = safeId(entry.id);
49
+ const destination = path.join(output, destinationName);
50
+ fs.cpSync(sourceDirectory, destination, { recursive: true, errorOnExist: true, force: false });
51
+ merged.push({
52
+ ...entry,
53
+ result: `${destinationName}/${path.basename(sourceResult)}`,
54
+ ...(entry.payload ? { payload: `${destinationName}/${path.relative(sourceDirectory, path.resolve(path.dirname(indexPath), entry.payload)).split(path.sep).join("/")}` } : {}),
55
+ ...(entry.envelope ? { envelope: `${destinationName}/${path.relative(sourceDirectory, path.resolve(path.dirname(indexPath), entry.envelope)).split(path.sep).join("/")}` } : {}),
56
+ ...(entry.receipt ? { receipt: `${destinationName}/${path.relative(sourceDirectory, path.resolve(path.dirname(indexPath), entry.receipt)).split(path.sep).join("/")}` } : {}),
57
+ });
58
+ }
59
+ }
60
+ if (merged.length === 0) throw new Error("no artifact signing results found");
61
+ merged.sort((a, b) => a.id.localeCompare(b.id));
62
+ const index = { schemaVersion: 1, contract: "kungfu-buildchain-artifact-signing-result-index/v1", results: merged };
63
+ const indexPath = path.join(output, "index.json");
64
+ fs.writeFileSync(indexPath, `${JSON.stringify(index, null, 2)}\n`);
65
+ writeGitHubOutputs({ "result-count": String(merged.length), "result-index": indexPath, "result-root": output });
66
+ return index;
67
+ }
68
+
69
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
70
+ try {
71
+ mergeArtifactSigningResults();
72
+ } catch (error) {
73
+ console.error(`::error::${String(error?.message || error).replace(/\r?\n/gu, "%0A")}`);
74
+ process.exitCode = 1;
75
+ }
76
+ }
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env node
2
+
3
+ import crypto from "node:crypto";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import process from "node:process";
7
+ import { spawnSync } from "node:child_process";
8
+
9
+ import {
10
+ createGitHubArtifactAttestationVerificationPlan,
11
+ verifyGitHubArtifactAttestationEvidence,
12
+ } from "../packages/core/github-artifact-attestation.js";
13
+
14
+ function parseArgs(argv) {
15
+ const result = {};
16
+ for (let index = 0; index < argv.length; index += 2) {
17
+ const key = argv[index];
18
+ const value = argv[index + 1];
19
+ if (!key?.startsWith("--") || value === undefined) throw new Error(`invalid argument near ${key || "<end>"}`);
20
+ result[key.slice(2)] = value;
21
+ }
22
+ return result;
23
+ }
24
+
25
+ function required(value, label) {
26
+ const normalized = String(value || "").trim();
27
+ if (!normalized) throw new Error(`${label} is required`);
28
+ return normalized;
29
+ }
30
+
31
+ function sha256(value) {
32
+ return crypto.createHash("sha256").update(value).digest("hex");
33
+ }
34
+
35
+ function sha256File(filePath) {
36
+ return `sha256:${sha256(fs.readFileSync(filePath))}`;
37
+ }
38
+
39
+ function readJson(filePath, label) {
40
+ try {
41
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
42
+ } catch (error) {
43
+ throw new Error(`${label} is not readable JSON: ${error.message}`);
44
+ }
45
+ }
46
+
47
+ function safeAssetStem(value) {
48
+ return path.basename(required(value, "subject name"))
49
+ .replace(/[^0-9A-Za-z._-]+/g, "-")
50
+ .replace(/^-+|-+$/g, "");
51
+ }
52
+
53
+ async function api({ token, apiUrl, route, method = "GET", headers = {}, body }) {
54
+ const response = await fetch(`${apiUrl}${route}`, {
55
+ method,
56
+ headers: {
57
+ accept: "application/vnd.github+json",
58
+ authorization: `Bearer ${token}`,
59
+ "x-github-api-version": "2022-11-28",
60
+ ...headers,
61
+ },
62
+ body,
63
+ });
64
+ if (!response.ok) {
65
+ const detail = (await response.text()).slice(0, 500);
66
+ throw new Error(`GitHub API ${method} ${route} failed with ${response.status}: ${detail}`);
67
+ }
68
+ return response;
69
+ }
70
+
71
+ async function remoteAssetDigest({ token, apiUrl, repository, asset }) {
72
+ const declared = String(asset.digest || "").match(/^sha256:([0-9a-f]{64})$/i);
73
+ if (declared) return `sha256:${declared[1].toLowerCase()}`;
74
+ const response = await api({
75
+ token,
76
+ apiUrl,
77
+ route: `/repos/${repository}/releases/assets/${asset.id}`,
78
+ headers: { accept: "application/octet-stream" },
79
+ });
80
+ return `sha256:${sha256(Buffer.from(await response.arrayBuffer()))}`;
81
+ }
82
+
83
+ async function uploadImmutable({ token, apiUrl, repository, release, filePath, assetName }) {
84
+ const localDigest = sha256File(filePath);
85
+ const existing = (release.assets || []).filter((asset) => asset.name === assetName);
86
+ if (existing.length > 1) throw new Error(`release asset ${assetName} exists more than once`);
87
+ if (existing.length === 1) {
88
+ const remoteDigest = await remoteAssetDigest({ token, apiUrl, repository, asset: existing[0] });
89
+ if (remoteDigest !== localDigest) {
90
+ throw new Error(`immutable release asset collision for ${assetName}: ${remoteDigest} != ${localDigest}`);
91
+ }
92
+ return { action: "preserved", name: assetName, digest: localDigest, url: existing[0].browser_download_url };
93
+ }
94
+ const uploadBase = required(release.upload_url, "release.upload_url").replace(/\{.*$/, "");
95
+ const response = await fetch(`${uploadBase}?name=${encodeURIComponent(assetName)}`, {
96
+ method: "POST",
97
+ headers: {
98
+ accept: "application/vnd.github+json",
99
+ authorization: `Bearer ${token}`,
100
+ "content-type": "application/octet-stream",
101
+ "x-github-api-version": "2022-11-28",
102
+ },
103
+ body: fs.readFileSync(filePath),
104
+ });
105
+ if (!response.ok) throw new Error(`GitHub release asset upload failed with ${response.status}: ${(await response.text()).slice(0, 500)}`);
106
+ const asset = await response.json();
107
+ release.assets = [...(release.assets || []), asset];
108
+ const remoteDigest = await remoteAssetDigest({ token, apiUrl, repository, asset });
109
+ if (remoteDigest !== localDigest) throw new Error(`release asset read-back mismatch for ${assetName}`);
110
+ return { action: "uploaded", name: assetName, digest: localDigest, url: asset.browser_download_url };
111
+ }
112
+
113
+ function appendOutput(name, value) {
114
+ if (process.env.GITHUB_OUTPUT) fs.appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${String(value)}\n`);
115
+ }
116
+
117
+ async function main() {
118
+ const options = parseArgs(process.argv.slice(2));
119
+ const token = required(process.env.GITHUB_TOKEN || process.env.GH_TOKEN, "GITHUB_TOKEN");
120
+ const apiUrl = (process.env.GITHUB_API_URL || "https://api.github.com").replace(/\/$/, "");
121
+ const repository = required(options.repository || process.env.GITHUB_REPOSITORY, "repository");
122
+ const tag = required(options.tag, "tag");
123
+ const subjectPath = path.resolve(required(options.subject, "subject"));
124
+ const manifestPath = path.resolve(required(options.manifest, "manifest"));
125
+ const passportPath = path.resolve(required(options.passport, "passport"));
126
+ const bundlePath = path.resolve(required(options.bundle, "bundle"));
127
+ const evidencePath = path.resolve(required(options.evidence, "evidence"));
128
+ const predicatePath = path.resolve(required(options.predicate, "predicate"));
129
+ const providerVerificationPath = path.resolve(required(options["provider-verification"], "provider-verification"));
130
+ const receiptPath = path.resolve(required(options.receipt, "receipt"));
131
+ const evidence = readJson(evidencePath, "Buildchain attestation evidence");
132
+ const plan = createGitHubArtifactAttestationVerificationPlan({ artifactPath: subjectPath, bundlePath, evidence });
133
+ const provider = spawnSync(plan.command, plan.args, { encoding: "utf8", env: process.env });
134
+ if (provider.status !== 0) throw new Error(`gh attestation verify failed: ${String(provider.stderr || provider.stdout).slice(0, 1000)}`);
135
+ const verificationResults = JSON.parse(provider.stdout);
136
+ const local = verifyGitHubArtifactAttestationEvidence({
137
+ artifactPath: subjectPath,
138
+ platformManifestPath: manifestPath,
139
+ releasePassportPath: passportPath,
140
+ bundlePath,
141
+ evidence,
142
+ verificationResults,
143
+ });
144
+ if (!local.ok) throw new Error(`Buildchain evidence verification failed: ${local.issues.map((issue) => issue.message).join("; ")}`);
145
+ const retainedProvider = readJson(providerVerificationPath, "retained provider verification");
146
+ const retained = verifyGitHubArtifactAttestationEvidence({
147
+ artifactPath: subjectPath,
148
+ platformManifestPath: manifestPath,
149
+ releasePassportPath: passportPath,
150
+ bundlePath,
151
+ evidence,
152
+ verificationResults: retainedProvider,
153
+ });
154
+ if (!retained.ok) {
155
+ throw new Error(`retained provider verification failed: ${retained.issues.map((issue) => issue.message).join("; ")}`);
156
+ }
157
+ const releaseResponse = await api({ token, apiUrl, route: `/repos/${repository}/releases/tags/${encodeURIComponent(tag)}` });
158
+ const release = await releaseResponse.json();
159
+ const stem = safeAssetStem(evidence.subject?.name);
160
+ const declarations = [
161
+ [bundlePath, `${stem}.sigstore-bundle.json`],
162
+ [evidencePath, `${stem}.buildchain-attestation.json`],
163
+ [predicatePath, `${stem}.buildchain-predicate.json`],
164
+ [providerVerificationPath, `${stem}.github-verification.json`],
165
+ ];
166
+ const assets = [];
167
+ for (const [filePath, assetName] of declarations) {
168
+ assets.push(await uploadImmutable({ token, apiUrl, repository, release, filePath, assetName }));
169
+ }
170
+ const receipt = {
171
+ contract: "buildchain.github-artifact-attestation-publication/v1",
172
+ repository,
173
+ tag,
174
+ releaseUrl: release.html_url,
175
+ subject: evidence.subject,
176
+ evidenceRoot: evidence.evidenceRoot,
177
+ attestation: evidence.attestation,
178
+ assets,
179
+ verified: true,
180
+ };
181
+ fs.mkdirSync(path.dirname(receiptPath), { recursive: true });
182
+ fs.writeFileSync(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`);
183
+ const receiptAsset = await uploadImmutable({
184
+ token,
185
+ apiUrl,
186
+ repository,
187
+ release,
188
+ filePath: receiptPath,
189
+ assetName: `${stem}.buildchain-attestation-publication.json`,
190
+ });
191
+ appendOutput("release-url", release.html_url);
192
+ appendOutput("evidence-root", evidence.evidenceRoot);
193
+ appendOutput("publication-receipt", receiptPath);
194
+ appendOutput("publication-receipt-digest", receiptAsset.digest);
195
+ process.stdout.write(`${JSON.stringify({ ...receipt, receiptAsset }, null, 2)}\n`);
196
+ }
197
+
198
+ main().catch((error) => {
199
+ console.error(error.message);
200
+ process.exitCode = 1;
201
+ });
@@ -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
  };