@kungfu-tech/buildchain 4.0.0 → 4.0.1-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +13 -5
- package/actions/promote-buildchain-ref/README.md +11 -6
- package/architecture/internal-capabilities.json +11 -2
- package/architecture/maintainability-policy.json +149 -17
- package/architecture/v4-floating-consumer-policy.json +75 -0
- package/architecture/v4-floating-consumer-policy.md +32 -0
- package/architecture/v4-runtime-ref-resume-authority.json +64 -0
- package/architecture/v4-stage-capsule-qualification.json +2 -2
- package/bin/internal/trust-release-release-handlers.mjs +5 -0
- package/contracts/fixtures/v4-floating-consumer-policy-v1/cases.json +53 -0
- package/contracts/fixtures/v4-runtime-ref-resume-authority-v1/scenario.json +15 -0
- package/contracts/v4-floating-consumer-policy-receipt-v1.schema.json +105 -0
- package/contracts/v4-runtime-ref-resume-authority-v1.schema.json +235 -0
- package/dist/site/buildchain-contract.json +101 -31
- package/dist/site/buildchain-site.json +73 -27
- package/dist/site/capability-registry.json +3 -3
- package/dist/site/controller-registry.json +62 -6
- package/dist/site/kfd-claims.json +68 -13
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +6 -6
- package/dist/site/node-api-registry.json +799 -127
- package/dist/site/page-registry.json +63 -17
- package/dist/site/public-surface-audit.json +49 -17
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +2 -0
- package/dist/site/site-manifest.json +10 -10
- package/dist/site/workflow-registry.json +46 -18
- package/docs/MAP.md +2 -0
- package/docs/dev-delivery-warrant.md +19 -1
- package/docs/lifecycle-protocol.md +41 -0
- package/docs/node-api-reference.md +207 -134
- package/docs/reusable-build-surface.md +41 -0
- package/docs/runtime-train-validation.md +10 -0
- package/docs/v4-runtime-ref-resume-authority.md +69 -0
- package/docs/versioning.md +1 -0
- package/package.json +6 -4
- package/packages/core/artifact-signing.js +61 -0
- package/packages/core/buildchain-config.js +66 -1
- package/packages/core/dev-delivery-proof.js +37 -3
- package/packages/core/dev-delivery-warrant.js +3 -3
- package/packages/core/index.js +2 -0
- package/packages/core/publication-authority.js +5 -5
- package/packages/core/release-candidate.js +79 -19
- package/packages/core/release-passport.js +239 -33
- package/packages/core/v4-floating-consumer-evidence.js +324 -0
- package/packages/core/v4-floating-consumer-policy.js +446 -0
- package/packages/core/v4-floating-consumer-release-passport.js +126 -0
- package/packages/core/v4-runtime-ref-resume-authority.js +625 -0
- package/packages/core/v4-runtime-selector-persistence.js +228 -0
- package/packages/core/workflow-yaml-contract.js +48 -0
- package/scripts/audit-publication-control-plane.mjs +31 -31
- package/scripts/check-inventory.mjs +1 -1
- package/scripts/check-v4-floating-consumer-policy-contract.mjs +198 -0
- package/scripts/check-v4-public-dogfood-contract.mjs +6 -11
- package/scripts/dev-delivery-source-proof-reuse.mjs +631 -0
- package/scripts/dev-pr-auto-merge.mjs +37 -48
- package/scripts/dev-pr-delivery-warrant.mjs +205 -2
- package/scripts/dev-pr-prequeue-guard.mjs +399 -0
- package/scripts/ensure-github-release.mjs +3 -3
- package/scripts/generate-channel-build-workflow.mjs +3 -0
- package/scripts/generate-channel-promotion-workflow.mjs +112 -12
- package/scripts/generate-release-candidate-passport.mjs +41 -33
- package/scripts/init-repo.mjs +5 -1
- package/scripts/inspect-artifact-signing-requests.mjs +6 -0
- package/scripts/npm-publish-transaction.mjs +101 -89
- package/scripts/resume-from-candidate-run.mjs +11 -14
- package/scripts/seal-artifact-signing-requests.mjs +6 -0
- package/scripts/site-capability-metadata.mjs +2 -0
- package/scripts/v4-consumer-policy.mjs +231 -0
|
@@ -3,8 +3,8 @@ 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 {
|
|
7
|
-
import {
|
|
6
|
+
import { admitExistingQueueEntry, createDevPrAdmissionReceipt, qualifyAtomicQueueAdmission, readDeliveryWarrantResult, runSourceQualification, runTargetedQueueAdmission, setActiveLeaseStatus, verifyCurrentDeliveryWarrant } from "./dev-pr-delivery-warrant.mjs";
|
|
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/"];
|
|
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),
|
|
@@ -85,8 +86,10 @@ function normalizeOptions(options = {}) {
|
|
|
85
86
|
warrantMode: choiceOption(options.warrantMode, VALID_WARRANT_MODES, "off", "delivery Warrant mode"),
|
|
86
87
|
warrantResultPath: String(options.warrantResultPath || "").trim(),
|
|
87
88
|
projectCutProofPath: String(options.projectCutProofPath || "").trim(),
|
|
89
|
+
sourceProofPath: String(options.sourceProofPath || ".buildchain/dev-delivery/source-proof.json").trim(),
|
|
88
90
|
sourcePatchRoot: String(options.sourcePatchRoot || "").trim().toLowerCase(),
|
|
89
91
|
qualificationOnly: boolOption(options.qualificationOnly, false),
|
|
92
|
+
verifiedDeliveryWarrant: options.verifiedDeliveryWarrant || null,
|
|
90
93
|
};
|
|
91
94
|
}
|
|
92
95
|
|
|
@@ -191,26 +194,8 @@ function mergeableAccepted(pr, landingMode = "direct", projectCutQualified = fal
|
|
|
191
194
|
return state ? ["clean", "has_hooks", "unstable", "unknown", ...(landingMode === "queue" && pr.mergeable === true ? ["blocked", ...(projectCutQualified ? ["behind"] : [])] : [])].includes(state) : pr.mergeable === true;
|
|
192
195
|
}
|
|
193
196
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
let proof;
|
|
197
|
-
try {
|
|
198
|
-
proof = JSON.parse(fs.readFileSync(options.projectCutProofPath, "utf8"));
|
|
199
|
-
} catch {
|
|
200
|
-
return { ok: false, reason: "project-cut-proof-invalid" };
|
|
201
|
-
}
|
|
202
|
-
const currentBase = await client.getBranchSha(options.targetBranch).catch(() => "");
|
|
203
|
-
const verification = verifyProjectCutReplayProof(proof, {
|
|
204
|
-
repository: options.repository.fullName,
|
|
205
|
-
protectedBase: options.targetBranch,
|
|
206
|
-
pullRequestNumber: Number(pr.number),
|
|
207
|
-
sourceHead: String(pr.head?.sha || "").toLowerCase(),
|
|
208
|
-
...(options.sourcePatchRoot ? { sourcePatchRoot: options.sourcePatchRoot } : {}),
|
|
209
|
-
currentBase,
|
|
210
|
-
});
|
|
211
|
-
return verification.ok
|
|
212
|
-
? { ok: true, reason: verification.reason, proofRoot: verification.proofRoot, currentBase }
|
|
213
|
-
: { ok: false, reason: `project-cut-${verification.reason}`, currentBase };
|
|
197
|
+
function rejectBaseMoveBeforeAtomicReplay(options, initialBaseSha, observedBaseSha) {
|
|
198
|
+
return observedBaseSha !== initialBaseSha && options.warrantMode !== "required";
|
|
214
199
|
}
|
|
215
200
|
async function setQueueAdmissionStatus(client, repository, sha, context, state) {
|
|
216
201
|
if (!context) return null;
|
|
@@ -253,6 +238,7 @@ export async function evaluatePullRequest(pr, options, client) {
|
|
|
253
238
|
? await projectCutQualification(detailed, options, client)
|
|
254
239
|
: null;
|
|
255
240
|
if (projectCut && !projectCut.ok) return skip(projectCut.reason, { projectCut });
|
|
241
|
+
if (options.landingMode === "queue" && detailed.mergeable === false) return skip("pre-enqueue-merge-conflict", { mergeable: false, mergeableState });
|
|
256
242
|
if (!mergeableAccepted(detailed, options.landingMode, projectCut?.ok === true)) {
|
|
257
243
|
return skip("not-mergeable", {
|
|
258
244
|
mergeable: detailed.mergeable,
|
|
@@ -493,11 +479,9 @@ export class GitHubClient {
|
|
|
493
479
|
);
|
|
494
480
|
return data;
|
|
495
481
|
}
|
|
496
|
-
|
|
497
482
|
async listIssueComments(number) {
|
|
498
483
|
return this.paginate(`/repos/${this.repository.owner}/${this.repository.repo}/issues/${number}/comments?per_page=100`);
|
|
499
484
|
}
|
|
500
|
-
|
|
501
485
|
async createIssueComment(number, body) {
|
|
502
486
|
const { data } = await this.request(
|
|
503
487
|
"POST",
|
|
@@ -506,7 +490,6 @@ export class GitHubClient {
|
|
|
506
490
|
);
|
|
507
491
|
return data;
|
|
508
492
|
}
|
|
509
|
-
|
|
510
493
|
async updateIssueComment(commentId, body) {
|
|
511
494
|
const { data } = await this.request(
|
|
512
495
|
"PATCH",
|
|
@@ -515,7 +498,6 @@ export class GitHubClient {
|
|
|
515
498
|
);
|
|
516
499
|
return data;
|
|
517
500
|
}
|
|
518
|
-
|
|
519
501
|
async setCommitStatus(sha, { state, context, description, targetUrl = "" }) {
|
|
520
502
|
const { data } = await this.request(
|
|
521
503
|
"POST",
|
|
@@ -525,12 +507,20 @@ export class GitHubClient {
|
|
|
525
507
|
return data;
|
|
526
508
|
}
|
|
527
509
|
}
|
|
528
|
-
|
|
510
|
+
function ghEnvironment() {
|
|
511
|
+
const environment = { ...process.env };
|
|
512
|
+
delete environment.GITHUB_TOKEN;
|
|
513
|
+
delete environment.GH_TOKEN;
|
|
514
|
+
return environment;
|
|
515
|
+
}
|
|
516
|
+
function ghAuthToken() {
|
|
517
|
+
const result = spawnSync("gh", ["auth", "token"], { encoding: "utf8", env: ghEnvironment() });
|
|
518
|
+
if (result.error) throw result.error;
|
|
519
|
+
if (result.status !== 0 || !result.stdout.trim()) throw new Error((result.stderr || "gh auth token failed").trim());
|
|
520
|
+
return result.stdout.trim();
|
|
521
|
+
}
|
|
529
522
|
function ghJson(args, { input } = {}) {
|
|
530
|
-
const
|
|
531
|
-
delete ghEnvironment.GITHUB_TOKEN;
|
|
532
|
-
delete ghEnvironment.GH_TOKEN;
|
|
533
|
-
const result = spawnSync("gh", args, { encoding: "utf8", input, env: ghEnvironment });
|
|
523
|
+
const result = spawnSync("gh", args, { encoding: "utf8", input, env: ghEnvironment() });
|
|
534
524
|
if (result.error) throw result.error;
|
|
535
525
|
if (result.status !== 0) {
|
|
536
526
|
const error = new Error((result.stderr || result.stdout || "gh command failed").trim());
|
|
@@ -539,12 +529,10 @@ function ghJson(args, { input } = {}) {
|
|
|
539
529
|
}
|
|
540
530
|
return result.stdout.trim() ? JSON.parse(result.stdout) : null;
|
|
541
531
|
}
|
|
542
|
-
|
|
543
532
|
export class GhCliClient extends GitHubClient {
|
|
544
|
-
constructor({ repository } = {}) {
|
|
545
|
-
super({ token:
|
|
533
|
+
constructor({ repository, token = "", fetchImpl = globalThis.fetch } = {}) {
|
|
534
|
+
super({ token: token || ghAuthToken(), repository, fetchImpl });
|
|
546
535
|
}
|
|
547
|
-
|
|
548
536
|
async request(method, requestPath, { body } = {}) {
|
|
549
537
|
const endpoint = requestPath.replace(/^https:\/\/api\.github\.com/, "");
|
|
550
538
|
const args = ["api", "--method", method, endpoint];
|
|
@@ -554,11 +542,9 @@ export class GhCliClient extends GitHubClient {
|
|
|
554
542
|
response: { headers: { get: () => "" } },
|
|
555
543
|
};
|
|
556
544
|
}
|
|
557
|
-
|
|
558
545
|
async paginate(requestPath) {
|
|
559
546
|
return ghJson(["api", "--paginate", requestPath, "--slurp"]).flatMap((page) => page);
|
|
560
547
|
}
|
|
561
|
-
|
|
562
548
|
async getMergeQueueState(branch) {
|
|
563
549
|
const query = `query($owner:String!,$repo:String!,$branch:String!){repository(owner:$owner,name:$repo){mergeQueue(branch:$branch){id entries(first:100){nodes{id position state baseCommit{oid} headCommit{oid} pullRequest{number headRefOid}}}}}}`;
|
|
564
550
|
const data = ghJson([
|
|
@@ -852,7 +838,7 @@ export async function runDevPrAdmission(optionsInput = {}, clientInput) {
|
|
|
852
838
|
const initialQueue = await client.getMergeQueueState(options.targetBranch);
|
|
853
839
|
let warrant = null;
|
|
854
840
|
try {
|
|
855
|
-
warrant = readDeliveryWarrantResult(options, pr);
|
|
841
|
+
warrant = readDeliveryWarrantResult(options, pr); await verifyCurrentDeliveryWarrant(client, options, pr, warrant);
|
|
856
842
|
} catch (error) {
|
|
857
843
|
return reject("blocked", error.code || "invalid-delivery-warrant");
|
|
858
844
|
}
|
|
@@ -890,9 +876,9 @@ function finalizePatrolResult(result) {
|
|
|
890
876
|
|
|
891
877
|
async function reconcileEnqueueError({ client, options, pr, expectedHeadSha, entry, result, error }) {
|
|
892
878
|
const queueReadback = await client.getMergeQueueState(options.targetBranch).catch(() => null);
|
|
893
|
-
const exactEntry = queueReadback?.entries?.find((candidate) =>
|
|
894
|
-
candidate.pullRequestNumber === pr.number && candidate.pullRequestHeadSha === expectedHeadSha);
|
|
879
|
+
const exactEntry = queueReadback?.entries?.find((candidate) => candidate.pullRequestNumber === pr.number && candidate.pullRequestHeadSha === expectedHeadSha);
|
|
895
880
|
if (exactEntry) {
|
|
881
|
+
entry.activeLeaseStatus = await setActiveLeaseStatus(client, options.repository, expectedHeadSha, options.activeLeaseContext, "pending");
|
|
896
882
|
entry.action = "enqueued";
|
|
897
883
|
entry.reason = "already-enqueued-exact-head";
|
|
898
884
|
entry.queueEntry = exactEntry;
|
|
@@ -902,9 +888,11 @@ async function reconcileEnqueueError({ client, options, pr, expectedHeadSha, ent
|
|
|
902
888
|
return;
|
|
903
889
|
}
|
|
904
890
|
entry.queueAdmissionStatus = await setQueueAdmissionStatus(client, options.repository, expectedHeadSha, options.queueAdmissionContext, "failure");
|
|
891
|
+
entry.activeLeaseStatus = await setActiveLeaseStatus(client, options.repository, expectedHeadSha, options.activeLeaseContext, "failure");
|
|
905
892
|
entry.action = "skip";
|
|
906
|
-
entry.reason = "enqueue-rejected";
|
|
893
|
+
entry.reason = error.preEnqueue ? (error.code || "pre-enqueue-qualification-failed") : "enqueue-rejected";
|
|
907
894
|
entry.enqueueError = {
|
|
895
|
+
phase: error.preEnqueue ? "pre-enqueue" : "enqueue",
|
|
908
896
|
status: error.status || null,
|
|
909
897
|
message: error.message || "GitHub rejected merge queue admission",
|
|
910
898
|
};
|
|
@@ -1027,7 +1015,7 @@ export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
|
|
|
1027
1015
|
admissionDecision = "blocked";
|
|
1028
1016
|
admissionReason = "blocked-by-predecessor";
|
|
1029
1017
|
predecessor = queuePredecessor(observedQueueState);
|
|
1030
|
-
} else if (
|
|
1018
|
+
} else if (rejectBaseMoveBeforeAtomicReplay(options, initialBaseSha, observedBaseSha)) {
|
|
1031
1019
|
admissionDecision = "rejected";
|
|
1032
1020
|
admissionReason = "base-sha-drift";
|
|
1033
1021
|
} else if (observedHeadSha !== expectedHeadSha) {
|
|
@@ -1067,13 +1055,12 @@ export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
|
|
|
1067
1055
|
result.skipped.push(entry);
|
|
1068
1056
|
} else {
|
|
1069
1057
|
try {
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
});
|
|
1058
|
+
const atomicAdmission = await qualifyAtomicQueueAdmission({ client, options, pullRequest: pr, expectedBaseSha: initialBaseSha, expectedHeadSha, projectCut: decision.projectCut, sleep: delay, mergeableAccepted, root: contentRoot,
|
|
1059
|
+
setActiveLeaseStatus: (state) => setActiveLeaseStatus(client, options.repository, expectedHeadSha, options.activeLeaseContext, state), setQueueAdmissionStatus: (state) => setQueueAdmissionStatus(client, options.repository, expectedHeadSha, options.queueAdmissionContext, state) });
|
|
1060
|
+
Object.assign(entry, atomicAdmission.entryFields);
|
|
1061
|
+
const queueEntry = atomicAdmission.exactEntry || await client.enqueuePullRequest({ pullRequestId: decision.pullRequestId, expectedHeadOid: expectedHeadSha });
|
|
1075
1062
|
entry.action = "enqueued";
|
|
1076
|
-
entry.reason = "enqueued-with-expected-head";
|
|
1063
|
+
entry.reason = atomicAdmission.exactEntry ? "already-enqueued-exact-head" : "enqueued-with-expected-head";
|
|
1077
1064
|
entry.queueEntry = queueEntry;
|
|
1078
1065
|
entry.admissionReceipt.reason = entry.reason;
|
|
1079
1066
|
result.actions.push(entry);
|
|
@@ -1155,10 +1142,12 @@ export function cliOptions(args = [], environment = process.env) {
|
|
|
1155
1142
|
allowedHeadPrefixes: cliValue(args, "allowed-head-prefixes", environment.BUILDCHAIN_DEV_PR_ALLOWED_HEAD_PREFIXES),
|
|
1156
1143
|
requiredChecks: cliValue(args, "required-checks", environment.BUILDCHAIN_DEV_PR_REQUIRED_CHECKS),
|
|
1157
1144
|
queueAdmissionContext: cliValue(args, "queue-admission-context", environment.BUILDCHAIN_DEV_PR_QUEUE_ADMISSION_CONTEXT),
|
|
1145
|
+
activeLeaseContext: cliValue(args, "active-lease-context", environment.BUILDCHAIN_DEV_PR_ACTIVE_LEASE_CONTEXT),
|
|
1158
1146
|
diagnosticContext: cliValue(args, "diagnostic-context", environment.BUILDCHAIN_DEV_PR_DIAGNOSTIC_CONTEXT),
|
|
1159
1147
|
warrantMode: cliValue(args, "warrant-mode", environment.BUILDCHAIN_DEV_PR_WARRANT_MODE),
|
|
1160
1148
|
warrantResultPath: cliValue(args, "warrant-result", environment.BUILDCHAIN_DEV_PR_WARRANT_RESULT_PATH),
|
|
1161
1149
|
projectCutProofPath: cliValue(args, "project-cut-proof", environment.BUILDCHAIN_DEV_PR_PROJECT_CUT_PROOF_PATH),
|
|
1150
|
+
sourceProofPath: cliValue(args, "source-proof", environment.BUILDCHAIN_DEV_PR_SOURCE_PROOF_PATH),
|
|
1162
1151
|
sourcePatchRoot: cliValue(args, "source-patch-root", environment.BUILDCHAIN_DEV_PR_SOURCE_PATCH_ROOT),
|
|
1163
1152
|
qualificationOnly: cliFlag(args, "qualification-only"),
|
|
1164
1153
|
requireApproval: environment.BUILDCHAIN_DEV_PR_REQUIRE_APPROVAL,
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import { defaultDevDeliveryStateRef, GitHubDevDeliveryStore } from "./dev-delivery-warrant.mjs";
|
|
3
|
+
import { qualifyPreEnqueueReadback } from "./dev-pr-prequeue-guard.mjs";
|
|
2
4
|
|
|
3
5
|
const ROOT_PATTERN = /^sha256:[0-9a-f]{64}$/;
|
|
4
6
|
const SHA_PATTERN = /^[0-9a-f]{40}$/;
|
|
@@ -113,9 +115,203 @@ export function readDeliveryWarrantResult(options, pullRequest) {
|
|
|
113
115
|
generation: warrant.generation,
|
|
114
116
|
issuedAt: warrant.issuedAt,
|
|
115
117
|
expiresAt: warrant.expiresAt,
|
|
118
|
+
repository: warrant.repository,
|
|
119
|
+
protectedBase: warrant.protectedBase,
|
|
120
|
+
pullRequestNumber: warrant.pullRequestNumber,
|
|
121
|
+
sourceHead: warrant.sourceHead,
|
|
122
|
+
sourceProofRoot: warrant.sourceProofRoot,
|
|
116
123
|
};
|
|
117
124
|
}
|
|
118
125
|
|
|
126
|
+
export async function readCurrentDeliveryQueueState(client, repository, targetBranch) {
|
|
127
|
+
if (typeof client.getDevDeliveryQueueState === "function") {
|
|
128
|
+
return client.getDevDeliveryQueueState(targetBranch);
|
|
129
|
+
}
|
|
130
|
+
const store = new GitHubDevDeliveryStore({
|
|
131
|
+
repository: `${repository.owner}/${repository.repo}`,
|
|
132
|
+
token: client.token,
|
|
133
|
+
apiUrl: client.apiUrl,
|
|
134
|
+
fetchImpl: client.fetch,
|
|
135
|
+
});
|
|
136
|
+
const { queue } = await store.read({
|
|
137
|
+
stateRef: defaultDevDeliveryStateRef(targetBranch),
|
|
138
|
+
protectedBase: targetBranch,
|
|
139
|
+
});
|
|
140
|
+
return queue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function verifyCurrentDeliveryWarrant(client, options, pullRequest, warrant) {
|
|
144
|
+
if (!warrant) return null;
|
|
145
|
+
let queue;
|
|
146
|
+
try {
|
|
147
|
+
queue = await readCurrentDeliveryQueueState(
|
|
148
|
+
client,
|
|
149
|
+
options.repository,
|
|
150
|
+
options.targetBranch,
|
|
151
|
+
);
|
|
152
|
+
} catch {
|
|
153
|
+
mismatch("delivery-warrant-current-readback-failed");
|
|
154
|
+
}
|
|
155
|
+
const active = queue?.activeWarrant;
|
|
156
|
+
const candidate = queue?.candidates?.find(
|
|
157
|
+
(entry) => entry.candidateId === active?.candidateId,
|
|
158
|
+
);
|
|
159
|
+
requireMatch(active?.candidateId === warrant.candidateId, "delivery-warrant-no-longer-active");
|
|
160
|
+
requireMatch(active?.fencingToken === warrant.fencingToken, "delivery-warrant-current-fencing-mismatch");
|
|
161
|
+
requireMatch(Number(active?.generation) === Number(warrant.generation), "delivery-warrant-current-generation-mismatch");
|
|
162
|
+
requireMatch(Number(active?.pullRequestNumber) === Number(pullRequest.number), "delivery-warrant-current-pr-mismatch");
|
|
163
|
+
requireMatch(String(active?.sourceHead || "").toLowerCase() === options.expectedHeadSha, "delivery-warrant-current-head-mismatch");
|
|
164
|
+
requireMatch(active?.sourceProofRoot === warrant.sourceProofRoot, "delivery-warrant-current-source-proof-mismatch");
|
|
165
|
+
requireMatch(Number.isFinite(Date.parse(active?.expiresAt)) && Date.parse(active.expiresAt) > Date.now(), "delivery-warrant-current-expired");
|
|
166
|
+
requireMatch(["selected", "proving", "waiting", "blocked", "qualified"].includes(candidate?.status), "delivery-warrant-current-candidate-not-selected");
|
|
167
|
+
requireMatch(candidate?.sourceHead === active.sourceHead, "delivery-warrant-current-candidate-head-mismatch");
|
|
168
|
+
return { activeWarrant: active, activeCandidate: candidate };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function latestStatusForContext(statuses, context) {
|
|
172
|
+
return (statuses || []).find(
|
|
173
|
+
(status) => String(status.context || "") === context,
|
|
174
|
+
) || null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function preEnqueueMismatch(code, details = {}) {
|
|
178
|
+
const error = new Error(code);
|
|
179
|
+
error.code = code;
|
|
180
|
+
Object.assign(error, details);
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function setActiveLeaseStatus(client, repository, sha, context, state) {
|
|
185
|
+
if (!context) return null;
|
|
186
|
+
await client.request("POST", `/repos/${repository.owner}/${repository.repo}/statuses/${sha}`, {
|
|
187
|
+
body: {
|
|
188
|
+
state,
|
|
189
|
+
context,
|
|
190
|
+
description: state === "pending"
|
|
191
|
+
? "Buildchain reactivated this exact lease for merge-group qualification"
|
|
192
|
+
: "Buildchain released this exact lease after queue admission failed",
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
return { context, state, sha };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function readBackAdmissionStatuses(client, options, expectedHeadSha, sleep) {
|
|
199
|
+
let last = { queueAdmission: null, activeLease: null };
|
|
200
|
+
const attempts = Math.max(1, options.pollMergeableAttempts);
|
|
201
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
202
|
+
const checks = await client.listCommitChecks(expectedHeadSha);
|
|
203
|
+
last = {
|
|
204
|
+
queueAdmission: latestStatusForContext(
|
|
205
|
+
checks?.statuses,
|
|
206
|
+
options.queueAdmissionContext,
|
|
207
|
+
),
|
|
208
|
+
activeLease: latestStatusForContext(
|
|
209
|
+
checks?.statuses,
|
|
210
|
+
options.activeLeaseContext,
|
|
211
|
+
),
|
|
212
|
+
};
|
|
213
|
+
if (
|
|
214
|
+
(!options.queueAdmissionContext || last.queueAdmission?.state === "success") &&
|
|
215
|
+
(!options.activeLeaseContext || last.activeLease?.state === "pending")
|
|
216
|
+
) {
|
|
217
|
+
return { ...last, attempts: attempt };
|
|
218
|
+
}
|
|
219
|
+
if (attempt < attempts) await sleep(options.pollMergeableDelayMs);
|
|
220
|
+
}
|
|
221
|
+
preEnqueueMismatch("queue-admission-lease-readback-mismatch", { readback: last });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function qualifyAtomicQueueAdmission({
|
|
225
|
+
client,
|
|
226
|
+
options,
|
|
227
|
+
pullRequest,
|
|
228
|
+
expectedBaseSha,
|
|
229
|
+
expectedHeadSha,
|
|
230
|
+
projectCut,
|
|
231
|
+
setActiveLeaseStatus,
|
|
232
|
+
setQueueAdmissionStatus,
|
|
233
|
+
sleep,
|
|
234
|
+
mergeableAccepted,
|
|
235
|
+
root,
|
|
236
|
+
}) {
|
|
237
|
+
try {
|
|
238
|
+
if (options.warrantMode === "required") {
|
|
239
|
+
if (!options.verifiedDeliveryWarrant) {
|
|
240
|
+
preEnqueueMismatch("delivery-warrant-pre-enqueue-readback-missing");
|
|
241
|
+
}
|
|
242
|
+
if (!options.queueAdmissionContext) {
|
|
243
|
+
preEnqueueMismatch("queue-admission-context-required");
|
|
244
|
+
}
|
|
245
|
+
if (!options.activeLeaseContext) {
|
|
246
|
+
preEnqueueMismatch("active-lease-context-required");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const activeLeaseStatus = await setActiveLeaseStatus("pending");
|
|
250
|
+
const queueAdmissionStatus = await setQueueAdmissionStatus("success");
|
|
251
|
+
const statusReadback = await readBackAdmissionStatuses(
|
|
252
|
+
client,
|
|
253
|
+
options,
|
|
254
|
+
expectedHeadSha,
|
|
255
|
+
sleep,
|
|
256
|
+
);
|
|
257
|
+
const { observedBaseSha, observedQueueState, currentWarrant, preEnqueueProjectCut } = await qualifyPreEnqueueReadback({
|
|
258
|
+
client, options, pullRequest, expectedBaseSha, expectedHeadSha, projectCut, mergeableAccepted, root, verifyCurrentWarrant: verifyCurrentDeliveryWarrant,
|
|
259
|
+
});
|
|
260
|
+
const exactEntry = observedQueueState.entries.find(
|
|
261
|
+
(candidate) => candidate.pullRequestNumber === pullRequest.number &&
|
|
262
|
+
candidate.pullRequestHeadSha === expectedHeadSha,
|
|
263
|
+
);
|
|
264
|
+
const predecessor = observedQueueState.entries.find(
|
|
265
|
+
(candidate) => candidate !== exactEntry,
|
|
266
|
+
) || null;
|
|
267
|
+
if (predecessor) {
|
|
268
|
+
preEnqueueMismatch("queue-predecessor-after-lease-readback", { predecessor });
|
|
269
|
+
}
|
|
270
|
+
const transaction = {
|
|
271
|
+
schema: "kungfu.buildchain.dev-queue-admission-transaction/v1",
|
|
272
|
+
repository: options.repository.fullName,
|
|
273
|
+
protectedBase: options.targetBranch,
|
|
274
|
+
pullRequestNumber: pullRequest.number,
|
|
275
|
+
sourceHead: expectedHeadSha,
|
|
276
|
+
frozenBase: expectedBaseSha,
|
|
277
|
+
admittedBase: observedBaseSha,
|
|
278
|
+
queueAdmission: {
|
|
279
|
+
context: options.queueAdmissionContext,
|
|
280
|
+
state: statusReadback.queueAdmission?.state || "",
|
|
281
|
+
},
|
|
282
|
+
activeLease: {
|
|
283
|
+
context: options.activeLeaseContext,
|
|
284
|
+
state: statusReadback.activeLease?.state || "",
|
|
285
|
+
},
|
|
286
|
+
warrant: options.verifiedDeliveryWarrant,
|
|
287
|
+
currentWarrant: currentWarrant ? {
|
|
288
|
+
candidateId: currentWarrant.activeWarrant.candidateId,
|
|
289
|
+
fencingToken: currentWarrant.activeWarrant.fencingToken,
|
|
290
|
+
generation: currentWarrant.activeWarrant.generation,
|
|
291
|
+
sourceHead: currentWarrant.activeWarrant.sourceHead,
|
|
292
|
+
expiresAt: currentWarrant.activeWarrant.expiresAt,
|
|
293
|
+
} : null,
|
|
294
|
+
projectCutProofRoot: projectCut?.proofRoot || "",
|
|
295
|
+
preEnqueueProjectCut: preEnqueueProjectCut?.receipt || null,
|
|
296
|
+
preEnqueueProjectCutRoot: preEnqueueProjectCut?.receiptRoot || "",
|
|
297
|
+
decision: exactEntry ? "already-enqueued" : "qualified",
|
|
298
|
+
};
|
|
299
|
+
return {
|
|
300
|
+
exactEntry,
|
|
301
|
+
entryFields: {
|
|
302
|
+
activeLeaseStatus,
|
|
303
|
+
queueAdmissionStatus,
|
|
304
|
+
atomicAdmissionReceipt: transaction,
|
|
305
|
+
atomicAdmissionReceiptRoot: root(transaction),
|
|
306
|
+
atomicAdmissionReadbackAttempts: statusReadback.attempts,
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
} catch (error) {
|
|
310
|
+
error.preEnqueue = true;
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
119
315
|
export async function runSourceQualification({ options, pullRequest, readiness, client, evaluate, admissionState, createReceipt, root, publishDiagnostic, reject }) {
|
|
120
316
|
const decision = await evaluate(pullRequest, { ...options, landingMode: options.landingMode, dryRun: true }, client);
|
|
121
317
|
if (decision.observedHeadSha && String(decision.observedHeadSha).toLowerCase() !== options.expectedHeadSha) {
|
|
@@ -147,13 +343,14 @@ export async function runSourceQualification({ options, pullRequest, readiness,
|
|
|
147
343
|
|
|
148
344
|
export async function admitExistingQueueEntry({ options, pullRequest, readiness, client, entry, warrant, createReceipt, root, publishDiagnostic }) {
|
|
149
345
|
if (!entry) return null;
|
|
346
|
+
const activeLeaseStatus = options.dryRun ? null : await setActiveLeaseStatus(client, options.repository, options.expectedHeadSha, options.activeLeaseContext, "pending");
|
|
150
347
|
const receipt = createReceipt({
|
|
151
348
|
options,
|
|
152
349
|
pr: pullRequest,
|
|
153
350
|
state: "queued",
|
|
154
351
|
reason: "already-enqueued-exact-head",
|
|
155
352
|
readiness,
|
|
156
|
-
queue: { enabled: true, entry },
|
|
353
|
+
queue: { enabled: true, entry, activeLeaseStatus },
|
|
157
354
|
warrant,
|
|
158
355
|
});
|
|
159
356
|
const result = {
|
|
@@ -172,7 +369,11 @@ export async function admitExistingQueueEntry({ options, pullRequest, readiness,
|
|
|
172
369
|
export async function runTargetedQueueAdmission({ options, pullRequest, readiness, client, warrant, runController, admissionState, createReceipt, root, publishDiagnostic }) {
|
|
173
370
|
const targetedClient = Object.create(client);
|
|
174
371
|
targetedClient.listPullRequests = async () => [pullRequest];
|
|
175
|
-
const controller = await runController({
|
|
372
|
+
const controller = await runController({
|
|
373
|
+
...options,
|
|
374
|
+
targetPullRequestNumber: 0,
|
|
375
|
+
verifiedDeliveryWarrant: warrant,
|
|
376
|
+
}, targetedClient);
|
|
176
377
|
controller.runKind = "targeted-admission-evaluation";
|
|
177
378
|
controller.outcome = controller.actions.length === 0 ? "target-not-admitted" : "target-action-selected";
|
|
178
379
|
controller.qualification = false;
|
|
@@ -190,6 +391,8 @@ export async function runTargetedQueueAdmission({ options, pullRequest, readines
|
|
|
190
391
|
enabled: controller.mergeQueue?.enabled === true,
|
|
191
392
|
predecessor: entry.admissionReceipt?.predecessor || null,
|
|
192
393
|
entry: entry.queueEntry || null,
|
|
394
|
+
admissionTransaction: entry.atomicAdmissionReceipt || null,
|
|
395
|
+
admissionTransactionRoot: entry.atomicAdmissionReceiptRoot || "",
|
|
193
396
|
},
|
|
194
397
|
warrant,
|
|
195
398
|
});
|