@kungfu-tech/buildchain 4.0.1-alpha.36 → 4.0.1-alpha.38
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/maintainability-policy.json +5 -5
- package/dist/site/buildchain-contract.json +3 -3
- package/dist/site/buildchain-site.json +4 -4
- package/dist/site/kfd-claims.json +2 -2
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/public-surface-audit.json +1 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +4 -4
- package/dist/site/workflow-registry.json +4 -4
- package/package.json +1 -1
- package/scripts/check-project-cut-merge-queue-admission.mjs +87 -0
- package/scripts/dev-pr-auto-merge.mjs +25 -11
- package/scripts/dev-pr-delivery-warrant.mjs +44 -0
- package/scripts/project-cut-merge-queue-admission.mjs +394 -0
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
"enforcementRevision": "5308189b09bcb5a1314195412f2807a40d678838",
|
|
6
6
|
"repositoryBudgets": {
|
|
7
7
|
"baselineRevision": "858dbe00b6206a8f356b4a12d2659bee7a72e71b",
|
|
8
|
-
"maxHandMaintainedSourceFiles":
|
|
9
|
-
"maxHandMaintainedSourceLines":
|
|
8
|
+
"maxHandMaintainedSourceFiles": 429,
|
|
9
|
+
"maxHandMaintainedSourceLines": 160394,
|
|
10
10
|
"maxWorkflowFiles": 76,
|
|
11
|
-
"maxWorkflowLines":
|
|
12
|
-
"latestTransition": "
|
|
13
|
-
"latestTransitionRationale": "The
|
|
11
|
+
"maxWorkflowLines": 26320,
|
|
12
|
+
"latestTransition": "Managed Project Cut admission binds exact native-family queue leases before protected Dev delivery.",
|
|
13
|
+
"latestTransitionRationale": "The bounded Project Cut adapter replays one immutable linear source range onto the exact latest protected base in a temporary index, rejects conflicts or stable patch and path drift, and emits deterministic base, head, fork, candidate tree, family lease, and terminal release roots. It grants no merge authority: the PR engine still owns exact-head status fencing, Warrant verification, independent review, merge-queue admission, protected readback, and lease release. Stable recovery continues to derive publication version state only from the validated candidate without changing sealed identity. These exact 429-file, 160394-source-line, 76-workflow-file and 26320-workflow-line ceilings account for the two bounded source adapters with no unrelated headroom.",
|
|
14
14
|
"rationale": "Bounded modules separate candidate, evidence, native proof and execution, canonical failure verification, closed-set execution transfer, process-boundary verification, Warrant state and qualification, authority state, qualification, Landing acquisition, verified-attempt admission, Landing settlement and fence validation, provider attempt and heartbeat readback, sealed terminal evidence, hosted heartbeat, resume, store, options, command-specific adapters, and CLI orchestration responsibilities. The protected delivery workflow gives candidate and seal jobs no provider write credential, recursively rejects transfer drift, scopes every transfer artifact to the exact provider run attempt, chains every durable heartbeat state and receipt root on a distinct hosted runner domain, and permits qualification or exact failure settlement only after the finalizer verifies live jobs and the latest durable authority state. Public Landing admission and heartbeat always perform non-injectable fresh exact-attempt provider readback, tolerate only a not-yet-materialized seal job while retaining exact native and duplicate-job rejection, and heartbeat-loss coordination never invokes GitHub's run-scoped cancellation API where a successor rerun could race. The hosted process boundary anchors trust in the kernel-resolved versioned Runner.Worker path while accepting an equivalent argv-zero spelling with the same basename; its public boundary marker is allowed only at the one exact non-secret value, so a forged value under the same credential-like name still fails closed. The credentialless transfer seal validates the rooted native reuse decision against the Warrant-bound source, closure, dependency, toolchain, environment and command inputs; qualifiedBase stays in the proof and decision where it is rooted and later independently rechecked, rather than being assumed on the Warrant schema. V2 mutation cannot initialize a second empty authority beside a legacy ref: migration reads and roots the canonical live v1 ref, rechecks it, applies the explicit configured v2 policy, and atomically replaces its bytes through one expected-old non-force update, so old v1 controllers fail closed while the prior commit remains immutable history. Candidate ancestry classifies the GitHub artifact runtime token as credential authority. Every Landing settlement, including an unexpired fence, requires product-owned terminal readback. Terminal cleanup reads the immutable historical attempt, derives workflow and hosted Landing job identity from live run data, reads the exact protected ref head and compares containment against that immutable SHA, and seals merged only when the admitted merge-group head is contained by that protected base; a later different-head merge settles the old attempt as dequeued. Terminal normalization preserves transfer, finalizer boundary, native and seal job, admitted provider attempt, protected-base head, containment result, and terminal readback coordinates. The public reusable workflow truthfully retains its single-flight v1 authority without advertising v2 migration, while bounded v2 remains a separate opt-in CLI, Node API, and schema contract; the tracked self-caller stays on public v4-alpha with matching stable and alpha locks, and transient train selection is dispatch-only. Next-development parity adds one versioned transition authority, one durable controller, one bounded reservation adapter, and pure public-contract recovery evidence while retaining the existing thin floating-channel callers byte-for-byte. The floating-consumer gate reuses the existing contract-world evaluator so SHA drift passes only under the selected lock's compatibility policy, breaking surface drift still fails closed, and external certification reconstructs the receipt from the observed workflow SHA rather than the lock's historical evidence SHA. Tail reseal adds no Stage Capsule effect or general rebuild authority: it verifies the exact retained four-platform bytes, admits one known macOS tail failure through live provider readback, fences signing and finalization to explicit credentials and Warrant identity, and requires standard candidate Passport plus provider and protected readback. Compatibility Facts parity isolates canonical encoding, temporal verification, lifecycle validation, fixture projection, public authority projection, and CLI adaptation into bounded modules; legacy digest and proof arrays are derived only from exact current Fact lineage. Publication Rehearsal adds no production release authority: the content-addressed capsule contract binds source, candidate, manifest, config, canonical data-only provider bindings, provider policy, observations and core version separately from filesystem, runtime and rehearsal-only provider authority; simulate, replay and explicitly authorized provider rehearsal all reuse the release-tail planner, while same-commit local dogfood resolves the new reusable contract and exact current runtime before external callers remain on floating v4-alpha. Cross-runtime candidate recovery preserves original runtime and provider roots and creates resume lineage only after exact public ref, tag, ancestry, npm version and integrity readback. Adopter Delivery restores the reviewed public v3 gate, drivers, Passport binding, N-1 bootstrap and exact archive loader through one public v4 surface while keeping provider and release authority explicit non-claims. Floating Alpha bootstrap uses only the existing exact resolver and protected Alpha private shell to move an otherwise stale floating shell, retains public v4 stable promotion, and removes itself after protected readback. CI lane change-budget enforcement adds a bounded parser, evaluator, guard, and tests without creating another merge authority."
|
|
15
15
|
},
|
|
16
16
|
"sourceBudgets": {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"product": {
|
|
5
5
|
"name": "Buildchain",
|
|
6
6
|
"package": "@kungfu-tech/buildchain",
|
|
7
|
-
"version": "4.0.1-alpha.
|
|
7
|
+
"version": "4.0.1-alpha.38",
|
|
8
8
|
"repository": "https://github.com/kungfu-systems/buildchain"
|
|
9
9
|
},
|
|
10
10
|
"majorLine": "v4",
|
|
@@ -1492,7 +1492,7 @@
|
|
|
1492
1492
|
"durable consumers call the public floating workflow and may route a train or exact runtime only through trusted non-persistent dispatch input"
|
|
1493
1493
|
],
|
|
1494
1494
|
"breakingDigest": "sha256:21b65d0746d2e8b3e8a7df4c6fab295a85c8fd42cc405ec3bc5df5cdab0421f0",
|
|
1495
|
-
"auditDigest": "sha256:
|
|
1495
|
+
"auditDigest": "sha256:6bd7c071717e16813c03c018e8bf868e54e9f98bff388b6790f6861d18a58831"
|
|
1496
1496
|
},
|
|
1497
1497
|
{
|
|
1498
1498
|
"contractVersion": 1,
|
|
@@ -5308,5 +5308,5 @@
|
|
|
5308
5308
|
"compatibilityFactCutRoot": "sha256:f61b975d4a96c921cc4bd37fb3ed9a0596b47004fe34dc7219979d697e3707f9",
|
|
5309
5309
|
"compatibilityProofRegistryRoot": "sha256:b7194b811e4d3af2df4390bc4d1731e16a86215bd64ee18cb630dd5492f4fb63",
|
|
5310
5310
|
"compatibilityDigest": "sha256:5de1c69386510b74b17eb70141b8bdd07063d62c504f91db5e84b67c6d4b64a2",
|
|
5311
|
-
"contractDigest": "sha256:
|
|
5311
|
+
"contractDigest": "sha256:20999ce26355490a526425b3c0524e6e98cda3fd2b38b30300e69ed0a434b80e"
|
|
5312
5312
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"contract": "kungfu-buildchain-site-bundle",
|
|
4
|
-
"generatedAt": "2026-08-
|
|
5
|
-
"publishedAt": "2026-08-
|
|
4
|
+
"generatedAt": "2026-08-24T12:50:00.932Z",
|
|
5
|
+
"publishedAt": "2026-08-24T12:50:00.932Z",
|
|
6
6
|
"reproducible": true,
|
|
7
7
|
"timestampPolicy": "ci-injected",
|
|
8
8
|
"deterministicInputs": [
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"declared Buildchain surface manifest contract"
|
|
20
20
|
],
|
|
21
21
|
"sourceDateEpoch": "0",
|
|
22
|
-
"sourceRevision": "
|
|
22
|
+
"sourceRevision": "396e7cb2eb240943f8ac6fa5d25e03cd5fc9051a",
|
|
23
23
|
"timestampPolicyDetails": {
|
|
24
24
|
"contract": "kungfu-buildchain-surface-timestamp-policy",
|
|
25
25
|
"timestampFields": [
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"package": {
|
|
39
39
|
"name": "@kungfu-tech/buildchain",
|
|
40
|
-
"version": "4.0.1-alpha.
|
|
40
|
+
"version": "4.0.1-alpha.38",
|
|
41
41
|
"versionSource": "package.json#version"
|
|
42
42
|
},
|
|
43
43
|
"source": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"contract": "kungfu-buildchain-public-surface-reverse-audit",
|
|
22
22
|
"path": "dist/site/public-surface-audit.json",
|
|
23
23
|
"status": "passed",
|
|
24
|
-
"sha256": "
|
|
24
|
+
"sha256": "17c50414e9933a686ee5583ea47ceffcaa696b2755775f9ea77d041e78038384",
|
|
25
25
|
"summary": {
|
|
26
26
|
"cliCommandCount": 117,
|
|
27
27
|
"workflowCount": 76,
|
|
@@ -246,7 +246,7 @@
|
|
|
246
246
|
"contract": "kungfu-buildchain-public-surface-reverse-audit",
|
|
247
247
|
"path": "dist/site/public-surface-audit.json",
|
|
248
248
|
"status": "passed",
|
|
249
|
-
"sha256": "
|
|
249
|
+
"sha256": "17c50414e9933a686ee5583ea47ceffcaa696b2755775f9ea77d041e78038384",
|
|
250
250
|
"summary": {
|
|
251
251
|
"cliCommandCount": 117,
|
|
252
252
|
"workflowCount": 76,
|
|
@@ -8846,7 +8846,7 @@
|
|
|
8846
8846
|
"workflowRegistryPath": "dist/site/workflow-registry.json",
|
|
8847
8847
|
"pageRegistryPath": "dist/site/page-registry.json",
|
|
8848
8848
|
"cliRegistryDigest": "ea47a31ce7ef3577f23e22055ab7be7715c3af494bcef63f76b426f2883086aa",
|
|
8849
|
-
"workflowRegistryDigest": "
|
|
8849
|
+
"workflowRegistryDigest": "79e6398226388dd0fddfaeceb13b72e3d7ba35b68ea396114da7fb7873981829",
|
|
8850
8850
|
"pageRegistryDigest": "619910e5c57fb8e94ea1525de1fb7699c412a8dafd7269bc9354cc6c4fe83f48"
|
|
8851
8851
|
},
|
|
8852
8852
|
"comparison": {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"contract": "kungfu-buildchain-publication-release-registry",
|
|
4
|
-
"generatedAt": "2026-08-
|
|
5
|
-
"publishedAt": "2026-08-
|
|
4
|
+
"generatedAt": "2026-08-24T12:50:00.932Z",
|
|
5
|
+
"publishedAt": "2026-08-24T12:50:00.932Z",
|
|
6
6
|
"reproducible": true,
|
|
7
7
|
"timestampPolicy": "ci-injected",
|
|
8
8
|
"deterministicInputs": [
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"declared Buildchain surface manifest contract"
|
|
20
20
|
],
|
|
21
21
|
"sourceDateEpoch": "0",
|
|
22
|
-
"sourceRevision": "
|
|
22
|
+
"sourceRevision": "396e7cb2eb240943f8ac6fa5d25e03cd5fc9051a",
|
|
23
23
|
"timestampPolicyDetails": {
|
|
24
24
|
"contract": "kungfu-buildchain-surface-timestamp-policy",
|
|
25
25
|
"timestampFields": [
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"package": {
|
|
34
34
|
"name": "@kungfu-tech/buildchain",
|
|
35
|
-
"version": "4.0.1-alpha.
|
|
35
|
+
"version": "4.0.1-alpha.38",
|
|
36
36
|
"versionSource": "package.json#version"
|
|
37
37
|
},
|
|
38
38
|
"sourceKind": "package-site-bundle",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"contract": "kungfu-buildchain-site-manifest",
|
|
4
|
-
"generatedAt": "2026-08-
|
|
5
|
-
"publishedAt": "2026-08-
|
|
4
|
+
"generatedAt": "2026-08-24T12:50:00.932Z",
|
|
5
|
+
"publishedAt": "2026-08-24T12:50:00.932Z",
|
|
6
6
|
"reproducible": true,
|
|
7
7
|
"timestampPolicy": "ci-injected",
|
|
8
8
|
"deterministicInputs": [
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"declared Buildchain surface manifest contract"
|
|
20
20
|
],
|
|
21
21
|
"sourceDateEpoch": "0",
|
|
22
|
-
"sourceRevision": "
|
|
22
|
+
"sourceRevision": "396e7cb2eb240943f8ac6fa5d25e03cd5fc9051a",
|
|
23
23
|
"timestampPolicyDetails": {
|
|
24
24
|
"contract": "kungfu-buildchain-surface-timestamp-policy",
|
|
25
25
|
"timestampFields": [
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"package": {
|
|
39
39
|
"name": "@kungfu-tech/buildchain",
|
|
40
|
-
"version": "4.0.1-alpha.
|
|
40
|
+
"version": "4.0.1-alpha.38",
|
|
41
41
|
"versionSource": "package.json#version"
|
|
42
42
|
},
|
|
43
43
|
"entrypoint": "buildchain-site.json",
|
|
@@ -2027,7 +2027,7 @@
|
|
|
2027
2027
|
"status": "preview",
|
|
2028
2028
|
"owner": "buildchain-workflows",
|
|
2029
2029
|
"maturity": "preview",
|
|
2030
|
-
"introducedVersion": "4.0.1-alpha.
|
|
2030
|
+
"introducedVersion": "4.0.1-alpha.38",
|
|
2031
2031
|
"compatibilityPromise": "preserved-through-the-v3-major-line",
|
|
2032
2032
|
"deprecationReplacement": "",
|
|
2033
2033
|
"sunsetCondition": "explicit-breaking-change-review-in-a-future-major-line",
|
|
@@ -2048,7 +2048,7 @@
|
|
|
2048
2048
|
"status": "preview",
|
|
2049
2049
|
"owner": "buildchain-workflows",
|
|
2050
2050
|
"maturity": "preview",
|
|
2051
|
-
"introducedVersion": "4.0.1-alpha.
|
|
2051
|
+
"introducedVersion": "4.0.1-alpha.38",
|
|
2052
2052
|
"compatibilityPromise": "preserved-through-the-v3-major-line",
|
|
2053
2053
|
"deprecationReplacement": "",
|
|
2054
2054
|
"sunsetCondition": "explicit-breaking-change-review-in-a-future-major-line",
|
|
@@ -2069,7 +2069,7 @@
|
|
|
2069
2069
|
"status": "preview",
|
|
2070
2070
|
"owner": "buildchain-workflows",
|
|
2071
2071
|
"maturity": "preview",
|
|
2072
|
-
"introducedVersion": "4.0.1-alpha.
|
|
2072
|
+
"introducedVersion": "4.0.1-alpha.38",
|
|
2073
2073
|
"compatibilityPromise": "preserved-through-the-v3-major-line",
|
|
2074
2074
|
"deprecationReplacement": "",
|
|
2075
2075
|
"sunsetCondition": "explicit-breaking-change-review-in-a-future-major-line",
|
|
@@ -2119,7 +2119,7 @@
|
|
|
2119
2119
|
"status": "preview",
|
|
2120
2120
|
"owner": "buildchain-workflows",
|
|
2121
2121
|
"maturity": "preview",
|
|
2122
|
-
"introducedVersion": "4.0.1-alpha.
|
|
2122
|
+
"introducedVersion": "4.0.1-alpha.38",
|
|
2123
2123
|
"compatibilityPromise": "preserved-through-the-v3-major-line",
|
|
2124
2124
|
"deprecationReplacement": "",
|
|
2125
2125
|
"sunsetCondition": "explicit-breaking-change-review-in-a-future-major-line",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kungfu-tech/buildchain",
|
|
3
|
-
"version": "4.0.1-alpha.
|
|
3
|
+
"version": "4.0.1-alpha.38",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Buildchain Release Passport, release governance, CLI toolkit, and site facts.",
|
|
6
6
|
"repository": "https://github.com/kungfu-systems/buildchain",
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import {
|
|
4
|
+
ProjectCutAdmissionError,
|
|
5
|
+
qualifyProjectCut,
|
|
6
|
+
releaseFamilyQueueLease,
|
|
7
|
+
} from "./project-cut-merge-queue-admission.mjs";
|
|
8
|
+
|
|
9
|
+
function parseArguments(argv) {
|
|
10
|
+
const options = {};
|
|
11
|
+
const names = new Map([
|
|
12
|
+
["--base", "base"],
|
|
13
|
+
["--head", "head"],
|
|
14
|
+
["--initiative-id", "initiativeId"],
|
|
15
|
+
["--assignment-id", "assignmentId"],
|
|
16
|
+
["--delivery-class", "deliveryClass"],
|
|
17
|
+
["--queue-attempt", "queueAttempt"],
|
|
18
|
+
["--admission-proof-root", "admissionProofRoot"],
|
|
19
|
+
["--release-family-marker", "releaseFamilyMarker"],
|
|
20
|
+
["--expected-pr-head", "expectedPullRequestHead"],
|
|
21
|
+
["--terminal-reason", "terminalReason"],
|
|
22
|
+
["--evidence-root", "evidenceRoot"],
|
|
23
|
+
]);
|
|
24
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
25
|
+
const name = argv[index];
|
|
26
|
+
const key = names.get(name);
|
|
27
|
+
if (!key) throw new Error(`unknown argument: ${name}`);
|
|
28
|
+
const value = argv[index + 1];
|
|
29
|
+
if (value === undefined || value.startsWith("--"))
|
|
30
|
+
throw new Error(`${name} requires a value`);
|
|
31
|
+
if (Object.hasOwn(options, key))
|
|
32
|
+
throw new Error(`${name} was provided more than once`);
|
|
33
|
+
options[key] = value;
|
|
34
|
+
index += 1;
|
|
35
|
+
}
|
|
36
|
+
return options;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function markerText(path) {
|
|
40
|
+
return path === "-"
|
|
41
|
+
? fs.readFileSync(0, "utf8")
|
|
42
|
+
: fs.readFileSync(path, "utf8");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function repairReceipt(error) {
|
|
46
|
+
return {
|
|
47
|
+
schema: "project.cut.merge-queue-admission/v1",
|
|
48
|
+
ok: false,
|
|
49
|
+
decision: "repair-required",
|
|
50
|
+
retryable: false,
|
|
51
|
+
compositionChanged: false,
|
|
52
|
+
reasonCodes: [error.reasonCode],
|
|
53
|
+
error: error.message,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function main(argv = process.argv.slice(2)) {
|
|
58
|
+
try {
|
|
59
|
+
const options = parseArguments(argv);
|
|
60
|
+
if (options.releaseFamilyMarker) {
|
|
61
|
+
console.log(
|
|
62
|
+
JSON.stringify(
|
|
63
|
+
releaseFamilyQueueLease(
|
|
64
|
+
markerText(options.releaseFamilyMarker),
|
|
65
|
+
options,
|
|
66
|
+
),
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
if (!options.base || !options.head)
|
|
72
|
+
throw new Error("--base and --head are required");
|
|
73
|
+
console.log(
|
|
74
|
+
JSON.stringify(qualifyProjectCut({ cwd: process.cwd(), ...options })),
|
|
75
|
+
);
|
|
76
|
+
return 0;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (error instanceof ProjectCutAdmissionError) {
|
|
79
|
+
console.log(JSON.stringify(repairReceipt(error)));
|
|
80
|
+
return 1;
|
|
81
|
+
}
|
|
82
|
+
console.error(`project-cut admission error: ${error.message}`);
|
|
83
|
+
return 2;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (import.meta.url === `file://${process.argv[1]}`) process.exitCode = main();
|
|
@@ -3,7 +3,7 @@ import crypto from "node:crypto";
|
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import { admitExistingQueueEntry, createDevPrAdmissionReceipt, qualifyAtomicQueueAdmission, readDeliveryWarrantResult, runSourceQualification, runTargetedQueueAdmission, setActiveLeaseStatus, verifyCurrentDeliveryWarrant } from "./dev-pr-delivery-warrant.mjs";
|
|
6
|
+
import { admitExistingQueueEntry, createDevPrAdmissionReceipt, createPreReadinessQueueFence, qualifyAtomicQueueAdmission, readDeliveryWarrantResult, runSourceQualification, runTargetedQueueAdmission, setActiveLeaseStatus, setQueueAdmissionStatus, verifyCurrentDeliveryWarrant } from "./dev-pr-delivery-warrant.mjs";
|
|
7
7
|
import { projectCutQualification } from "./dev-pr-prequeue-guard.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/"];
|
|
@@ -197,12 +197,6 @@ function mergeableAccepted(pr, landingMode = "direct", projectCutQualified = fal
|
|
|
197
197
|
function rejectBaseMoveBeforeAtomicReplay(options, initialBaseSha, observedBaseSha) {
|
|
198
198
|
return observedBaseSha !== initialBaseSha && options.warrantMode !== "required";
|
|
199
199
|
}
|
|
200
|
-
async function setQueueAdmissionStatus(client, repository, sha, context, state) {
|
|
201
|
-
if (!context) return null;
|
|
202
|
-
await client.request("POST", `/repos/${repository.owner}/${repository.repo}/statuses/${sha}`, { body: { state, context, description: state === "success" ? "Buildchain admitted this exact PR head to the merge queue" : "Buildchain rejected merge queue admission for this exact PR head" } });
|
|
203
|
-
return { context, state, sha };
|
|
204
|
-
}
|
|
205
|
-
|
|
206
200
|
function skip(reason, details = {}) {
|
|
207
201
|
return { action: "skip", reason, ...details };
|
|
208
202
|
}
|
|
@@ -790,8 +784,10 @@ export async function runDevPrAdmission(optionsInput = {}, clientInput) {
|
|
|
790
784
|
return targetedFailure({ options, pr: {}, state: "missing", reason: "pull-request-not-found" });
|
|
791
785
|
}
|
|
792
786
|
|
|
787
|
+
const readinessFence = createPreReadinessQueueFence({ client, options, sleep: delay });
|
|
793
788
|
const reject = async (state, reason, readiness = { observed: hasReadyLabel(pr, options.readyLabel), established: false }) => {
|
|
794
789
|
const result = targetedFailure({ options, pr, state, reason, readiness });
|
|
790
|
+
await readinessFence.finish(result);
|
|
795
791
|
if (!options.dryRun) result.diagnostic = await publishAdmissionDiagnostic(client, options, result.receipt, result.receiptRoot);
|
|
796
792
|
return result;
|
|
797
793
|
};
|
|
@@ -803,6 +799,12 @@ export async function runDevPrAdmission(optionsInput = {}, clientInput) {
|
|
|
803
799
|
if (pr.draft) return reject("blocked", "draft");
|
|
804
800
|
if (!headPrefixAllowed(pr, options.allowedHeadPrefixes)) return reject("blocked", "head-prefix-not-allowed");
|
|
805
801
|
|
|
802
|
+
try {
|
|
803
|
+
await readinessFence.establish();
|
|
804
|
+
} catch (error) {
|
|
805
|
+
return reject("blocked", error.code || "pre-readiness-queue-fence-failed");
|
|
806
|
+
}
|
|
807
|
+
|
|
806
808
|
let readiness = { observed: hasReadyLabel(pr, options.readyLabel), established: false };
|
|
807
809
|
if (!readiness.observed) {
|
|
808
810
|
if (options.dryRun) return reject("rejected", "missing-ready-label", readiness);
|
|
@@ -825,7 +827,7 @@ export async function runDevPrAdmission(optionsInput = {}, clientInput) {
|
|
|
825
827
|
}
|
|
826
828
|
|
|
827
829
|
if (options.qualificationOnly) {
|
|
828
|
-
|
|
830
|
+
const result = await runSourceQualification({
|
|
829
831
|
options, pullRequest: pr, readiness, client, reject,
|
|
830
832
|
evaluate: evaluatePullRequest,
|
|
831
833
|
admissionState: admissionStateFor,
|
|
@@ -833,6 +835,7 @@ export async function runDevPrAdmission(optionsInput = {}, clientInput) {
|
|
|
833
835
|
root: contentRoot,
|
|
834
836
|
publishDiagnostic: publishAdmissionDiagnostic,
|
|
835
837
|
});
|
|
838
|
+
return readinessFence.finish(result);
|
|
836
839
|
}
|
|
837
840
|
|
|
838
841
|
const initialQueue = await client.getMergeQueueState(options.targetBranch);
|
|
@@ -844,6 +847,7 @@ export async function runDevPrAdmission(optionsInput = {}, clientInput) {
|
|
|
844
847
|
}
|
|
845
848
|
const matchingEntry = initialQueue.entries.find((entry) =>
|
|
846
849
|
entry.pullRequestNumber === pr.number && entry.pullRequestHeadSha === options.expectedHeadSha);
|
|
850
|
+
await readinessFence.qualifyExisting(matchingEntry);
|
|
847
851
|
const existing = await admitExistingQueueEntry({
|
|
848
852
|
options, pullRequest: pr, readiness, client, entry: matchingEntry, warrant,
|
|
849
853
|
createReceipt: createAdmissionReceipt,
|
|
@@ -852,7 +856,7 @@ export async function runDevPrAdmission(optionsInput = {}, clientInput) {
|
|
|
852
856
|
});
|
|
853
857
|
if (existing) return existing;
|
|
854
858
|
|
|
855
|
-
|
|
859
|
+
const result = await runTargetedQueueAdmission({
|
|
856
860
|
options, pullRequest: pr, readiness, client, warrant,
|
|
857
861
|
runController: runDevPrAutoMerge,
|
|
858
862
|
admissionState: admissionStateFor,
|
|
@@ -860,6 +864,7 @@ export async function runDevPrAdmission(optionsInput = {}, clientInput) {
|
|
|
860
864
|
root: contentRoot,
|
|
861
865
|
publishDiagnostic: publishAdmissionDiagnostic,
|
|
862
866
|
});
|
|
867
|
+
return readinessFence.finish(result);
|
|
863
868
|
}
|
|
864
869
|
|
|
865
870
|
function finalizePatrolResult(result) {
|
|
@@ -1132,6 +1137,11 @@ function cliFlag(args, name) {
|
|
|
1132
1137
|
|
|
1133
1138
|
export function cliOptions(args = [], environment = process.env) {
|
|
1134
1139
|
const targetPullRequestNumber = cliValue(args, "pull-request", environment.BUILDCHAIN_DEV_PR_EXPECTED_PR_NUMBER);
|
|
1140
|
+
const warrantMode = cliValue(
|
|
1141
|
+
args,
|
|
1142
|
+
"warrant-mode",
|
|
1143
|
+
environment.BUILDCHAIN_DEV_PR_WARRANT_MODE || environment.WARRANT_MODE,
|
|
1144
|
+
);
|
|
1135
1145
|
return {
|
|
1136
1146
|
repository: cliValue(args, "repository", environment.BUILDCHAIN_DEV_PR_REPOSITORY || environment.GITHUB_REPOSITORY),
|
|
1137
1147
|
targetBranch: cliValue(args, "branch", environment.BUILDCHAIN_DEV_PR_TARGET_BRANCH || environment.GITHUB_REF_NAME),
|
|
@@ -1141,10 +1151,14 @@ export function cliOptions(args = [], environment = process.env) {
|
|
|
1141
1151
|
blockLabels: cliValue(args, "block-labels", environment.BUILDCHAIN_DEV_PR_BLOCK_LABELS),
|
|
1142
1152
|
allowedHeadPrefixes: cliValue(args, "allowed-head-prefixes", environment.BUILDCHAIN_DEV_PR_ALLOWED_HEAD_PREFIXES),
|
|
1143
1153
|
requiredChecks: cliValue(args, "required-checks", environment.BUILDCHAIN_DEV_PR_REQUIRED_CHECKS),
|
|
1144
|
-
queueAdmissionContext: cliValue(
|
|
1154
|
+
queueAdmissionContext: cliValue(
|
|
1155
|
+
args,
|
|
1156
|
+
"queue-admission-context",
|
|
1157
|
+
environment.BUILDCHAIN_DEV_PR_QUEUE_ADMISSION_CONTEXT || (warrantMode === "required" ? "Queue admission lease" : ""),
|
|
1158
|
+
),
|
|
1145
1159
|
activeLeaseContext: cliValue(args, "active-lease-context", environment.BUILDCHAIN_DEV_PR_ACTIVE_LEASE_CONTEXT),
|
|
1146
1160
|
diagnosticContext: cliValue(args, "diagnostic-context", environment.BUILDCHAIN_DEV_PR_DIAGNOSTIC_CONTEXT),
|
|
1147
|
-
warrantMode
|
|
1161
|
+
warrantMode,
|
|
1148
1162
|
warrantResultPath: cliValue(args, "warrant-result", environment.BUILDCHAIN_DEV_PR_WARRANT_RESULT_PATH),
|
|
1149
1163
|
projectCutProofPath: cliValue(args, "project-cut-proof", environment.BUILDCHAIN_DEV_PR_PROJECT_CUT_PROOF_PATH),
|
|
1150
1164
|
sourceProofPath: cliValue(args, "source-proof", environment.BUILDCHAIN_DEV_PR_SOURCE_PROOF_PATH),
|
|
@@ -206,6 +206,50 @@ export async function setActiveLeaseStatus(client, repository, sha, context, sta
|
|
|
206
206
|
return { context, state, sha };
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
export async function setQueueAdmissionStatus(client, repository, sha, context, state) {
|
|
210
|
+
if (!context) return null;
|
|
211
|
+
const descriptions = {
|
|
212
|
+
pending: "Buildchain is awaiting a qualified Warrant for this exact PR head",
|
|
213
|
+
success: "Buildchain admitted this exact PR head to the merge queue",
|
|
214
|
+
failure: "Buildchain rejected merge queue admission for this exact PR head",
|
|
215
|
+
};
|
|
216
|
+
await client.request("POST", `/repos/${repository.owner}/${repository.repo}/statuses/${sha}`, {
|
|
217
|
+
body: { state, context, description: descriptions[state] || descriptions.failure },
|
|
218
|
+
});
|
|
219
|
+
return { context, state, sha };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function createPreReadinessQueueFence({ client, options, sleep }) {
|
|
223
|
+
let status = null;
|
|
224
|
+
const write = async (state) => {
|
|
225
|
+
status = await setQueueAdmissionStatus(client, options.repository, options.expectedHeadSha, options.queueAdmissionContext, state);
|
|
226
|
+
return status;
|
|
227
|
+
};
|
|
228
|
+
return {
|
|
229
|
+
async establish() {
|
|
230
|
+
if (options.dryRun || options.warrantMode !== "required") return null;
|
|
231
|
+
if (!options.queueAdmissionContext) mismatch("queue-admission-context-required-before-readiness");
|
|
232
|
+
await write("pending");
|
|
233
|
+
for (let attempt = 1; attempt <= Math.max(1, options.pollMergeableAttempts); attempt += 1) {
|
|
234
|
+
const checks = await client.listCommitChecks(options.expectedHeadSha);
|
|
235
|
+
const readback = (checks?.statuses || []).find((entry) => entry.context === options.queueAdmissionContext);
|
|
236
|
+
if (readback?.state === "pending") return { ...status, readbackAttempts: attempt };
|
|
237
|
+
if (attempt < options.pollMergeableAttempts) await sleep(options.pollMergeableDelayMs);
|
|
238
|
+
}
|
|
239
|
+
await write("failure");
|
|
240
|
+
mismatch("pre-readiness-queue-fence-readback-mismatch");
|
|
241
|
+
},
|
|
242
|
+
async qualifyExisting(entry) {
|
|
243
|
+
if (entry && status?.state === "pending") await write("success");
|
|
244
|
+
},
|
|
245
|
+
async finish(result) {
|
|
246
|
+
const controllerFailed = result.controller?.evaluated?.some((entry) => entry.queueAdmissionStatus?.state === "failure");
|
|
247
|
+
if (!result.ok && status?.state === "pending" && !controllerFailed) await write("failure");
|
|
248
|
+
return result;
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
209
253
|
async function readBackAdmissionStatuses(client, options, expectedHeadSha, sleep) {
|
|
210
254
|
let last = { queueAdmission: null, activeLease: null };
|
|
211
255
|
const attempts = Math.max(1, options.pollMergeableAttempts);
|
|
@@ -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
|
+
git(cwd, ["apply", "--cached", "--3way", "--whitespace=nowarn", "-"], {
|
|
196
|
+
env: indexEnvironment,
|
|
197
|
+
input: sourcePatch,
|
|
198
|
+
encoding: "utf8",
|
|
199
|
+
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
|
+
if (
|
|
230
|
+
patchId(cwd, sourcePatch) !== patchId(cwd, replayPatch) ||
|
|
231
|
+
sourceNames !== replayNames
|
|
232
|
+
) {
|
|
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
|
+
}
|