@kungfu-tech/buildchain 4.0.1 → 4.0.2-alpha.2

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 (50) hide show
  1. package/architecture/agent-change-map.md +7 -5
  2. package/architecture/ci-lane-change-budget.json +76 -8
  3. package/architecture/internal-capabilities.json +4 -1
  4. package/architecture/maintainability-debt.json +45 -119
  5. package/architecture/maintainability-policy.json +13 -8
  6. package/architecture/release-tail-contract-inventory.json +4 -4
  7. package/architecture/v3-core-mechanism-inventory.json +2 -0
  8. package/architecture/v3-v4-live-capability-inventory.json +17 -17
  9. package/architecture/v4-release-invocation-fixtures.json +119 -0
  10. package/architecture/v4-release-topology.json +238 -0
  11. package/contracts/buildchain-v2-residuals-v1.json +0 -9
  12. package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
  13. package/contracts/v4-release-invocation-v1.schema.json +86 -0
  14. package/dist/site/buildchain-contract.json +9 -9
  15. package/dist/site/buildchain-site.json +9 -9
  16. package/dist/site/kfd-claims.json +9 -7
  17. package/dist/site/kfd-upstream-aggregate.json +1 -1
  18. package/dist/site/manual-registry.json +1 -1
  19. package/dist/site/node-api-registry.json +3 -3
  20. package/dist/site/page-registry.json +4 -4
  21. package/dist/site/public-surface-audit.json +23 -10
  22. package/dist/site/publication-authority-registry.json +2 -3
  23. package/dist/site/publication-registry.json +4 -4
  24. package/dist/site/site-manifest.json +5 -5
  25. package/dist/site/workflow-registry.json +19 -11
  26. package/docs/dev-delivery-warrant.md +21 -0
  27. package/docs/node-api-reference.md +3 -3
  28. package/package.json +2 -2
  29. package/packages/core/dev-delivery-candidate-identity.js +45 -17
  30. package/packages/core/dev-delivery-provider-heartbeat.js +22 -6
  31. package/packages/core/dev-delivery-warrant-legacy-recovery.js +246 -0
  32. package/packages/core/dev-delivery-warrant-state.js +12 -28
  33. package/packages/core/v4-canonical-contracts.js +8 -0
  34. package/packages/core/v4-floating-consumer-policy.js +4 -1
  35. package/packages/core/v4-release-invocation.js +356 -0
  36. package/scripts/audit-publication-control-plane.mjs +0 -1
  37. package/scripts/check-inventory.mjs +27 -59
  38. package/scripts/check-maintainability.mjs +13 -5
  39. package/scripts/check-v3-v4-capability-inventory.mjs +3 -61
  40. package/scripts/check-v4-floating-consumer-policy-contract.mjs +10 -20
  41. package/scripts/check-v4-release-topology.mjs +237 -0
  42. package/scripts/dev-delivery-warrant-options.mjs +15 -4
  43. package/scripts/dev-delivery-warrant.mjs +29 -7
  44. package/scripts/generate-channel-promotion-workflow.mjs +12 -54
  45. package/scripts/v3-v4-capability-catalog.mjs +107 -0
  46. package/scripts/v4-declarative-promotion-admission.mjs +4 -1
  47. package/scripts/capture-package-release-propagation.mjs +0 -263
  48. package/scripts/publication-commit-evidence.mjs +0 -444
  49. package/scripts/publish-github-artifact-attestation-evidence.mjs +0 -201
  50. package/scripts/stage-github-artifact-attestation-inputs.mjs +0 -65
@@ -74,7 +74,7 @@ function publicInputs(inputBlock) {
74
74
  function publicOutputs(outputBlock) {
75
75
  const forwarded = entries(outputBlock).map((entry) => entry.source.replace(
76
76
  /^ value:.*$/m,
77
- ` value: \${{ jobs.alpha.outputs.${entry.name} || jobs.stable.outputs.${entry.name} }}`,
77
+ ` value: \${{ jobs.invoke.outputs.${entry.name} }}`,
78
78
  ));
79
79
  const routed = [
80
80
  ["buildchain-channel", "Selected promotion workflow-shell channel", "channel"],
@@ -292,7 +292,7 @@ export function generateChannelPromotionWorkflow(source, { major = 2, shellRouti
292
292
  },
293
293
  };
294
294
  const alphaRoute = validateWorkflowRoute("alpha", routes.alpha, `v${major}-alpha`);
295
- const stableRoute = validateWorkflowRoute("stable", routes.stable, `v${major}`);
295
+ validateWorkflowRoute("stable", routes.stable, `v${major}`);
296
296
  const inputs = blockBetween(source, " inputs:\n", " secrets:\n");
297
297
  const secrets = blockBetween(source, " secrets:\n", " outputs:\n");
298
298
  const outputs = blockBetween(source, " outputs:\n", "\nconcurrency:\n");
@@ -300,8 +300,7 @@ export function generateChannelPromotionWorkflow(source, { major = 2, shellRouti
300
300
  for (const required of ["buildchain-ref", "buildchain-contract-lock-path", "channel", "target-ref", ...internalInputs]) {
301
301
  if (!inputNames.includes(required)) throw new Error(`advanced promotion workflow missing input: ${required}`);
302
302
  }
303
- const alphaForwarded = routedInputs(inputNames, alphaRoute, major);
304
- const stableForwarded = routedInputs(inputNames, stableRoute, major);
303
+ const invokeForwarded = routedInputs(inputNames, alphaRoute, major);
305
304
  return `# Generated by scripts/generate-channel-promotion-workflow.mjs. Do not edit directly.
306
305
  name: Release Candidate Promote
307
306
 
@@ -336,7 +335,7 @@ jobs:
336
335
  target-ref: \${{ steps.route.outputs.target-ref }}
337
336
  router-ref: \${{ steps.router.outputs.ref }}
338
337
  router-sha: \${{ steps.router.outputs.sha }}
339
- shell-ref: \${{ steps.route.outputs.shell-ref }}
338
+ shell-ref: \${{ steps.identities.outputs.shell-ref }}
340
339
  shell-call-ref: \${{ steps.identities.outputs.shell-call-ref }}
341
340
  shell-sha: \${{ steps.identities.outputs.shell-sha }}
342
341
  runtime-ref: \${{ steps.route.outputs.runtime-ref }}
@@ -425,32 +424,17 @@ jobs:
425
424
  shell: bash
426
425
  env:
427
426
  GITHUB_TOKEN: \${{ github.token }}
428
- CHANNEL: \${{ steps.route.outputs.channel }}
429
- SELECTED_SHELL_REF: \${{ steps.route.outputs.shell-ref }}
430
- ALPHA_SHELL_REF: ${alphaRoute.logicalRef}
431
- ALPHA_SHELL_CALL_REF: ${alphaRoute.callRef}
432
- STABLE_SHELL_REF: ${stableRoute.logicalRef}
433
- STABLE_SHELL_CALL_REF: ${stableRoute.callRef}
427
+ SELECTED_SHELL_REF: ${alphaRoute.logicalRef}
434
428
  run: |
435
429
  set -euo pipefail
436
- if [[ "\${CHANNEL}" = "alpha" ]]; then
437
- expected_ref="\${ALPHA_SHELL_REF}"
438
- call_ref="\${ALPHA_SHELL_CALL_REF}"
439
- else
440
- expected_ref="\${STABLE_SHELL_REF}"
441
- call_ref="\${STABLE_SHELL_CALL_REF}"
442
- fi
443
- if [[ "\${SELECTED_SHELL_REF}" != "\${expected_ref}" ]]; then
444
- echo "::error::Selected promotion shell ref \${SELECTED_SHELL_REF} does not match configured \${expected_ref}"
445
- exit 1
446
- fi
447
430
  node .buildchain/router/scripts/promotion-identity-resolver.mjs \\
448
431
  --repository "\${{ steps.router.outputs.repository }}" \\
449
432
  --router-ref "\${{ steps.router.outputs.ref }}" \\
450
433
  --router-sha "\${{ steps.router.outputs.sha }}" \\
451
434
  --shell-ref "\${SELECTED_SHELL_REF}" \\
452
- --shell-call-ref "\${call_ref}" \\
435
+ --shell-call-ref "\${SELECTED_SHELL_REF}" \\
453
436
  --runtime-ref "\${{ steps.route.outputs.runtime-ref }}"
437
+ echo "shell-ref=\${SELECTED_SHELL_REF}" >> "$GITHUB_OUTPUT"
454
438
 
455
439
  - name: Checkout selected promotion workflow shell
456
440
  uses: actions/checkout@v7.0.0
@@ -500,29 +484,21 @@ jobs:
500
484
  - name: Verify immutable promotion checkouts
501
485
  shell: bash
502
486
  env:
503
- CHANNEL: \${{ steps.route.outputs.channel }}
504
- ALPHA_SHELL_WORKFLOW_PATH: ${alphaRoute.workflowPath}
505
- STABLE_SHELL_WORKFLOW_PATH: ${stableRoute.workflowPath}
487
+ SHELL_WORKFLOW_PATH: ${alphaRoute.workflowPath}
506
488
  ROUTER_SHA: \${{ steps.router.outputs.sha }}
507
489
  SHELL_SHA: \${{ steps.identities.outputs.shell-sha }}
508
490
  RUNTIME_SHA: \${{ steps.identities.outputs.runtime-sha }}
509
491
  run: |
510
492
  set -euo pipefail
511
- if [[ "\${CHANNEL}" = "alpha" ]]; then
512
- workflow_path="\${ALPHA_SHELL_WORKFLOW_PATH}"
513
- else
514
- workflow_path="\${STABLE_SHELL_WORKFLOW_PATH}"
515
- fi
516
- test -f ".buildchain/shell/\${workflow_path}"
493
+ test -f ".buildchain/shell/\${SHELL_WORKFLOW_PATH}"
517
494
  [[ "$(git -C .buildchain/router rev-parse HEAD)" = "\${ROUTER_SHA}" ]] || { echo "::error::Promotion router checkout moved"; exit 1; }
518
495
  [[ "$(git -C .buildchain/shell rev-parse HEAD)" = "\${SHELL_SHA}" ]] || { echo "::error::Promotion shell checkout moved"; exit 1; }
519
496
  [[ "$(git -C .buildchain/runtime rev-parse HEAD)" = "\${RUNTIME_SHA}" ]] || { echo "::error::Promotion runtime checkout moved"; exit 1; }
520
497
 
521
498
  ${consumerAdmissionJob()}
522
- alpha:
523
- name: Promote with alpha workflow shell
499
+ invoke:
500
+ name: Invoke the single v4 publisher adapter
524
501
  needs: [resolve-promotion, consumer-admission]
525
- if: \${{ needs.resolve-promotion.outputs.channel == 'alpha' }}
526
502
  uses: kungfu-systems/buildchain/${alphaRoute.workflowPath}@${alphaRoute.callRef}
527
503
  permissions:
528
504
  actions: write
@@ -534,25 +510,7 @@ ${consumerAdmissionJob()}
534
510
  issues: write
535
511
  pull-requests: write
536
512
  with:
537
- ${alphaForwarded}
538
- secrets: inherit
539
-
540
- stable:
541
- name: Promote with stable workflow shell
542
- needs: [resolve-promotion, consumer-admission]
543
- if: \${{ needs.resolve-promotion.outputs.channel == 'stable' }}
544
- uses: kungfu-systems/buildchain/${stableRoute.workflowPath}@${stableRoute.callRef}
545
- permissions:
546
- actions: write
547
- artifact-metadata: write
548
- attestations: write
549
- checks: write
550
- contents: write
551
- id-token: write
552
- issues: write
553
- pull-requests: write
554
- with:
555
- ${stableForwarded}
513
+ ${invokeForwarded}
556
514
  secrets: inherit
557
515
  `;
558
516
  }
@@ -23,6 +23,9 @@ const PLATFORM_MARKERS = Object.freeze({
23
23
  "self-hosted": /\bself-hosted\b/iu,
24
24
  windows: /\bwindows\b/iu,
25
25
  });
26
+ const LIVE_V4_PROTECTED_BRANCH = "refs/heads/dev/v4/v4.0";
27
+ const LIVE_V4_PROTECTED_LINEAGE_REF = "refs/remotes/origin/dev/v4/v4.0";
28
+ const PROTECTED_LINEAGE_DEPTH = 256;
26
29
 
27
30
  export function sha256(value) {
28
31
  return `sha256:${crypto.createHash("sha256").update(value).digest("hex")}`;
@@ -47,6 +50,110 @@ export function git(root, args, { trim = true } = {}) {
47
50
  return trim ? output.trim() : output;
48
51
  }
49
52
 
53
+ export function assertCapabilityCutAncestor({
54
+ root = process.cwd(),
55
+ revision,
56
+ descendant = "HEAD",
57
+ label = "capability cut",
58
+ } = {}) {
59
+ try {
60
+ execFileSync("git", ["merge-base", "--is-ancestor", revision, descendant], {
61
+ cwd: root,
62
+ stdio: "ignore",
63
+ });
64
+ } catch {
65
+ throw new Error(
66
+ `${label} ${revision} must be an ancestor of ${descendant}; regenerate the cut after rebasing instead of relying on a retained local object`,
67
+ );
68
+ }
69
+ }
70
+
71
+ export function ensureCapabilityCutAncestor({
72
+ root = process.cwd(),
73
+ revision,
74
+ descendant = "HEAD",
75
+ label = "capability cut",
76
+ } = {}) {
77
+ try {
78
+ assertCapabilityCutAncestor({ root, revision, descendant, label });
79
+ return;
80
+ } catch (error) {
81
+ if (git(root, ["rev-parse", "--is-shallow-repository"]) !== "true")
82
+ throw error;
83
+ }
84
+ const descendantCommit = git(root, ["rev-parse", `${descendant}^{commit}`]);
85
+ try {
86
+ execFileSync(
87
+ "git",
88
+ ["fetch", "--no-tags", "--depth=128", "origin", descendantCommit],
89
+ { cwd: root, stdio: "ignore" },
90
+ );
91
+ } catch {
92
+ throw new Error(
93
+ `${label} ${revision} ancestry could not be hydrated from ${descendantCommit} through a bounded origin fetch`,
94
+ );
95
+ }
96
+ assertCapabilityCutAncestor({ root, revision, descendant, label });
97
+ }
98
+
99
+ function protectedV4TreeWitness(root, descendant) {
100
+ try {
101
+ execFileSync(
102
+ "git",
103
+ [
104
+ "fetch",
105
+ "--no-tags",
106
+ `--depth=${PROTECTED_LINEAGE_DEPTH}`,
107
+ "origin",
108
+ `${LIVE_V4_PROTECTED_BRANCH}:${LIVE_V4_PROTECTED_LINEAGE_REF}`,
109
+ ],
110
+ { cwd: root, stdio: "ignore" },
111
+ );
112
+ } catch {
113
+ throw new Error(
114
+ `protected v4 lineage ${LIVE_V4_PROTECTED_BRANCH} could not be hydrated through a bounded origin fetch`,
115
+ );
116
+ }
117
+ const tree = git(root, ["rev-parse", `${descendant}^{tree}`]);
118
+ return (
119
+ git(root, [
120
+ "log",
121
+ `--max-count=${PROTECTED_LINEAGE_DEPTH}`,
122
+ "--format=%H %T",
123
+ LIVE_V4_PROTECTED_LINEAGE_REF,
124
+ ])
125
+ .split("\n")
126
+ .map((row) => row.split(" "))
127
+ .find(([, candidateTree]) => candidateTree === tree)?.[0] || ""
128
+ );
129
+ }
130
+
131
+ export function ensureCapabilityCutLineage({
132
+ root = process.cwd(),
133
+ revision,
134
+ descendant = "HEAD",
135
+ label = "capability cut",
136
+ } = {}) {
137
+ try {
138
+ ensureCapabilityCutAncestor({ root, revision, descendant, label });
139
+ return { mode: "direct-ancestry", witness: descendant };
140
+ } catch (directError) {
141
+ const witness = protectedV4TreeWitness(root, descendant);
142
+ if (!witness)
143
+ throw new Error(
144
+ `${label} ${revision} is not an ancestor of ${descendant}, and ${descendant} has no tree-equivalent commit in the bounded protected v4 lineage`,
145
+ { cause: directError },
146
+ );
147
+ ensureCapabilityCutAncestor({
148
+ root,
149
+ revision,
150
+ descendant: witness,
151
+ label: `${label} protected-lineage witness`,
152
+ });
153
+ return { mode: "protected-tree-equivalent", witness };
154
+ }
155
+ }
156
+
50
157
  function gitJson(root, revision, relPath) {
51
158
  return JSON.parse(
52
159
  git(root, ["show", `${revision}:${relPath}`], { trim: false }),
@@ -8,7 +8,10 @@ export function admitV4DeclarativePromotion({
8
8
  runtimeRef,
9
9
  declarative,
10
10
  }) {
11
- if (!/^v4(?:$|[-./])/u.test(String(runtimeRef || ""))) {
11
+ if (
12
+ declarative !== true &&
13
+ !/^v4(?:$|[-./])/u.test(String(runtimeRef || ""))
14
+ ) {
12
15
  return Object.freeze({ mode: "legacy", admitted: true });
13
16
  }
14
17
  if (declarative !== true) {
@@ -1,263 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import crypto from "node:crypto";
4
- import fs from "node:fs";
5
- import os from "node:os";
6
- import path from "node:path";
7
- import { execFileSync } from "node:child_process";
8
- import { fileURLToPath } from "node:url";
9
- import {
10
- createPackageReleasePropagationCapture,
11
- normalizePackageReleasePropagationConfig,
12
- } from "../packages/core/release-propagation-capture.js";
13
- import { stableJson } from "../packages/core/release-propagation-common.js";
14
-
15
- function parseArgs(argv) {
16
- const args = {};
17
- for (let index = 0; index < argv.length; index += 1) {
18
- const token = argv[index];
19
- if (!token.startsWith("--")) throw new Error(`unexpected argument: ${token}`);
20
- const key = token.slice(2);
21
- const value = argv[index + 1];
22
- if (!value || value.startsWith("--")) throw new Error(`${token} requires a value`);
23
- args[key] = value;
24
- index += 1;
25
- }
26
- return args;
27
- }
28
-
29
- function required(args, key) {
30
- const value = String(args[key] || "").trim();
31
- if (!value) throw new Error(`--${key} is required`);
32
- return value;
33
- }
34
-
35
- function sha256(bytes) {
36
- return crypto.createHash("sha256").update(bytes).digest("hex");
37
- }
38
-
39
- function commandJson(command, args, options = {}) {
40
- const text = execFileSync(command, args, { encoding: "utf8", ...options });
41
- return JSON.parse(text);
42
- }
43
-
44
- function assertSourcePath(value) {
45
- const sourcePath = String(value || "").trim();
46
- if (!sourcePath || path.isAbsolute(sourcePath) || sourcePath.split("/").includes("..") || /[\r\n]/.test(sourcePath)) {
47
- throw new Error("release propagation config path must be a safe repository-relative path");
48
- }
49
- return sourcePath;
50
- }
51
-
52
- function hasCommit(sourceSha, cwd) {
53
- try {
54
- execFileSync("git", ["cat-file", "-e", `${sourceSha}^{commit}`], {
55
- cwd,
56
- stdio: "ignore",
57
- });
58
- return true;
59
- } catch {
60
- return false;
61
- }
62
- }
63
-
64
- export function readConfigAtSource(sourceSha, configPath, cwd) {
65
- if (!hasCommit(sourceSha, cwd)) {
66
- try {
67
- execFileSync("git", ["fetch", "--no-tags", "--depth=1", "origin", sourceSha], {
68
- cwd,
69
- stdio: ["ignore", "pipe", "pipe"],
70
- });
71
- } catch (error) {
72
- const detail = String(error.stderr || error.message || "unknown git fetch failure").trim();
73
- throw new Error(`exact release source ${sourceSha} is unavailable from origin: ${detail}`);
74
- }
75
- }
76
- const bytes = execFileSync("git", ["show", `${sourceSha}:${configPath}`], {
77
- cwd,
78
- encoding: "utf8",
79
- });
80
- const parsed = JSON.parse(bytes);
81
- return { bytes, parsed, normalized: normalizePackageReleasePropagationConfig(parsed) };
82
- }
83
-
84
- function resolveTagTarget(repository, tag) {
85
- let object = commandJson("gh", ["api", `repos/${repository}/git/ref/tags/${tag}`]).object;
86
- for (let depth = 0; depth < 8 && object?.type === "tag"; depth += 1) {
87
- object = commandJson("gh", ["api", `repos/${repository}/git/tags/${object.sha}`]).object;
88
- }
89
- if (object?.type !== "commit" || !/^[0-9a-f]{40}$/i.test(object.sha || "")) {
90
- throw new Error(`release tag ${tag} does not resolve to one exact commit`);
91
- }
92
- return object.sha.toLowerCase();
93
- }
94
-
95
- function resolvePackageFact(packageName, version) {
96
- const fact = commandJson("npm", [
97
- "view",
98
- `${packageName}@${version}`,
99
- "version",
100
- "dist.integrity",
101
- "gitHead",
102
- "--json",
103
- "--registry=https://registry.npmjs.org/",
104
- ]);
105
- return {
106
- name: packageName,
107
- version: String(fact.version || ""),
108
- integrity: String(fact.dist?.integrity || fact["dist.integrity"] || ""),
109
- gitHead: String(fact.gitHead || "").toLowerCase(),
110
- };
111
- }
112
-
113
- function verifyPublicReleasePassport({ repository, tag, localPath }) {
114
- const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "buildchain-release-passport-"));
115
- try {
116
- execFileSync("gh", [
117
- "release", "download", tag,
118
- "--repo", repository,
119
- "--pattern", "buildchain.release.json",
120
- "--dir", temporary,
121
- ], { stdio: ["ignore", "pipe", "pipe"] });
122
- const localBytes = fs.readFileSync(localPath);
123
- const remotePath = path.join(temporary, "buildchain.release.json");
124
- const remoteBytes = fs.readFileSync(remotePath);
125
- const localDigest = sha256(localBytes);
126
- const remoteDigest = sha256(remoteBytes);
127
- if (localDigest !== remoteDigest) {
128
- throw new Error("public release passport bytes disagree with finalized local passport");
129
- }
130
- return {
131
- url: `https://github.com/${repository}/releases/download/${tag}/buildchain.release.json`,
132
- sha256: remoteDigest,
133
- };
134
- } finally {
135
- fs.rmSync(temporary, { recursive: true, force: true });
136
- }
137
- }
138
-
139
- function resolveBaseShas(config) {
140
- return Object.fromEntries(config.targets.map((targetId) => {
141
- const node = config.graph.nodes.find((entry) => entry.id === targetId);
142
- if (!node?.baseRef) throw new Error(`configured propagation target ${targetId} has no baseRef`);
143
- const response = commandJson("gh", ["api", `repos/${node.repository}/commits/${node.baseRef}`]);
144
- const sha = String(response.sha || "").toLowerCase();
145
- if (!/^[0-9a-f]{40}$/.test(sha)) {
146
- throw new Error(`configured propagation target ${targetId} baseRef did not resolve exactly`);
147
- }
148
- return [targetId, sha];
149
- }));
150
- }
151
-
152
- function writeOutput(name, value) {
153
- if (!process.env.GITHUB_OUTPUT) return;
154
- fs.appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`);
155
- }
156
-
157
- export function capturePackageReleasePropagation({
158
- config,
159
- upstreamRelease,
160
- expectedBaseShas,
161
- outputDir,
162
- configBytes = "",
163
- } = {}) {
164
- const captured = createPackageReleasePropagationCapture({
165
- config,
166
- upstreamRelease,
167
- expectedBaseShas,
168
- });
169
- fs.mkdirSync(outputDir, { recursive: true });
170
- fs.writeFileSync(
171
- path.join(outputDir, "config.json"),
172
- configBytes || stableJson(captured.config),
173
- );
174
- fs.writeFileSync(path.join(outputDir, "upstream-release.json"), stableJson(captured.upstreamRelease));
175
- fs.writeFileSync(path.join(outputDir, "plan.json"), stableJson(captured.plan));
176
- for (const item of captured.works) {
177
- const targetDir = path.join(outputDir, "work", item.propagationKey);
178
- fs.mkdirSync(targetDir, { recursive: true });
179
- fs.writeFileSync(path.join(targetDir, "work.json"), stableJson(item.work));
180
- fs.writeFileSync(path.join(targetDir, "status.json"), stableJson(item.status));
181
- }
182
- return captured;
183
- }
184
-
185
- export function main(argv = process.argv.slice(2)) {
186
- const args = parseArgs(argv);
187
- const repository = required(args, "repository");
188
- const channel = required(args, "channel");
189
- const sourceSha = required(args, "source-sha").toLowerCase();
190
- const tag = required(args, "tag");
191
- const configPath = assertSourcePath(required(args, "config-path"));
192
- const releasePassportPath = path.resolve(required(args, "release-passport-path"));
193
- const outputDir = path.resolve(required(args, "output-dir"));
194
- if (!/^[0-9a-f]{40}$/.test(sourceSha)) throw new Error("--source-sha must be an exact commit SHA");
195
- if (tag !== `v${tag.replace(/^v/, "")}`) throw new Error("--tag must be an exact v-prefixed release tag");
196
- const version = tag.slice(1);
197
- const configSource = readConfigAtSource(sourceSha, configPath, process.cwd());
198
- const sourceNode = configSource.normalized.graph.nodes.find(
199
- (node) => node.id === configSource.normalized.sourceNode,
200
- );
201
- const packageFact = resolvePackageFact(sourceNode.package, version);
202
- const tagTargetSha = resolveTagTarget(repository, tag);
203
- const releasePassport = verifyPublicReleasePassport({
204
- repository,
205
- tag,
206
- localPath: releasePassportPath,
207
- });
208
- const upstreamRelease = {
209
- repository,
210
- channel,
211
- tag,
212
- sourceSha,
213
- tagTargetSha,
214
- package: packageFact,
215
- releasePassport,
216
- };
217
- const expectedBaseShas = resolveBaseShas(configSource.normalized);
218
- const captured = capturePackageReleasePropagation({
219
- config: configSource.parsed,
220
- configBytes: configSource.bytes,
221
- upstreamRelease,
222
- expectedBaseShas,
223
- outputDir,
224
- });
225
- const artifactName = `package-propagation-work-${version}-${sourceSha}`;
226
- const workRoots = captured.works.map((item) => item.work.contentRoot);
227
- writeOutput("configured", "true");
228
- writeOutput("artifact-name", artifactName);
229
- writeOutput("work-roots-json", JSON.stringify(workRoots));
230
- process.stdout.write(stableJson({
231
- schemaVersion: 1,
232
- contract: "kungfu-buildchain-package-release-propagation-capture-result",
233
- artifactName,
234
- release: {
235
- repository,
236
- channel,
237
- tag,
238
- sourceSha,
239
- tagTargetSha,
240
- package: packageFact,
241
- releasePassport,
242
- },
243
- workCount: captured.works.length,
244
- works: captured.works.map((item) => ({
245
- target: item.target,
246
- repository: item.repository,
247
- propagationKey: item.propagationKey,
248
- workId: item.work.workId,
249
- workRoot: item.work.contentRoot,
250
- nextAction: item.status.nextAction,
251
- })),
252
- }));
253
- }
254
-
255
- const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
256
- if (isMain) {
257
- try {
258
- main();
259
- } catch (error) {
260
- console.error(error.message);
261
- process.exitCode = 1;
262
- }
263
- }