@kungfu-tech/buildchain 3.0.6 → 3.0.7-alpha.1
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 +4 -2
- package/actions/promote-buildchain-ref/README.md +10 -0
- package/actions/release-tail/README.md +17 -0
- package/bin/buildchain.mjs +11 -0
- package/bin/internal/command-registry.mjs +2 -0
- package/contracts/auditable-demo-scenario-v1.schema.json +1 -1
- package/contracts/engineering-housekeeper-v1.schema.json +143 -0
- package/contracts/fixtures/engineering-housekeeper-v1/cases.json +68 -0
- package/contracts/fixtures/release-tail-capabilities-v1/kungfu-alpha.json +273 -0
- package/contracts/publication-rehearsal-capsule-v1.schema.json +173 -0
- package/contracts/release-tail-capabilities-v1.schema.json +199 -0
- package/contracts/release-tail-provider-bindings-v1.schema.json +56 -0
- package/dist/site/agent-index.json +3 -0
- package/dist/site/artifact-schemas.json +6 -0
- package/dist/site/buildchain-contract.json +42 -27
- package/dist/site/buildchain-site.json +384 -33
- package/dist/site/capability-registry.json +15 -12
- package/dist/site/cli-registry.json +102 -0
- package/dist/site/controller-registry.json +19 -3
- package/dist/site/kfd-claims.json +359 -17
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +53 -7
- package/dist/site/node-api-registry.json +5882 -3605
- package/dist/site/page-registry.json +348 -21
- package/dist/site/public-surface-audit.json +396 -16
- package/dist/site/publication-authority-registry.json +106 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +7 -0
- package/dist/site/schemas/publication-rehearsal-capsule-v1.schema.json +269 -0
- package/dist/site/schemas/release-tail-capabilities-v1.schema.json +353 -0
- package/dist/site/schemas/release-tail-provider-bindings-v1.schema.json +94 -0
- package/dist/site/site-manifest.json +35 -11
- package/dist/site/workflow-registry.json +204 -10
- package/docs/MAP.md +6 -2
- package/docs/auditable-demo.md +2 -2
- package/docs/cli-reference.md +136 -0
- package/docs/dev-delivery-warrant.md +49 -4
- package/docs/engineering-housekeeper.md +138 -0
- package/docs/lifecycle-protocol.md +4 -2
- package/docs/node-api-reference.md +495 -281
- package/docs/publication-rehearsal.md +94 -0
- package/docs/release-governance.md +17 -2
- package/docs/release-tail-contract.md +160 -0
- package/docs/release-tail-provider-plane.md +120 -0
- package/docs/reusable-build-surface.md +11 -0
- package/package.json +11 -3
- package/packages/core/artifact-signing.js +61 -0
- package/packages/core/buildchain-agent-manuals.js +3 -0
- package/packages/core/buildchain-config.js +66 -6
- package/packages/core/buildchain-kfd-claims.js +1 -1
- package/packages/core/buildchain-publication-authority.js +5 -0
- package/packages/core/controller-evidence.js +2 -1
- package/packages/core/dev-delivery-warrant-cancellation.js +1 -0
- package/packages/core/dev-delivery-warrant-shadow.js +502 -0
- package/packages/core/dev-delivery-warrant.js +15 -6
- package/packages/core/diagnostics.js +8 -3
- package/packages/core/engineering-housekeeper-github-client.js +222 -0
- package/packages/core/engineering-housekeeper-github.js +501 -0
- package/packages/core/engineering-housekeeper.js +259 -0
- package/packages/core/index.js +42 -0
- package/packages/core/kfd-gate.js +45 -15
- package/packages/core/paper-agent-entry.js +11 -5
- package/packages/core/paper-repository.js +1 -0
- package/packages/core/paper-scaffold-content.js +21 -0
- package/packages/core/paper.js +28 -2
- package/packages/core/publication-rehearsal-projection.js +173 -0
- package/packages/core/publication-rehearsal-runtime.js +921 -0
- package/packages/core/release-passport.js +130 -20
- package/packages/core/release-tail-compatibility.js +60 -0
- package/packages/core/release-tail-provider-adapters.js +461 -0
- package/packages/core/release-tail-provider-plane.js +1228 -0
- package/scripts/assemble-publication-artifact-admission.mjs +1 -1
- package/scripts/assemble-self-publication-admission.mjs +2 -2
- package/scripts/audit-publication-control-plane.mjs +1 -1
- package/scripts/auditable-demo-bundle-verification.mjs +2 -3
- package/scripts/auditable-demo-platform.mjs +2 -2
- package/scripts/auditable-demo-renditions.mjs +1 -1
- package/scripts/auditable-demo.mjs +2 -2
- package/scripts/build-contract-core.mjs +8 -3
- package/scripts/build-standalone-binary.mjs +14 -3
- package/scripts/buildchain-cli-help.mjs +13 -0
- package/scripts/check-core-mechanism-inventory.mjs +347 -0
- package/scripts/check-inventory.mjs +10 -8
- package/scripts/check-maintainability.mjs +9 -2
- package/scripts/check-release-tail-contract.mjs +435 -0
- package/scripts/dev-delivery-warrant.mjs +31 -4
- package/scripts/dev-pr-auto-merge.mjs +30 -4
- package/scripts/dev-pr-delivery-warrant.mjs +50 -0
- package/scripts/engineering-housekeeper-workflow.mjs +394 -0
- package/scripts/generate-channel-promotion-workflow.mjs +10 -8
- package/scripts/generate-site-bundle.mjs +47 -4
- package/scripts/init-repo.mjs +26 -2
- package/scripts/inspect-artifact-signing-requests.mjs +6 -0
- package/scripts/materialize-self-release-candidate-version.mjs +137 -0
- package/scripts/publication-commit-evidence.mjs +69 -23
- package/scripts/release-candidate-resolver.mjs +16 -10
- package/scripts/release-tail.mjs +159 -0
- package/scripts/resume-from-candidate-run.mjs +123 -9
- package/scripts/seal-artifact-signing-requests.mjs +6 -0
- package/scripts/site-capability-metadata.mjs +13 -0
- package/scripts/v4-architecture.mjs +600 -0
- package/scripts/web-surface-core.mjs +8 -2
- package/scripts/workflow-call-contract.mjs +184 -5
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
|
|
7
|
+
const INVENTORY_PATH = "architecture/release-tail-contract-inventory.json";
|
|
8
|
+
const ROOT_PATTERN = /^sha256:[0-9a-f]{64}$/u;
|
|
9
|
+
const SHA_PATTERN = /^[0-9a-f]{40}$/u;
|
|
10
|
+
|
|
11
|
+
function loadJson(root, file) {
|
|
12
|
+
return JSON.parse(fs.readFileSync(path.join(root, file), "utf8"));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function read(root, file) {
|
|
16
|
+
return fs.readFileSync(path.join(root, file), "utf8");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function commandInputs(text, indent, stopAt = "") {
|
|
20
|
+
const source = stopAt ? text.split(new RegExp(`^${stopAt}`, "mu"))[0] : text;
|
|
21
|
+
const pattern = new RegExp(`^ {${indent}}([a-z0-9-]+-command):\\s*$`, "gmu");
|
|
22
|
+
return [...source.matchAll(pattern)].map((match) => match[1]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function workflowCoordinates(root, inventory) {
|
|
26
|
+
return (inventory.reverseScan.workflowInputs || []).flatMap((file) =>
|
|
27
|
+
commandInputs(read(root, file), 6, "jobs:").map(
|
|
28
|
+
(name) => `workflow:${file}#${name}`,
|
|
29
|
+
),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function actionCoordinates(root, inventory) {
|
|
34
|
+
return (inventory.reverseScan.actionInputs || []).flatMap((file) =>
|
|
35
|
+
commandInputs(read(root, file).split(/^outputs:/mu)[0], 2).map(
|
|
36
|
+
(name) => `action:${file}#${name}`,
|
|
37
|
+
),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function sortedUnique(values) {
|
|
42
|
+
return [...new Set(values)].sort();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function sameSet(left, right) {
|
|
46
|
+
return (
|
|
47
|
+
JSON.stringify(sortedUnique(left)) === JSON.stringify(sortedUnique(right))
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function validateReverseScan(root, inventory, surfaces, issues) {
|
|
52
|
+
const coordinates = [
|
|
53
|
+
...workflowCoordinates(root, inventory),
|
|
54
|
+
...actionCoordinates(root, inventory),
|
|
55
|
+
...(inventory.reverseScan.configAndCliAliases || []),
|
|
56
|
+
];
|
|
57
|
+
const owned = new Map();
|
|
58
|
+
for (const surface of surfaces) {
|
|
59
|
+
for (const coordinate of surface.coordinates || []) {
|
|
60
|
+
if (owned.has(coordinate))
|
|
61
|
+
issues.push(
|
|
62
|
+
`${coordinate}: ambiguous surface ownership (${owned.get(coordinate)}, ${surface.id})`,
|
|
63
|
+
);
|
|
64
|
+
owned.set(coordinate, surface.id);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
for (const coordinate of coordinates) {
|
|
68
|
+
if (!owned.has(coordinate))
|
|
69
|
+
issues.push(`unclassified release-tail command surface: ${coordinate}`);
|
|
70
|
+
}
|
|
71
|
+
for (const coordinate of owned.keys()) {
|
|
72
|
+
if (!coordinates.includes(coordinate))
|
|
73
|
+
issues.push(
|
|
74
|
+
`declared release-tail coordinate is not reverse-discovered: ${coordinate}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const workflowNames = coordinates
|
|
79
|
+
.filter((entry) => entry.startsWith("workflow:"))
|
|
80
|
+
.map((entry) => entry.split("#")[1]);
|
|
81
|
+
const actionNames = coordinates
|
|
82
|
+
.filter((entry) => entry.startsWith("action:"))
|
|
83
|
+
.map((entry) => entry.split("#")[1]);
|
|
84
|
+
if (!sameSet(workflowNames, inventory.reverseScan.workflowCommandNames || []))
|
|
85
|
+
issues.push("reverse scan workflow command-name inventory drifted");
|
|
86
|
+
if (!sameSet(actionNames, inventory.reverseScan.actionCommandNames || []))
|
|
87
|
+
issues.push("reverse scan Action command-name inventory drifted");
|
|
88
|
+
|
|
89
|
+
let executionSites = 0;
|
|
90
|
+
for (const surface of surfaces) {
|
|
91
|
+
for (const site of surface.executionSites || []) {
|
|
92
|
+
const file = path.join(root, site.path || "");
|
|
93
|
+
if (!site.path || !site.marker || !fs.existsSync(file)) {
|
|
94
|
+
issues.push(
|
|
95
|
+
`${surface.id}: execution site is incomplete: ${site.path || "<empty>"}`,
|
|
96
|
+
);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (!fs.readFileSync(file, "utf8").includes(site.marker))
|
|
100
|
+
issues.push(
|
|
101
|
+
`${surface.id}: execution marker is missing from ${site.path}`,
|
|
102
|
+
);
|
|
103
|
+
executionSites += 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { coordinates: coordinates.length, executionSites };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function validateTransaction(inventory, issues) {
|
|
110
|
+
const transaction = inventory.canonicalTransaction || {};
|
|
111
|
+
const phases = (transaction.phases || []).map((entry) => entry.id);
|
|
112
|
+
if (
|
|
113
|
+
transaction.id !== "buildchain.release-tail/v1" ||
|
|
114
|
+
transaction.singleWriter !== true
|
|
115
|
+
)
|
|
116
|
+
issues.push(
|
|
117
|
+
"canonical release transaction identity or single-writer invariant is invalid",
|
|
118
|
+
);
|
|
119
|
+
if (
|
|
120
|
+
!sameSet(phases, [
|
|
121
|
+
"prepare",
|
|
122
|
+
"publish",
|
|
123
|
+
"commit",
|
|
124
|
+
"activate",
|
|
125
|
+
"readback",
|
|
126
|
+
"settle",
|
|
127
|
+
])
|
|
128
|
+
)
|
|
129
|
+
issues.push("canonical release transaction must own all six frozen phases");
|
|
130
|
+
if (
|
|
131
|
+
transaction.effectSchema !== "kungfu.buildchain.release-tail.effect/v1" ||
|
|
132
|
+
transaction.observationSchema !==
|
|
133
|
+
"kungfu.buildchain.release-tail.observation/v1" ||
|
|
134
|
+
transaction.receiptSchema !== "kungfu.buildchain.release-tail.receipt/v1"
|
|
135
|
+
)
|
|
136
|
+
issues.push("canonical effect, observation, and receipt schemas drifted");
|
|
137
|
+
if ((transaction.retryClasses || []).some((entry) => entry.localAttempts > 3))
|
|
138
|
+
issues.push("canonical transaction permits unbounded local retry");
|
|
139
|
+
for (const terminal of [
|
|
140
|
+
"complete",
|
|
141
|
+
"blocked",
|
|
142
|
+
"repair-required",
|
|
143
|
+
"terminal-failure",
|
|
144
|
+
]) {
|
|
145
|
+
if (!(transaction.terminalClasses || []).includes(terminal))
|
|
146
|
+
issues.push(`canonical transaction omits terminal class ${terminal}`);
|
|
147
|
+
}
|
|
148
|
+
for (const forbidden of [
|
|
149
|
+
"choose transaction transitions",
|
|
150
|
+
"execute repository-supplied shell",
|
|
151
|
+
]) {
|
|
152
|
+
if (!(transaction.adapterBoundary?.mustNot || []).includes(forbidden))
|
|
153
|
+
issues.push(`adapter boundary omits ${forbidden}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function findForbiddenKey(value, forbidden, at = "$") {
|
|
158
|
+
if (Array.isArray(value)) {
|
|
159
|
+
for (const [index, entry] of value.entries()) {
|
|
160
|
+
const found = findForbiddenKey(entry, forbidden, `${at}[${index}]`);
|
|
161
|
+
if (found) return found;
|
|
162
|
+
}
|
|
163
|
+
return "";
|
|
164
|
+
}
|
|
165
|
+
if (!value || typeof value !== "object") return "";
|
|
166
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
167
|
+
if (forbidden.has(key.toLowerCase())) return `${at}.${key}`;
|
|
168
|
+
const found = findForbiddenKey(entry, forbidden, `${at}.${key}`);
|
|
169
|
+
if (found) return found;
|
|
170
|
+
}
|
|
171
|
+
return "";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function validateCapabilityMessages(
|
|
175
|
+
inventory,
|
|
176
|
+
capability,
|
|
177
|
+
fixturePath,
|
|
178
|
+
issues,
|
|
179
|
+
) {
|
|
180
|
+
if (
|
|
181
|
+
capability.effect?.schema !==
|
|
182
|
+
inventory.canonicalTransaction?.effectSchema ||
|
|
183
|
+
capability.observation?.schema !==
|
|
184
|
+
inventory.canonicalTransaction?.observationSchema ||
|
|
185
|
+
capability.receipt?.schema !== inventory.canonicalTransaction?.receiptSchema
|
|
186
|
+
)
|
|
187
|
+
issues.push(`${fixturePath}: ${capability.id} message schema drifted`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function validateCapabilityOperation(capability, fixturePath, issues) {
|
|
191
|
+
if (
|
|
192
|
+
!ROOT_PATTERN.test(capability.operationIdentity?.transactionRoot || "") ||
|
|
193
|
+
capability.operationIdentity?.capabilityId !== capability.id ||
|
|
194
|
+
!ROOT_PATTERN.test(capability.operationIdentity?.subjectRoot || "") ||
|
|
195
|
+
!ROOT_PATTERN.test(capability.operationIdentity?.targetRoot || "") ||
|
|
196
|
+
!String(capability.operationIdentity?.attemptKey || "").trim()
|
|
197
|
+
)
|
|
198
|
+
issues.push(
|
|
199
|
+
`${fixturePath}: ${capability.id} operation identity is incomplete or inconsistent`,
|
|
200
|
+
);
|
|
201
|
+
if (
|
|
202
|
+
!Number.isInteger(capability.retry?.localAttempts) ||
|
|
203
|
+
capability.retry.localAttempts < 0 ||
|
|
204
|
+
capability.retry.localAttempts > 3
|
|
205
|
+
)
|
|
206
|
+
issues.push(`${fixturePath}: ${capability.id} local retry is invalid`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function validateCapabilityDimensions(
|
|
210
|
+
inventory,
|
|
211
|
+
capability,
|
|
212
|
+
fixturePath,
|
|
213
|
+
issues,
|
|
214
|
+
) {
|
|
215
|
+
for (const field of inventory.declarativeContract.requiredDimensions || []) {
|
|
216
|
+
if (capability[field] === undefined)
|
|
217
|
+
issues.push(`${fixturePath}: ${capability.id} omits ${field}`);
|
|
218
|
+
}
|
|
219
|
+
if (!(capability.readbackPredicates || []).length)
|
|
220
|
+
issues.push(`${fixturePath}: ${capability.id} has no readback predicate`);
|
|
221
|
+
if (!(capability.evidenceRequirements || []).length)
|
|
222
|
+
issues.push(`${fixturePath}: ${capability.id} has no evidence requirement`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function validateCapabilityFixture(inventory, fixture, fixturePath, issues) {
|
|
226
|
+
const contract = inventory.declarativeContract || {};
|
|
227
|
+
if (
|
|
228
|
+
fixture.contract !== contract.contract ||
|
|
229
|
+
fixture.schemaVersion !== contract.schemaVersion ||
|
|
230
|
+
fixture.transactionPolicy !== inventory.canonicalTransaction?.id
|
|
231
|
+
)
|
|
232
|
+
issues.push(`${fixturePath}: declaration identity is invalid`);
|
|
233
|
+
if (!SHA_PATTERN.test(fixture.subject?.sourceSha || ""))
|
|
234
|
+
issues.push(`${fixturePath}: subject sourceSha is not exact`);
|
|
235
|
+
const forbidden = new Set(contract.forbiddenKeys || []);
|
|
236
|
+
const forbiddenPath = findForbiddenKey(fixture, forbidden);
|
|
237
|
+
if (forbiddenPath)
|
|
238
|
+
issues.push(
|
|
239
|
+
`${fixturePath}: executable key is forbidden at ${forbiddenPath}`,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const capabilities = fixture.capabilities || [];
|
|
243
|
+
const ids = capabilities.map((entry) => entry.id);
|
|
244
|
+
if (!sameSet(ids, contract.requiredCapabilityIds || []))
|
|
245
|
+
issues.push(`${fixturePath}: required capability set drifted`);
|
|
246
|
+
if (ids.length !== new Set(ids).size)
|
|
247
|
+
issues.push(`${fixturePath}: capability ids must be unique`);
|
|
248
|
+
for (const capability of capabilities) {
|
|
249
|
+
validateCapabilityDimensions(inventory, capability, fixturePath, issues);
|
|
250
|
+
validateCapabilityMessages(inventory, capability, fixturePath, issues);
|
|
251
|
+
validateCapabilityOperation(capability, fixturePath, issues);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function validateContract(root, inventory, issues) {
|
|
256
|
+
const contract = inventory.declarativeContract || {};
|
|
257
|
+
if (!fs.existsSync(path.join(root, contract.schemaPath || "")))
|
|
258
|
+
issues.push("declarative release-tail schema path is missing");
|
|
259
|
+
else {
|
|
260
|
+
const schema = loadJson(root, contract.schemaPath);
|
|
261
|
+
if (
|
|
262
|
+
schema.properties?.contract?.const !== contract.contract ||
|
|
263
|
+
schema.properties?.schemaVersion?.const !== contract.schemaVersion
|
|
264
|
+
)
|
|
265
|
+
issues.push("declarative release-tail JSON Schema identity drifted");
|
|
266
|
+
}
|
|
267
|
+
for (const fixturePath of contract.fixturePaths || []) {
|
|
268
|
+
if (!fs.existsSync(path.join(root, fixturePath))) {
|
|
269
|
+
issues.push(`declarative fixture is missing: ${fixturePath}`);
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
validateCapabilityFixture(
|
|
273
|
+
inventory,
|
|
274
|
+
loadJson(root, fixturePath),
|
|
275
|
+
fixturePath,
|
|
276
|
+
issues,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function validateSurfaceInventory(inventory, issues) {
|
|
282
|
+
const surfaces = inventory.legacyExecutableSurfaces || [];
|
|
283
|
+
const surfaceIds = new Set();
|
|
284
|
+
for (const surface of surfaces) {
|
|
285
|
+
if (!surface.id || surfaceIds.has(surface.id))
|
|
286
|
+
issues.push(
|
|
287
|
+
`release-tail surface id is missing or duplicated: ${surface.id || "<empty>"}`,
|
|
288
|
+
);
|
|
289
|
+
surfaceIds.add(surface.id);
|
|
290
|
+
for (const field of [
|
|
291
|
+
"owner",
|
|
292
|
+
"classification",
|
|
293
|
+
"default",
|
|
294
|
+
"replacement",
|
|
295
|
+
"disposition",
|
|
296
|
+
]) {
|
|
297
|
+
if (!String(surface[field] || "").trim())
|
|
298
|
+
issues.push(`${surface.id}: ${field} is empty`);
|
|
299
|
+
}
|
|
300
|
+
if (!(surface.publicNames || []).length)
|
|
301
|
+
issues.push(`${surface.id}: publicNames is empty`);
|
|
302
|
+
if (!(surface.coordinates || []).length)
|
|
303
|
+
issues.push(`${surface.id}: coordinates is empty`);
|
|
304
|
+
}
|
|
305
|
+
return { surfaces, surfaceIds };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function validateManagedCallers(inventory, surfaceIds, issues) {
|
|
309
|
+
for (const caller of inventory.managedConsumerCallers || []) {
|
|
310
|
+
if (
|
|
311
|
+
!caller.id ||
|
|
312
|
+
!caller.repository ||
|
|
313
|
+
!caller.workflow ||
|
|
314
|
+
!caller.runtimeRef
|
|
315
|
+
)
|
|
316
|
+
issues.push(
|
|
317
|
+
`managed consumer caller is incomplete: ${caller.id || "<empty>"}`,
|
|
318
|
+
);
|
|
319
|
+
if (
|
|
320
|
+
!SHA_PATTERN.test(caller.sourceCommit || "") ||
|
|
321
|
+
!SHA_PATTERN.test(caller.sourceTree || "")
|
|
322
|
+
)
|
|
323
|
+
issues.push(`${caller.id}: managed consumer cut is not exact`);
|
|
324
|
+
for (const surfaceId of caller.executableSurfaceIds || []) {
|
|
325
|
+
if (!surfaceIds.has(surfaceId))
|
|
326
|
+
issues.push(`${caller.id}: unknown executable surface ${surfaceId}`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function validateInventory(inventory, issues) {
|
|
332
|
+
if (
|
|
333
|
+
inventory.schemaVersion !== 1 ||
|
|
334
|
+
inventory.contract !== "kungfu-buildchain-release-tail-contract-inventory"
|
|
335
|
+
)
|
|
336
|
+
issues.push("release-tail inventory identity is invalid");
|
|
337
|
+
if (
|
|
338
|
+
!SHA_PATTERN.test(inventory.baseline?.commit || "") ||
|
|
339
|
+
!SHA_PATTERN.test(inventory.baseline?.tree || "")
|
|
340
|
+
)
|
|
341
|
+
issues.push("release-tail baseline must bind an exact commit and tree");
|
|
342
|
+
const { surfaces, surfaceIds } = validateSurfaceInventory(inventory, issues);
|
|
343
|
+
validateManagedCallers(inventory, surfaceIds, issues);
|
|
344
|
+
return surfaces;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function validateMigration(inventory, issues) {
|
|
348
|
+
const migration = inventory.migration || {};
|
|
349
|
+
const window = migration.compatibilityWindow || {};
|
|
350
|
+
if (
|
|
351
|
+
window.maximumDurationDays !== 90 ||
|
|
352
|
+
window.maximumMinorLines !== 2 ||
|
|
353
|
+
window.permanentEscapeHatch !== false
|
|
354
|
+
)
|
|
355
|
+
issues.push(
|
|
356
|
+
"compatibility window is not bounded by time, minor lines, and no-escape policy",
|
|
357
|
+
);
|
|
358
|
+
if ((migration.publishedReleasePreservation || []).length < 3)
|
|
359
|
+
issues.push("published-release preservation rules are incomplete");
|
|
360
|
+
const codes = new Set(
|
|
361
|
+
(migration.rejectionRules || []).map((entry) => entry.code),
|
|
362
|
+
);
|
|
363
|
+
for (const code of [
|
|
364
|
+
"release-tail-command-forbidden",
|
|
365
|
+
"release-tail-alias-collision",
|
|
366
|
+
"release-tail-operation-id-missing",
|
|
367
|
+
"release-tail-readback-missing",
|
|
368
|
+
"release-tail-retry-unbounded",
|
|
369
|
+
]) {
|
|
370
|
+
if (!codes.has(code)) issues.push(`migration rejection rules omit ${code}`);
|
|
371
|
+
}
|
|
372
|
+
const excepted = new Set(
|
|
373
|
+
(migration.exceptionLedger || []).flatMap(
|
|
374
|
+
(entry) => entry.surfaceIds || [],
|
|
375
|
+
),
|
|
376
|
+
);
|
|
377
|
+
for (const surface of inventory.legacyExecutableSurfaces || []) {
|
|
378
|
+
if (
|
|
379
|
+
surface.classification !== "adjacent-non-tail" &&
|
|
380
|
+
!excepted.has(surface.id)
|
|
381
|
+
)
|
|
382
|
+
issues.push(
|
|
383
|
+
`${surface.id}: compatibility exception has no owner and sunset test`,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
for (const exception of migration.exceptionLedger || []) {
|
|
387
|
+
if (!exception.owner || !exception.expires || !exception.removalTest)
|
|
388
|
+
issues.push(`${exception.id}: exception ledger entry is incomplete`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function checkReleaseTailContract({
|
|
393
|
+
root = process.cwd(),
|
|
394
|
+
inventory = loadJson(root, INVENTORY_PATH),
|
|
395
|
+
fixtures,
|
|
396
|
+
} = {}) {
|
|
397
|
+
const issues = [];
|
|
398
|
+
const surfaces = validateInventory(inventory, issues);
|
|
399
|
+
validateTransaction(inventory, issues);
|
|
400
|
+
if (fixtures) {
|
|
401
|
+
for (const [fixturePath, fixture] of Object.entries(fixtures))
|
|
402
|
+
validateCapabilityFixture(inventory, fixture, fixturePath, issues);
|
|
403
|
+
} else {
|
|
404
|
+
validateContract(root, inventory, issues);
|
|
405
|
+
}
|
|
406
|
+
const reverseScan = validateReverseScan(root, inventory, surfaces, issues);
|
|
407
|
+
validateMigration(inventory, issues);
|
|
408
|
+
if (issues.length)
|
|
409
|
+
throw new Error(
|
|
410
|
+
`release-tail contract check failed:\n- ${issues.join("\n- ")}`,
|
|
411
|
+
);
|
|
412
|
+
return {
|
|
413
|
+
surfaces: surfaces.length,
|
|
414
|
+
managedCallers: inventory.managedConsumerCallers.length,
|
|
415
|
+
capabilities: inventory.declarativeContract.requiredCapabilityIds.length,
|
|
416
|
+
...reverseScan,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (
|
|
421
|
+
process.argv[1] &&
|
|
422
|
+
import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href
|
|
423
|
+
) {
|
|
424
|
+
try {
|
|
425
|
+
const report = checkReleaseTailContract();
|
|
426
|
+
console.log(
|
|
427
|
+
`release-tail contract check passed: ${report.surfaces} classified surfaces, ${report.coordinates} reverse-discovered coordinates, ${report.executionSites} execution sites, ${report.capabilities} declarative capabilities, ${report.managedCallers} managed callers`,
|
|
428
|
+
);
|
|
429
|
+
} catch (error) {
|
|
430
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
431
|
+
process.exitCode = 1;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export { checkReleaseTailContract };
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { cancelQueuedDevDeliveryCandidate, closeDevDeliveryWarrant, createDevDeliveryQueue, heartbeatDevDeliveryWarrant, observeDevDeliveryQueue, recoverExpiredDevDeliveryWarrant, selectDevDeliveryWarrant, settleDevDeliveryTerminalEvent, submitDevDeliveryCandidate } from "../packages/core/dev-delivery-warrant.js";
|
|
5
|
+
import { planDevDeliveryWarrantShadow, qualifyDevDeliveryWarrantShadow } from "../packages/core/dev-delivery-warrant-shadow.js";
|
|
5
6
|
|
|
6
7
|
const STATE_PATH = "queue.json";
|
|
7
8
|
const STATE_REF_PREFIX = "buildchain/dev-delivery-warrant/";
|
|
@@ -189,6 +190,7 @@ function transitionFor(command, queue, options) {
|
|
|
189
190
|
closureRoot: exactRoot(options.closureRoot, "closureRoot"),
|
|
190
191
|
dependencyRoot: exactRoot(options.dependencyRoot, "dependencyRoot"),
|
|
191
192
|
toolchainRoot: exactRoot(options.toolchainRoot, "toolchainRoot"),
|
|
193
|
+
sourceWorkflowRunId: options.sourceWorkflowRunId ? positiveInteger(options.sourceWorkflowRunId, "sourceWorkflowRunId") : 0,
|
|
192
194
|
deliveryClass: options.deliveryClass,
|
|
193
195
|
priority: options.priority || "ordinary",
|
|
194
196
|
},
|
|
@@ -345,6 +347,23 @@ export async function runDevDeliveryCommand(optionsInput = {}, clientInput) {
|
|
|
345
347
|
};
|
|
346
348
|
}
|
|
347
349
|
|
|
350
|
+
export function runDevDeliveryShadowCommand(options = {}) {
|
|
351
|
+
if (bool(options.execute, false)) {
|
|
352
|
+
throw new Error("shadow qualification is effect-disabled and rejects --execute");
|
|
353
|
+
}
|
|
354
|
+
if (!options.inputPath) throw new Error("shadow qualification requires --input FILE");
|
|
355
|
+
const input = JSON.parse(fs.readFileSync(options.inputPath, "utf8"));
|
|
356
|
+
if (options.command === "shadow-plan") {
|
|
357
|
+
return planDevDeliveryWarrantShadow(input, {
|
|
358
|
+
maxConcurrency: positiveInteger(options.maxConcurrency, "maxConcurrency", 2),
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
if (options.command === "shadow-qualify") {
|
|
362
|
+
return qualifyDevDeliveryWarrantShadow(input);
|
|
363
|
+
}
|
|
364
|
+
throw new Error(`unsupported shadow command ${options.command || "<empty>"}`);
|
|
365
|
+
}
|
|
366
|
+
|
|
348
367
|
function flag(args, name, fallback = "") {
|
|
349
368
|
const index = args.indexOf(`--${name}`);
|
|
350
369
|
return index === -1 ? fallback : args[index + 1] || "";
|
|
@@ -386,6 +405,8 @@ export function devDeliveryCliOptions(args = [], environment = process.env) {
|
|
|
386
405
|
evidenceRoot: flag(rest, "evidence-root", environment.BUILDCHAIN_DEV_DELIVERY_EVIDENCE_ROOT),
|
|
387
406
|
reason: flag(rest, "reason", environment.BUILDCHAIN_DEV_DELIVERY_REASON),
|
|
388
407
|
now: flag(rest, "now", environment.BUILDCHAIN_DEV_DELIVERY_NOW),
|
|
408
|
+
inputPath: flag(rest, "input", environment.BUILDCHAIN_DEV_DELIVERY_SHADOW_INPUT),
|
|
409
|
+
maxConcurrency: flag(rest, "max-concurrency", environment.BUILDCHAIN_DEV_DELIVERY_SHADOW_MAX_CONCURRENCY || "2"),
|
|
389
410
|
outputPath: flag(rest, "output", environment.BUILDCHAIN_DEV_DELIVERY_OUTPUT || ".buildchain/dev-delivery/result.json"),
|
|
390
411
|
execute: hasFlag(rest, "execute"),
|
|
391
412
|
json: hasFlag(rest, "json"),
|
|
@@ -393,7 +414,7 @@ export function devDeliveryCliOptions(args = [], environment = process.env) {
|
|
|
393
414
|
}
|
|
394
415
|
|
|
395
416
|
function usage() {
|
|
396
|
-
return "Usage:\n buildchain dev warrant <submit|select|heartbeat|recover|close|settle|cancel-queued|observe> --repository owner/repo --branch dev/vN/vN.M [--execute] [--output FILE] [--json]\n";
|
|
417
|
+
return "Usage:\n buildchain dev warrant <submit|select|heartbeat|recover|close|settle|cancel-queued|observe> --repository owner/repo --branch dev/vN/vN.M [--execute] [--output FILE] [--json]\n buildchain dev warrant <shadow-plan|shadow-qualify> --input FILE [--max-concurrency 1|2] [--output FILE] [--json]\n";
|
|
397
418
|
}
|
|
398
419
|
|
|
399
420
|
async function main() {
|
|
@@ -403,16 +424,22 @@ async function main() {
|
|
|
403
424
|
return;
|
|
404
425
|
}
|
|
405
426
|
const options = devDeliveryCliOptions(args);
|
|
406
|
-
if (!["submit", "select", "heartbeat", "recover", "close", "settle", "cancel-queued", "observe"].includes(options.command)) {
|
|
427
|
+
if (!["submit", "select", "heartbeat", "recover", "close", "settle", "cancel-queued", "observe", "shadow-plan", "shadow-qualify"].includes(options.command)) {
|
|
407
428
|
throw new Error(usage().trim());
|
|
408
429
|
}
|
|
409
|
-
const
|
|
430
|
+
const shadow = options.command.startsWith("shadow-");
|
|
431
|
+
const result = shadow ? runDevDeliveryShadowCommand(options) : await runDevDeliveryCommand(options);
|
|
410
432
|
fs.mkdirSync(path.dirname(options.outputPath), { recursive: true });
|
|
411
433
|
fs.writeFileSync(options.outputPath, `${JSON.stringify(result, null, 2)}\n`);
|
|
412
434
|
if (options.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
413
435
|
else {
|
|
414
436
|
process.stdout.write(`Buildchain dev delivery ${options.command}: ${result.receipt?.reason || result.mode}\n`);
|
|
415
|
-
|
|
437
|
+
if (shadow) {
|
|
438
|
+
process.stdout.write(`Decision: ${result.decision}\n`);
|
|
439
|
+
process.stdout.write(`Evidence root: ${result.planRoot || result.qualificationRoot}\n`);
|
|
440
|
+
} else {
|
|
441
|
+
process.stdout.write(`State root: ${result.after?.stateRoot || result.observation.stateRoot}\n`);
|
|
442
|
+
}
|
|
416
443
|
if (result.receiptRoot) process.stdout.write(`Receipt root: ${result.receiptRoot}\n`);
|
|
417
444
|
process.stdout.write(`Result: ${options.outputPath}\n`);
|
|
418
445
|
}
|
|
@@ -4,7 +4,7 @@ import { spawnSync } from "node:child_process";
|
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { verifyProjectCutReplayProof } from "../packages/core/dev-delivery-warrant.js";
|
|
7
|
-
import { admitExistingQueueEntry, createDevPrAdmissionReceipt, readDeliveryWarrantResult, runSourceQualification, runTargetedQueueAdmission } from "./dev-pr-delivery-warrant.mjs";
|
|
7
|
+
import { admitExistingQueueEntry, createDevPrAdmissionReceipt, enqueueAfterStatusPropagation, readDeliveryWarrantResult, runSourceQualification, runTargetedQueueAdmission, verifyCurrentDeliveryWarrant } from "./dev-pr-delivery-warrant.mjs";
|
|
8
8
|
const DEFAULT_BLOCK_LABELS = ["blocked", "do-not-merge", "work-in-progress"];
|
|
9
9
|
const DEFAULT_ALLOWED_HEAD_PREFIXES = ["feature/", "fix/", "chore/", "docs/", "ci/", "refactor/"];
|
|
10
10
|
const DEFAULT_REQUIRED_CHECKS = ["check"];
|
|
@@ -70,6 +70,7 @@ function normalizeOptions(options = {}) {
|
|
|
70
70
|
allowedHeadPrefixes: splitList(options.allowedHeadPrefixes, DEFAULT_ALLOWED_HEAD_PREFIXES),
|
|
71
71
|
requiredChecks: splitList(options.requiredChecks, DEFAULT_REQUIRED_CHECKS),
|
|
72
72
|
queueAdmissionContext: String(options.queueAdmissionContext || "").trim(),
|
|
73
|
+
activeLeaseContext: String(options.activeLeaseContext || "").trim(),
|
|
73
74
|
requireApproval: boolOption(options.requireApproval, true),
|
|
74
75
|
sameRepositoryOnly: boolOption(options.sameRepositoryOnly, true),
|
|
75
76
|
maxMerges: intOption(options.maxMerges, 1),
|
|
@@ -218,6 +219,20 @@ async function setQueueAdmissionStatus(client, repository, sha, context, state)
|
|
|
218
219
|
return { context, state, sha };
|
|
219
220
|
}
|
|
220
221
|
|
|
222
|
+
async function setActiveLeaseStatus(client, repository, sha, context, state) {
|
|
223
|
+
if (!context) return null;
|
|
224
|
+
await client.request("POST", `/repos/${repository.owner}/${repository.repo}/statuses/${sha}`, {
|
|
225
|
+
body: {
|
|
226
|
+
state,
|
|
227
|
+
context,
|
|
228
|
+
description: state === "pending"
|
|
229
|
+
? "Buildchain reactivated this exact lease for merge-group qualification"
|
|
230
|
+
: "Buildchain released this exact lease after queue admission failed",
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
return { context, state, sha };
|
|
234
|
+
}
|
|
235
|
+
|
|
221
236
|
function skip(reason, details = {}) {
|
|
222
237
|
return { action: "skip", reason, ...details };
|
|
223
238
|
}
|
|
@@ -853,11 +868,15 @@ export async function runDevPrAdmission(optionsInput = {}, clientInput) {
|
|
|
853
868
|
let warrant = null;
|
|
854
869
|
try {
|
|
855
870
|
warrant = readDeliveryWarrantResult(options, pr);
|
|
871
|
+
await verifyCurrentDeliveryWarrant(client, options, pr, warrant);
|
|
856
872
|
} catch (error) {
|
|
857
873
|
return reject("blocked", error.code || "invalid-delivery-warrant");
|
|
858
874
|
}
|
|
859
875
|
const matchingEntry = initialQueue.entries.find((entry) =>
|
|
860
876
|
entry.pullRequestNumber === pr.number && entry.pullRequestHeadSha === options.expectedHeadSha);
|
|
877
|
+
if (matchingEntry && !options.dryRun) {
|
|
878
|
+
await setActiveLeaseStatus(client, options.repository, options.expectedHeadSha, options.activeLeaseContext, "pending");
|
|
879
|
+
}
|
|
861
880
|
const existing = await admitExistingQueueEntry({
|
|
862
881
|
options, pullRequest: pr, readiness, client, entry: matchingEntry, warrant,
|
|
863
882
|
createReceipt: createAdmissionReceipt,
|
|
@@ -893,6 +912,7 @@ async function reconcileEnqueueError({ client, options, pr, expectedHeadSha, ent
|
|
|
893
912
|
const exactEntry = queueReadback?.entries?.find((candidate) =>
|
|
894
913
|
candidate.pullRequestNumber === pr.number && candidate.pullRequestHeadSha === expectedHeadSha);
|
|
895
914
|
if (exactEntry) {
|
|
915
|
+
entry.activeLeaseStatus = await setActiveLeaseStatus(client, options.repository, expectedHeadSha, options.activeLeaseContext, "pending");
|
|
896
916
|
entry.action = "enqueued";
|
|
897
917
|
entry.reason = "already-enqueued-exact-head";
|
|
898
918
|
entry.queueEntry = exactEntry;
|
|
@@ -902,6 +922,7 @@ async function reconcileEnqueueError({ client, options, pr, expectedHeadSha, ent
|
|
|
902
922
|
return;
|
|
903
923
|
}
|
|
904
924
|
entry.queueAdmissionStatus = await setQueueAdmissionStatus(client, options.repository, expectedHeadSha, options.queueAdmissionContext, "failure");
|
|
925
|
+
entry.activeLeaseStatus = await setActiveLeaseStatus(client, options.repository, expectedHeadSha, options.activeLeaseContext, "failure");
|
|
905
926
|
entry.action = "skip";
|
|
906
927
|
entry.reason = "enqueue-rejected";
|
|
907
928
|
entry.enqueueError = {
|
|
@@ -1067,10 +1088,14 @@ export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
|
|
|
1067
1088
|
result.skipped.push(entry);
|
|
1068
1089
|
} else {
|
|
1069
1090
|
try {
|
|
1091
|
+
entry.activeLeaseStatus = await setActiveLeaseStatus(client, options.repository, expectedHeadSha, options.activeLeaseContext, "pending");
|
|
1070
1092
|
entry.queueAdmissionStatus = await setQueueAdmissionStatus(client, options.repository, expectedHeadSha, options.queueAdmissionContext, "success");
|
|
1071
|
-
const queueEntry = await
|
|
1072
|
-
|
|
1073
|
-
expectedHeadOid: expectedHeadSha,
|
|
1093
|
+
const queueEntry = await enqueueAfterStatusPropagation({
|
|
1094
|
+
enqueue: (input) => client.enqueuePullRequest(input),
|
|
1095
|
+
input: { pullRequestId: decision.pullRequestId, expectedHeadOid: expectedHeadSha },
|
|
1096
|
+
attempts: options.pollMergeableAttempts,
|
|
1097
|
+
delayMs: options.pollMergeableDelayMs,
|
|
1098
|
+
sleep: delay,
|
|
1074
1099
|
});
|
|
1075
1100
|
entry.action = "enqueued";
|
|
1076
1101
|
entry.reason = "enqueued-with-expected-head";
|
|
@@ -1155,6 +1180,7 @@ export function cliOptions(args = [], environment = process.env) {
|
|
|
1155
1180
|
allowedHeadPrefixes: cliValue(args, "allowed-head-prefixes", environment.BUILDCHAIN_DEV_PR_ALLOWED_HEAD_PREFIXES),
|
|
1156
1181
|
requiredChecks: cliValue(args, "required-checks", environment.BUILDCHAIN_DEV_PR_REQUIRED_CHECKS),
|
|
1157
1182
|
queueAdmissionContext: cliValue(args, "queue-admission-context", environment.BUILDCHAIN_DEV_PR_QUEUE_ADMISSION_CONTEXT),
|
|
1183
|
+
activeLeaseContext: cliValue(args, "active-lease-context", environment.BUILDCHAIN_DEV_PR_ACTIVE_LEASE_CONTEXT),
|
|
1158
1184
|
diagnosticContext: cliValue(args, "diagnostic-context", environment.BUILDCHAIN_DEV_PR_DIAGNOSTIC_CONTEXT),
|
|
1159
1185
|
warrantMode: cliValue(args, "warrant-mode", environment.BUILDCHAIN_DEV_PR_WARRANT_MODE),
|
|
1160
1186
|
warrantResultPath: cliValue(args, "warrant-result", environment.BUILDCHAIN_DEV_PR_WARRANT_RESULT_PATH),
|
|
@@ -116,6 +116,56 @@ export function readDeliveryWarrantResult(options, pullRequest) {
|
|
|
116
116
|
};
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
export async function readCurrentDeliveryQueueState(client, repository, targetBranch) {
|
|
120
|
+
if (typeof client.getDevDeliveryQueueState === "function") {
|
|
121
|
+
return client.getDevDeliveryQueueState(targetBranch);
|
|
122
|
+
}
|
|
123
|
+
const stateRef = `buildchain/dev-delivery-warrant/${targetBranch.replaceAll("/", "-")}`;
|
|
124
|
+
const query = new URLSearchParams({ ref: stateRef });
|
|
125
|
+
const { data } = await client.request(
|
|
126
|
+
"GET",
|
|
127
|
+
`/repos/${repository.owner}/${repository.repo}/contents/queue.json?${query}`,
|
|
128
|
+
);
|
|
129
|
+
if (data?.type !== "file" || data?.encoding !== "base64" || !data?.content) {
|
|
130
|
+
mismatch("delivery-warrant-current-readback-invalid");
|
|
131
|
+
}
|
|
132
|
+
return JSON.parse(Buffer.from(String(data.content).replace(/\s+/g, ""), "base64").toString("utf8"));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function verifyCurrentDeliveryWarrant(client, options, pullRequest, warrant) {
|
|
136
|
+
if (!warrant) return;
|
|
137
|
+
let queue;
|
|
138
|
+
try {
|
|
139
|
+
queue = await readCurrentDeliveryQueueState(client, options.repository, options.targetBranch);
|
|
140
|
+
} catch {
|
|
141
|
+
mismatch("delivery-warrant-current-readback-failed");
|
|
142
|
+
}
|
|
143
|
+
const active = queue?.activeWarrant;
|
|
144
|
+
const candidate = queue?.candidates?.find((entry) => entry.candidateId === active?.candidateId);
|
|
145
|
+
requireMatch(active?.candidateId === warrant.candidateId, "delivery-warrant-no-longer-active");
|
|
146
|
+
requireMatch(active?.fencingToken === warrant.fencingToken, "delivery-warrant-current-fencing-mismatch");
|
|
147
|
+
requireMatch(Number(active?.generation) === Number(warrant.generation), "delivery-warrant-current-generation-mismatch");
|
|
148
|
+
requireMatch(Number(active?.pullRequestNumber) === Number(pullRequest.number), "delivery-warrant-current-pr-mismatch");
|
|
149
|
+
requireMatch(String(active?.sourceHead || "").toLowerCase() === options.expectedHeadSha, "delivery-warrant-current-head-mismatch");
|
|
150
|
+
requireMatch(["selected", "proving", "waiting", "blocked"].includes(candidate?.status), "delivery-warrant-current-candidate-not-selected");
|
|
151
|
+
requireMatch(candidate?.sourceHead === active.sourceHead, "delivery-warrant-current-candidate-head-mismatch");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function enqueueAfterStatusPropagation({ enqueue, input, attempts, delayMs, sleep }) {
|
|
155
|
+
let lastError;
|
|
156
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
157
|
+
try {
|
|
158
|
+
return await enqueue(input);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
lastError = error;
|
|
161
|
+
const propagation = /required status(?:es| check).*failing|failing required status|cannot change this locked branch/i.test(String(error?.message || ""));
|
|
162
|
+
if (!propagation || attempt === attempts) throw error;
|
|
163
|
+
await sleep(delayMs);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
throw lastError;
|
|
167
|
+
}
|
|
168
|
+
|
|
119
169
|
export async function runSourceQualification({ options, pullRequest, readiness, client, evaluate, admissionState, createReceipt, root, publishDiagnostic, reject }) {
|
|
120
170
|
const decision = await evaluate(pullRequest, { ...options, landingMode: options.landingMode, dryRun: true }, client);
|
|
121
171
|
if (decision.observedHeadSha && String(decision.observedHeadSha).toLowerCase() !== options.expectedHeadSha) {
|