@kungfu-tech/buildchain 4.0.1-alpha.5 → 4.0.1-alpha.55
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/README.md +9 -6
- package/actions/release-tail/README.md +8 -0
- package/actions/v4-release-candidate-promote/README.md +3 -0
- package/architecture/agent-change-map.md +1015 -0
- package/architecture/ci-lane-change-budget.json +256 -0
- package/architecture/decisions/0004-v4-declarative-release-tail.md +47 -0
- package/architecture/internal-capabilities.json +220 -15
- package/architecture/maintainability-debt.json +3229 -0
- package/architecture/maintainability-policy.json +489 -132
- package/architecture/release-tail-contract-inventory.json +5 -2
- package/architecture/v3-core-mechanism-inventory.json +4 -3
- package/architecture/v3-v4-capability-closure.md +59 -0
- package/architecture/v3-v4-live-capability-inventory.json +102404 -0
- package/architecture/v4-adopter-delivery-parity.json +32 -5
- package/architecture/v4-cross-platform-adopter-qualification.json +52 -0
- package/architecture/v4-delivery-warrant-shadow-fixtures.json +3 -3
- package/architecture/v4-floating-consumer-policy.json +6 -0
- package/architecture/v4-publication-rehearsal-parity.json +84 -0
- package/architecture/v4-runtime-semantic-closure.json +710 -0
- package/bin/buildchain.mjs +7 -1
- package/bin/internal/adopter-delivery-cli.mjs +70 -0
- package/bin/internal/command-registry.mjs +1 -0
- package/bin/internal/trust-release-release-handlers.mjs +5 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/archive-template.json +19 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/bootstrap-positive.json +93 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/gate-positive.json +29 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/gate-unknown-selector.json +26 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/offline-vectors.json +50 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/artifacts/product.bin +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/config/buildchain.toml +5 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/release-activation.json +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/signed-channel.json +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/evidence/qualification.json +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/candidate.json +5 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/release-passport.json +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/capsule.json +707 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/offline-vectors.json +27 -0
- package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
- package/contracts/v4-adopter-delivery-v1.schema.json +81 -0
- package/contracts/v4-floating-consumer-policy-receipt-v1.schema.json +4 -2
- package/contracts/v4-publication-qualification-v1.schema.json +51 -0
- package/contracts/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
- package/dist/site/agent-index.json +4 -0
- package/dist/site/artifact-schemas.json +8 -0
- package/dist/site/buildchain-contract.json +125 -34
- package/dist/site/buildchain-site.json +308 -33
- package/dist/site/capability-registry.json +13 -12
- package/dist/site/cli-registry.json +57 -2
- package/dist/site/controller-registry.json +41 -5
- package/dist/site/kfd-claims.json +438 -24
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +22 -7
- package/dist/site/node-api-registry.json +12843 -9314
- package/dist/site/page-registry.json +288 -21
- package/dist/site/public-surface-audit.json +281 -21
- package/dist/site/publication-authority-registry.json +66 -8
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +20 -0
- package/dist/site/schemas/release-tail-capabilities-v1.schema.json +199 -0
- package/dist/site/schemas/v4-adopter-delivery-v1.schema.json +81 -0
- package/dist/site/schemas/v4-publication-qualification-v1.schema.json +51 -0
- package/dist/site/schemas/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
- package/dist/site/site-manifest.json +19 -11
- package/dist/site/workflow-registry.json +195 -17
- package/docs/MAP.md +4 -2
- package/docs/cli-reference.md +70 -2
- package/docs/cli.md +8 -0
- package/docs/dev-delivery-warrant.md +34 -10
- package/docs/node-api-reference.md +524 -279
- package/docs/publish-transaction.md +11 -4
- package/docs/release-candidate.md +7 -0
- package/docs/release-tail-provider-plane.md +30 -0
- package/docs/v4-adopter-delivery.md +152 -0
- package/docs/v4-declarative-release-tail-migration.md +43 -0
- package/docs/v4-publication-rehearsal.md +130 -0
- package/package.json +24 -4
- package/packages/core/adopter-delivery-passport.js +295 -0
- package/packages/core/adopter-delivery-vectors.js +158 -0
- package/packages/core/adopter-delivery-vectors.json +77 -0
- package/packages/core/buildchain-agent-manuals.js +1 -0
- package/packages/core/buildchain-config.js +84 -0
- package/packages/core/buildchain-contract.js +31 -1
- package/packages/core/buildchain-delivery-bootstrap.js +240 -0
- package/packages/core/buildchain-delivery-infrastructure.js +164 -0
- package/packages/core/buildchain-delivery-self-dogfood.js +395 -0
- package/packages/core/buildchain-publication-authority.js +2 -0
- package/packages/core/ci-lane-change-budget.js +247 -0
- package/packages/core/dev-delivery-native-proof.js +43 -27
- package/packages/core/dev-delivery-warrant-settlement.js +239 -3
- package/packages/core/dev-delivery-warrant-state.js +46 -10
- package/packages/core/dev-delivery-warrant.js +29 -7
- package/packages/core/github-governance-authority.js +5 -4
- package/packages/core/index.js +6 -0
- package/packages/core/legacy-kfd-adopter-driver.js +167 -0
- package/packages/core/paper-scaffold-content.js +401 -0
- package/packages/core/paper.js +12 -378
- package/packages/core/publication-control-plane-audit.js +135 -111
- package/packages/core/publication-rehearsal-projection.js +104 -0
- package/packages/core/publication-rehearsal-runtime.js +190 -0
- package/packages/core/published-delivery-authority.js +266 -0
- package/packages/core/release-candidate-recovery.js +71 -15
- package/packages/core/release-candidate.js +2 -2
- package/packages/core/release-passport.js +95 -170
- package/packages/core/v4-adopter-delivery-parity.js +3 -3
- package/packages/core/v4-adopter-delivery.js +359 -0
- package/packages/core/v4-cross-platform-adopter-qualification.js +343 -0
- package/packages/core/v4-delivery-warrant-read-candidate.js +10 -3
- package/packages/core/v4-floating-consumer-evidence.js +18 -4
- package/packages/core/v4-floating-consumer-policy.js +139 -35
- package/packages/core/v4-floating-consumer-release-passport.js +154 -8
- package/packages/core/v4-publication-qualification.js +222 -0
- package/packages/core/v4-publication-rehearsal-capsule.js +468 -0
- package/packages/core/v4-publication-rehearsal-provider-bindings.js +212 -0
- package/packages/core/v4-publication-rehearsal.js +481 -0
- package/packages/core/workflow-yaml-contract.js +24 -2
- package/scripts/artifact-signing-controller-core.mjs +1 -1
- package/scripts/audit-publication-control-plane.mjs +17 -17
- package/scripts/buildchain-cli-help.mjs +13 -0
- package/scripts/check-ci-lane-change-budget.mjs +100 -0
- package/scripts/check-internal-architecture.mjs +23 -3
- package/scripts/check-inventory.mjs +24 -6
- package/scripts/check-maintainability.mjs +145 -4
- package/scripts/check-project-cut-merge-queue-admission.mjs +87 -0
- package/scripts/check-v3-v4-capability-inventory.mjs +574 -0
- package/scripts/check-v4-floating-consumer-policy-contract.mjs +30 -4
- package/scripts/check-v4-runtime-semantic-closure.mjs +266 -0
- package/scripts/dev-delivery-source-proof-replay.mjs +153 -0
- package/scripts/dev-delivery-source-proof-reuse.mjs +13 -97
- package/scripts/dev-delivery-warrant-options.mjs +27 -11
- package/scripts/dev-delivery-warrant-store.mjs +10 -6
- package/scripts/dev-delivery-warrant.mjs +50 -6
- package/scripts/dev-pr-auto-merge.mjs +25 -11
- package/scripts/dev-pr-delivery-warrant.mjs +45 -0
- package/scripts/dev-pr-prequeue-guard.mjs +0 -18
- package/scripts/dev-qualification-patrol.mjs +9 -9
- package/scripts/dispatch-artifact-signing-authority.mjs +1 -1
- package/scripts/generate-agent-change-map.mjs +118 -0
- package/scripts/generate-channel-promotion-workflow.mjs +2 -2
- package/scripts/generate-release-candidate-passport.mjs +177 -0
- package/scripts/generate-site-bundle.mjs +36 -0
- package/scripts/maintainability-domain-metrics.mjs +237 -0
- package/scripts/maintainability-governance.mjs +473 -0
- package/scripts/maintainability-metrics.mjs +28 -6
- package/scripts/project-cut-merge-queue-admission.mjs +394 -0
- package/scripts/promotion-channel-router.mjs +4 -4
- package/scripts/promotion-routing-evidence.mjs +35 -6
- package/scripts/release-candidate-anchor-provenance.mjs +99 -0
- package/scripts/release-candidate-resolver.mjs +44 -0
- package/scripts/release-line-policy.mjs +25 -15
- package/scripts/release-tail.mjs +68 -3
- package/scripts/resume-from-candidate-run.mjs +595 -65
- package/scripts/run-lifecycle-core.mjs +341 -376
- package/scripts/site-capability-metadata.mjs +21 -0
- package/scripts/stable-candidate-qualification.mjs +9 -10
- package/scripts/v3-v4-capability-catalog.mjs +439 -0
- package/scripts/v4-consumer-policy.mjs +4 -0
- package/scripts/v4-cross-platform-adopter-qualification.mjs +228 -0
- package/scripts/v4-declarative-promotion-admission.mjs +42 -0
- package/scripts/v4-publication-rehearsal-fixture.mjs +144 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
const GIT_OID = /^[0-9a-f]{40}$/u;
|
|
8
|
+
const CONTENT_ROOT = /^sha256:[0-9a-f]{64}$/u;
|
|
9
|
+
const FAMILY_ID = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/u;
|
|
10
|
+
const FAMILY_MARKER =
|
|
11
|
+
/<!-- kungfu-family-queue-lease:v1 ([A-Za-z0-9_-]+) -->/gu;
|
|
12
|
+
const FAMILY_LEASE_SCHEMA = "kungfu.project-cut.family-queue-lease/v1";
|
|
13
|
+
|
|
14
|
+
export class ProjectCutAdmissionError extends Error {
|
|
15
|
+
constructor(reasonCode, message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "ProjectCutAdmissionError";
|
|
18
|
+
this.reasonCode = reasonCode;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function stableValue(value) {
|
|
23
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
24
|
+
if (value && typeof value === "object") {
|
|
25
|
+
return Object.fromEntries(
|
|
26
|
+
Object.entries(value)
|
|
27
|
+
.filter(([, entry]) => entry !== undefined)
|
|
28
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
29
|
+
.map(([key, entry]) => [key, stableValue(entry)]),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function canonicalBytes(value) {
|
|
36
|
+
return Buffer.from(`${JSON.stringify(stableValue(value))}\n`, "utf8");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function contentRoot(value) {
|
|
40
|
+
return `sha256:${crypto.createHash("sha256").update(canonicalBytes(value)).digest("hex")}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function exactRoot(value, label) {
|
|
44
|
+
const normalized = String(value || "")
|
|
45
|
+
.trim()
|
|
46
|
+
.toLowerCase();
|
|
47
|
+
if (!CONTENT_ROOT.test(normalized))
|
|
48
|
+
throw new Error(`${label} must be an exact SHA-256 root`);
|
|
49
|
+
return normalized;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function familyId(value, label) {
|
|
53
|
+
const normalized = String(value || "").trim();
|
|
54
|
+
if (!FAMILY_ID.test(normalized)) throw new Error(`${label} is invalid`);
|
|
55
|
+
return normalized;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function git(
|
|
59
|
+
cwd,
|
|
60
|
+
args,
|
|
61
|
+
{
|
|
62
|
+
env = {},
|
|
63
|
+
input = undefined,
|
|
64
|
+
encoding = "utf8",
|
|
65
|
+
reason = "git-failed",
|
|
66
|
+
} = {},
|
|
67
|
+
) {
|
|
68
|
+
const result = spawnSync("git", args, {
|
|
69
|
+
cwd,
|
|
70
|
+
env: { ...process.env, ...env },
|
|
71
|
+
input,
|
|
72
|
+
encoding,
|
|
73
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
74
|
+
});
|
|
75
|
+
if (result.status !== 0) {
|
|
76
|
+
const detail = String(result.stderr || result.stdout || "").trim();
|
|
77
|
+
throw new ProjectCutAdmissionError(
|
|
78
|
+
reason,
|
|
79
|
+
`git ${args.join(" ")} failed${detail ? `: ${detail}` : ""}`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return encoding === null ? result.stdout : String(result.stdout || "").trim();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function resolveCommit(cwd, revision, label) {
|
|
86
|
+
const oid = git(cwd, ["rev-parse", "--verify", `${revision}^{commit}`], {
|
|
87
|
+
reason: `${label}-unresolved`,
|
|
88
|
+
});
|
|
89
|
+
if (!GIT_OID.test(oid))
|
|
90
|
+
throw new ProjectCutAdmissionError(
|
|
91
|
+
`${label}-unresolved`,
|
|
92
|
+
`${label} is not an exact commit`,
|
|
93
|
+
);
|
|
94
|
+
return oid;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function linearSourceCommits(cwd, fork, head) {
|
|
98
|
+
const commits = git(
|
|
99
|
+
cwd,
|
|
100
|
+
["rev-list", "--reverse", "--topo-order", `${fork}..${head}`],
|
|
101
|
+
{
|
|
102
|
+
reason: "source-history-unreadable",
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
.split(/\s+/u)
|
|
106
|
+
.filter(Boolean);
|
|
107
|
+
if (commits.length === 0)
|
|
108
|
+
throw new ProjectCutAdmissionError(
|
|
109
|
+
"empty-source-range",
|
|
110
|
+
"source range is empty",
|
|
111
|
+
);
|
|
112
|
+
let expectedParent = fork;
|
|
113
|
+
for (const commit of commits) {
|
|
114
|
+
const [observed, ...parents] = git(
|
|
115
|
+
cwd,
|
|
116
|
+
["rev-list", "--parents", "-n", "1", commit],
|
|
117
|
+
{
|
|
118
|
+
reason: "source-history-unreadable",
|
|
119
|
+
},
|
|
120
|
+
).split(/\s+/u);
|
|
121
|
+
if (
|
|
122
|
+
observed !== commit ||
|
|
123
|
+
parents.length !== 1 ||
|
|
124
|
+
parents[0] !== expectedParent
|
|
125
|
+
) {
|
|
126
|
+
throw new ProjectCutAdmissionError(
|
|
127
|
+
"nonlinear-source-range",
|
|
128
|
+
"source range is not one immutable linear patch series",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
expectedParent = commit;
|
|
132
|
+
}
|
|
133
|
+
return commits;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function patchId(cwd, patch) {
|
|
137
|
+
const output = git(cwd, ["patch-id", "--stable"], {
|
|
138
|
+
input: patch,
|
|
139
|
+
encoding: "utf8",
|
|
140
|
+
reason: "patch-id-failed",
|
|
141
|
+
});
|
|
142
|
+
const id = output.split(/\s+/u)[0] || "";
|
|
143
|
+
if (!GIT_OID.test(id))
|
|
144
|
+
throw new ProjectCutAdmissionError(
|
|
145
|
+
"patch-id-failed",
|
|
146
|
+
"patch has no stable identity",
|
|
147
|
+
);
|
|
148
|
+
return id;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function virtualCommitOid(cwd, tree, base, head) {
|
|
152
|
+
const body = [
|
|
153
|
+
`tree ${tree}`,
|
|
154
|
+
`parent ${base}`,
|
|
155
|
+
"author Buildchain Project Cut <project-cut@kungfu.tech> 0 +0000",
|
|
156
|
+
"committer Buildchain Project Cut <project-cut@kungfu.tech> 0 +0000",
|
|
157
|
+
"",
|
|
158
|
+
`Replay ${head} onto ${base}`,
|
|
159
|
+
"",
|
|
160
|
+
].join("\n");
|
|
161
|
+
return git(cwd, ["hash-object", "-t", "commit", "--stdin"], {
|
|
162
|
+
input: body,
|
|
163
|
+
reason: "candidate-commit-hash-failed",
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function replayProjectCut(cwd, base, head) {
|
|
168
|
+
const fork = git(cwd, ["merge-base", base, head], {
|
|
169
|
+
reason: "no-common-fork",
|
|
170
|
+
});
|
|
171
|
+
if (!GIT_OID.test(fork))
|
|
172
|
+
throw new ProjectCutAdmissionError(
|
|
173
|
+
"no-common-fork",
|
|
174
|
+
"base and head have no exact fork",
|
|
175
|
+
);
|
|
176
|
+
const commits = linearSourceCommits(cwd, fork, head);
|
|
177
|
+
const temporary = fs.mkdtempSync(
|
|
178
|
+
path.join(os.tmpdir(), "buildchain-project-cut-"),
|
|
179
|
+
);
|
|
180
|
+
const indexPath = path.join(temporary, "index");
|
|
181
|
+
const indexEnvironment = { GIT_INDEX_FILE: indexPath };
|
|
182
|
+
try {
|
|
183
|
+
git(cwd, ["read-tree", base], {
|
|
184
|
+
env: indexEnvironment,
|
|
185
|
+
reason: "base-tree-unreadable",
|
|
186
|
+
});
|
|
187
|
+
const sourcePatch = git(
|
|
188
|
+
cwd,
|
|
189
|
+
["diff", "--binary", "--full-index", "--no-ext-diff", fork, head],
|
|
190
|
+
{
|
|
191
|
+
encoding: null,
|
|
192
|
+
reason: "source-patch-unreadable",
|
|
193
|
+
},
|
|
194
|
+
);
|
|
195
|
+
const emptySourcePatch = sourcePatch.length === 0;
|
|
196
|
+
if (emptySourcePatch && commits.length !== 1)
|
|
197
|
+
throw new ProjectCutAdmissionError("empty-source-composition", "an empty source composition must be exactly one linear commit");
|
|
198
|
+
if (!emptySourcePatch) git(cwd, ["apply", "--cached", "--3way", "--whitespace=nowarn", "-"], {
|
|
199
|
+
env: indexEnvironment, input: sourcePatch, encoding: "utf8", reason: "project-cut-conflict",
|
|
200
|
+
});
|
|
201
|
+
const candidateTreeOid = git(cwd, ["write-tree"], {
|
|
202
|
+
env: indexEnvironment,
|
|
203
|
+
reason: "candidate-tree-unreadable",
|
|
204
|
+
});
|
|
205
|
+
const replayPatch = git(
|
|
206
|
+
cwd,
|
|
207
|
+
["diff", "--cached", "--binary", "--full-index", "--no-ext-diff", base],
|
|
208
|
+
{
|
|
209
|
+
env: indexEnvironment,
|
|
210
|
+
encoding: null,
|
|
211
|
+
reason: "replay-patch-unreadable",
|
|
212
|
+
},
|
|
213
|
+
);
|
|
214
|
+
const sourceNames = git(
|
|
215
|
+
cwd,
|
|
216
|
+
["diff", "--name-status", "--no-renames", fork, head],
|
|
217
|
+
{
|
|
218
|
+
reason: "source-paths-unreadable",
|
|
219
|
+
},
|
|
220
|
+
);
|
|
221
|
+
const replayNames = git(
|
|
222
|
+
cwd,
|
|
223
|
+
["diff", "--cached", "--name-status", "--no-renames", base],
|
|
224
|
+
{
|
|
225
|
+
env: indexEnvironment,
|
|
226
|
+
reason: "replay-paths-unreadable",
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
const compositionDrifted = emptySourcePatch ? replayPatch.length !== 0 ||
|
|
230
|
+
sourceNames !== "" || replayNames !== "" : patchId(cwd, sourcePatch) !==
|
|
231
|
+
patchId(cwd, replayPatch) || sourceNames !== replayNames;
|
|
232
|
+
if (compositionDrifted) {
|
|
233
|
+
throw new ProjectCutAdmissionError(
|
|
234
|
+
"composition-drift",
|
|
235
|
+
"latest-base replay changes the immutable source composition",
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
baseCommitOid: base,
|
|
240
|
+
headCommitOid: head,
|
|
241
|
+
forkCommitOid: fork,
|
|
242
|
+
candidateCommitOid: virtualCommitOid(cwd, candidateTreeOid, base, head),
|
|
243
|
+
candidateTreeOid,
|
|
244
|
+
replayedCommitCount: commits.length,
|
|
245
|
+
};
|
|
246
|
+
} finally {
|
|
247
|
+
fs.rmSync(temporary, { recursive: true, force: true });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function encodeMarker(payload) {
|
|
252
|
+
return canonicalBytes(payload)
|
|
253
|
+
.toString("base64")
|
|
254
|
+
.replace(/\+/gu, "-")
|
|
255
|
+
.replace(/\//gu, "_")
|
|
256
|
+
.replace(/=+$/gu, "");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function decodeMarker(token) {
|
|
260
|
+
const padded = `${token}${"=".repeat((4 - (token.length % 4)) % 4)}`
|
|
261
|
+
.replace(/-/gu, "+")
|
|
262
|
+
.replace(/_/gu, "/");
|
|
263
|
+
return JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function familyLease(input, replay) {
|
|
267
|
+
const binding = {
|
|
268
|
+
schema: FAMILY_LEASE_SCHEMA,
|
|
269
|
+
state: "active",
|
|
270
|
+
initiativeId: familyId(input.initiativeId, "initiativeId"),
|
|
271
|
+
assignmentId: familyId(input.assignmentId, "assignmentId"),
|
|
272
|
+
deliveryClass: familyId(input.deliveryClass, "deliveryClass"),
|
|
273
|
+
admissionProofRoot: exactRoot(
|
|
274
|
+
input.admissionProofRoot,
|
|
275
|
+
"admissionProofRoot",
|
|
276
|
+
),
|
|
277
|
+
queueAttempt: String(input.queueAttempt || "").trim(),
|
|
278
|
+
pullRequestHead: replay.headCommitOid,
|
|
279
|
+
baseCommitOid: replay.baseCommitOid,
|
|
280
|
+
forkCommitOid: replay.forkCommitOid,
|
|
281
|
+
candidateTreeOid: replay.candidateTreeOid,
|
|
282
|
+
};
|
|
283
|
+
const expectedAttempt = `${replay.headCommitOid}@${replay.baseCommitOid}`;
|
|
284
|
+
if (binding.queueAttempt !== expectedAttempt) {
|
|
285
|
+
throw new ProjectCutAdmissionError(
|
|
286
|
+
"queue-attempt-drift",
|
|
287
|
+
`queueAttempt must equal ${expectedAttempt}`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
const bindingRoot = contentRoot(binding);
|
|
291
|
+
const statusContext = `Queue family lease/${bindingRoot.slice(7, 23)}`;
|
|
292
|
+
const payload = { ...binding, bindingRoot, statusContext };
|
|
293
|
+
const leaseRoot = contentRoot(payload);
|
|
294
|
+
const sealed = { ...payload, leaseRoot };
|
|
295
|
+
return {
|
|
296
|
+
marker: `<!-- kungfu-family-queue-lease:v1 ${encodeMarker(sealed)} -->`,
|
|
297
|
+
leaseRoot,
|
|
298
|
+
statusContext,
|
|
299
|
+
pullRequestHead: replay.headCommitOid,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function parseFamilyQueueLeaseMarker(text) {
|
|
304
|
+
const matches = [...String(text || "").matchAll(FAMILY_MARKER)];
|
|
305
|
+
if (matches.length !== 1) return null;
|
|
306
|
+
try {
|
|
307
|
+
const payload = decodeMarker(matches[0][1]);
|
|
308
|
+
const { leaseRoot, bindingRoot, statusContext, ...binding } = payload;
|
|
309
|
+
if (payload.schema !== FAMILY_LEASE_SCHEMA || payload.state !== "active")
|
|
310
|
+
return null;
|
|
311
|
+
if (contentRoot(binding) !== bindingRoot) return null;
|
|
312
|
+
if (statusContext !== `Queue family lease/${bindingRoot.slice(7, 23)}`)
|
|
313
|
+
return null;
|
|
314
|
+
if (contentRoot({ ...binding, bindingRoot, statusContext }) !== leaseRoot)
|
|
315
|
+
return null;
|
|
316
|
+
if (encodeMarker(payload) !== matches[0][1]) return null;
|
|
317
|
+
exactRoot(leaseRoot, "leaseRoot");
|
|
318
|
+
exactRoot(payload.admissionProofRoot, "admissionProofRoot");
|
|
319
|
+
for (const field of [
|
|
320
|
+
"pullRequestHead",
|
|
321
|
+
"baseCommitOid",
|
|
322
|
+
"forkCommitOid",
|
|
323
|
+
"candidateTreeOid",
|
|
324
|
+
]) {
|
|
325
|
+
if (!GIT_OID.test(String(payload[field] || ""))) return null;
|
|
326
|
+
}
|
|
327
|
+
return payload;
|
|
328
|
+
} catch {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function releaseFamilyQueueLease(markerText, input = {}) {
|
|
334
|
+
const lease = parseFamilyQueueLeaseMarker(markerText);
|
|
335
|
+
if (!lease) throw new Error("family queue lease marker is invalid");
|
|
336
|
+
const expectedHead = String(input.expectedPullRequestHead || "")
|
|
337
|
+
.trim()
|
|
338
|
+
.toLowerCase();
|
|
339
|
+
if (!GIT_OID.test(expectedHead) || expectedHead !== lease.pullRequestHead) {
|
|
340
|
+
throw new Error("family queue lease pull request head drifted");
|
|
341
|
+
}
|
|
342
|
+
const terminalReason = String(input.terminalReason || "").trim();
|
|
343
|
+
if (!["controller-failed", "merged"].includes(terminalReason))
|
|
344
|
+
throw new Error("terminalReason is invalid");
|
|
345
|
+
const evidenceRoot = input.evidenceRoot
|
|
346
|
+
? exactRoot(input.evidenceRoot, "evidenceRoot")
|
|
347
|
+
: null;
|
|
348
|
+
if (terminalReason === "merged" && !evidenceRoot)
|
|
349
|
+
throw new Error("merged family lease requires evidenceRoot");
|
|
350
|
+
const receipt = {
|
|
351
|
+
schema: "project.cut.family-queue-release/v1",
|
|
352
|
+
state: "released",
|
|
353
|
+
leaseRoot: lease.leaseRoot,
|
|
354
|
+
pullRequestHead: lease.pullRequestHead,
|
|
355
|
+
terminalReason,
|
|
356
|
+
evidenceRoot,
|
|
357
|
+
};
|
|
358
|
+
return { ...receipt, releaseRoot: contentRoot(receipt) };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function qualifyProjectCut(input = {}) {
|
|
362
|
+
const cwd = path.resolve(input.cwd || process.cwd());
|
|
363
|
+
const base = resolveCommit(cwd, input.base, "base");
|
|
364
|
+
const head = resolveCommit(cwd, input.head, "head");
|
|
365
|
+
const replay = replayProjectCut(cwd, base, head);
|
|
366
|
+
const familyInputs = [
|
|
367
|
+
input.initiativeId,
|
|
368
|
+
input.assignmentId,
|
|
369
|
+
input.deliveryClass,
|
|
370
|
+
input.queueAttempt,
|
|
371
|
+
input.admissionProofRoot,
|
|
372
|
+
];
|
|
373
|
+
const familyCount = familyInputs.filter((value) =>
|
|
374
|
+
String(value || "").trim(),
|
|
375
|
+
).length;
|
|
376
|
+
if (familyCount !== 0 && familyCount !== familyInputs.length) {
|
|
377
|
+
throw new ProjectCutAdmissionError(
|
|
378
|
+
"partial-family-binding",
|
|
379
|
+
"family admission inputs must be all present or all absent",
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
schema: "project.cut.merge-queue-admission/v1",
|
|
384
|
+
ok: true,
|
|
385
|
+
decision: "qualified",
|
|
386
|
+
retryable: false,
|
|
387
|
+
compositionChanged: false,
|
|
388
|
+
reasonCodes: [],
|
|
389
|
+
...replay,
|
|
390
|
+
...(familyCount === familyInputs.length
|
|
391
|
+
? { familyLease: familyLease(input, replay) }
|
|
392
|
+
: {}),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
@@ -66,8 +66,8 @@ export function resolvePromotionChannel({
|
|
|
66
66
|
}
|
|
67
67
|
const shellRef = intent.shellChannel === "alpha" ? `v${selected.major}-alpha` : `v${selected.major}`;
|
|
68
68
|
return {
|
|
69
|
-
targetRef: intent.targetRef,
|
|
70
|
-
|
|
69
|
+
targetRef: intent.targetRef, publicationChannel: intent.publicationChannel,
|
|
70
|
+
routerRef: normalized(routerRef), routerSha: normalized(routerSha).toLowerCase(),
|
|
71
71
|
channel: intent.shellChannel,
|
|
72
72
|
major: selected.major,
|
|
73
73
|
shellRef,
|
|
@@ -99,8 +99,8 @@ function readPackageVersion(cwd) {
|
|
|
99
99
|
|
|
100
100
|
function writeOutputs(file, result) {
|
|
101
101
|
const outputs = {
|
|
102
|
-
"target-ref": result.targetRef,
|
|
103
|
-
"
|
|
102
|
+
"target-ref": result.targetRef, "publication-channel": result.publicationChannel,
|
|
103
|
+
"router-ref": result.routerRef, "router-sha": result.routerSha,
|
|
104
104
|
channel: result.channel,
|
|
105
105
|
major: String(result.major),
|
|
106
106
|
"shell-ref": result.shellRef,
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
2
|
import fs from "node:fs";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { pathToFileURL } from "node:url";
|
|
6
|
-
|
|
7
5
|
function createPromotionRoutingEvidence({
|
|
8
6
|
env = process.env,
|
|
9
7
|
cwd = process.cwd(),
|
|
@@ -55,13 +53,45 @@ function createPromotionRoutingEvidence({
|
|
|
55
53
|
}
|
|
56
54
|
return { routing, routingPath, candidatePath };
|
|
57
55
|
}
|
|
58
|
-
|
|
56
|
+
function bundlePromotionControllerEvidence({
|
|
57
|
+
env = process.env,
|
|
58
|
+
cwd = process.cwd(),
|
|
59
|
+
} = {}) {
|
|
60
|
+
const finalizationNeeded = env.FINALIZATION_NEEDED === "true";
|
|
61
|
+
const files = [
|
|
62
|
+
[env.RELEASE_CANDIDATE_PASSPORT, "release-candidate-passport.json"],
|
|
63
|
+
[env.PUBLISH_EVIDENCE, "publish-evidence.json"],
|
|
64
|
+
...(!finalizationNeeded
|
|
65
|
+
? [[env.RELEASE_PASSPORT, "release-passport.json"]]
|
|
66
|
+
: []),
|
|
67
|
+
...(env.PUBLICATION_COMMIT_ENABLED === "true" && !finalizationNeeded
|
|
68
|
+
? [[env.PUBLICATION_COMMIT_EVIDENCE, "publication-commit-evidence.json"]]
|
|
69
|
+
: []),
|
|
70
|
+
...(!finalizationNeeded ? [
|
|
71
|
+
[env.RELEASE_ACTIVATION_RECEIPTS, "release-activation-receipt-set.json"],
|
|
72
|
+
[env.RELEASED_PRODUCT_EVIDENCE, "released-product-evidence.json"],
|
|
73
|
+
[env.RELEASE_PROPAGATION_CAPTURE, "release-propagation-capture.json"],
|
|
74
|
+
].filter(([source]) => source) : []),
|
|
75
|
+
];
|
|
76
|
+
const outputDir = path.join(cwd, ".buildchain/controller/promotion-evidence");
|
|
77
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
78
|
+
for (const [source, name] of files) {
|
|
79
|
+
if (!source || !fs.existsSync(path.resolve(cwd, source)))
|
|
80
|
+
throw new Error(`promotion controller evidence is missing: ${source || name}`);
|
|
81
|
+
fs.copyFileSync(path.resolve(cwd, source), path.join(outputDir, name));
|
|
82
|
+
}
|
|
83
|
+
return { files: files.map(([, name]) => name), outputDir };
|
|
84
|
+
}
|
|
59
85
|
if (
|
|
60
86
|
process.argv[1] &&
|
|
61
87
|
import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href
|
|
62
88
|
) {
|
|
63
89
|
try {
|
|
64
|
-
|
|
90
|
+
if (process.argv[2] === "bundle-controller-evidence") {
|
|
91
|
+
bundlePromotionControllerEvidence();
|
|
92
|
+
} else {
|
|
93
|
+
createPromotionRoutingEvidence();
|
|
94
|
+
}
|
|
65
95
|
} catch (error) {
|
|
66
96
|
console.error(
|
|
67
97
|
`promotion routing evidence failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -69,5 +99,4 @@ if (
|
|
|
69
99
|
process.exitCode = 1;
|
|
70
100
|
}
|
|
71
101
|
}
|
|
72
|
-
|
|
73
|
-
export { createPromotionRoutingEvidence };
|
|
102
|
+
export { bundlePromotionControllerEvidence, createPromotionRoutingEvidence };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
export function resolveAnchorRecoveryRequest({ passport, buildSummary, transactionId = "" } = {}) {
|
|
4
|
+
if (Number(passport?.pullRequest?.number || 0)) return undefined;
|
|
5
|
+
if (passport?.target?.channel !== "anchor" || passport?.target?.ref !== "publish-gate/anchor") {
|
|
6
|
+
throw new Error("Release Candidate Passport has no PR identity");
|
|
7
|
+
}
|
|
8
|
+
let releaseManifest;
|
|
9
|
+
try {
|
|
10
|
+
releaseManifest = JSON.parse(buildSummary?.publishSource?.releaseManifest || "");
|
|
11
|
+
} catch {
|
|
12
|
+
throw new Error("anchored candidate release manifest is invalid");
|
|
13
|
+
}
|
|
14
|
+
const request = releaseManifest?.anchorRequest;
|
|
15
|
+
if (
|
|
16
|
+
releaseManifest?.sourceRef !== "publish-gate/anchor" ||
|
|
17
|
+
releaseManifest?.sourceSha !== passport.source?.headSha ||
|
|
18
|
+
request?.schema !== 1 ||
|
|
19
|
+
request?.contract !== "kungfu-buildchain-explicit-publish-anchor-request/v1" ||
|
|
20
|
+
!request.assignmentId ||
|
|
21
|
+
!/^native:[0-9a-f-]+$/u.test(String(request.attemptId || "")) ||
|
|
22
|
+
request.transactionId !== transactionId ||
|
|
23
|
+
request.source?.sha !== passport.source?.headSha ||
|
|
24
|
+
request.source?.tree !== passport.source?.treeHash ||
|
|
25
|
+
!/^\d+$/u.test(String(request.supersededCandidate?.workflowRunId || "")) ||
|
|
26
|
+
!/^[0-9a-f]{40}$/u.test(String(request.supersededCandidate?.sha || "")) ||
|
|
27
|
+
!/^sha256:[0-9a-f]{64}$/u.test(String(request.supersededCandidate?.root || "")) ||
|
|
28
|
+
request.runtime?.sha !== passport.buildchain?.sha
|
|
29
|
+
) {
|
|
30
|
+
throw new Error("anchored candidate request does not bind the sealed recovery identity");
|
|
31
|
+
}
|
|
32
|
+
return request;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeRun(run, repository) {
|
|
36
|
+
return {
|
|
37
|
+
id: String(run.id), repository,
|
|
38
|
+
headRepository: run.head_repository?.full_name || "",
|
|
39
|
+
status: run.status, conclusion: run.conclusion,
|
|
40
|
+
event: run.event, path: run.path, name: run.name,
|
|
41
|
+
headSha: run.head_sha || "", headBranch: run.head_branch || "",
|
|
42
|
+
pullRequestNumbers: (run.pull_requests || []).map((entry) => Number(entry.number)),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function normalizeCandidateRun(run, repository) {
|
|
47
|
+
return normalizeRun(run, repository);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function normalizeAnchorProvenance(anchorProvenance, repository) {
|
|
51
|
+
if (!anchorProvenance) return undefined;
|
|
52
|
+
return {
|
|
53
|
+
request: anchorProvenance.request,
|
|
54
|
+
run: normalizeRun(anchorProvenance.run, repository),
|
|
55
|
+
workflow: {
|
|
56
|
+
path: anchorProvenance.workflow.path,
|
|
57
|
+
name: anchorProvenance.workflow.name,
|
|
58
|
+
state: anchorProvenance.workflow.state,
|
|
59
|
+
},
|
|
60
|
+
passport: anchorProvenance.passport,
|
|
61
|
+
buildSummary: anchorProvenance.buildSummary,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function recoverCandidateProvenance({
|
|
66
|
+
passport, buildSummary, transactionId, repoInfo, artifactName,
|
|
67
|
+
expectedWorkflowFile, expectedWorkflowName, channel, apiUrl, token,
|
|
68
|
+
fetchImpl, archiveDir, bundleRoot, githubJson, selectReleaseCandidateArtifacts,
|
|
69
|
+
downloadArtifact, readOnlyJson,
|
|
70
|
+
}) {
|
|
71
|
+
const request = resolveAnchorRecoveryRequest({ passport, buildSummary, transactionId });
|
|
72
|
+
if (!request) {
|
|
73
|
+
return { anchorProvenance: undefined, provenancePassport: passport };
|
|
74
|
+
}
|
|
75
|
+
const runId = String(request.supersededCandidate.workflowRunId);
|
|
76
|
+
const run = await githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/runs/${runId}` });
|
|
77
|
+
const workflow = await githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/workflows/${run.workflow_id}` });
|
|
78
|
+
const artifactResponse = await githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/runs/${runId}/artifacts?per_page=100` });
|
|
79
|
+
const artifacts = Array.isArray(artifactResponse.artifacts) ? artifactResponse.artifacts : [];
|
|
80
|
+
if (Number(artifactResponse.total_count || artifacts.length) !== artifacts.length) throw new Error("superseded candidate run has more than 100 artifacts; complete pagination is required before recovery");
|
|
81
|
+
const selected = selectReleaseCandidateArtifacts({ artifacts, artifactName });
|
|
82
|
+
const provenanceRoot = path.join(bundleRoot, "anchored-provenance");
|
|
83
|
+
const passportDownload = await downloadArtifact({ artifact: selected.passport, repoInfo, apiUrl, token, archiveDir, bundleRoot: provenanceRoot, fetchImpl });
|
|
84
|
+
const summaryDownload = await downloadArtifact({ artifact: selected.summary, repoInfo, apiUrl, token, archiveDir, bundleRoot: provenanceRoot, fetchImpl });
|
|
85
|
+
const recoveredPassport = readOnlyJson(passportDownload.files.filter((file) => path.basename(file.path) === "release-candidate-passport.json"), "superseded release-candidate-passport.json");
|
|
86
|
+
const recoveredBuildSummary = readOnlyJson(summaryDownload.files.filter((file) => path.basename(file.path) === "build-summary.json"), "superseded build-summary.json");
|
|
87
|
+
if (
|
|
88
|
+
String(run.id) !== runId ||
|
|
89
|
+
recoveredPassport.workflow?.runId !== runId ||
|
|
90
|
+
recoveredPassport.workflow?.name !== expectedWorkflowName ||
|
|
91
|
+
String(workflow.path || run.path || "").split("@")[0].replace(/^\.github\/workflows\//u, "") !== expectedWorkflowFile.replace(/^\.github\/workflows\//u, "") ||
|
|
92
|
+
recoveredPassport.target?.channel !== channel ||
|
|
93
|
+
recoveredPassport.source?.headSha !== request.supersededCandidate.sha ||
|
|
94
|
+
recoveredPassport.source?.treeHash !== request.source.tree ||
|
|
95
|
+
`sha256:${recoveredPassport.candidateHash}` !== request.supersededCandidate.root
|
|
96
|
+
) throw new Error("superseded candidate provenance does not match the anchored request");
|
|
97
|
+
const anchorProvenance = { request, run, workflow, passport: recoveredPassport, buildSummary: recoveredBuildSummary };
|
|
98
|
+
return { anchorProvenance, provenancePassport: recoveredPassport };
|
|
99
|
+
}
|
|
@@ -9,6 +9,8 @@ import { pipeline } from "node:stream/promises";
|
|
|
9
9
|
import { pathToFileURL } from "node:url";
|
|
10
10
|
import { createResolvedPublicationSealedBundle } from "./publication-candidate-sealer.mjs";
|
|
11
11
|
import { writeGitHubOutputs } from "./build-contract-core.mjs";
|
|
12
|
+
import { v4ContentRoot } from "../packages/core/v4-canonical-contracts.js";
|
|
13
|
+
import { v4PublicationQualificationRoot, validateV4PublicationQualificationReceipt } from "../packages/core/v4-publication-qualification.js";
|
|
12
14
|
|
|
13
15
|
const DEFAULT_WORKFLOW_FILE = "build-surface-fixture.yml";
|
|
14
16
|
|
|
@@ -434,6 +436,42 @@ function findDownloadedFile(root, filename) {
|
|
|
434
436
|
return "";
|
|
435
437
|
}
|
|
436
438
|
|
|
439
|
+
function resolveV4PublicationEvidence(passportDir, passport, outputPath) {
|
|
440
|
+
const stageCapsulesPath = findDownloadedFile(passportDir, "release-candidate-stage-capsules.json");
|
|
441
|
+
const publicationQualificationPath = findDownloadedFile(passportDir, "release-candidate-publication-qualification.json");
|
|
442
|
+
if (!passport.consumerPolicy?.receiptRoot) {
|
|
443
|
+
return {
|
|
444
|
+
paths: {
|
|
445
|
+
stageCapsules: stageCapsulesPath ? outputPath(stageCapsulesPath) : "",
|
|
446
|
+
publicationQualification: publicationQualificationPath ? outputPath(publicationQualificationPath) : "",
|
|
447
|
+
},
|
|
448
|
+
publicationQualificationRoot: "",
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
if (!stageCapsulesPath || !publicationQualificationPath) {
|
|
452
|
+
throw new Error("v4 release candidate is missing Stage Capsules or publication qualification receipt");
|
|
453
|
+
}
|
|
454
|
+
const stageCapsules = JSON.parse(fs.readFileSync(stageCapsulesPath, "utf8"));
|
|
455
|
+
const qualification = JSON.parse(fs.readFileSync(publicationQualificationPath, "utf8"));
|
|
456
|
+
validateV4PublicationQualificationReceipt(qualification, {
|
|
457
|
+
repository: passport.repository,
|
|
458
|
+
candidateRoot: `sha256:${passport.candidateHash}`,
|
|
459
|
+
sourceSha: passport.source.headSha,
|
|
460
|
+
sourceRoot: v4ContentRoot("candidate-identity", passport.source),
|
|
461
|
+
artifactRoot: v4PublicationQualificationRoot(stageCapsules.capsules.map(({ publicationArtifact }) => publicationArtifact)),
|
|
462
|
+
policyDigest: passport.consumerPolicy.receiptRoot,
|
|
463
|
+
});
|
|
464
|
+
if (stageCapsules.publicationQualificationRoot !== qualification.receiptRoot) throw new Error("Stage Capsules do not bind the publication qualification receipt");
|
|
465
|
+
if (stageCapsules.capsules.some(({ capsule }) => capsule.identity.qualificationRoot !== qualification.receiptRoot)) throw new Error("a Stage Capsule does not bind the publication qualification receipt");
|
|
466
|
+
return {
|
|
467
|
+
paths: {
|
|
468
|
+
stageCapsules: outputPath(stageCapsulesPath),
|
|
469
|
+
publicationQualification: outputPath(publicationQualificationPath),
|
|
470
|
+
},
|
|
471
|
+
publicationQualificationRoot: qualification.receiptRoot,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
437
475
|
export async function resolveReleaseCandidateArtifacts({
|
|
438
476
|
repository,
|
|
439
477
|
targetRef,
|
|
@@ -667,6 +705,7 @@ export async function resolveReleaseCandidateArtifacts({
|
|
|
667
705
|
throw new Error("downloaded release-candidate artifacts did not contain release-candidate-passport.json and build-summary.json");
|
|
668
706
|
}
|
|
669
707
|
const passport = JSON.parse(fs.readFileSync(passportPath, "utf8"));
|
|
708
|
+
const v4Publication = resolveV4PublicationEvidence(passportDir, passport, outputPath);
|
|
670
709
|
const platformManifestPaths = findDownloadedFiles(payloadDir, "manifest.json");
|
|
671
710
|
const githubArtifactAttestationPolicyPaths = findDownloadedFiles(
|
|
672
711
|
payloadDir,
|
|
@@ -722,6 +761,7 @@ export async function resolveReleaseCandidateArtifacts({
|
|
|
722
761
|
paths: {
|
|
723
762
|
passport: outputPath(passportPath),
|
|
724
763
|
buildSummary: outputPath(buildSummaryPath),
|
|
764
|
+
...v4Publication.paths,
|
|
725
765
|
payloads: outputPath(payloadDir),
|
|
726
766
|
platformManifests: platformManifestPaths.map(outputPath),
|
|
727
767
|
githubArtifactAttestationPolicies: githubArtifactAttestationPolicyPaths.map(outputPath),
|
|
@@ -738,6 +778,7 @@ export async function resolveReleaseCandidateArtifacts({
|
|
|
738
778
|
githubArtifactAttestationPolicyCount: githubArtifactAttestationPolicyPaths.length,
|
|
739
779
|
npmTarballCount: npmTarballPaths.length,
|
|
740
780
|
publishRequiredArtifacts: generatedRequiredArtifacts,
|
|
781
|
+
publicationQualificationRoot: v4Publication.publicationQualificationRoot,
|
|
741
782
|
};
|
|
742
783
|
}
|
|
743
784
|
|
|
@@ -766,6 +807,9 @@ export async function resolveReleaseCandidateArtifactsCli() {
|
|
|
766
807
|
"promote-only-release-candidate": String(result.enabled === true),
|
|
767
808
|
"release-candidate-passport-path": result.paths?.passport || "",
|
|
768
809
|
"release-candidate-build-summary-path": result.paths?.buildSummary || "",
|
|
810
|
+
"release-candidate-stage-capsules-path": result.paths?.stageCapsules || "",
|
|
811
|
+
"release-candidate-publication-qualification-path": result.paths?.publicationQualification || "",
|
|
812
|
+
"release-candidate-publication-qualification-root": result.publicationQualificationRoot || "",
|
|
769
813
|
"release-candidate-version": result.version || "",
|
|
770
814
|
"release-candidate-source-sha": result.artifacts?.sourceSha || "",
|
|
771
815
|
"release-candidate-artifact": result.artifacts?.passport || "",
|