@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.
- package/README.md +4 -4
- package/actions/promote-buildchain-ref/README.md +8 -0
- package/bin/buildchain.mjs +13 -1
- package/contracts/auditable-demo-scenario-v1.schema.json +52 -0
- package/dist/site/buildchain-contract.json +47 -27
- package/dist/site/buildchain-site.json +158 -42
- package/dist/site/capability-registry.json +3 -3
- package/dist/site/cli-registry.json +40 -4
- package/dist/site/controller-registry.json +20 -4
- package/dist/site/kfd-claims.json +139 -19
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +9 -9
- package/dist/site/node-api-registry.json +1161 -180
- package/dist/site/page-registry.json +145 -29
- package/dist/site/public-surface-audit.json +385 -19
- package/dist/site/publication-authority-registry.json +61 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +1 -0
- package/dist/site/site-manifest.json +13 -13
- package/dist/site/workflow-registry.json +150 -13
- package/docs/MAP.md +1 -0
- package/docs/auditable-demo.md +58 -11
- package/docs/aws-us-elastic-runner-burst-plane.md +23 -19
- package/docs/cli-reference.md +154 -0
- package/docs/dev-alpha-candidate-patrol.md +13 -5
- package/docs/dev-delivery-warrant.md +158 -0
- package/docs/node-api-reference.md +54 -15
- package/docs/publication-authority.md +11 -0
- package/docs/release-candidate.md +19 -2
- package/docs/release-governance.md +52 -0
- package/docs/reusable-build-surface.md +11 -1
- package/docs/shifu-gate-profiles.md +12 -1
- package/docs/versioning.md +2 -0
- package/package.json +2 -1
- package/packages/core/buildchain-publication-authority.js +3 -1
- package/packages/core/channel-candidate.js +2 -21
- package/packages/core/channel-promotion-baseline.js +199 -0
- package/packages/core/dev-delivery-candidate-identity.js +94 -0
- package/packages/core/dev-delivery-common.js +73 -0
- package/packages/core/dev-delivery-proof.js +252 -0
- package/packages/core/dev-delivery-warrant-cancellation.js +94 -0
- package/packages/core/dev-delivery-warrant-settlement.js +73 -0
- package/packages/core/dev-delivery-warrant.js +591 -0
- package/scripts/auditable-demo-bundle-verification.mjs +148 -0
- package/scripts/auditable-demo-platform.mjs +86 -50
- package/scripts/auditable-demo-presentation.mjs +83 -0
- package/scripts/auditable-demo-renditions.mjs +264 -0
- package/scripts/auditable-demo.mjs +24 -30
- package/scripts/build-contract-core.mjs +58 -3
- package/scripts/buildchain-cli-help.mjs +8 -0
- package/scripts/buildchain-patrol.mjs +9 -0
- package/scripts/check-inventory.mjs +1 -0
- package/scripts/dev-alpha-candidate-patrol.mjs +45 -48
- package/scripts/dev-delivery-proof.mjs +193 -0
- package/scripts/dev-delivery-warrant.mjs +426 -0
- package/scripts/dev-pr-auto-merge.mjs +488 -46
- package/scripts/dev-pr-delivery-warrant.mjs +209 -0
- package/scripts/gate-profile-core.mjs +24 -0
- package/scripts/generate-site-bundle.mjs +2 -2
- package/scripts/git-fetch-process-tree.mjs +142 -0
- package/scripts/lifecycle-substage-evidence.mjs +274 -0
- package/scripts/locked-source-checkout.mjs +6 -3
- package/scripts/resolve-artifact-transfer-mode.mjs +9 -0
- package/scripts/resolve-build-contract.mjs +7 -0
- package/scripts/route-offline-runners.mjs +1 -0
- package/scripts/run-lifecycle-core.mjs +9 -9
- package/scripts/shifu-gate-profile.mjs +10 -16
- 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
|
-
|
|
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.
|
|
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.
|
package/docs/versioning.md
CHANGED
|
@@ -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.
|
|
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
|
+
}
|