@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.
- package/architecture/agent-change-map.md +7 -5
- package/architecture/ci-lane-change-budget.json +76 -8
- package/architecture/internal-capabilities.json +4 -1
- package/architecture/maintainability-debt.json +45 -119
- package/architecture/maintainability-policy.json +13 -8
- package/architecture/release-tail-contract-inventory.json +4 -4
- package/architecture/v3-core-mechanism-inventory.json +2 -0
- package/architecture/v3-v4-live-capability-inventory.json +17 -17
- package/architecture/v4-release-invocation-fixtures.json +119 -0
- package/architecture/v4-release-topology.json +238 -0
- package/contracts/buildchain-v2-residuals-v1.json +0 -9
- package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
- package/contracts/v4-release-invocation-v1.schema.json +86 -0
- package/dist/site/buildchain-contract.json +9 -9
- package/dist/site/buildchain-site.json +9 -9
- package/dist/site/kfd-claims.json +9 -7
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +1 -1
- package/dist/site/node-api-registry.json +3 -3
- package/dist/site/page-registry.json +4 -4
- package/dist/site/public-surface-audit.json +23 -10
- package/dist/site/publication-authority-registry.json +2 -3
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +5 -5
- package/dist/site/workflow-registry.json +19 -11
- package/docs/dev-delivery-warrant.md +21 -0
- package/docs/node-api-reference.md +3 -3
- package/package.json +2 -2
- package/packages/core/dev-delivery-candidate-identity.js +45 -17
- package/packages/core/dev-delivery-provider-heartbeat.js +22 -6
- package/packages/core/dev-delivery-warrant-legacy-recovery.js +246 -0
- package/packages/core/dev-delivery-warrant-state.js +12 -28
- package/packages/core/v4-canonical-contracts.js +8 -0
- package/packages/core/v4-floating-consumer-policy.js +4 -1
- package/packages/core/v4-release-invocation.js +356 -0
- package/scripts/audit-publication-control-plane.mjs +0 -1
- package/scripts/check-inventory.mjs +27 -59
- package/scripts/check-maintainability.mjs +13 -5
- package/scripts/check-v3-v4-capability-inventory.mjs +3 -61
- package/scripts/check-v4-floating-consumer-policy-contract.mjs +10 -20
- package/scripts/check-v4-release-topology.mjs +237 -0
- package/scripts/dev-delivery-warrant-options.mjs +15 -4
- package/scripts/dev-delivery-warrant.mjs +29 -7
- package/scripts/generate-channel-promotion-workflow.mjs +12 -54
- package/scripts/v3-v4-capability-catalog.mjs +107 -0
- package/scripts/v4-declarative-promotion-admission.mjs +4 -1
- package/scripts/capture-package-release-propagation.mjs +0 -263
- package/scripts/publication-commit-evidence.mjs +0 -444
- package/scripts/publish-github-artifact-attestation-evidence.mjs +0 -201
- 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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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 "\${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
523
|
-
name:
|
|
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
|
-
${
|
|
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 (
|
|
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
|
-
}
|