@kungfu-tech/buildchain 4.1.3-alpha.0 → 4.1.3-alpha.10
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 +41 -75
- package/CONTRIBUTING.md +19 -26
- package/architecture/action-taxonomy.json +24 -0
- package/architecture/agent-change-map.md +272 -12
- package/architecture/capability-state-machine-manifest.json +1 -4
- package/architecture/ci-lane-change-budget.json +190 -807
- package/architecture/core-mechanisms.json +3 -6
- package/architecture/decisions/0005-minimal-consumer-contract.md +122 -0
- package/architecture/decisions/0006-business-attempt-journal.md +89 -0
- package/architecture/decisions/0007-hosted-pipeline-controller.md +106 -0
- package/architecture/decisions/0008-pipeline-product-publication.md +120 -0
- package/architecture/decisions/0009-pipeline-version-preparation.md +186 -0
- package/architecture/internal-capabilities.json +371 -9
- package/architecture/maintainability-debt.json +50 -34
- package/architecture/maintainability-policy.json +8 -8
- package/architecture/minimal-consumer-migration.json +5670 -0
- package/architecture/platform-stage-checkpoints.json +31 -7
- package/architecture/product-upstreams.json +20 -0
- package/architecture/release-topology.json +331 -74
- package/architecture/stage-capsule-qualification.json +3 -1
- package/architecture/universal-workflow-bootstrap.json +15 -1
- package/architecture/universal-workflow-capability-policy.json +9 -9
- package/architecture/universal-workflow-fault-campaign.json +3 -1
- package/architecture/workflow-taxonomy.json +77 -328
- package/contracts/release-discussion-v1.schema.json +3 -0
- package/dist/readers/business-attempt.cjs +809 -0
- package/dist/readers/release-discussion.cjs +4 -0
- package/dist/site/buildchain-contract.json +16 -68
- package/dist/site/buildchain-site.json +47 -27
- package/dist/site/capability-registry.json +4 -4
- package/dist/site/controller-registry.json +1 -47
- package/dist/site/kfd-claims.json +499 -494
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +3 -3
- package/dist/site/node-api-registry.json +56 -56
- package/dist/site/page-registry.json +40 -20
- package/dist/site/public-surface-audit.json +286 -349
- package/dist/site/publication-authority-registry.json +203 -523
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +7 -7
- package/dist/site/workflow-registry.json +590 -940
- package/docs/code-organization.md +9 -6
- package/docs/node-api-reference.md +103 -103
- package/docs/release-discussions.md +44 -5
- package/docs/release-governance.md +10 -12
- package/docs/runtime-entry.md +73 -5
- package/docs/runtime-train-validation.md +39 -11
- package/docs/stable-candidate-patrol.md +6 -5
- package/docs/workflow-catalog.md +9 -32
- package/package.json +4 -3
- package/packages/core/adoption/buildchain-kfd-claims.js +7 -7
- package/packages/core/build/standalone/build.js +7 -1
- package/packages/core/consumer/buildchain-config.js +2 -21
- package/packages/core/consumer/contract/entries.js +85 -0
- package/packages/core/consumer/contract/examples.js +96 -0
- package/packages/core/consumer/contract/identity.js +65 -0
- package/packages/core/consumer/contract/inspection.js +45 -0
- package/packages/core/consumer/contract/plan.js +149 -0
- package/packages/core/consumer/contract/products.js +119 -0
- package/packages/core/consumer/contract/reader.js +47 -0
- package/packages/core/consumer/contract/shape.js +46 -0
- package/packages/core/dev-delivery/candidate/admission.js +9 -5
- package/packages/core/dev-delivery/candidate/reservation.js +11 -0
- package/packages/core/dev-delivery/candidate/source-paths.js +12 -4
- package/packages/core/dev-delivery/commands/project-cut-merge-queue-admission.mjs +47 -20
- package/packages/core/dev-delivery/dev-delivery-provider-heartbeat.js +18 -15
- package/packages/core/dev-delivery/native/actions.js +9 -1
- package/packages/core/dev-delivery/native/heartbeat-action.js +16 -0
- package/packages/core/dev-delivery/native/heartbeat.js +6 -4
- package/packages/core/dev-delivery/native/process-entry.js +19 -0
- package/packages/core/dev-delivery/queue/landing-action.js +53 -0
- package/packages/core/dev-delivery/warrant/failure-settlement.js +8 -6
- package/packages/core/dev-delivery/warrant/service.js +7 -0
- package/packages/core/governance/buildchain-publication-authority.js +8 -23
- package/packages/core/governance/github-governance-authority.js +1 -0
- package/packages/core/observability/controller-evidence.js +0 -8
- package/packages/core/paper/operations/bootstrap.js +2 -0
- package/packages/core/paper/paper-npm-bootstrap.js +2 -2
- package/packages/core/providers/github/attempt-index.js +60 -0
- package/packages/core/providers/github/attempt-journal.js +154 -0
- package/packages/core/providers/github/discussions/materials.js +35 -11
- package/packages/core/providers/github/discussions/transport.js +16 -5
- package/packages/core/providers/github/pipeline-checkout.js +49 -0
- package/packages/core/providers/github/pipeline-entry-runs.js +210 -0
- package/packages/core/providers/github/pipeline-events.js +131 -0
- package/packages/core/providers/github/pipeline-integration.js +95 -0
- package/packages/core/providers/github/pipeline-policy.js +240 -0
- package/packages/core/providers/github/pipeline-product-payload.js +50 -0
- package/packages/core/providers/github/pipeline-product-release.js +198 -0
- package/packages/core/providers/github/pipeline-publication-artifacts.js +153 -0
- package/packages/core/providers/github/pipeline-release-evidence.js +30 -0
- package/packages/core/providers/github/pipeline-run-entry.js +86 -0
- package/packages/core/providers/github/pipeline-runs.js +132 -0
- package/packages/core/providers/github/pipeline-source.js +195 -0
- package/packages/core/providers/github/pipeline-stable-baseline.js +86 -0
- package/packages/core/providers/github/pipeline-stable-source.js +372 -0
- package/packages/core/providers/github/pipeline-version-artifacts.js +91 -0
- package/packages/core/providers/github/pipeline-version-readback.js +117 -0
- package/packages/core/providers/github/pipeline-version.js +199 -0
- package/packages/core/providers/github/pipeline-worker.js +108 -0
- package/packages/core/providers/github/release-evidence-assets.js +97 -0
- package/packages/core/publication/candidate/registry-hydration.js +2 -2
- package/packages/core/publication/npm/pack-preview.js +2 -2
- package/packages/core/publication/npm/pack-result.js +31 -0
- package/packages/core/publication/npm/package.js +3 -6
- package/packages/core/publication/npm/pipeline-channel.js +36 -0
- package/packages/core/publication/npm/pipeline-provider.js +137 -0
- package/packages/core/publication/npm/registry.js +12 -9
- package/packages/core/publication/pipeline/actions.js +116 -0
- package/packages/core/publication/pipeline/apply.js +189 -0
- package/packages/core/publication/pipeline/build-segments.js +107 -0
- package/packages/core/publication/pipeline/capsules.js +92 -0
- package/packages/core/publication/pipeline/context.js +39 -0
- package/packages/core/publication/pipeline/development-anchor.js +91 -0
- package/packages/core/publication/pipeline/development-current.js +63 -0
- package/packages/core/publication/pipeline/development-pr.js +112 -0
- package/packages/core/publication/pipeline/development-proof.js +76 -0
- package/packages/core/publication/pipeline/development-transition.js +112 -0
- package/packages/core/publication/pipeline/distribution.js +130 -0
- package/packages/core/publication/pipeline/documents.js +192 -0
- package/packages/core/publication/pipeline/effects.js +192 -0
- package/packages/core/publication/pipeline/files.js +66 -0
- package/packages/core/publication/pipeline/imported-materials.js +32 -0
- package/packages/core/publication/pipeline/journal.js +122 -0
- package/packages/core/publication/pipeline/next-development.js +117 -0
- package/packages/core/publication/pipeline/pack.js +180 -0
- package/packages/core/publication/pipeline/package-policy.js +22 -0
- package/packages/core/publication/pipeline/plan.js +136 -0
- package/packages/core/publication/pipeline/prepare.js +139 -0
- package/packages/core/publication/pipeline/qualification.js +175 -0
- package/packages/core/publication/pipeline/qualify.js +104 -0
- package/packages/core/publication/pipeline/recovery-admission.js +122 -0
- package/packages/core/publication/pipeline/recovery-build-download.js +80 -0
- package/packages/core/publication/pipeline/recovery-build-plan.js +122 -0
- package/packages/core/publication/pipeline/recovery-build-readback.js +68 -0
- package/packages/core/publication/pipeline/recovery-capsules.js +54 -0
- package/packages/core/publication/pipeline/recovery-import.js +80 -0
- package/packages/core/publication/pipeline/recovery-inspection.js +218 -0
- package/packages/core/publication/pipeline/recovery-materials.js +104 -0
- package/packages/core/publication/pipeline/recovery-plan.js +46 -0
- package/packages/core/publication/pipeline/recovery-prepare.js +73 -0
- package/packages/core/publication/pipeline/recovery-qualification.js +128 -0
- package/packages/core/publication/pipeline/recovery-readback.js +56 -0
- package/packages/core/publication/pipeline/recovery-signing.js +58 -0
- package/packages/core/publication/pipeline/sealed-products.js +79 -0
- package/packages/core/publication/pipeline/settle.js +42 -0
- package/packages/core/publication/pipeline/signing.js +140 -0
- package/packages/core/publication/pipeline/source-plan.js +83 -0
- package/packages/core/publication/pipeline/stable-entry.js +219 -0
- package/packages/core/publication/pipeline/stable-products.js +317 -0
- package/packages/core/publication/pipeline/stable-wait.js +170 -0
- package/packages/core/publication/pipeline/stable.js +82 -0
- package/packages/core/publication/pipeline/version-actions.js +93 -0
- package/packages/core/publication/pipeline/version-build.js +130 -0
- package/packages/core/publication/pipeline/version-context.js +143 -0
- package/packages/core/publication/pipeline/version-material.js +36 -0
- package/packages/core/publication/pipeline/version-preparation.js +69 -0
- package/packages/core/publication/pipeline/version-regeneration.js +162 -0
- package/packages/core/publication/pipeline/version-source-guard.js +69 -0
- package/packages/core/publication/pipeline/version.js +81 -0
- package/packages/core/publication/pipeline/worker.js +72 -0
- package/packages/core/publication/publication-reproducibility.js +2 -2
- package/packages/core/release/discussion/binary.js +11 -0
- package/packages/core/release/discussion/checkpoints.js +45 -4
- package/packages/core/release/discussion/envelope.js +1 -0
- package/packages/core/release/discussion/evidence.js +51 -0
- package/packages/core/release/discussion/presentation.js +92 -0
- package/packages/core/release/discussion/publication.js +3 -1
- package/packages/core/release/discussion/qualification.js +1 -0
- package/packages/core/release/discussion/reader.js +9 -0
- package/packages/core/release/discussion/session.js +2 -0
- package/packages/core/release/discussion/store.js +28 -31
- package/packages/core/release/discussion/threads.js +83 -0
- package/packages/core/release/next-development-transition.js +15 -15
- package/packages/core/release/promote-candidate/product-provider-adapters.js +4 -3
- package/packages/core/release/promote-ref/internal/npm-existing-evidence.js +2 -1
- package/packages/core/release/promote-ref/internal/publish-command.js +16 -5
- package/packages/core/release/stable-release-gate.js +11 -0
- package/packages/core/runtime/buildchain-domain.wasm +0 -0
- package/packages/core/runtime/domain-wasm-artifact.js +2 -2
- package/packages/core/runtime/entry/actions.js +27 -25
- package/packages/core/runtime/entry/attempt.js +25 -0
- package/packages/core/runtime/entry/selection.js +14 -0
- package/packages/core/runtime/entry/source.js +62 -0
- package/packages/core/workflow/attempt/identity.js +170 -0
- package/packages/core/workflow/attempt/journal.js +89 -0
- package/packages/core/workflow/attempt/materials.js +72 -0
- package/packages/core/workflow/attempt/reader-entry.js +12 -0
- package/packages/core/workflow/attempt/reader.js +149 -0
- package/packages/core/workflow/attempt/records.js +153 -0
- package/packages/core/workflow/attempt/store.js +75 -0
- package/packages/core/workflow/commands/pipeline-build.mjs +23 -0
- package/packages/core/workflow/pipeline/action-output.js +11 -0
- package/packages/core/workflow/pipeline/actions.js +52 -0
- package/packages/core/workflow/pipeline/build-control.js +140 -0
- package/packages/core/workflow/pipeline/build-evidence.js +210 -0
- package/packages/core/workflow/pipeline/build-qualification.js +145 -0
- package/packages/core/workflow/pipeline/build-result.js +78 -0
- package/packages/core/workflow/pipeline/build.js +142 -0
- package/packages/core/workflow/pipeline/cancellation.js +169 -0
- package/packages/core/workflow/pipeline/channel-control.js +157 -0
- package/packages/core/workflow/pipeline/channel-source.js +53 -0
- package/packages/core/workflow/pipeline/controller.js +141 -0
- package/packages/core/workflow/pipeline/delivery-control.js +249 -0
- package/packages/core/workflow/pipeline/delivery-observation.js +124 -0
- package/packages/core/workflow/pipeline/delivery-request.js +198 -0
- package/packages/core/workflow/pipeline/events.js +84 -0
- package/packages/core/workflow/pipeline/fence.js +42 -0
- package/packages/core/workflow/pipeline/group-control.js +59 -0
- package/packages/core/workflow/pipeline/guard-build.js +74 -0
- package/packages/core/workflow/pipeline/guard.js +158 -0
- package/packages/core/workflow/pipeline/host.js +144 -0
- package/packages/core/workflow/pipeline/integration-status.js +113 -0
- package/packages/core/workflow/pipeline/materials.js +58 -0
- package/packages/core/workflow/pipeline/notifications.js +36 -0
- package/packages/core/workflow/pipeline/parent-notification.js +27 -0
- package/packages/core/workflow/pipeline/platforms.js +21 -0
- package/packages/core/workflow/pipeline/progress.js +108 -0
- package/packages/core/workflow/pipeline/projection.js +20 -0
- package/packages/core/workflow/pipeline/reconcile.js +194 -0
- package/packages/core/workflow/pipeline/record-action.js +27 -0
- package/packages/core/workflow/pipeline/recorder-status.js +69 -0
- package/packages/core/workflow/pipeline/recovery-action.js +30 -0
- package/packages/core/workflow/pipeline/recovery-admission.js +122 -0
- package/packages/core/workflow/pipeline/recovery-build-control.js +95 -0
- package/packages/core/workflow/pipeline/recovery-build-evidence.js +71 -0
- package/packages/core/workflow/pipeline/recovery-build.js +121 -0
- package/packages/core/workflow/pipeline/recovery-controller.js +91 -0
- package/packages/core/workflow/pipeline/recovery-integration.js +79 -0
- package/packages/core/workflow/pipeline/recovery-merge-proof.js +90 -0
- package/packages/core/workflow/pipeline/recovery-ownership-settlement.js +150 -0
- package/packages/core/workflow/pipeline/recovery-ownership.js +91 -0
- package/packages/core/workflow/pipeline/recovery-plan.js +126 -0
- package/packages/core/workflow/pipeline/recovery-runtime.js +68 -0
- package/packages/core/workflow/pipeline/recovery-session.js +129 -0
- package/packages/core/workflow/pipeline/recovery-transition.js +135 -0
- package/packages/core/workflow/pipeline/recovery-unrecorded-build.js +74 -0
- package/packages/core/workflow/pipeline/runtime-source.js +111 -0
- package/packages/core/workflow/pipeline/selection.js +111 -0
- package/packages/core/workflow/pipeline/session.js +72 -0
- package/packages/core/workflow/pipeline/settlement.js +137 -0
- package/packages/core/workflow/pipeline/wake.js +38 -0
- package/packages/core/workflow/pipeline/web-status.js +108 -0
- package/packages/core/workflow/workflow-taxonomy.mjs +67 -47
- package/scripts/build-release-discussion-reader.mjs +11 -7
- package/scripts/check-inventory.mjs +7 -70
- package/scripts/check-pipeline-publication-topology.mjs +50 -0
- package/scripts/check-public-dogfood-contract.mjs +38 -51
- package/scripts/check-release-topology.mjs +15 -8
- package/scripts/check-self-consumer-contract.mjs +133 -0
- package/scripts/check-universal-workflow-bootstrap.mjs +4 -6
- package/scripts/generate-minimal-consumer-contract.mjs +155 -0
- package/scripts/generate-site-bundle.mjs +1 -1
- package/scripts/inventory/binary.mjs +9 -19
- package/scripts/inventory/promotion.mjs +2 -13
- package/scripts/prepare-product-toolchain.mjs +10 -0
- package/scripts/verify-golden-path.mjs +5 -1
- package/scripts/verify-product-platform.mjs +65 -0
- package/scripts/verify-standalone-product.mjs +42 -0
- package/templates/minimal-consumer/binary/.buildchain/buildchain.toml +45 -0
- package/templates/minimal-consumer/binary/.github/workflows/buildchain-recover.yml +30 -0
- package/templates/minimal-consumer/binary/.github/workflows/buildchain.yml +28 -0
- package/templates/minimal-consumer/binary/package.json +6 -0
- package/templates/minimal-consumer/binary/src/build.mjs +5 -0
- package/templates/minimal-consumer/binary/src/hello.c +2 -0
- package/templates/minimal-consumer/binary/src/verify.mjs +3 -0
- package/templates/minimal-consumer/npm/.buildchain/buildchain.toml +46 -0
- package/templates/minimal-consumer/npm/.github/workflows/buildchain-recover.yml +30 -0
- package/templates/minimal-consumer/npm/.github/workflows/buildchain.yml +28 -0
- package/templates/minimal-consumer/npm/package.json +6 -0
- package/templates/minimal-consumer/npm/src/build.mjs +5 -0
- package/templates/minimal-consumer/npm/src/verify.mjs +3 -0
- package/templates/minimal-consumer/paper/.buildchain/buildchain.toml +45 -0
- package/templates/minimal-consumer/paper/.github/workflows/buildchain-recover.yml +30 -0
- package/templates/minimal-consumer/paper/.github/workflows/buildchain.yml +28 -0
- package/templates/minimal-consumer/paper/package.json +6 -0
- package/templates/minimal-consumer/paper/src/build.mjs +12 -0
- package/templates/minimal-consumer/paper/src/verify.mjs +3 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { readBusinessAttempt } from "../../workflow/attempt/reader.js";
|
|
2
|
+
|
|
3
|
+
// The current provider timeline survives webhook/runtime handoff. Compare it
|
|
4
|
+
// with this candidate's lifetime so an earlier removal cannot cancel recovery.
|
|
5
|
+
export function githubPipelineQueueExit(graphql, repository) {
|
|
6
|
+
if (!/^[\w.-]+\/[\w.-]+$/u.test(repository || ""))
|
|
7
|
+
throw new Error("Invalid pipeline queue repository");
|
|
8
|
+
const [owner, repo] = repository.split("/");
|
|
9
|
+
return async (current, candidate) => {
|
|
10
|
+
if (!candidate || candidate.terminal) return null;
|
|
11
|
+
const started = Date.parse(candidate.enqueuedAt);
|
|
12
|
+
if (!Number.isFinite(started))
|
|
13
|
+
throw new Error("Queue candidate timestamp is missing");
|
|
14
|
+
const number = current.intent.source.pullRequest;
|
|
15
|
+
const result = await graphql(
|
|
16
|
+
`
|
|
17
|
+
query ($owner: String!, $repo: String!, $number: Int!) {
|
|
18
|
+
repository(owner: $owner, name: $repo) {
|
|
19
|
+
pullRequest(number: $number) {
|
|
20
|
+
number
|
|
21
|
+
headRefOid
|
|
22
|
+
baseRefName
|
|
23
|
+
state
|
|
24
|
+
mergeQueueEntry {
|
|
25
|
+
id
|
|
26
|
+
}
|
|
27
|
+
timelineItems(
|
|
28
|
+
last: 2
|
|
29
|
+
itemTypes: [
|
|
30
|
+
ADDED_TO_MERGE_QUEUE_EVENT
|
|
31
|
+
REMOVED_FROM_MERGE_QUEUE_EVENT
|
|
32
|
+
]
|
|
33
|
+
) {
|
|
34
|
+
nodes {
|
|
35
|
+
__typename
|
|
36
|
+
... on AddedToMergeQueueEvent {
|
|
37
|
+
id
|
|
38
|
+
createdAt
|
|
39
|
+
}
|
|
40
|
+
... on RemovedFromMergeQueueEvent {
|
|
41
|
+
id
|
|
42
|
+
createdAt
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
`,
|
|
50
|
+
{ owner, repo, number },
|
|
51
|
+
);
|
|
52
|
+
const pr = result.repository?.pullRequest;
|
|
53
|
+
if (
|
|
54
|
+
pr?.number !== number ||
|
|
55
|
+
pr.headRefOid !== current.generation.source.commit ||
|
|
56
|
+
pr.baseRefName !== current.intent.source.targetBranch
|
|
57
|
+
)
|
|
58
|
+
throw new Error("Queue exit source identity changed; reobserve");
|
|
59
|
+
if (pr.state !== "OPEN" || pr.mergeQueueEntry) return null;
|
|
60
|
+
const nodes = pr.timelineItems?.nodes;
|
|
61
|
+
if (!Array.isArray(nodes) || nodes.length > 2)
|
|
62
|
+
throw new Error("Queue exit timeline readback is missing or ambiguous");
|
|
63
|
+
const event = nodes.at(-1);
|
|
64
|
+
if (event?.__typename !== "RemovedFromMergeQueueEvent") return null;
|
|
65
|
+
const queued = nodes.at(-2);
|
|
66
|
+
if (queued?.__typename !== "AddedToMergeQueueEvent")
|
|
67
|
+
throw new Error("Queue removal has no matching admission readback");
|
|
68
|
+
const removed = Date.parse(event.createdAt);
|
|
69
|
+
const entered = Date.parse(queued.createdAt);
|
|
70
|
+
if (
|
|
71
|
+
!event.id ||
|
|
72
|
+
!queued.id ||
|
|
73
|
+
!Number.isFinite(removed) ||
|
|
74
|
+
!Number.isFinite(entered) ||
|
|
75
|
+
entered > removed
|
|
76
|
+
)
|
|
77
|
+
throw new Error("Queue removal identity or timestamp is missing");
|
|
78
|
+
return entered > started ? event : null;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Branch/terminal notifications wake existing intents only. They cannot create
|
|
83
|
+
// historical releases or infer an execution request from an untracked old PR.
|
|
84
|
+
export function githubPipelineEvents(request, journal, repository) {
|
|
85
|
+
if (!/^[\w.-]+\/[\w.-]+$/u.test(repository || ""))
|
|
86
|
+
throw new Error("Invalid pipeline event repository");
|
|
87
|
+
const base = `/repos/${repository}`;
|
|
88
|
+
async function list(url) {
|
|
89
|
+
const rows = [];
|
|
90
|
+
for (let page = 1; page <= 100; page++) {
|
|
91
|
+
const entries = await request(
|
|
92
|
+
`${url}${url.includes("?") ? "&" : "?"}per_page=100&page=${page}`,
|
|
93
|
+
);
|
|
94
|
+
if (!Array.isArray(entries))
|
|
95
|
+
throw new Error("Invalid pipeline PR event readback");
|
|
96
|
+
rows.push(...entries);
|
|
97
|
+
if (entries.length < 100) return rows;
|
|
98
|
+
}
|
|
99
|
+
throw new Error("Pipeline PR event readback exceeds its bound");
|
|
100
|
+
}
|
|
101
|
+
async function branch(branch, commit = "") {
|
|
102
|
+
if (
|
|
103
|
+
!/^(?:dev|alpha|release|publish-gate)\/[A-Za-z0-9/._-]+$/u.test(
|
|
104
|
+
branch || "",
|
|
105
|
+
) ||
|
|
106
|
+
branch.includes("..")
|
|
107
|
+
)
|
|
108
|
+
throw new Error("Invalid pipeline branch event");
|
|
109
|
+
const pulls = await list(
|
|
110
|
+
`${base}/pulls?state=open&base=${encodeURIComponent(branch)}`,
|
|
111
|
+
);
|
|
112
|
+
if (commit) {
|
|
113
|
+
if (!/^[0-9a-f]{40}$/u.test(commit))
|
|
114
|
+
throw new Error("Branch wake requires exact source commit");
|
|
115
|
+
pulls.push(...(await list(`${base}/commits/${commit}/pulls`)));
|
|
116
|
+
}
|
|
117
|
+
const result = new Set();
|
|
118
|
+
for (const number of [
|
|
119
|
+
...new Set(
|
|
120
|
+
pulls.filter((pr) => pr.base?.ref === branch).map((pr) => pr.number),
|
|
121
|
+
),
|
|
122
|
+
]) {
|
|
123
|
+
const retained = await journal.lookup(repository, number, branch);
|
|
124
|
+
if (!retained) continue;
|
|
125
|
+
const state = readBusinessAttempt(retained.snapshot);
|
|
126
|
+
if (state.attempt) result.add(state.attempt);
|
|
127
|
+
}
|
|
128
|
+
return [...result];
|
|
129
|
+
}
|
|
130
|
+
return { branch };
|
|
131
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
2
|
+
import { pipelinePlatforms } from "../../workflow/pipeline/platforms.js";
|
|
3
|
+
|
|
4
|
+
export function githubPipelineIntegration(request, repository, source, runs) {
|
|
5
|
+
const base = `/repos/${repository}`;
|
|
6
|
+
async function observe(current) {
|
|
7
|
+
const number = current.intent.source.pullRequest;
|
|
8
|
+
const branch = current.intent.source.targetBranch;
|
|
9
|
+
const pr = await request(`${base}/pulls/${number}`);
|
|
10
|
+
if (
|
|
11
|
+
!pr.merged ||
|
|
12
|
+
pr.head?.sha !== current.generation.source.commit ||
|
|
13
|
+
pr.base?.ref !== branch ||
|
|
14
|
+
pr.head?.repo?.full_name !== repository ||
|
|
15
|
+
pr.base?.repo?.full_name !== repository ||
|
|
16
|
+
!/^[0-9a-f]{40}$/u.test(pr.merge_commit_sha || "")
|
|
17
|
+
)
|
|
18
|
+
throw new Error(
|
|
19
|
+
"Integration needs the exact source PR merged into its protected target",
|
|
20
|
+
);
|
|
21
|
+
const mergeCommit = pr.merge_commit_sha;
|
|
22
|
+
const protectedHead = await source.branchHead(branch);
|
|
23
|
+
const ancestry = await request(
|
|
24
|
+
`${base}/compare/${mergeCommit}...${protectedHead}`,
|
|
25
|
+
);
|
|
26
|
+
if (
|
|
27
|
+
!["identical", "ahead"].includes(ancestry.status) ||
|
|
28
|
+
ancestry.merge_base_commit?.sha !== mergeCommit
|
|
29
|
+
)
|
|
30
|
+
throw new Error(
|
|
31
|
+
"Exact merge commit is not contained in the protected branch",
|
|
32
|
+
);
|
|
33
|
+
const commits = await source.source(
|
|
34
|
+
mergeCommit,
|
|
35
|
+
current.generation.source.configPath,
|
|
36
|
+
);
|
|
37
|
+
const response = await request(
|
|
38
|
+
`${base}/actions/runs?event=merge_group&head_sha=${mergeCommit}&per_page=100`,
|
|
39
|
+
);
|
|
40
|
+
if (!Array.isArray(response.workflow_runs) || response.total_count > 100)
|
|
41
|
+
throw new Error(
|
|
42
|
+
"Exact merge-group run inventory is unavailable or ambiguous",
|
|
43
|
+
);
|
|
44
|
+
const candidates = response.workflow_runs.filter(
|
|
45
|
+
(run) =>
|
|
46
|
+
run.status === "completed" &&
|
|
47
|
+
run.conclusion === "success" &&
|
|
48
|
+
run.path?.split("@")[0] === ".github/workflows/buildchain.yml",
|
|
49
|
+
);
|
|
50
|
+
if (!candidates.length)
|
|
51
|
+
throw new Error(
|
|
52
|
+
"Exact merged commit has no successful normal pipeline merge-group run",
|
|
53
|
+
);
|
|
54
|
+
const chosen = candidates.sort((a, b) => b.id - a.id)[0];
|
|
55
|
+
const { run } = await runs.read(chosen.id, chosen.run_attempt);
|
|
56
|
+
if (
|
|
57
|
+
run.status !== "completed" ||
|
|
58
|
+
run.conclusion !== "success" ||
|
|
59
|
+
run.event !== "merge_group" ||
|
|
60
|
+
run.head_sha !== mergeCommit
|
|
61
|
+
)
|
|
62
|
+
throw new Error("Integration merge-group execution changed");
|
|
63
|
+
const build = await runs.build(
|
|
64
|
+
run.id,
|
|
65
|
+
run.run_attempt,
|
|
66
|
+
commits.identity,
|
|
67
|
+
pipelinePlatforms(commits.plan).map((item) => item.platform),
|
|
68
|
+
);
|
|
69
|
+
if (build.outcome !== "success")
|
|
70
|
+
throw new Error("Exact merged tree product verification failed");
|
|
71
|
+
const again = await request(`${base}/pulls/${number}`);
|
|
72
|
+
if (
|
|
73
|
+
!again.merged ||
|
|
74
|
+
again.merge_commit_sha !== mergeCommit ||
|
|
75
|
+
again.head.sha !== pr.head.sha ||
|
|
76
|
+
again.base.ref !== branch
|
|
77
|
+
)
|
|
78
|
+
throw new Error("Integration provider source changed during readback");
|
|
79
|
+
const body = {
|
|
80
|
+
schema: "buildchain.pipeline-integration-readback/v1",
|
|
81
|
+
repository,
|
|
82
|
+
pullRequest: number,
|
|
83
|
+
sourceHead: pr.head.sha,
|
|
84
|
+
branch,
|
|
85
|
+
protectedHead,
|
|
86
|
+
mergeCommit,
|
|
87
|
+
mergeTree: commits.identity.tree,
|
|
88
|
+
run,
|
|
89
|
+
build,
|
|
90
|
+
ancestry,
|
|
91
|
+
};
|
|
92
|
+
return { ...body, root: recordDigest(body) };
|
|
93
|
+
}
|
|
94
|
+
return { observe };
|
|
95
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
2
|
+
|
|
3
|
+
const QUERY = `query PipelinePolicy($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){pullRequest(number:$number){id number headRefOid baseRefName isDraft reviewDecision author{login}}}}`;
|
|
4
|
+
const BRANCH_QUERY = `query PipelineBranchPolicy($owner:String!,$name:String!,$branch:String!,$ref:String!){repository(owner:$owner,name:$name){ref(qualifiedName:$ref){refUpdateRule{requiredApprovingReviewCount requiresCodeOwnerReviews requiredStatusCheckContexts}}mergeQueue(branch:$branch){id}}}`;
|
|
5
|
+
|
|
6
|
+
async function classicPolicy(request, owner, name, branch) {
|
|
7
|
+
// Read viewer-enforced rules and the ordinary Contents:read branch projection.
|
|
8
|
+
// Administrative branchProtectionRule and /protection queries are not needed.
|
|
9
|
+
const result = await request("/graphql", {
|
|
10
|
+
method: "POST",
|
|
11
|
+
body: {
|
|
12
|
+
query: BRANCH_QUERY,
|
|
13
|
+
variables: { owner, name, branch, ref: `refs/heads/${branch}` },
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
const repository = result.data?.repository;
|
|
17
|
+
if (!repository?.ref)
|
|
18
|
+
throw new Error("Protected branch metadata is unavailable");
|
|
19
|
+
const rule = repository.ref.refUpdateRule;
|
|
20
|
+
const observed = await request(
|
|
21
|
+
`/repos/${owner}/${name}/branches/${encodeURIComponent(branch)}`,
|
|
22
|
+
);
|
|
23
|
+
const status = observed.protection?.required_status_checks;
|
|
24
|
+
const contexts = status?.contexts;
|
|
25
|
+
const checks = status?.checks;
|
|
26
|
+
if (
|
|
27
|
+
observed.name !== branch ||
|
|
28
|
+
!Array.isArray(contexts) ||
|
|
29
|
+
!Array.isArray(checks) ||
|
|
30
|
+
checks.some(
|
|
31
|
+
(check) =>
|
|
32
|
+
typeof check.context !== "string" ||
|
|
33
|
+
!check.context ||
|
|
34
|
+
!Object.hasOwn(check, "app_id") ||
|
|
35
|
+
!(
|
|
36
|
+
check.app_id === null ||
|
|
37
|
+
check.app_id === -1 ||
|
|
38
|
+
(Number.isSafeInteger(check.app_id) && check.app_id > 0)
|
|
39
|
+
),
|
|
40
|
+
) ||
|
|
41
|
+
contexts.some(
|
|
42
|
+
(context) => !checks.some((check) => check.context === context),
|
|
43
|
+
) ||
|
|
44
|
+
checks.some((check) => !contexts.includes(check.context)) ||
|
|
45
|
+
(rule?.requiredStatusCheckContexts || []).some(
|
|
46
|
+
(context) => !contexts.includes(context),
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
throw new Error("Protected branch check identity metadata is incomplete");
|
|
50
|
+
return {
|
|
51
|
+
required_merge_queue: Boolean(repository.mergeQueue?.id),
|
|
52
|
+
required_pull_request_reviews:
|
|
53
|
+
rule?.requiredApprovingReviewCount > 0
|
|
54
|
+
? {
|
|
55
|
+
require_code_owner_reviews: rule.requiresCodeOwnerReviews,
|
|
56
|
+
required_approving_review_count: rule.requiredApprovingReviewCount,
|
|
57
|
+
}
|
|
58
|
+
: null,
|
|
59
|
+
required_status_checks: {
|
|
60
|
+
checks,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function inventory(request, url, key = "") {
|
|
66
|
+
const rows = [];
|
|
67
|
+
for (let page = 1; page <= 100; page++) {
|
|
68
|
+
const response = await request(
|
|
69
|
+
`${url}${url.includes("?") ? "&" : "?"}per_page=100&page=${page}`,
|
|
70
|
+
);
|
|
71
|
+
const entries = key ? response[key] : response;
|
|
72
|
+
if (!Array.isArray(entries))
|
|
73
|
+
throw new Error("Missing protected policy inventory");
|
|
74
|
+
rows.push(...entries);
|
|
75
|
+
if (entries.length < 100) return rows;
|
|
76
|
+
}
|
|
77
|
+
throw new Error("Protected policy inventory exceeds its bound");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function protections(rules, classic, policy) {
|
|
81
|
+
const reviews = rules
|
|
82
|
+
.filter((rule) => rule.type === "pull_request")
|
|
83
|
+
.map((rule) => rule.parameters);
|
|
84
|
+
if (classic?.required_pull_request_reviews)
|
|
85
|
+
reviews.push({
|
|
86
|
+
...classic.required_pull_request_reviews,
|
|
87
|
+
require_code_owner_review:
|
|
88
|
+
classic.required_pull_request_reviews.require_code_owner_reviews,
|
|
89
|
+
});
|
|
90
|
+
if (
|
|
91
|
+
!reviews.some((rule) => rule?.require_code_owner_review === true) ||
|
|
92
|
+
!reviews.some(
|
|
93
|
+
(rule) =>
|
|
94
|
+
rule?.required_approving_review_count >= policy.minimum_approvals,
|
|
95
|
+
) ||
|
|
96
|
+
!(
|
|
97
|
+
classic?.required_merge_queue ||
|
|
98
|
+
rules.some((rule) => rule.type === "merge_queue")
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
throw new Error(
|
|
102
|
+
"Protected branch must enforce declared approvals, code owners and merge queue",
|
|
103
|
+
);
|
|
104
|
+
const contexts = rules
|
|
105
|
+
.filter((rule) => rule.type === "required_status_checks")
|
|
106
|
+
.flatMap((rule) => rule.parameters?.required_status_checks || []);
|
|
107
|
+
for (const check of classic?.required_status_checks?.checks || [])
|
|
108
|
+
contexts.push({ context: check.context, integration_id: check.app_id });
|
|
109
|
+
if (!contexts.length || contexts.some((check) => !check.context))
|
|
110
|
+
throw new Error(
|
|
111
|
+
"Protected pipeline requires provider-enforced status checks",
|
|
112
|
+
);
|
|
113
|
+
return contexts;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function approved(pr, reviews, minimum) {
|
|
117
|
+
const latest = new Map();
|
|
118
|
+
for (const review of [...reviews].sort((a, b) => a.id - b.id)) {
|
|
119
|
+
if (
|
|
120
|
+
review.user?.login &&
|
|
121
|
+
["APPROVED", "CHANGES_REQUESTED", "DISMISSED"].includes(review.state)
|
|
122
|
+
)
|
|
123
|
+
latest.set(review.user.login, review);
|
|
124
|
+
}
|
|
125
|
+
const independent = [...latest.values()].filter(
|
|
126
|
+
(review) => review.user.login !== pr.author?.login,
|
|
127
|
+
);
|
|
128
|
+
return (
|
|
129
|
+
pr.reviewDecision === "APPROVED" &&
|
|
130
|
+
!independent.some((review) => review.state === "CHANGES_REQUESTED") &&
|
|
131
|
+
independent.filter(
|
|
132
|
+
(review) =>
|
|
133
|
+
review.state === "APPROVED" && review.commit_id === pr.headRefOid,
|
|
134
|
+
).length >= minimum
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function checksPass(contexts, checks, statuses) {
|
|
139
|
+
return contexts.every(({ context, integration_id: app }) => {
|
|
140
|
+
const candidates = checks.filter(
|
|
141
|
+
(check) =>
|
|
142
|
+
check.name === context && (!app || app === -1 || check.app?.id === app),
|
|
143
|
+
);
|
|
144
|
+
const latest = candidates.sort((a, b) => b.id - a.id)[0];
|
|
145
|
+
if (latest)
|
|
146
|
+
return (
|
|
147
|
+
latest.status === "completed" &&
|
|
148
|
+
["success", "neutral", "skipped"].includes(latest.conclusion)
|
|
149
|
+
);
|
|
150
|
+
if (app && app !== -1) return false;
|
|
151
|
+
return (
|
|
152
|
+
statuses
|
|
153
|
+
.filter((status) => status.context === context)
|
|
154
|
+
.sort((a, b) => b.id - a.id)[0]?.state === "success"
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function githubPipelinePolicy(request, repository) {
|
|
160
|
+
const [owner, name] = repository.split("/");
|
|
161
|
+
const base = `/repos/${repository}`;
|
|
162
|
+
async function pull(number) {
|
|
163
|
+
const response = await request("/graphql", {
|
|
164
|
+
method: "POST",
|
|
165
|
+
body: { query: QUERY, variables: { owner, name, number } },
|
|
166
|
+
});
|
|
167
|
+
const pr = response.data?.repository?.pullRequest;
|
|
168
|
+
if (!pr?.id || pr.number !== number)
|
|
169
|
+
throw new Error("Protected PR policy identity is unavailable");
|
|
170
|
+
return pr;
|
|
171
|
+
}
|
|
172
|
+
async function observe(current, policy, integratedHead) {
|
|
173
|
+
const { source, baseCommit } = current.generation;
|
|
174
|
+
const number = current.intent.source.pullRequest,
|
|
175
|
+
branch = current.intent.source.targetBranch;
|
|
176
|
+
const pr = await pull(number);
|
|
177
|
+
if (pr.headRefOid !== source.commit || pr.baseRefName !== branch)
|
|
178
|
+
throw new Error("Protected PR policy source drift");
|
|
179
|
+
const branchPath = encodeURIComponent(branch);
|
|
180
|
+
const rules = await inventory(
|
|
181
|
+
request,
|
|
182
|
+
`${base}/rules/branches/${branchPath}`,
|
|
183
|
+
);
|
|
184
|
+
const classic = await classicPolicy(request, owner, name, branch);
|
|
185
|
+
const contexts = protections(rules, classic, policy);
|
|
186
|
+
const reviews = await inventory(request, `${base}/pulls/${number}/reviews`);
|
|
187
|
+
const checks = await inventory(
|
|
188
|
+
request,
|
|
189
|
+
`${base}/commits/${source.commit}/check-runs?filter=latest`,
|
|
190
|
+
"check_runs",
|
|
191
|
+
);
|
|
192
|
+
const statuses = await inventory(
|
|
193
|
+
request,
|
|
194
|
+
`${base}/commits/${source.commit}/statuses`,
|
|
195
|
+
);
|
|
196
|
+
const again = await pull(number);
|
|
197
|
+
const ref = await request(`${base}/git/ref/heads/${branch}`);
|
|
198
|
+
if (
|
|
199
|
+
recordDigest(pr) !== recordDigest(again) ||
|
|
200
|
+
ref.object?.sha !== (integratedHead || baseCommit)
|
|
201
|
+
)
|
|
202
|
+
throw new Error("Protected policy changed during readback");
|
|
203
|
+
const body = {
|
|
204
|
+
schema: "buildchain.pipeline-policy-readback/v1",
|
|
205
|
+
source,
|
|
206
|
+
baseCommit,
|
|
207
|
+
pr,
|
|
208
|
+
rules,
|
|
209
|
+
classic: classic || null,
|
|
210
|
+
reviews,
|
|
211
|
+
checks,
|
|
212
|
+
statuses,
|
|
213
|
+
contexts,
|
|
214
|
+
review: !pr.isDraft && approved(pr, reviews, policy.minimum_approvals),
|
|
215
|
+
checksPassing: checksPass(contexts, checks, statuses),
|
|
216
|
+
...(integratedHead ? { integratedHead } : {}),
|
|
217
|
+
};
|
|
218
|
+
return { ...body, root: recordDigest(body) };
|
|
219
|
+
}
|
|
220
|
+
async function observeMerged(current, policy, integration) {
|
|
221
|
+
const { root, ...body } = integration;
|
|
222
|
+
if (
|
|
223
|
+
root !== recordDigest(body) ||
|
|
224
|
+
body.schema !== "buildchain.pipeline-integration-readback/v1" ||
|
|
225
|
+
body.repository !== repository ||
|
|
226
|
+
body.sourceHead !== current.generation.source.commit ||
|
|
227
|
+
body.branch !== current.intent.source.targetBranch ||
|
|
228
|
+
body.pullRequest !== current.intent.source.pullRequest ||
|
|
229
|
+
!/^[0-9a-f]{40}$/u.test(body.protectedHead || "")
|
|
230
|
+
)
|
|
231
|
+
throw new Error(
|
|
232
|
+
"Merged review qualification requires exact protected integration evidence",
|
|
233
|
+
);
|
|
234
|
+
return observe(current, policy, body.protectedHead);
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
observe: (current, policy) => observe(current, policy),
|
|
238
|
+
observeMerged,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { canonicalJson } from "../../release/discussion/envelope.js";
|
|
3
|
+
import {
|
|
4
|
+
publicationFile,
|
|
5
|
+
publicationPath,
|
|
6
|
+
} from "../../publication/pipeline/files.js";
|
|
7
|
+
const sha256 = (bytes) =>
|
|
8
|
+
`sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
9
|
+
|
|
10
|
+
export function pipelineProductPayload({
|
|
11
|
+
qualified,
|
|
12
|
+
documents,
|
|
13
|
+
directory,
|
|
14
|
+
evidence,
|
|
15
|
+
}) {
|
|
16
|
+
const payload = (effect) => {
|
|
17
|
+
if (effect.product.startsWith("evidence:")) {
|
|
18
|
+
const asset = evidence.find(
|
|
19
|
+
(entry) => `evidence:${entry.id}` === effect.product,
|
|
20
|
+
);
|
|
21
|
+
if (
|
|
22
|
+
!asset ||
|
|
23
|
+
sha256(asset.bytes) !== effect.digest ||
|
|
24
|
+
asset.bytes.length !== effect.size
|
|
25
|
+
)
|
|
26
|
+
throw new Error(
|
|
27
|
+
"Release evidence differs from admitted immutable bytes",
|
|
28
|
+
);
|
|
29
|
+
return asset.bytes;
|
|
30
|
+
}
|
|
31
|
+
if (effect.product === "release-passport")
|
|
32
|
+
return Buffer.from(`${canonicalJson(documents.passport)}\n`);
|
|
33
|
+
const artifact = qualified.artifacts.find(
|
|
34
|
+
(entry) => entry.id === effect.product,
|
|
35
|
+
);
|
|
36
|
+
if (
|
|
37
|
+
!artifact ||
|
|
38
|
+
artifact.digest !== effect.digest ||
|
|
39
|
+
artifact.size !== effect.size
|
|
40
|
+
)
|
|
41
|
+
throw new Error(
|
|
42
|
+
"GitHub Release payload does not match qualified product",
|
|
43
|
+
);
|
|
44
|
+
const file = publicationFile(publicationPath(directory, artifact.file));
|
|
45
|
+
if (file.digest !== artifact.digest || file.size !== artifact.size)
|
|
46
|
+
throw new Error("GitHub Release payload bytes changed");
|
|
47
|
+
return file.bytes;
|
|
48
|
+
};
|
|
49
|
+
return payload;
|
|
50
|
+
}
|