@kungfu-tech/buildchain 3.0.8 → 3.0.9-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/actions/promote-buildchain-ref/README.md +8 -7
- package/bin/buildchain.mjs +34 -33
- package/bin/internal/trust-release-release-handlers.mjs +5 -0
- package/contracts/fixtures/kfd-adopter-release-v1/kfd-4-perspective.json +16 -0
- package/contracts/fixtures/kfd-adopter-release-v1/kfd-4-replay.json +57 -0
- package/contracts/release-cut-v1.schema.json +1 -0
- package/contracts/release-train-transition-v1.schema.json +1 -0
- package/dist/site/agent-index.json +0 -1
- package/dist/site/artifact-schemas.json +0 -2
- package/dist/site/badge-endpoint-registry.json +9 -9
- package/dist/site/badges/v1/kfd-8/aligned.json +1 -1
- package/dist/site/badges/v1/kfd-8/declared.json +1 -1
- package/dist/site/badges/v1/kfd-8/downgraded.json +1 -1
- package/dist/site/badges/v1/kfd-8/draft.json +1 -1
- package/dist/site/badges/v1/kfd-8/failed.json +1 -1
- package/dist/site/badges/v1/kfd-8/missing.json +1 -1
- package/dist/site/badges/v1/kfd-8/passed.json +1 -1
- package/dist/site/badges/v1/kfd-8/planned.json +1 -1
- package/dist/site/buildchain-contract.json +38 -34
- package/dist/site/buildchain-site.json +59 -39
- package/dist/site/capability-registry.json +1 -1
- package/dist/site/cli-registry.json +8 -7
- package/dist/site/controller-registry.json +13 -5
- package/dist/site/kfd-claims.json +35 -8
- package/dist/site/kfd-upstream-aggregate.json +10 -10
- package/dist/site/manual-registry.json +9 -9
- package/dist/site/node-api-registry.json +1143 -320
- package/dist/site/page-registry.json +46 -26
- package/dist/site/public-surface-audit.json +19 -9
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-passport-check-manifest.json +33 -5
- package/dist/site/release-provenance.json +2 -1
- package/dist/site/schemas/release-passport-v1.schema.json +3 -0
- package/dist/site/site-manifest.json +13 -13
- package/dist/site/workflow-registry.json +19 -9
- package/docs/aws-us-elastic-runner-burst-plane.md +15 -3
- package/docs/cli-reference.md +6 -6
- package/docs/cli.md +2 -3
- package/docs/dev-alpha-candidate-patrol.md +45 -20
- package/docs/dev-delivery-warrant.md +12 -2
- package/docs/kfd-support.md +7 -7
- package/docs/node-api-reference.md +295 -246
- package/docs/release-passport.md +25 -12
- package/docs/release-train.md +54 -4
- package/docs/reusable-build-surface.md +26 -9
- package/docs/runtime-train-validation.md +6 -0
- package/docs/versioning.md +3 -2
- package/package.json +4 -3
- package/packages/core/artifact-verification-envelope.js +114 -0
- package/packages/core/buildchain-channel-identity.js +119 -0
- package/packages/core/buildchain-compatibility-proof.js +32 -0
- package/packages/core/buildchain-contract.js +7 -0
- package/packages/core/controller-evidence.js +2 -1
- package/packages/core/dev-alpha-active-release-train.js +460 -0
- package/packages/core/dev-alpha-candidate-selection.js +181 -0
- package/packages/core/dev-delivery-warrant.js +15 -4
- package/packages/core/index.js +4 -5
- package/packages/core/kfd-adopter-manifest.js +390 -0
- package/packages/core/kfd-product-gates.js +0 -300
- package/packages/core/release-blocker-priority.js +192 -0
- package/packages/core/release-passport-contract.js +219 -6
- package/packages/core/release-passport.js +139 -242
- package/packages/core/release-train.js +220 -0
- package/scripts/aws-macos-jit-controller-core.mjs +76 -0
- package/scripts/aws-macos-jit-controller.mjs +29 -0
- package/scripts/aws-macos-jit-source-rebind.mjs +503 -0
- package/scripts/buildchain-channel-router.mjs +18 -2
- package/scripts/buildchain-cli-help.mjs +4 -2
- package/scripts/buildchain-contract-lock.mjs +11 -1
- package/scripts/check-inventory.mjs +4 -4
- package/scripts/dev-alpha-candidate-patrol.mjs +196 -201
- package/scripts/generate-buildchain-kfd-witnesses.mjs +88 -94
- package/scripts/generate-channel-build-workflow.mjs +78 -31
- package/scripts/generate-site-bundle.mjs +1 -8
- package/scripts/promotion-channel-router.mjs +5 -3
- package/scripts/release-train-self-dogfood.mjs +579 -0
- package/scripts/runtime-ref-core.mjs +4 -17
- package/scripts/site-capability-metadata.mjs +3 -1
- package/dist/site/schemas/kfd-support-projection-v1.schema.json +0 -106
|
@@ -9,6 +9,19 @@ import {
|
|
|
9
9
|
resolveManagedPromotionBaseline,
|
|
10
10
|
resolvePatrolSourceInputs,
|
|
11
11
|
} from "../packages/core/channel-promotion-baseline.js";
|
|
12
|
+
import {
|
|
13
|
+
cutInitialReleaseTrain,
|
|
14
|
+
reconcileActiveReleaseTrain,
|
|
15
|
+
resolveManagedCandidate,
|
|
16
|
+
runActiveReleaseTrain,
|
|
17
|
+
} from "../packages/core/dev-alpha-active-release-train.js";
|
|
18
|
+
import {
|
|
19
|
+
blockedCandidateDecision,
|
|
20
|
+
candidateFromDecision,
|
|
21
|
+
selectLatestQualifiedSource,
|
|
22
|
+
} from "../packages/core/dev-alpha-candidate-selection.js";
|
|
23
|
+
|
|
24
|
+
export { reconcileActiveReleaseTrain, selectLatestQualifiedSource };
|
|
12
25
|
|
|
13
26
|
export const DEV_ALPHA_CANDIDATE_STATE_SCHEMA =
|
|
14
27
|
"kungfu-buildchain-dev-alpha-candidate-state/v1";
|
|
@@ -191,6 +204,8 @@ export function managedCandidateFromPullRequest(pullRequest, targetBranch) {
|
|
|
191
204
|
sourceLockRef: headRef,
|
|
192
205
|
decisionRoot: text(marker.activeCandidate?.decisionRoot),
|
|
193
206
|
nextCandidate: marker.nextCandidate || null,
|
|
207
|
+
releaseTrain: marker.releaseTrain || null,
|
|
208
|
+
hold: marker.hold || null,
|
|
194
209
|
state: marker,
|
|
195
210
|
};
|
|
196
211
|
}
|
|
@@ -215,6 +230,8 @@ export function managedCandidateFromPullRequest(pullRequest, targetBranch) {
|
|
|
215
230
|
sourceLockRef: headRef,
|
|
216
231
|
decisionRoot: "",
|
|
217
232
|
nextCandidate: null,
|
|
233
|
+
releaseTrain: null,
|
|
234
|
+
hold: null,
|
|
218
235
|
state: null,
|
|
219
236
|
};
|
|
220
237
|
}
|
|
@@ -274,6 +291,25 @@ export function normalizeDevAlphaPatrolOptions(options = {}) {
|
|
|
274
291
|
process.env.BUILDCHAIN_CHANNEL_PATROL_EXPECTED_PRIOR_STATE_ROOT,
|
|
275
292
|
"expectedPriorStateRoot",
|
|
276
293
|
),
|
|
294
|
+
expectedCutRoot: optionalStateRoot(
|
|
295
|
+
options.expectedCutRoot ??
|
|
296
|
+
process.env.BUILDCHAIN_CHANNEL_PATROL_EXPECTED_CUT_ROOT,
|
|
297
|
+
"expectedCutRoot",
|
|
298
|
+
),
|
|
299
|
+
cutCreatedAt: text(
|
|
300
|
+
options.cutCreatedAt ??
|
|
301
|
+
process.env.BUILDCHAIN_CHANNEL_PATROL_CUT_CREATED_AT,
|
|
302
|
+
),
|
|
303
|
+
requireActiveReleaseTrain: bool(
|
|
304
|
+
options.requireActiveReleaseTrain ??
|
|
305
|
+
process.env.BUILDCHAIN_CHANNEL_PATROL_REQUIRE_ACTIVE_TRAIN,
|
|
306
|
+
true,
|
|
307
|
+
),
|
|
308
|
+
buildchainRuntimeSha: optionalExactSha(
|
|
309
|
+
options.buildchainRuntimeSha ??
|
|
310
|
+
process.env.BUILDCHAIN_CHANNEL_PATROL_RUNTIME_SHA,
|
|
311
|
+
"buildchainRuntimeSha",
|
|
312
|
+
),
|
|
277
313
|
reactivationAuthorized: bool(
|
|
278
314
|
options.reactivationAuthorized ??
|
|
279
315
|
process.env.BUILDCHAIN_CHANNEL_PATROL_REACTIVATION_AUTHORIZED,
|
|
@@ -321,157 +357,6 @@ export function normalizeDevAlphaPatrolOptions(options = {}) {
|
|
|
321
357
|
};
|
|
322
358
|
}
|
|
323
359
|
|
|
324
|
-
function latestWorkflowEvidence(runs, workflowPathValue, sourceSha) {
|
|
325
|
-
const matching = runs
|
|
326
|
-
.filter(
|
|
327
|
-
(run) =>
|
|
328
|
-
run.conclusion !== "cancelled" &&
|
|
329
|
-
run.path === workflowPathValue &&
|
|
330
|
-
run.head_sha === sourceSha,
|
|
331
|
-
)
|
|
332
|
-
.sort((left, right) => Number(right.id) - Number(left.id));
|
|
333
|
-
if (matching.length === 0)
|
|
334
|
-
throw new Error(
|
|
335
|
-
`missing completed same-SHA workflow run: ${workflowPathValue}`,
|
|
336
|
-
);
|
|
337
|
-
const run = matching[0];
|
|
338
|
-
return {
|
|
339
|
-
workflowPath: workflowPathValue,
|
|
340
|
-
workflowName: run.name,
|
|
341
|
-
runId: run.id,
|
|
342
|
-
runAttempt: run.run_attempt,
|
|
343
|
-
headSha: run.head_sha,
|
|
344
|
-
status: run.status,
|
|
345
|
-
conclusion: run.conclusion,
|
|
346
|
-
completedAt: run.updated_at,
|
|
347
|
-
url: run.html_url,
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function workflowEvidenceIsFreshAndSuccessful(run, { now, maxAgeSeconds }) {
|
|
352
|
-
const completedAt = Date.parse(run.updated_at);
|
|
353
|
-
const ageSeconds = (Date.parse(now) - completedAt) / 1000;
|
|
354
|
-
return (
|
|
355
|
-
run.status === "completed" &&
|
|
356
|
-
run.conclusion === "success" &&
|
|
357
|
-
Number.isFinite(ageSeconds) &&
|
|
358
|
-
ageSeconds >= 0 &&
|
|
359
|
-
ageSeconds <= maxAgeSeconds
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function latestRunsBySha(runs, workflowPathValue) {
|
|
364
|
-
const latest = new Map();
|
|
365
|
-
for (const run of runs.filter(
|
|
366
|
-
(row) => row.path === workflowPathValue && row.conclusion !== "cancelled",
|
|
367
|
-
)) {
|
|
368
|
-
const current = latest.get(run.head_sha);
|
|
369
|
-
if (!current || Number(run.id) > Number(current.id))
|
|
370
|
-
latest.set(run.head_sha, run);
|
|
371
|
-
}
|
|
372
|
-
return latest;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
export function selectLatestQualifiedSource({
|
|
376
|
-
sourceHistory,
|
|
377
|
-
workflowRunsByPath,
|
|
378
|
-
requiredWorkflowPaths,
|
|
379
|
-
now,
|
|
380
|
-
maxAgeSeconds,
|
|
381
|
-
}) {
|
|
382
|
-
const latestByPath = new Map(
|
|
383
|
-
requiredWorkflowPaths.map((workflow) => [
|
|
384
|
-
workflow,
|
|
385
|
-
latestRunsBySha(workflowRunsByPath.get(workflow) || [], workflow),
|
|
386
|
-
]),
|
|
387
|
-
);
|
|
388
|
-
for (let index = 0; index < sourceHistory.length; index += 1) {
|
|
389
|
-
const sourceSha = sourceHistory[index];
|
|
390
|
-
const rows = requiredWorkflowPaths.map((workflow) =>
|
|
391
|
-
latestByPath.get(workflow).get(sourceSha),
|
|
392
|
-
);
|
|
393
|
-
if (
|
|
394
|
-
rows.every((run) =>
|
|
395
|
-
workflowEvidenceIsFreshAndSuccessful(run || {}, { now, maxAgeSeconds }),
|
|
396
|
-
)
|
|
397
|
-
) {
|
|
398
|
-
return {
|
|
399
|
-
sourceSha,
|
|
400
|
-
skippedNewerCommitCount: index,
|
|
401
|
-
workflowEvidence: requiredWorkflowPaths.map((workflow) =>
|
|
402
|
-
latestWorkflowEvidence(
|
|
403
|
-
workflowRunsByPath.get(workflow) || [],
|
|
404
|
-
workflow,
|
|
405
|
-
sourceSha,
|
|
406
|
-
),
|
|
407
|
-
),
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
const staleSuccessfulPair = sourceHistory.some((sourceSha) => {
|
|
412
|
-
const rows = requiredWorkflowPaths.map((workflow) =>
|
|
413
|
-
latestByPath.get(workflow).get(sourceSha),
|
|
414
|
-
);
|
|
415
|
-
return (
|
|
416
|
-
rows.every(
|
|
417
|
-
(run) =>
|
|
418
|
-
run?.status === "completed" &&
|
|
419
|
-
run?.conclusion === "success" &&
|
|
420
|
-
run?.head_sha === sourceSha,
|
|
421
|
-
) &&
|
|
422
|
-
rows.some(
|
|
423
|
-
(run) =>
|
|
424
|
-
!workflowEvidenceIsFreshAndSuccessful(run, { now, maxAgeSeconds }),
|
|
425
|
-
)
|
|
426
|
-
);
|
|
427
|
-
});
|
|
428
|
-
if (staleSuccessfulPair)
|
|
429
|
-
throw new Error(
|
|
430
|
-
"same-SHA workflow evidence is stale for every qualified source commit",
|
|
431
|
-
);
|
|
432
|
-
throw new Error(
|
|
433
|
-
"no source commit ahead of target has fresh completed successful same-SHA workflow evidence",
|
|
434
|
-
);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
function blockedCandidateDecision({
|
|
438
|
-
options,
|
|
439
|
-
sourceSha,
|
|
440
|
-
targetSha,
|
|
441
|
-
comparison,
|
|
442
|
-
reason,
|
|
443
|
-
}) {
|
|
444
|
-
const body = {
|
|
445
|
-
schema: "kungfu-buildchain-channel-candidate-decision/v1",
|
|
446
|
-
eligible: false,
|
|
447
|
-
reason: "qualification-evidence-blocked",
|
|
448
|
-
repository: options.repository,
|
|
449
|
-
source: { branch: options.sourceBranch, sha: sourceSha },
|
|
450
|
-
target: { branch: options.targetBranch, sha: targetSha },
|
|
451
|
-
comparison: {
|
|
452
|
-
status: text(comparison.status || "unknown"),
|
|
453
|
-
aheadBy: Number(comparison.ahead_by || 0),
|
|
454
|
-
},
|
|
455
|
-
blockReason: text(reason),
|
|
456
|
-
decidedAt: options.now,
|
|
457
|
-
};
|
|
458
|
-
return { ...body, decisionRoot: evidenceRoot(body) };
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
function candidateFromDecision(decision) {
|
|
462
|
-
if (!decision.eligible) return null;
|
|
463
|
-
return {
|
|
464
|
-
sourceSha: decision.source.sha,
|
|
465
|
-
sourceLockRef: decision.sourceLockRef,
|
|
466
|
-
decisionRoot: decision.decisionRoot,
|
|
467
|
-
workflowEvidence: decision.workflowEvidence,
|
|
468
|
-
qualificationRoot: evidenceRoot({
|
|
469
|
-
sourceSha: decision.source.sha,
|
|
470
|
-
workflowEvidence: decision.workflowEvidence,
|
|
471
|
-
}),
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
|
|
475
360
|
function candidateStateBody({
|
|
476
361
|
options,
|
|
477
362
|
targetSha,
|
|
@@ -482,6 +367,8 @@ function candidateStateBody({
|
|
|
482
367
|
priorStateRoot = ABSENT_STATE_ROOT,
|
|
483
368
|
generation = 1,
|
|
484
369
|
tombstones = [],
|
|
370
|
+
releaseTrain = null,
|
|
371
|
+
hold = null,
|
|
485
372
|
}) {
|
|
486
373
|
const body = {
|
|
487
374
|
schema: DEV_ALPHA_CANDIDATE_STATE_SCHEMA,
|
|
@@ -494,6 +381,8 @@ function candidateStateBody({
|
|
|
494
381
|
activeCandidate,
|
|
495
382
|
nextCandidate,
|
|
496
383
|
tombstones,
|
|
384
|
+
...(releaseTrain ? { releaseTrain } : {}),
|
|
385
|
+
...(hold ? { hold } : {}),
|
|
497
386
|
observationDecisionRoot: decision.decisionRoot || null,
|
|
498
387
|
observedAt: options.now,
|
|
499
388
|
...(supersededCandidate ? { supersededCandidate } : {}),
|
|
@@ -615,6 +504,80 @@ function activePullRequest(candidate) {
|
|
|
615
504
|
};
|
|
616
505
|
}
|
|
617
506
|
|
|
507
|
+
async function resumeActiveTrainIfPresent(options, client, candidate) {
|
|
508
|
+
if (!options.requireActiveReleaseTrain || !candidate) return null;
|
|
509
|
+
if (!candidate.releaseTrain) {
|
|
510
|
+
throw new Error(
|
|
511
|
+
`candidate PR #${candidate.number} has no authoritative active Release Train`,
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
return runActiveReleaseTrain({
|
|
515
|
+
options,
|
|
516
|
+
client,
|
|
517
|
+
activeCandidate: candidate,
|
|
518
|
+
adapters: {
|
|
519
|
+
stateSchema: DEV_ALPHA_CANDIDATE_STATE_SCHEMA,
|
|
520
|
+
persistedStateRoot,
|
|
521
|
+
candidateStateBody,
|
|
522
|
+
replaceCandidateStateMarker,
|
|
523
|
+
activePullRequest,
|
|
524
|
+
armCandidateAutoMerge,
|
|
525
|
+
},
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function assertExpectedSelection(options, decision) {
|
|
530
|
+
if (
|
|
531
|
+
options.expectedSelectedSha &&
|
|
532
|
+
decision.source.sha !== options.expectedSelectedSha
|
|
533
|
+
) {
|
|
534
|
+
throw new Error(
|
|
535
|
+
`selected source changed between observation and settlement: expected ${options.expectedSelectedSha}, observed ${decision.source.sha}`,
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function assertExpectedStateRoot(options, priorStateRoot) {
|
|
541
|
+
if (
|
|
542
|
+
options.expectedPriorStateRoot &&
|
|
543
|
+
options.expectedPriorStateRoot !== priorStateRoot
|
|
544
|
+
) {
|
|
545
|
+
throw new Error(
|
|
546
|
+
`candidate controller compare-and-swap failed: expected prior state root ${options.expectedPriorStateRoot}, observed ${priorStateRoot}`,
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async function maybeCutReleaseTrain({
|
|
552
|
+
options,
|
|
553
|
+
client,
|
|
554
|
+
activeCandidate,
|
|
555
|
+
decision,
|
|
556
|
+
observedCandidate,
|
|
557
|
+
sourceSha,
|
|
558
|
+
targetSha,
|
|
559
|
+
observedSourceHeadSha,
|
|
560
|
+
}) {
|
|
561
|
+
if (!options.requireActiveReleaseTrain || activeCandidate) {
|
|
562
|
+
return activeCandidate?.releaseTrain || null;
|
|
563
|
+
}
|
|
564
|
+
return cutInitialReleaseTrain({
|
|
565
|
+
options,
|
|
566
|
+
client,
|
|
567
|
+
decision,
|
|
568
|
+
candidate: observedCandidate,
|
|
569
|
+
sourceSha,
|
|
570
|
+
targetSha,
|
|
571
|
+
observedSourceHeadSha,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function initialControllerState(decision, qualificationError) {
|
|
576
|
+
if (decision.eligible) return "eligible-for-settlement";
|
|
577
|
+
if (!qualificationError) return "observed";
|
|
578
|
+
return /stale/u.test(qualificationError.message) ? "stale" : "blocked";
|
|
579
|
+
}
|
|
580
|
+
|
|
618
581
|
export async function runDevAlphaCandidatePatrol(
|
|
619
582
|
optionsInput = {},
|
|
620
583
|
clientInput,
|
|
@@ -632,6 +595,19 @@ export async function runDevAlphaCandidatePatrol(
|
|
|
632
595
|
options.devWorkflowPath,
|
|
633
596
|
options.alphaWorkflowPath,
|
|
634
597
|
];
|
|
598
|
+
const persistedCandidate = await resolveManagedCandidate({
|
|
599
|
+
client,
|
|
600
|
+
targetBranch: options.targetBranch,
|
|
601
|
+
repository: options.repository,
|
|
602
|
+
sourceBranch: options.sourceBranch,
|
|
603
|
+
parseCandidate: managedCandidateFromPullRequest,
|
|
604
|
+
});
|
|
605
|
+
const resumed = await resumeActiveTrainIfPresent(
|
|
606
|
+
options,
|
|
607
|
+
client,
|
|
608
|
+
persistedCandidate,
|
|
609
|
+
);
|
|
610
|
+
if (resumed) return resumed;
|
|
635
611
|
const {
|
|
636
612
|
observedSourceHeadSha,
|
|
637
613
|
targetSha,
|
|
@@ -676,66 +652,31 @@ export async function runDevAlphaCandidatePatrol(
|
|
|
676
652
|
maxAgeSeconds: options.maxAgeSeconds,
|
|
677
653
|
now: options.now,
|
|
678
654
|
});
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
decision.source.sha !== options.expectedSelectedSha
|
|
682
|
-
) {
|
|
683
|
-
throw new Error(
|
|
684
|
-
`selected source changed between observation and settlement: expected ${options.expectedSelectedSha}, observed ${decision.source.sha}`,
|
|
685
|
-
);
|
|
686
|
-
}
|
|
687
|
-
const openPullRequests = await client.listOpenPullRequests(
|
|
688
|
-
options.targetBranch,
|
|
689
|
-
);
|
|
690
|
-
const managedCandidates = openPullRequests
|
|
691
|
-
.map((pullRequest) =>
|
|
692
|
-
managedCandidateFromPullRequest(pullRequest, options.targetBranch),
|
|
693
|
-
)
|
|
694
|
-
.filter(Boolean);
|
|
695
|
-
for (const candidate of managedCandidates) {
|
|
696
|
-
if (
|
|
697
|
-
candidate.state &&
|
|
698
|
-
(candidate.state.repository !== options.repository ||
|
|
699
|
-
candidate.state.sourceBranch !== options.sourceBranch)
|
|
700
|
-
) {
|
|
701
|
-
throw new Error(
|
|
702
|
-
`candidate PR #${candidate.number} state does not bind ${options.repository} ${options.sourceBranch}`,
|
|
703
|
-
);
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
if (managedCandidates.length > 1) {
|
|
707
|
-
throw new Error(
|
|
708
|
-
`multiple open Buildchain candidate PRs target ${options.targetBranch}: ${managedCandidates
|
|
709
|
-
.map((candidate) => `#${candidate.number}`)
|
|
710
|
-
.join(", ")}`,
|
|
711
|
-
);
|
|
712
|
-
}
|
|
713
|
-
let activeCandidate = managedCandidates[0] || null;
|
|
655
|
+
assertExpectedSelection(options, decision);
|
|
656
|
+
let activeCandidate = persistedCandidate;
|
|
714
657
|
const priorStateRoot = persistedStateRoot(activeCandidate);
|
|
715
|
-
|
|
716
|
-
options.expectedPriorStateRoot &&
|
|
717
|
-
options.expectedPriorStateRoot !== priorStateRoot
|
|
718
|
-
) {
|
|
719
|
-
throw new Error(
|
|
720
|
-
`candidate controller compare-and-swap failed: expected prior state root ${options.expectedPriorStateRoot}, observed ${priorStateRoot}`,
|
|
721
|
-
);
|
|
722
|
-
}
|
|
658
|
+
assertExpectedStateRoot(options, priorStateRoot);
|
|
723
659
|
const priorGeneration = Number(activeCandidate?.state?.generation || 0);
|
|
724
660
|
let generation = Math.max(priorGeneration, 1);
|
|
725
661
|
const priorTombstones = Array.isArray(activeCandidate?.state?.tombstones)
|
|
726
662
|
? activeCandidate.state.tombstones
|
|
727
663
|
: [];
|
|
728
664
|
const observedCandidate = candidateFromDecision(decision);
|
|
665
|
+
const releaseTrain = await maybeCutReleaseTrain({
|
|
666
|
+
options,
|
|
667
|
+
client,
|
|
668
|
+
activeCandidate,
|
|
669
|
+
decision,
|
|
670
|
+
observedCandidate,
|
|
671
|
+
sourceSha,
|
|
672
|
+
targetSha,
|
|
673
|
+
observedSourceHeadSha,
|
|
674
|
+
});
|
|
675
|
+
generation = releaseTrain?.releaseCut.generation || generation;
|
|
729
676
|
let nextCandidate = null;
|
|
730
677
|
let supersededCandidate = null;
|
|
731
678
|
let tombstones = [...priorTombstones];
|
|
732
|
-
let controllerState = decision
|
|
733
|
-
? "eligible-for-settlement"
|
|
734
|
-
: qualificationError
|
|
735
|
-
? /stale/u.test(qualificationError.message)
|
|
736
|
-
? "stale"
|
|
737
|
-
: "blocked"
|
|
738
|
-
: "observed";
|
|
679
|
+
let controllerState = initialControllerState(decision, qualificationError);
|
|
739
680
|
if (activeCandidate) {
|
|
740
681
|
controllerState = "active";
|
|
741
682
|
nextCandidate = activeCandidate.nextCandidate || null;
|
|
@@ -820,6 +761,7 @@ export async function runDevAlphaCandidatePatrol(
|
|
|
820
761
|
priorStateRoot,
|
|
821
762
|
generation,
|
|
822
763
|
tombstones,
|
|
764
|
+
releaseTrain,
|
|
823
765
|
});
|
|
824
766
|
let pullRequest;
|
|
825
767
|
let settlementAction = "none";
|
|
@@ -848,6 +790,8 @@ export async function runDevAlphaCandidatePatrol(
|
|
|
848
790
|
activeCandidate: observedCandidate,
|
|
849
791
|
nextCandidate: null,
|
|
850
792
|
supersededCandidate: null,
|
|
793
|
+
generation,
|
|
794
|
+
releaseTrain,
|
|
851
795
|
});
|
|
852
796
|
pullRequest = await client.ensurePullRequest({
|
|
853
797
|
head: decision.sourceLockRef,
|
|
@@ -885,6 +829,7 @@ export async function runDevAlphaCandidatePatrol(
|
|
|
885
829
|
priorStateRoot,
|
|
886
830
|
generation,
|
|
887
831
|
tombstones,
|
|
832
|
+
releaseTrain,
|
|
888
833
|
});
|
|
889
834
|
const nextBody = replaceCandidateStateMarker(pullRequest.body, state);
|
|
890
835
|
await client.updatePullRequestBody(
|
|
@@ -925,6 +870,20 @@ export async function runDevAlphaCandidatePatrol(
|
|
|
925
870
|
settlementAuthorized: options.settlementAuthorized,
|
|
926
871
|
autoMerge,
|
|
927
872
|
decision,
|
|
873
|
+
releaseTrain,
|
|
874
|
+
drift: releaseTrain
|
|
875
|
+
? {
|
|
876
|
+
observedDevSha: observedSourceHeadSha,
|
|
877
|
+
originDevSha: releaseTrain.releaseCut.originDevSha,
|
|
878
|
+
moved: observedSourceHeadSha !== releaseTrain.releaseCut.originDevSha,
|
|
879
|
+
observationRoot:
|
|
880
|
+
releaseTrain.observations.find(
|
|
881
|
+
(observation) =>
|
|
882
|
+
observation.observedDevSha === observedSourceHeadSha,
|
|
883
|
+
)?.observationRoot || null,
|
|
884
|
+
}
|
|
885
|
+
: null,
|
|
886
|
+
hold: null,
|
|
928
887
|
controller: {
|
|
929
888
|
schema: DEV_ALPHA_CANDIDATE_STATE_SCHEMA,
|
|
930
889
|
state: controllerState,
|
|
@@ -935,6 +894,16 @@ export async function runDevAlphaCandidatePatrol(
|
|
|
935
894
|
stateRoot: state.stateRoot,
|
|
936
895
|
priorStateRoot,
|
|
937
896
|
generation: state.generation,
|
|
897
|
+
trainRoot: releaseTrain?.trainRoot || null,
|
|
898
|
+
cutRoot: releaseTrain?.releaseCut.cutRoot || null,
|
|
899
|
+
candidateTreeSha: releaseTrain?.releaseCut.candidateTreeSha || null,
|
|
900
|
+
buildchainRuntimeSha:
|
|
901
|
+
releaseTrain?.releaseCut.buildchainRuntimeSha ||
|
|
902
|
+
options.buildchainRuntimeSha ||
|
|
903
|
+
null,
|
|
904
|
+
cutCreatedAt: releaseTrain?.releaseCut.createdAt || null,
|
|
905
|
+
observedAt: options.now,
|
|
906
|
+
holdRoot: null,
|
|
938
907
|
tombstones: state.tombstones || [],
|
|
939
908
|
},
|
|
940
909
|
pullRequest: pullRequest || null,
|
|
@@ -1002,6 +971,17 @@ export function createGitHubChannelCandidateClient({
|
|
|
1002
971
|
);
|
|
1003
972
|
return text(payload.object?.sha);
|
|
1004
973
|
},
|
|
974
|
+
async resolveCommitTreeSha(commitSha) {
|
|
975
|
+
const payload = await api(
|
|
976
|
+
`/repos/${owner}/${repo}/git/commits/${encodeURIComponent(commitSha)}`,
|
|
977
|
+
);
|
|
978
|
+
if (text(payload.sha) !== commitSha) {
|
|
979
|
+
throw new Error(
|
|
980
|
+
`candidate commit readback returned ${payload.sha || "<empty>"}, not ${commitSha}`,
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
return optionalExactSha(payload.tree?.sha, "candidateTreeSha");
|
|
984
|
+
},
|
|
1005
985
|
async compare(baseSha, headSha) {
|
|
1006
986
|
return api(`/repos/${owner}/${repo}/compare/${baseSha}...${headSha}`);
|
|
1007
987
|
},
|
|
@@ -1133,6 +1113,12 @@ function markdown(result) {
|
|
|
1133
1113
|
`Source: \`${result.decision.source.branch}@${result.decision.source.sha}\``,
|
|
1134
1114
|
`Target: \`${result.decision.target.branch}@${result.decision.target.sha}\``,
|
|
1135
1115
|
`Controller state: \`${result.controller.state}\``,
|
|
1116
|
+
`Release Train: \`${result.controller.trainRoot || "none"}\``,
|
|
1117
|
+
`Candidate generation: \`${result.controller.generation}\``,
|
|
1118
|
+
`Candidate tree: \`${result.controller.candidateTreeSha || "none"}\``,
|
|
1119
|
+
`Buildchain runtime: \`${result.controller.buildchainRuntimeSha || "none"}\``,
|
|
1120
|
+
`Drift observation: \`${result.drift?.observationRoot || "none"}\``,
|
|
1121
|
+
`Hold: \`${result.controller.holdRoot || "none"}\``,
|
|
1136
1122
|
`Active candidate: ${result.controller.activeCandidate?.url || "none"}`,
|
|
1137
1123
|
`Next candidate: \`${result.controller.nextCandidate?.sourceSha || "none"}\``,
|
|
1138
1124
|
`Settlement action: \`${result.controller.settlementAction}\``,
|
|
@@ -1163,6 +1149,15 @@ async function main() {
|
|
|
1163
1149
|
"next-candidate-sha": result.controller.nextCandidate?.sourceSha || "",
|
|
1164
1150
|
"settlement-action": result.controller.settlementAction,
|
|
1165
1151
|
"prior-state-root": result.controller.priorStateRoot,
|
|
1152
|
+
"train-root": result.controller.trainRoot || "",
|
|
1153
|
+
"cut-root": result.controller.cutRoot || "",
|
|
1154
|
+
"candidate-generation": String(result.controller.generation),
|
|
1155
|
+
"candidate-tree-sha": result.controller.candidateTreeSha || "",
|
|
1156
|
+
"runtime-sha": result.controller.buildchainRuntimeSha || "",
|
|
1157
|
+
"cut-created-at": result.controller.cutCreatedAt || "",
|
|
1158
|
+
"observed-at": result.controller.observedAt || "",
|
|
1159
|
+
"drift-root": result.drift?.observationRoot || "",
|
|
1160
|
+
"hold-root": result.controller.holdRoot || "",
|
|
1166
1161
|
};
|
|
1167
1162
|
fs.appendFileSync(
|
|
1168
1163
|
process.env.GITHUB_OUTPUT,
|