@kungfu-tech/buildchain 3.0.6-alpha.1 → 3.0.6-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.
Files changed (68) hide show
  1. package/README.md +4 -4
  2. package/actions/promote-buildchain-ref/README.md +8 -0
  3. package/bin/buildchain.mjs +13 -1
  4. package/contracts/auditable-demo-scenario-v1.schema.json +52 -0
  5. package/dist/site/buildchain-contract.json +47 -27
  6. package/dist/site/buildchain-site.json +158 -42
  7. package/dist/site/capability-registry.json +3 -3
  8. package/dist/site/cli-registry.json +40 -4
  9. package/dist/site/controller-registry.json +20 -4
  10. package/dist/site/kfd-claims.json +139 -19
  11. package/dist/site/kfd-upstream-aggregate.json +1 -1
  12. package/dist/site/manual-registry.json +9 -9
  13. package/dist/site/node-api-registry.json +1161 -180
  14. package/dist/site/page-registry.json +145 -29
  15. package/dist/site/public-surface-audit.json +385 -19
  16. package/dist/site/publication-authority-registry.json +61 -1
  17. package/dist/site/publication-registry.json +4 -4
  18. package/dist/site/release-provenance.json +1 -0
  19. package/dist/site/site-manifest.json +13 -13
  20. package/dist/site/workflow-registry.json +150 -13
  21. package/docs/MAP.md +1 -0
  22. package/docs/auditable-demo.md +58 -11
  23. package/docs/aws-us-elastic-runner-burst-plane.md +23 -19
  24. package/docs/cli-reference.md +154 -0
  25. package/docs/dev-alpha-candidate-patrol.md +13 -5
  26. package/docs/dev-delivery-warrant.md +158 -0
  27. package/docs/node-api-reference.md +54 -15
  28. package/docs/publication-authority.md +11 -0
  29. package/docs/release-candidate.md +19 -2
  30. package/docs/release-governance.md +52 -0
  31. package/docs/reusable-build-surface.md +11 -1
  32. package/docs/shifu-gate-profiles.md +12 -1
  33. package/docs/versioning.md +2 -0
  34. package/package.json +2 -1
  35. package/packages/core/buildchain-publication-authority.js +3 -1
  36. package/packages/core/channel-candidate.js +2 -21
  37. package/packages/core/channel-promotion-baseline.js +199 -0
  38. package/packages/core/dev-delivery-candidate-identity.js +94 -0
  39. package/packages/core/dev-delivery-common.js +73 -0
  40. package/packages/core/dev-delivery-proof.js +252 -0
  41. package/packages/core/dev-delivery-warrant-cancellation.js +94 -0
  42. package/packages/core/dev-delivery-warrant-settlement.js +73 -0
  43. package/packages/core/dev-delivery-warrant.js +591 -0
  44. package/scripts/auditable-demo-bundle-verification.mjs +148 -0
  45. package/scripts/auditable-demo-platform.mjs +86 -50
  46. package/scripts/auditable-demo-presentation.mjs +83 -0
  47. package/scripts/auditable-demo-renditions.mjs +264 -0
  48. package/scripts/auditable-demo.mjs +24 -30
  49. package/scripts/build-contract-core.mjs +58 -3
  50. package/scripts/buildchain-cli-help.mjs +8 -0
  51. package/scripts/buildchain-patrol.mjs +9 -0
  52. package/scripts/check-inventory.mjs +1 -0
  53. package/scripts/dev-alpha-candidate-patrol.mjs +45 -48
  54. package/scripts/dev-delivery-proof.mjs +193 -0
  55. package/scripts/dev-delivery-warrant.mjs +426 -0
  56. package/scripts/dev-pr-auto-merge.mjs +488 -46
  57. package/scripts/dev-pr-delivery-warrant.mjs +209 -0
  58. package/scripts/gate-profile-core.mjs +24 -0
  59. package/scripts/generate-site-bundle.mjs +2 -2
  60. package/scripts/git-fetch-process-tree.mjs +142 -0
  61. package/scripts/lifecycle-substage-evidence.mjs +274 -0
  62. package/scripts/locked-source-checkout.mjs +6 -3
  63. package/scripts/resolve-artifact-transfer-mode.mjs +9 -0
  64. package/scripts/resolve-build-contract.mjs +7 -0
  65. package/scripts/route-offline-runners.mjs +1 -0
  66. package/scripts/run-lifecycle-core.mjs +9 -9
  67. package/scripts/shifu-gate-profile.mjs +10 -16
  68. package/scripts/site-capability-metadata.mjs +14 -0
@@ -487,6 +487,15 @@ same slow checks. Buildchain supports GitHub merge queues for that channel
487
487
  shape. The queue validates the projected merged result and serializes the final
488
488
  ref update, so concurrent channel movement no longer invalidates the candidate.
489
489
 
490
+ GitHub Merge Queue does not by itself decide which candidate may spend a long
491
+ native proof before enqueue. Repositories with that workload use the
492
+ [Dev Delivery Warrant Queue](dev-delivery-warrant.md) as the durable,
493
+ FIFO-aging scheduling and fencing authority before native queue admission. A
494
+ selected Warrant owns one complete delivery attempt and later candidates remain
495
+ visibly queued; GitHub still owns the exact `merge_group` proof and final ref
496
+ mutation. Workflow concurrency remains only a process critical section and is
497
+ not fairness or ownership authority.
498
+
490
499
  Every required workflow must handle both `pull_request` and `merge_group`
491
500
  before the queue is enabled. Queue runs do not provide
492
501
  `github.event.pull_request`; required workflows must use the checked-out
@@ -545,6 +554,33 @@ inputs: target dev branch, required status/check names, ready and block labels,
545
554
  allowed work-branch prefixes, review requirements, maximum merges per run,
546
555
  merge method, and dry-run mode.
547
556
 
557
+ Agent delivery uses the targeted Buildchain command instead of relying on a
558
+ scheduled scan:
559
+
560
+ ```sh
561
+ buildchain dev pr-admit \
562
+ --repository kungfu-systems/example \
563
+ --branch dev/v3/v3.0 \
564
+ --pull-request 123 \
565
+ --expected-head 0123456789abcdef0123456789abcdef01234567
566
+ ```
567
+
568
+ The default is a mutation-free plan. After reviewing it, add `--execute` to
569
+ establish the configured readiness label for only that PR and exact head, read
570
+ the state back, and attempt native queue admission. Repeating execute is
571
+ idempotent: an exact matching queue entry is adopted, not submitted again. A
572
+ stale head or base, fork, draft, block label, missing approval, failed check,
573
+ active predecessor, or rejected enqueue exits nonzero. Execute mode also
574
+ creates or updates an exact-head PR comment and named commit status containing
575
+ the current state, reason, receipt root, and copyable next action. The JSON
576
+ receipt remains the complete content-addressed evidence.
577
+
578
+ GitHub auto-merge is observed but is never readiness or admission authority.
579
+ Approval plus green checks plus auto-merge enabled does not qualify a PR that
580
+ lacks explicit Buildchain delivery intent. The targeted workflow interface
581
+ exposes the same contract through `expected-pr-number` and
582
+ `expected-head-sha`; cadence patrol runs leave those inputs empty.
583
+
548
584
  The workflow defaults are conservative. A PR is skipped unless it targets the
549
585
  configured dev line, is not a draft, has the ready label, has no block label,
550
586
  comes from the same repository, uses an allowed work-branch prefix, has a
@@ -565,6 +601,15 @@ admission runs; GitHub still owns the atomic queue and protected-ref update.
565
601
  Repositories may explicitly select `landing-mode: direct` only when the target
566
602
  branch has no native queue. Queue presence always disables the direct path.
567
603
 
604
+ For slow candidates on a frequently advancing dev line, the optional
605
+ [Dev Delivery Warrant Queue](dev-delivery-warrant.md) adds durable FIFO plus
606
+ aging scheduling before native queue admission. It separates reusable source
607
+ qualification from exact merge-group integration, uses expected-old Git-ref
608
+ updates and fenced leases, and keeps the PR head unchanged when only dev moves.
609
+ The reusable caller supports `off`, read-only `shadow`, and fail-closed
610
+ `required` rollout modes. GitHub Merge Queue remains the final protected-ref
611
+ authority in every mode.
612
+
568
613
  The canonical consumer required check context is `check / check`, matching the
569
614
  reusable workflow call plus its `check` job. Buildchain's own `Verify` workflow
570
615
  emits the repository-local context `check`, so Buildchain self-promotion,
@@ -687,6 +732,13 @@ The cadence names describe patrol intensity, not release cadence:
687
732
  - monthly patrol is for structural drift checks that should not block ordinary
688
733
  development velocity.
689
734
 
735
+ Every cadence result is typed `runKind: cadence-patrol` with
736
+ `qualification: false`. A run with no open candidates reports
737
+ `no-op-no-candidates`; a run where every candidate is skipped reports
738
+ `no-op-all-skipped`. Either no-op can keep maintenance green, but neither is a
739
+ delivery qualification, a targeted admission receipt, or evidence that an
740
+ expected PR entered the merge queue.
741
+
690
742
  Stable Candidate Patrol is separate from those maintenance cadences because its
691
743
  caller-owned cron is a release-intent window. Its candidate ledger and selection
692
744
  remain generic; registry-specific side effects still run through the normal
@@ -774,6 +774,9 @@ Direct GitHub Artifact payloads default to compression level `0`. Buildchain
774
774
  artifacts are commonly already-compressed archives; storing them without a
775
775
  second compression pass shortens the upload window while preserving the same
776
776
  artifact name, run/id/digest binding, retention, and no-overwrite behavior.
777
+ Direct build and signed-finalization payload uploads include hidden files under
778
+ the caller-declared artifact paths, matching the relay path so manifest-bound
779
+ dotfiles are not silently removed in transit.
777
780
  Callers may select `1` through `9` for payloads that materially benefit from
778
781
  compression. Manifests and diagnostics retain their existing small-artifact
779
782
  behavior.
@@ -792,7 +795,14 @@ jobs:
792
795
  artifact-relay-s3-prefix: ${{ vars.BUILDCHAIN_ARTIFACT_RELAY_S3_PREFIX }}
793
796
  ```
794
797
 
795
- In relay mode, each self-hosted platform job uploads the heavy payload files to
798
+ The mode is a policy for platforms that run outside GitHub. GitHub-hosted
799
+ platforms always upload directly with `actions/upload-artifact`, even when a
800
+ mixed matrix requests `s3-to-github-artifacts`; they never send their payloads
801
+ through S3 or the replay job. Buildchain recognizes its hosted presets and the
802
+ standard hosted runner labels. Custom matrices with non-standard hosted labels
803
+ must declare `"githubHosted": true` on those platform rows.
804
+
805
+ For remaining relay-mode platforms, each job uploads the heavy payload files to
796
806
  S3 and uploads only a small `relay-manifest.json` to GitHub. A GitHub-hosted
797
807
  `relay-artifacts` job then assumes the configured download role, downloads the
798
808
  payloads from S3, verifies every file by SHA256, and re-uploads the normal
@@ -135,7 +135,18 @@ a shell string.
135
135
 
136
136
  Projects may also pass non-sensitive scalar environment through
137
137
  `gate-environment-json`; Buildchain validates the JSON shape and forwards it
138
- without interpreting names or values. Cache profile references use the same
138
+ without interpreting names or values. A custom `platforms-json` entry may add
139
+ an `environment` object when a value is platform-specific; those scalars
140
+ override the shared environment only for that matrix job. For example, a
141
+ GitHub-hosted Linux lane can select an installed compiler without leaking the
142
+ same setting into macOS or Windows:
143
+
144
+ ```yaml
145
+ platforms-json: >-
146
+ [{"id":"linux-x64","name":"Linux x64","platform":"linux","runner":"[\"ubuntu-24.04\"]","capabilities":["node","native-toolchain"],"environment":{"CC":"gcc-14","CXX":"g++-14"}}]
147
+ ```
148
+
149
+ Cache profile references use the same
139
150
  opaque `shifu-cache-profile-ref` and `shifu-cache-profile-digest` inputs as the
140
151
  reusable build. Do not place tokens, credentials, or other secrets in workflow
141
152
  inputs or Gate receipts.
@@ -79,6 +79,8 @@ repository changes are patch".
79
79
 
80
80
  | Date | Action | Line | Faces | Class | Rationale | PR |
81
81
  | ---------- | ------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
82
+ | 2026-08-05 | extend-minor | `v3.0` | auditable-demo-scenario, auditable-demo-capture, auditable-demo-adapter | additive | Add an opt-in deterministic readable-playback declaration that preserves captured terminal payloads and order while normalizing only presentation timing; omitted playback continues to use observed PTY timestamps, and existing composition modes remain unchanged. | |
83
+ | 2026-08-04 | extend-minor | `v3.0` | dev-delivery-warrant-queue, source-qualification-proof, integration-delivery-proof, dev-delivery-cli, reusable-dev-delivery-workflows | additive | Add durable fair and fenced protected-dev scheduling plus split source/integration proof contracts. Existing PR admission remains available through explicit `off` and `shadow` rollout modes; no existing command, export, or release flow is removed. | |
82
84
  | 2026-08-01 | extend-minor | `v3.0` | cli-reference-registry, node-api-symbol-registry, golden-path-manual | additive | Add generated, drift-checked CLI and Node API reference registries to the public site bundle, plus a package-tested first-user Golden Path; existing command execution and import semantics remain unchanged. | |
83
85
  | 2026-07-31 | extend-minor | `v3.0` | auditable-demo-workflow, auditable-demo-media-profile, auditable-demo-media-receipt | additive | Add an opt-in responsive profile that binds source-resolution and exact 1280x720 MP4/WebM/GIF renditions to one Gate and receipt while rejecting upscales, aspect-ratio drift, and profile changes between Gate-only and full-render paths. | |
84
86
  | 2026-07-31 | extend-minor | `v3.0` | release-candidate-family-evidence, release-passport-evidence-attachment, promotion-action, release-passport-cli | additive | Restore all-ref v2 parity for optional Initiative-family candidate binding and typed product-owned release evidence attachments while retaining Kungfu native Family State authority and the newer v3 post-activation released-evidence stage. | #2089 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "3.0.6-alpha.1",
3
+ "version": "3.0.6-alpha.2",
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",
@@ -28,6 +28,7 @@
28
28
  "./cache-evidence": "./packages/core/cache-evidence.js",
29
29
  "./controller-evidence": "./packages/core/controller-evidence.js",
30
30
  "./diagnostics": "./packages/core/diagnostics.js",
31
+ "./dev-delivery-warrant": "./packages/core/dev-delivery-warrant.js",
31
32
  "./homebrew": "./packages/core/homebrew.js",
32
33
  "./issue-reporting": "./packages/core/issue-reporting.js",
33
34
  "./kfd": "./packages/core/kfd.js",
@@ -24,6 +24,7 @@ const DESCRIPTORS = Object.freeze([
24
24
  [".github/workflows/build-surface-fixture.yml", "non-publication-oidc"],
25
25
  [".github/workflows/build.yml", "non-publication-oidc"],
26
26
  [".github/workflows/buildchain-alpha-self-dogfood.yml", "non-publication-oidc"],
27
+ [".github/workflows/buildchain-dev-delivery.yml", "governance-write"],
27
28
  [".github/workflows/buildchain-patrol-daily.yml", "governance-write"],
28
29
  [".github/workflows/buildchain-patrol-monthly.yml", "governance-write"],
29
30
  [".github/workflows/buildchain-patrol-weekly.yml", "governance-write"],
@@ -32,6 +33,8 @@ const DESCRIPTORS = Object.freeze([
32
33
  [".github/workflows/buildchain-stable-candidate-patrol.yml", "governance-write"],
33
34
  [".github/workflows/dev-qualification-patrol.yml", "governance-write"],
34
35
  [".github/workflows/dev-alpha-candidate-patrol.yml", "governance-write"],
36
+ [".github/workflows/dev-delivery-warrant-close.yml", "governance-write"],
37
+ [".github/workflows/dev-delivery-warrant-cancel.yml", "governance-write"],
35
38
  [".github/workflows/dev-merge-queue-governance.yml", "governance-write"],
36
39
  [".github/workflows/dev-pr-auto-merge.yml", "governance-write"],
37
40
  [".github/workflows/github-governance-audit.yml", "governance-write"],
@@ -75,7 +78,6 @@ export function buildchainPublicationAuthorityDescriptors() {
75
78
  runnerPolicy: publicationCapable ? "qualified-measured" : "unqualified",
76
79
  }));
77
80
  }
78
-
79
81
  export function createBuildchainPublicationAuthorityRegistry({ root = process.cwd() } = {}) {
80
82
  const workflowsDir = path.join(root, ".github", "workflows");
81
83
  const workflows = fs.readdirSync(workflowsDir)
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
 
3
3
  import crypto from "node:crypto";
4
+ import { normalizeChannelCandidateSelection } from "./channel-promotion-baseline.js";
4
5
 
5
6
  export const CHANNEL_CANDIDATE_DECISION_SCHEMA =
6
7
  "kungfu-buildchain-channel-candidate-decision/v1";
@@ -95,27 +96,7 @@ export function decideChannelCandidate(input) {
95
96
  throw new Error("sourceBranch and targetBranch must differ");
96
97
  const sourceSha = sha(input.sourceSha, "sourceSha");
97
98
  const targetSha = sha(input.targetSha, "targetSha");
98
- const selection = input.selection
99
- ? {
100
- mode: required(input.selection.mode, "selection.mode"),
101
- observedSourceHeadSha: sha(
102
- input.selection.observedSourceHeadSha,
103
- "selection.observedSourceHeadSha",
104
- ),
105
- skippedNewerCommitCount: Number(
106
- input.selection.skippedNewerCommitCount || 0,
107
- ),
108
- }
109
- : undefined;
110
- if (
111
- selection &&
112
- (!Number.isSafeInteger(selection.skippedNewerCommitCount) ||
113
- selection.skippedNewerCommitCount < 0)
114
- ) {
115
- throw new Error(
116
- "selection.skippedNewerCommitCount must be a non-negative integer",
117
- );
118
- }
99
+ const selection = normalizeChannelCandidateSelection(input.selection);
119
100
  const now = required(input.now || new Date().toISOString(), "now");
120
101
  const maxAgeSeconds = Number(input.maxAgeSeconds ?? 7 * 24 * 60 * 60);
121
102
  if (!Number.isSafeInteger(maxAgeSeconds) || maxAgeSeconds <= 0) {
@@ -0,0 +1,199 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+
3
+ const SHA = /^[0-9a-f]{40}$/;
4
+
5
+ function text(value = "") {
6
+ return String(value ?? "").trim();
7
+ }
8
+
9
+ function exactSha(value, name) {
10
+ const normalized = text(value).toLowerCase();
11
+ if (!SHA.test(normalized))
12
+ throw new Error(`${name} must be an exact 40-character SHA`);
13
+ return normalized;
14
+ }
15
+
16
+ export function normalizeManagedPromotionBaseline(input) {
17
+ const pullRequestNumber = Number(input?.pullRequestNumber);
18
+ if (!Number.isSafeInteger(pullRequestNumber) || pullRequestNumber <= 0) {
19
+ throw new Error(
20
+ "targetBaseline.pullRequestNumber must be a positive integer",
21
+ );
22
+ }
23
+ const pullRequestUrl = text(input?.pullRequestUrl);
24
+ if (!pullRequestUrl)
25
+ throw new Error("targetBaseline.pullRequestUrl is required");
26
+ return {
27
+ mode: text(input?.mode) || "managed-candidate-merge-source",
28
+ targetSha: exactSha(input?.targetSha, "targetBaseline.targetSha"),
29
+ sourceSha: exactSha(input?.sourceSha, "targetBaseline.sourceSha"),
30
+ pullRequestNumber,
31
+ pullRequestUrl,
32
+ };
33
+ }
34
+
35
+ export function normalizeChannelCandidateSelection(input) {
36
+ if (!input) return undefined;
37
+ const mode = text(input.mode);
38
+ if (!mode) throw new Error("selection.mode is required");
39
+ const skippedNewerCommitCount = Number(input.skippedNewerCommitCount || 0);
40
+ if (
41
+ !Number.isSafeInteger(skippedNewerCommitCount) ||
42
+ skippedNewerCommitCount < 0
43
+ ) {
44
+ throw new Error(
45
+ "selection.skippedNewerCommitCount must be a non-negative integer",
46
+ );
47
+ }
48
+ return {
49
+ mode,
50
+ observedSourceHeadSha: exactSha(
51
+ input.observedSourceHeadSha,
52
+ "selection.observedSourceHeadSha",
53
+ ),
54
+ skippedNewerCommitCount,
55
+ ...(input.targetBaseline
56
+ ? {
57
+ targetBaseline: normalizeManagedPromotionBaseline(
58
+ input.targetBaseline,
59
+ ),
60
+ }
61
+ : {}),
62
+ };
63
+ }
64
+
65
+ export async function resolveChannelComparisonBaseline({
66
+ client,
67
+ targetSha,
68
+ targetBranch,
69
+ sourceSha,
70
+ directComparison,
71
+ }) {
72
+ if (
73
+ directComparison.status !== "diverged" ||
74
+ !client.resolveManagedPromotionBaseline
75
+ ) {
76
+ return { comparison: directComparison, targetBaseline: undefined };
77
+ }
78
+ const promoted = await client.resolveManagedPromotionBaseline(
79
+ targetSha,
80
+ targetBranch,
81
+ );
82
+ if (!promoted)
83
+ return { comparison: directComparison, targetBaseline: undefined };
84
+ const comparison = await client.compare(promoted.sourceSha, sourceSha);
85
+ if (!["ahead", "identical"].includes(comparison.status)) {
86
+ return { comparison: directComparison, targetBaseline: undefined };
87
+ }
88
+ return {
89
+ comparison,
90
+ targetBaseline: normalizeManagedPromotionBaseline(promoted),
91
+ };
92
+ }
93
+
94
+ export async function resolvePatrolSourceInputs({
95
+ client,
96
+ options,
97
+ requiredWorkflowPaths,
98
+ selectQualifiedSource,
99
+ }) {
100
+ const [observedSourceHeadSha, targetSha] = await Promise.all([
101
+ client.resolveBranch(options.sourceBranch),
102
+ client.resolveBranch(options.targetBranch),
103
+ ]);
104
+ const directComparison = await client.compare(
105
+ targetSha,
106
+ observedSourceHeadSha,
107
+ );
108
+ const { comparison: headComparison, targetBaseline } =
109
+ await resolveChannelComparisonBaseline({
110
+ client,
111
+ targetSha,
112
+ targetBranch: options.targetBranch,
113
+ sourceSha: observedSourceHeadSha,
114
+ directComparison,
115
+ });
116
+ let sourceSha = observedSourceHeadSha;
117
+ let comparison = headComparison;
118
+ let workflowEvidence = [];
119
+ let skippedNewerCommitCount = 0;
120
+ let qualificationError;
121
+ if (
122
+ headComparison.status === "ahead" &&
123
+ Number(headComparison.ahead_by) > 0
124
+ ) {
125
+ const boundarySha = targetBaseline?.sourceSha || targetSha;
126
+ const [sourceHistory, ...workflowRunSets] = await Promise.all([
127
+ client.listBranchHistory(options.sourceBranch, boundarySha),
128
+ ...requiredWorkflowPaths.map((workflow) =>
129
+ client.listCompletedWorkflowRuns(workflow, options.sourceBranch),
130
+ ),
131
+ ]);
132
+ try {
133
+ const selected = selectQualifiedSource({
134
+ sourceHistory,
135
+ workflowRunsByPath: new Map(
136
+ requiredWorkflowPaths.map((workflow, index) => [
137
+ workflow,
138
+ workflowRunSets[index],
139
+ ]),
140
+ ),
141
+ requiredWorkflowPaths,
142
+ now: options.now,
143
+ maxAgeSeconds: options.maxAgeSeconds,
144
+ });
145
+ ({ sourceSha, skippedNewerCommitCount, workflowEvidence } = selected);
146
+ if (sourceSha !== observedSourceHeadSha)
147
+ comparison = await client.compare(boundarySha, sourceSha);
148
+ } catch (error) {
149
+ qualificationError = error;
150
+ }
151
+ }
152
+ return {
153
+ observedSourceHeadSha,
154
+ targetSha,
155
+ headComparison,
156
+ targetBaseline,
157
+ sourceSha,
158
+ comparison,
159
+ workflowEvidence,
160
+ skippedNewerCommitCount,
161
+ qualificationError,
162
+ };
163
+ }
164
+
165
+ export async function resolveManagedPromotionBaseline({
166
+ api,
167
+ targetSha,
168
+ targetBranch,
169
+ parseCandidate,
170
+ }) {
171
+ const commit = await api(`/git/commits/${targetSha}`);
172
+ const parents = Array.isArray(commit.parents) ? commit.parents : [];
173
+ if (text(commit.sha) !== targetSha || parents.length !== 2) return null;
174
+ const sourceSha = text(parents[1]?.sha);
175
+ if (!SHA.test(sourceSha)) return null;
176
+ const associated = await api(`/commits/${targetSha}/pulls`);
177
+ const managed = associated
178
+ .filter(
179
+ (pullRequest) =>
180
+ text(pullRequest.merged_at) &&
181
+ text(pullRequest.merge_commit_sha) === targetSha &&
182
+ text(pullRequest.base?.ref) === targetBranch &&
183
+ text(pullRequest.head?.sha) === sourceSha,
184
+ )
185
+ .map((pullRequest) => ({
186
+ pullRequest,
187
+ candidate: parseCandidate(pullRequest, targetBranch),
188
+ }))
189
+ .filter(({ candidate }) => candidate?.sourceSha === sourceSha);
190
+ if (managed.length > 1)
191
+ throw new Error(`multiple managed promotion PRs bind target ${targetSha}`);
192
+ if (managed.length === 0) return null;
193
+ return normalizeManagedPromotionBaseline({
194
+ targetSha,
195
+ sourceSha,
196
+ pullRequestNumber: managed[0].pullRequest.number,
197
+ pullRequestUrl: managed[0].pullRequest.html_url,
198
+ });
199
+ }
@@ -0,0 +1,94 @@
1
+ import {
2
+ devDeliveryContentRoot,
3
+ devDeliveryExactRoot as exactRoot,
4
+ devDeliveryPositiveInteger as positiveInteger,
5
+ devDeliveryText as text,
6
+ } from "./dev-delivery-common.js";
7
+
8
+ export const CHAINED_ATTEMPT_IDENTITY = "chained-attempt-v2";
9
+
10
+ export function createDevDeliveryCandidateIdentity(
11
+ input,
12
+ expected,
13
+ deliveryClass,
14
+ ) {
15
+ const identity = {
16
+ repository: expected.repository,
17
+ protectedBase: expected.protectedBase,
18
+ pullRequestNumber: positiveInteger(
19
+ input.pullRequestNumber,
20
+ "pullRequestNumber",
21
+ ),
22
+ assignmentRoot: exactRoot(input.assignmentRoot, "assignmentRoot"),
23
+ initiativeRoot: exactRoot(input.initiativeRoot, "initiativeRoot"),
24
+ sourceIdentityRoot: exactRoot(
25
+ input.sourceIdentityRoot,
26
+ "sourceIdentityRoot",
27
+ ),
28
+ deliveryClass: deliveryClass(input.deliveryClass),
29
+ };
30
+ if (input.identitySemantics) {
31
+ if (text(input.identitySemantics) !== CHAINED_ATTEMPT_IDENTITY) {
32
+ throw new Error(
33
+ `unsupported candidate identity semantics ${input.identitySemantics}`,
34
+ );
35
+ }
36
+ identity.identitySemantics = CHAINED_ATTEMPT_IDENTITY;
37
+ identity.predecessorCandidateId = exactRoot(
38
+ input.predecessorCandidateId,
39
+ "predecessorCandidateId",
40
+ );
41
+ }
42
+ return { ...identity, candidateId: devDeliveryContentRoot(identity) };
43
+ }
44
+
45
+ export function validateDevDeliveryCandidateChain(candidates, terminalStates) {
46
+ const precedingCandidates = new Map();
47
+ for (const candidate of candidates) {
48
+ if (candidate.identitySemantics === CHAINED_ATTEMPT_IDENTITY) {
49
+ const predecessor = precedingCandidates.get(
50
+ candidate.predecessorCandidateId,
51
+ );
52
+ if (!predecessor)
53
+ throw new Error(
54
+ "chained candidate predecessor must appear earlier in the queue",
55
+ );
56
+ if (predecessor.pullRequestNumber !== candidate.pullRequestNumber) {
57
+ throw new Error(
58
+ "chained candidate predecessor must belong to the same pull request",
59
+ );
60
+ }
61
+ if (!terminalStates.has(predecessor.status)) {
62
+ throw new Error("chained candidate predecessor must be terminal");
63
+ }
64
+ }
65
+ precedingCandidates.set(candidate.candidateId, candidate);
66
+ }
67
+ }
68
+
69
+ export function chainedDevDeliveryAttemptInput({
70
+ queue,
71
+ candidate,
72
+ input,
73
+ currentTime,
74
+ terminalStates,
75
+ }) {
76
+ const predecessor = [...queue.candidates]
77
+ .reverse()
78
+ .find(
79
+ (entry) =>
80
+ entry.pullRequestNumber === candidate.pullRequestNumber &&
81
+ terminalStates.has(entry.status),
82
+ );
83
+ if (!predecessor) return null;
84
+ return {
85
+ ...input,
86
+ identitySemantics: CHAINED_ATTEMPT_IDENTITY,
87
+ predecessorCandidateId: predecessor.candidateId,
88
+ enqueuedAt: input.enqueuedAt || currentTime,
89
+ updatedAt: currentTime,
90
+ attempts: input.attempts || 1,
91
+ recoveries: input.recoveries || 0,
92
+ status: "queued",
93
+ };
94
+ }
@@ -0,0 +1,73 @@
1
+ import crypto from "node:crypto";
2
+
3
+ const SHA_PATTERN = /^[0-9a-f]{40}$/;
4
+ const ROOT_PATTERN = /^sha256:[0-9a-f]{64}$/;
5
+
6
+ export function devDeliveryText(value = "") {
7
+ return String(value ?? "").trim();
8
+ }
9
+
10
+ export function devDeliveryClone(value) {
11
+ return JSON.parse(JSON.stringify(value));
12
+ }
13
+
14
+ function stableValue(value) {
15
+ if (Array.isArray(value)) return value.map(stableValue);
16
+ if (value && typeof value === "object") {
17
+ return Object.fromEntries(
18
+ Object.entries(value)
19
+ .filter(([, entry]) => entry !== undefined)
20
+ .sort(([left], [right]) => left.localeCompare(right))
21
+ .map(([key, entry]) => [key, stableValue(entry)]),
22
+ );
23
+ }
24
+ return value;
25
+ }
26
+
27
+ export function devDeliveryContentRoot(value) {
28
+ return `sha256:${crypto
29
+ .createHash("sha256")
30
+ .update(`${JSON.stringify(stableValue(value))}\n`)
31
+ .digest("hex")}`;
32
+ }
33
+
34
+ export function devDeliveryExactRoot(value, label) {
35
+ const normalized = devDeliveryText(value).toLowerCase();
36
+ if (!ROOT_PATTERN.test(normalized)) throw new Error(`${label} must be a sha256 content root`);
37
+ return normalized;
38
+ }
39
+
40
+ export function devDeliveryExactSha(value, label) {
41
+ const normalized = devDeliveryText(value).toLowerCase();
42
+ if (!SHA_PATTERN.test(normalized)) throw new Error(`${label} must be a 40-character Git SHA`);
43
+ return normalized;
44
+ }
45
+
46
+ export function devDeliveryPositiveInteger(value, label, fallback) {
47
+ const parsed = Number(value ?? fallback);
48
+ if (!Number.isInteger(parsed) || parsed < 1) throw new Error(`${label} must be a positive integer`);
49
+ return parsed;
50
+ }
51
+
52
+ export function devDeliveryTimestamp(value, label) {
53
+ const normalized = devDeliveryText(value);
54
+ const milliseconds = Date.parse(normalized);
55
+ if (!normalized || !Number.isFinite(milliseconds)) throw new Error(`${label} must be an ISO-8601 timestamp`);
56
+ return new Date(milliseconds).toISOString();
57
+ }
58
+
59
+ export function devDeliveryRepository(value) {
60
+ const normalized = devDeliveryText(value);
61
+ if (!/^[^/\s]+\/[^/\s]+$/.test(normalized)) {
62
+ throw new Error(`repository must be owner/repo, got ${normalized || "<empty>"}`);
63
+ }
64
+ return normalized;
65
+ }
66
+
67
+ export function devDeliveryProtectedBase(value) {
68
+ const normalized = devDeliveryText(value).replace(/^refs\/heads\//, "");
69
+ if (!/^dev\/v\d+\/v\d+\.\d+$/.test(normalized)) {
70
+ throw new Error(`protectedBase must be dev/vN/vN.M, got ${normalized || "<empty>"}`);
71
+ }
72
+ return normalized;
73
+ }