@kungfu-tech/buildchain 4.1.3-alpha.1 → 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 +266 -10
- 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 +362 -7
- 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 +328 -74
- package/architecture/stage-capsule-qualification.json +3 -1
- package/architecture/universal-workflow-bootstrap.json +15 -1
- package/architecture/universal-workflow-capability-policy.json +4 -4
- package/architecture/universal-workflow-fault-campaign.json +3 -1
- package/architecture/workflow-taxonomy.json +77 -328
- package/dist/readers/business-attempt.cjs +809 -0
- package/dist/site/buildchain-contract.json +16 -68
- package/dist/site/buildchain-site.json +40 -25
- 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 +54 -54
- package/dist/site/page-registry.json +33 -18
- 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 +102 -102
- 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 +19 -6
- 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/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,111 @@
|
|
|
1
|
+
import { readBusinessAttempt } from "../attempt/reader.js";
|
|
2
|
+
import { openPipelineSession, resumePipelineSession } from "./session.js";
|
|
3
|
+
import { sourceGeneration } from "../attempt/identity.js";
|
|
4
|
+
|
|
5
|
+
async function successorSelection(
|
|
6
|
+
session,
|
|
7
|
+
admission,
|
|
8
|
+
event,
|
|
9
|
+
payload,
|
|
10
|
+
inputs,
|
|
11
|
+
host,
|
|
12
|
+
) {
|
|
13
|
+
const failedBuild =
|
|
14
|
+
session.observed.status === "failure" &&
|
|
15
|
+
session.observed.phases.build?.payload.state === "failure";
|
|
16
|
+
if (
|
|
17
|
+
(!failedBuild &&
|
|
18
|
+
!["superseded", "cancelled"].includes(session.observed.status)) ||
|
|
19
|
+
event.terminalOnly ||
|
|
20
|
+
admission.live.state !== "open" ||
|
|
21
|
+
admission.live.targetBranch !== session.intent.source.targetBranch
|
|
22
|
+
)
|
|
23
|
+
return { session, admission };
|
|
24
|
+
const next = await host.source.observe(
|
|
25
|
+
session.intent.source.pullRequest,
|
|
26
|
+
inputs,
|
|
27
|
+
);
|
|
28
|
+
if (!next.eligible) return { session, admission };
|
|
29
|
+
const changed =
|
|
30
|
+
sourceGeneration(session.intent, next.live.source, next.live.baseCommit)
|
|
31
|
+
.id !== session.observed.generation;
|
|
32
|
+
if (
|
|
33
|
+
!changed &&
|
|
34
|
+
(failedBuild ||
|
|
35
|
+
!["reopened", "enqueued", "ready_for_review", "labeled"].includes(
|
|
36
|
+
payload.action,
|
|
37
|
+
))
|
|
38
|
+
)
|
|
39
|
+
return { session, admission };
|
|
40
|
+
await host.notifyTerminal?.(session);
|
|
41
|
+
return {
|
|
42
|
+
session: await openPipelineSession(
|
|
43
|
+
{ admission: next, ...host, successor: true },
|
|
44
|
+
host,
|
|
45
|
+
),
|
|
46
|
+
admission: next,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Event branch names are lookup hints only. The immutable intent and live PR
|
|
51
|
+
// readback decide whether an old route still needs cleanup before a new route.
|
|
52
|
+
export async function selectPipelineSession(event, payload, inputs, host) {
|
|
53
|
+
if (event.kind === "attempt") {
|
|
54
|
+
const session = await resumePipelineSession(
|
|
55
|
+
{ ...host, attempt: event.attempt },
|
|
56
|
+
host,
|
|
57
|
+
);
|
|
58
|
+
const admission = await host.source.observeIntent(
|
|
59
|
+
session.intent,
|
|
60
|
+
session.observed.history.at(-1).generation,
|
|
61
|
+
inputs,
|
|
62
|
+
);
|
|
63
|
+
return successorSelection(session, admission, event, payload, inputs, host);
|
|
64
|
+
}
|
|
65
|
+
if (event.kind !== "pull-request")
|
|
66
|
+
throw new Error("Not a PR pipeline selection");
|
|
67
|
+
const pr = await host.request(
|
|
68
|
+
`/repos/${host.repository}/pulls/${event.pullRequest}`,
|
|
69
|
+
);
|
|
70
|
+
const branches = [
|
|
71
|
+
...new Set(
|
|
72
|
+
[payload.changes?.base?.ref?.from, pr.base?.ref].filter(Boolean),
|
|
73
|
+
),
|
|
74
|
+
];
|
|
75
|
+
for (const branch of branches) {
|
|
76
|
+
if (!/^(?:dev|alpha|release|publish-gate)\/[A-Za-z0-9/._-]+$/u.test(branch))
|
|
77
|
+
continue;
|
|
78
|
+
const retained = await host.provider.lookup(
|
|
79
|
+
host.repository,
|
|
80
|
+
event.pullRequest,
|
|
81
|
+
branch,
|
|
82
|
+
);
|
|
83
|
+
if (!retained) continue;
|
|
84
|
+
const observed = readBusinessAttempt(retained.snapshot);
|
|
85
|
+
if (!observed.attempt) continue;
|
|
86
|
+
if (
|
|
87
|
+
branch !== pr.base.ref &&
|
|
88
|
+
["cancelled", "failure", "superseded", "complete"].includes(
|
|
89
|
+
observed.status,
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
continue;
|
|
93
|
+
const session = await resumePipelineSession(
|
|
94
|
+
{ ...host, attempt: observed.attempt },
|
|
95
|
+
host,
|
|
96
|
+
);
|
|
97
|
+
const admission = await host.source.observeIntent(
|
|
98
|
+
session.intent,
|
|
99
|
+
observed.history.at(-1).generation,
|
|
100
|
+
inputs,
|
|
101
|
+
);
|
|
102
|
+
return successorSelection(session, admission, event, payload, inputs, host);
|
|
103
|
+
}
|
|
104
|
+
if (event.terminalOnly || pr.state !== "open") return null;
|
|
105
|
+
const admission = await host.source.observe(event.pullRequest, inputs);
|
|
106
|
+
if (!admission.eligible) return null;
|
|
107
|
+
return {
|
|
108
|
+
session: await openPipelineSession({ admission, ...host }, host),
|
|
109
|
+
admission,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { pipelineIntent } from "../attempt/identity.js";
|
|
2
|
+
import { atomicAttemptJournal } from "../attempt/journal.js";
|
|
3
|
+
import { pipelineProgress } from "./progress.js";
|
|
4
|
+
|
|
5
|
+
export function pipelinePhases(operation) {
|
|
6
|
+
if (operation === "develop")
|
|
7
|
+
return ["admission", "build", "review", "warrant", "merge"];
|
|
8
|
+
if (["alpha", "stable", "major"].includes(operation))
|
|
9
|
+
return [
|
|
10
|
+
"admission",
|
|
11
|
+
"build",
|
|
12
|
+
"review",
|
|
13
|
+
"merge",
|
|
14
|
+
"publish",
|
|
15
|
+
"distribution",
|
|
16
|
+
"next-development",
|
|
17
|
+
];
|
|
18
|
+
throw new Error("Unsupported pipeline channel operation");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function openPipelineSession(
|
|
22
|
+
{ admission, runtime, writer, successor = false },
|
|
23
|
+
{ provider, index },
|
|
24
|
+
) {
|
|
25
|
+
if (!admission.eligible || !admission.route || !admission.live.source)
|
|
26
|
+
throw new Error(
|
|
27
|
+
"A new pipeline attempt requires an admitted source and channel",
|
|
28
|
+
);
|
|
29
|
+
const { live } = admission;
|
|
30
|
+
const retained = await provider.lookup(
|
|
31
|
+
live.source.repository,
|
|
32
|
+
live.pullRequest,
|
|
33
|
+
admission.route.to,
|
|
34
|
+
);
|
|
35
|
+
const intent =
|
|
36
|
+
retained?.snapshot.intent ||
|
|
37
|
+
pipelineIntent({
|
|
38
|
+
repository: live.source.repository,
|
|
39
|
+
repositoryId: admission.repositoryId,
|
|
40
|
+
pullRequest: live.pullRequest,
|
|
41
|
+
targetBranch: admission.route.to,
|
|
42
|
+
phases: pipelinePhases(admission.route.operation),
|
|
43
|
+
runtime,
|
|
44
|
+
});
|
|
45
|
+
const journal = atomicAttemptJournal(provider, intent);
|
|
46
|
+
const progress = pipelineProgress(journal, { intent, writer, runtime });
|
|
47
|
+
const observed = await progress.open(live.source, live.baseCommit, {
|
|
48
|
+
successor,
|
|
49
|
+
});
|
|
50
|
+
await index.retain(intent, observed.attempt);
|
|
51
|
+
return { intent, journal, progress, observed };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function resumePipelineSession(
|
|
55
|
+
{ attempt, runtime, writer },
|
|
56
|
+
{ provider, index },
|
|
57
|
+
) {
|
|
58
|
+
const retained = await index.resolve(attempt);
|
|
59
|
+
const intent = retained.snapshot.intent;
|
|
60
|
+
const journal = atomicAttemptJournal(provider, intent);
|
|
61
|
+
const observed = await journal.read();
|
|
62
|
+
if (observed.attempt !== attempt)
|
|
63
|
+
throw new Error(
|
|
64
|
+
"Selected attempt is historical; it cannot regain current write authority",
|
|
65
|
+
);
|
|
66
|
+
return {
|
|
67
|
+
intent,
|
|
68
|
+
journal,
|
|
69
|
+
observed,
|
|
70
|
+
progress: pipelineProgress(journal, { intent, writer, runtime }),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createIntegrationDeliveryProof,
|
|
3
|
+
verifyIntegrationDeliveryProof,
|
|
4
|
+
} from "../../dev-delivery/dev-delivery-warrant.js";
|
|
5
|
+
import { readBusinessAttempt } from "../attempt/reader.js";
|
|
6
|
+
import { pipelineCandidate } from "./reconcile.js";
|
|
7
|
+
|
|
8
|
+
async function qualifyWarrant(session, current, queue, materials) {
|
|
9
|
+
if (current.phases.warrant?.payload.state === "success") return;
|
|
10
|
+
if (queue.activeWarrant?.phase !== "qualified")
|
|
11
|
+
throw new Error("Merged pipeline has no qualified active Warrant");
|
|
12
|
+
const reference = await materials.retain(
|
|
13
|
+
`warrant/qualified-${queue.stateRoot.slice(7)}`,
|
|
14
|
+
queue,
|
|
15
|
+
"provider-readback",
|
|
16
|
+
);
|
|
17
|
+
await session.progress.progress({
|
|
18
|
+
attempt: current.identity.id,
|
|
19
|
+
phase: "warrant",
|
|
20
|
+
state: "success",
|
|
21
|
+
eventKey: `qualified:${queue.stateRoot}`,
|
|
22
|
+
materials: [reference],
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function integrationProof(session, fresh, host, materials) {
|
|
27
|
+
const { queue, current } = fresh;
|
|
28
|
+
const candidate = pipelineCandidate(queue, current);
|
|
29
|
+
if (!candidate)
|
|
30
|
+
throw new Error("Merged pipeline candidate authority is missing");
|
|
31
|
+
if (candidate.status === "merged") {
|
|
32
|
+
const reference =
|
|
33
|
+
current.materials[
|
|
34
|
+
`merge/proof-${candidate.terminal.evidenceRoot.slice(7)}`
|
|
35
|
+
];
|
|
36
|
+
if (!reference)
|
|
37
|
+
throw new Error(
|
|
38
|
+
"Merged Warrant has no retained pre-effect integration proof",
|
|
39
|
+
);
|
|
40
|
+
const proof = await materials.read(reference);
|
|
41
|
+
if (
|
|
42
|
+
!verifyIntegrationDeliveryProof(proof, {
|
|
43
|
+
sourceProofRoot: candidate.sourceProofRoot,
|
|
44
|
+
warrantCandidateId: candidate.candidateId,
|
|
45
|
+
}).ok
|
|
46
|
+
)
|
|
47
|
+
throw new Error("Retained integration proof is invalid");
|
|
48
|
+
return { candidate, proof, reference };
|
|
49
|
+
}
|
|
50
|
+
if (candidate.candidateId !== queue.activeWarrant?.candidateId)
|
|
51
|
+
throw new Error("Merged candidate is not the active Warrant");
|
|
52
|
+
await qualifyWarrant(session, current, queue, materials);
|
|
53
|
+
const readback = await host.integration.observe(current);
|
|
54
|
+
const proof = createIntegrationDeliveryProof({
|
|
55
|
+
repository: host.repository,
|
|
56
|
+
protectedBase: session.intent.source.targetBranch,
|
|
57
|
+
sourceProofRoot: candidate.sourceProofRoot,
|
|
58
|
+
warrant: queue.activeWarrant,
|
|
59
|
+
currentBase: readback.mergeCommit,
|
|
60
|
+
replayTree: readback.mergeTree,
|
|
61
|
+
mergeGroupHead: readback.mergeCommit,
|
|
62
|
+
mergeGroupTree: readback.mergeTree,
|
|
63
|
+
requiredContextRoots: [readback.build.root, readback.root],
|
|
64
|
+
verifiedAt: new Date().toISOString(),
|
|
65
|
+
});
|
|
66
|
+
const reference = await materials.retain(
|
|
67
|
+
`merge/proof-${proof.proofRoot.slice(7)}`,
|
|
68
|
+
proof,
|
|
69
|
+
);
|
|
70
|
+
const provider = await materials.retain(
|
|
71
|
+
`merge/provider-${readback.root.slice(7)}`,
|
|
72
|
+
readback,
|
|
73
|
+
"provider-readback",
|
|
74
|
+
);
|
|
75
|
+
await session.progress.progress({
|
|
76
|
+
attempt: current.identity.id,
|
|
77
|
+
phase: "merge",
|
|
78
|
+
state: "waiting",
|
|
79
|
+
eventKey: `merge-request:${proof.proofRoot}`,
|
|
80
|
+
materials: [reference, provider],
|
|
81
|
+
});
|
|
82
|
+
return { candidate, proof, reference };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function settlePipeline(session, fresh, delivery, host) {
|
|
86
|
+
const materials = host.materialStore(session);
|
|
87
|
+
const { candidate, proof, reference } = await integrationProof(
|
|
88
|
+
session,
|
|
89
|
+
fresh,
|
|
90
|
+
host,
|
|
91
|
+
materials,
|
|
92
|
+
);
|
|
93
|
+
const warrant = fresh.queue.activeWarrant;
|
|
94
|
+
const result = await delivery.service.settle({
|
|
95
|
+
expectedOldStateRoot: fresh.queue.stateRoot,
|
|
96
|
+
pullRequestNumber: candidate.pullRequestNumber,
|
|
97
|
+
expectedSourceHead: candidate.sourceHead,
|
|
98
|
+
outcome: "merged",
|
|
99
|
+
reason: "exact-protected-pipeline-integration",
|
|
100
|
+
evidenceRoot: proof.proofRoot,
|
|
101
|
+
execute: true,
|
|
102
|
+
...(warrant?.candidateId === candidate.candidateId
|
|
103
|
+
? {
|
|
104
|
+
fencingToken: warrant.fencingToken,
|
|
105
|
+
leaseGeneration: warrant.generation,
|
|
106
|
+
}
|
|
107
|
+
: {}),
|
|
108
|
+
});
|
|
109
|
+
if (result.observation.activeWarrant?.candidateId === candidate.candidateId)
|
|
110
|
+
throw new Error("Merged settlement retained its active Warrant");
|
|
111
|
+
const receipt = await materials.retain(
|
|
112
|
+
`merge/settlement-${result.receiptRoot.slice(7)}`,
|
|
113
|
+
result,
|
|
114
|
+
);
|
|
115
|
+
await session.progress.progress({
|
|
116
|
+
attempt: fresh.attempt,
|
|
117
|
+
phase: "merge",
|
|
118
|
+
state: "success",
|
|
119
|
+
eventKey: `merged:${result.receiptRoot}`,
|
|
120
|
+
materials: [reference, receipt],
|
|
121
|
+
});
|
|
122
|
+
await host.notifyTerminal?.(session);
|
|
123
|
+
const successor = result.receipt.successorWake;
|
|
124
|
+
if (successor) {
|
|
125
|
+
const next = await host.provider.lookup(
|
|
126
|
+
host.repository,
|
|
127
|
+
successor.pullRequestNumber,
|
|
128
|
+
session.intent.source.targetBranch,
|
|
129
|
+
);
|
|
130
|
+
if (next) await host.wake(readBusinessAttempt(next.snapshot).attempt);
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
operation: "wait",
|
|
134
|
+
reason: "protected-delivery-settled",
|
|
135
|
+
attempt: fresh.attempt,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { githubJsonClient } from "../../providers/github/json-client.js";
|
|
2
|
+
import { githubAttemptJournal } from "../../providers/github/attempt-journal.js";
|
|
3
|
+
import { readBusinessAttempt } from "../attempt/reader.js";
|
|
4
|
+
import { pipelineCandidateRoot } from "./reconcile.js";
|
|
5
|
+
|
|
6
|
+
export async function wakePipelineCandidate(candidate, connection, ports = {}) {
|
|
7
|
+
const repository = connection.repository;
|
|
8
|
+
const request =
|
|
9
|
+
ports.request ||
|
|
10
|
+
githubJsonClient({
|
|
11
|
+
token: connection.token,
|
|
12
|
+
userAgent: "buildchain-pipeline-wake",
|
|
13
|
+
});
|
|
14
|
+
const provider = ports.provider || githubAttemptJournal(request);
|
|
15
|
+
const branch = candidate.protectedBase || connection.branch;
|
|
16
|
+
const loaded = await provider.lookup(
|
|
17
|
+
repository,
|
|
18
|
+
candidate.pullRequestNumber,
|
|
19
|
+
branch,
|
|
20
|
+
);
|
|
21
|
+
if (!loaded)
|
|
22
|
+
throw new Error("Warrant handoff has no admitted business intent");
|
|
23
|
+
const observed = readBusinessAttempt(loaded.snapshot);
|
|
24
|
+
const current = { ...observed.history.at(-1), intent: observed.intent };
|
|
25
|
+
if (
|
|
26
|
+
candidate.sourceHead !== current.generation.source.commit ||
|
|
27
|
+
candidate.sourceRoot !== pipelineCandidateRoot(current)
|
|
28
|
+
)
|
|
29
|
+
throw new Error("Warrant handoff changed its admitted business attempt");
|
|
30
|
+
await request(`/repos/${repository}/dispatches`, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
body: {
|
|
33
|
+
event_type: "buildchain-attempt-wake",
|
|
34
|
+
client_payload: { attempt: observed.attempt },
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
return observed.attempt;
|
|
38
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const labels = {
|
|
2
|
+
admission: "Source admission",
|
|
3
|
+
build: "Product verification",
|
|
4
|
+
review: "Protected review",
|
|
5
|
+
warrant: "Delivery qualification",
|
|
6
|
+
merge: "Protected integration",
|
|
7
|
+
publish: "Product publication",
|
|
8
|
+
distribution: "Distribution",
|
|
9
|
+
"next-development": "Next development version",
|
|
10
|
+
};
|
|
11
|
+
const safe = (value) =>
|
|
12
|
+
String(value || "")
|
|
13
|
+
.replace(/[\r\n|]/gu, " ")
|
|
14
|
+
.replaceAll("&", "&")
|
|
15
|
+
.replaceAll("<", "<")
|
|
16
|
+
.replaceAll(">", ">")
|
|
17
|
+
.replace(/[\\`*_[\]()]/gu, (character) => `\\${character}`)
|
|
18
|
+
.slice(0, 1000);
|
|
19
|
+
|
|
20
|
+
export function pipelineWebStatus(observed, discussionUrl = "") {
|
|
21
|
+
const { intent, attempt } = observed;
|
|
22
|
+
const repositoryUrl = `https://github.com/${intent.repository}`;
|
|
23
|
+
const current = observed.history.at(-1);
|
|
24
|
+
if (!current) throw new Error("Pipeline status requires an admitted attempt");
|
|
25
|
+
const table = [
|
|
26
|
+
"| Step | State |",
|
|
27
|
+
"| --- | --- |",
|
|
28
|
+
...intent.expectedNodes.map(
|
|
29
|
+
(phase) =>
|
|
30
|
+
`| ${labels[phase]} | ${safe(observed.phases[phase]?.payload.state || "pending")} |`,
|
|
31
|
+
),
|
|
32
|
+
].join("\n");
|
|
33
|
+
const lines = [
|
|
34
|
+
`## Buildchain · PR #${intent.source.pullRequest}`,
|
|
35
|
+
`Status: **${safe(observed.status)}**. ${safe(observed.reason)}`,
|
|
36
|
+
`Attempt: \`${attempt}\``,
|
|
37
|
+
`[Pull request](${repositoryUrl}/pull/${intent.source.pullRequest}) · ` +
|
|
38
|
+
`[Source ${current.generation.source.commit.slice(0, 7)}](${repositoryUrl}/commit/${current.generation.source.commit})`,
|
|
39
|
+
table,
|
|
40
|
+
];
|
|
41
|
+
const publisher = [...observed.history]
|
|
42
|
+
.reverse()
|
|
43
|
+
.find(
|
|
44
|
+
(item) =>
|
|
45
|
+
item.generation.id === current.generation.id &&
|
|
46
|
+
item.phases.publish?.payload.state === "success",
|
|
47
|
+
);
|
|
48
|
+
const published = Boolean(publisher);
|
|
49
|
+
if (published && observed.missing.length)
|
|
50
|
+
lines.push(
|
|
51
|
+
`Publication succeeded and its immutable results remain preserved. The whole attempt is incomplete: ${observed.missing.map((phase) => labels[phase]).join(", ")}.`,
|
|
52
|
+
);
|
|
53
|
+
if (publisher && publisher.identity.id !== current.identity.id)
|
|
54
|
+
lines.push(
|
|
55
|
+
`Publication succeeded in predecessor \`${publisher.identity.id}\`. Current qualification and remaining steps are shown separately above.`,
|
|
56
|
+
);
|
|
57
|
+
if (current.identity.predecessor)
|
|
58
|
+
lines.push(
|
|
59
|
+
`Recovered from \`${current.identity.predecessor}\`; its original evidence remains in history.`,
|
|
60
|
+
);
|
|
61
|
+
if (observed.status !== "complete")
|
|
62
|
+
lines.push(
|
|
63
|
+
`[Recover this attempt](${repositoryUrl}/actions/workflows/buildchain-recover.yml): choose Run workflow and enter the exact attempt above. Select a repaired runtime only when needed.`,
|
|
64
|
+
);
|
|
65
|
+
lines.push(
|
|
66
|
+
"Execution links provide the provider's ordinary cancellation control. Cancellation stops remaining work; it does not undo completed publication.",
|
|
67
|
+
);
|
|
68
|
+
const runs = [
|
|
69
|
+
...new Map(
|
|
70
|
+
current.runs.map((run) => [`${run.runId}:${run.runAttempt}`, run]),
|
|
71
|
+
).values(),
|
|
72
|
+
];
|
|
73
|
+
for (const run of runs.slice(-10))
|
|
74
|
+
lines.push(
|
|
75
|
+
`[Execution ${run.runId}, attempt ${run.runAttempt}](${repositoryUrl}/actions/runs/${run.runId}/attempts/${run.runAttempt})`,
|
|
76
|
+
);
|
|
77
|
+
if (discussionUrl) {
|
|
78
|
+
const url = new URL(discussionUrl);
|
|
79
|
+
if (
|
|
80
|
+
url.origin !== "https://github.com" ||
|
|
81
|
+
!url.pathname.startsWith(`/${intent.repository}/discussions/`) ||
|
|
82
|
+
!/\/discussions\/[1-9][0-9]*$/u.test(url.pathname) ||
|
|
83
|
+
url.search ||
|
|
84
|
+
url.hash ||
|
|
85
|
+
url.username ||
|
|
86
|
+
url.password
|
|
87
|
+
)
|
|
88
|
+
throw new Error(
|
|
89
|
+
"Pipeline evidence link belongs to another provider or repository",
|
|
90
|
+
);
|
|
91
|
+
lines.push(`[Retained evidence](${discussionUrl})`);
|
|
92
|
+
}
|
|
93
|
+
return `${lines.join("\n\n")}\n`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function projectPipelineStatus(session, { project, core }) {
|
|
97
|
+
let result;
|
|
98
|
+
try {
|
|
99
|
+
result = await project(session);
|
|
100
|
+
} catch {
|
|
101
|
+
core.warning(
|
|
102
|
+
"Discussion projection unavailable; canonical attempt evidence remains retained.",
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
const observed = await session.journal.read();
|
|
106
|
+
await core.summary.addRaw(pipelineWebStatus(observed, result?.url)).write();
|
|
107
|
+
return result || { url: "", journalHead: observed.head };
|
|
108
|
+
}
|
|
@@ -20,7 +20,8 @@ const PROTECTED = [
|
|
|
20
20
|
"scripts/generate-workflow-taxonomy.mjs",
|
|
21
21
|
"scripts/check-workflows.sh",
|
|
22
22
|
"tests/workflow-taxonomy.test.mjs",
|
|
23
|
-
".github/workflows/
|
|
23
|
+
".github/workflows/buildchain.yml",
|
|
24
|
+
".github/workflows/buildchain-recover.yml",
|
|
24
25
|
"package.json",
|
|
25
26
|
".buildchain/buildchain.toml",
|
|
26
27
|
];
|
|
@@ -63,12 +64,13 @@ function validateEntries(policy, errors) {
|
|
|
63
64
|
if (
|
|
64
65
|
entry.path &&
|
|
65
66
|
!(
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
[".build", "build", "buildchain", "buildchain-recover"].includes(
|
|
68
|
+
entry.id,
|
|
69
|
+
) && entry.path === `.github/workflows/${entry.id}.yml`
|
|
68
70
|
)
|
|
69
71
|
)
|
|
70
72
|
errors.push(
|
|
71
|
-
`${entry.id}: explicit path is reserved for
|
|
73
|
+
`${entry.id}: explicit path is reserved for build APIs and the generated consumer pair`,
|
|
72
74
|
);
|
|
73
75
|
const file = workflowPath(entry);
|
|
74
76
|
if (!WORKFLOW.test(file) || paths.has(file))
|
|
@@ -191,35 +193,53 @@ function validateGateIntegration(root, errors) {
|
|
|
191
193
|
)
|
|
192
194
|
)
|
|
193
195
|
errors.push("required check chain does not enforce check:workflows");
|
|
194
|
-
const
|
|
195
|
-
const
|
|
196
|
+
const require = createRequire(import.meta.url);
|
|
197
|
+
const { parse: parseYaml } = require("yaml");
|
|
198
|
+
const { compileConsumerPlan } = require("../consumer/contract/plan.js");
|
|
199
|
+
const { consumerWorkflows } = require("../consumer/contract/entries.js");
|
|
200
|
+
const config = compileConsumerPlan(read(".buildchain/buildchain.toml"));
|
|
201
|
+
for (const platform of ["linux-x64", "macos-arm64", "windows-x64"])
|
|
202
|
+
if (
|
|
203
|
+
!config.products.some(
|
|
204
|
+
(product) =>
|
|
205
|
+
product.platforms.includes(platform) &&
|
|
206
|
+
product.verify.includes("corepack pnpm@11.7.0 run check") &&
|
|
207
|
+
product.verify.includes("node scripts/verify-product-platform.mjs"),
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
errors.push(
|
|
211
|
+
`${platform}: declared product verification must execute the full required check and checkpoint recovery`,
|
|
212
|
+
);
|
|
213
|
+
const caller = parseYaml(read(".github/workflows/buildchain.yml"));
|
|
214
|
+
const channel = caller.jobs?.buildchain?.uses?.split("@").at(-1);
|
|
215
|
+
const configPath = caller.jobs?.buildchain?.with?.["config-path"];
|
|
216
|
+
for (const [file, expected] of Object.entries(
|
|
217
|
+
consumerWorkflows(channel, configPath),
|
|
218
|
+
))
|
|
219
|
+
if (read(file) !== expected)
|
|
220
|
+
errors.push(`${file}: required generated pipeline caller drift`);
|
|
221
|
+
const execution = parseYaml(
|
|
222
|
+
read(".github/workflows/.ops-pipeline-execute.yml"),
|
|
223
|
+
);
|
|
224
|
+
const build = execution.jobs?.build;
|
|
225
|
+
const record = execution.jobs?.["record-build"];
|
|
196
226
|
if (
|
|
197
|
-
|
|
198
|
-
|
|
227
|
+
build?.permissions?.contents !== "read" ||
|
|
228
|
+
Object.values(build?.permissions || {}).includes("write") ||
|
|
229
|
+
!build?.steps?.some(
|
|
230
|
+
(step) =>
|
|
231
|
+
step.uses === "./.buildchain/runtime/actions/workflow/pipeline/build",
|
|
232
|
+
) ||
|
|
233
|
+
![record?.needs].flat().includes("build") ||
|
|
234
|
+
!record?.steps?.some(
|
|
235
|
+
(step) =>
|
|
236
|
+
step.uses ===
|
|
237
|
+
"./.buildchain/runtime/actions/workflow/pipeline/record-build",
|
|
199
238
|
)
|
|
200
239
|
)
|
|
201
240
|
errors.push(
|
|
202
|
-
"
|
|
241
|
+
"required product execution and independent source qualification boundary is missing",
|
|
203
242
|
);
|
|
204
|
-
const verify = read(".github/workflows/self-build-verify.yml");
|
|
205
|
-
const document = parseWorkflowDocument(verify);
|
|
206
|
-
for (const trigger of ["pull_request", "merge_group", "push"]) {
|
|
207
|
-
if (!document.triggers.some((item) => item.split(":")[0] === trigger))
|
|
208
|
-
errors.push(`required Verify workflow lacks ${trigger}`);
|
|
209
|
-
}
|
|
210
|
-
const checkJob = verify.match(/^ check:\n([\s\S]*?)(?=^ [\w-]+:|(?![\s\S]))/mu)?.[1] || "";
|
|
211
|
-
const verifyNode = "./.buildchain/runtime/actions/build/verification/repository";
|
|
212
|
-
const verifyAction = read("actions/build/verification/repository/action.yml");
|
|
213
|
-
const sourceAction = read("actions/build/verification/qualify-source/action.yml");
|
|
214
|
-
const sourceVerification = read("packages/core/build/verification/source.js");
|
|
215
|
-
const sourceLifecycle = read("packages/core/build/source/lifecycle.js");
|
|
216
|
-
if (!parseYamlUses(checkJob).some(call => call.value === verifyNode) ||
|
|
217
|
-
!parseYamlUses(verifyAction).some(call => call.value === "./.buildchain/runtime/actions/build/verification/qualify-source") ||
|
|
218
|
-
!/using: node24/u.test(sourceAction) ||
|
|
219
|
-
!/qualify = qualifySourceLifecycle/u.test(sourceVerification) ||
|
|
220
|
-
!/mode: "verify"/u.test(sourceVerification) ||
|
|
221
|
-
!/lifecycle\(\{[\s\S]*stageName,[\s\S]*required: true/u.test(sourceLifecycle))
|
|
222
|
-
errors.push("required Verify lifecycle integration is missing");
|
|
223
243
|
const owners = read(".github/CODEOWNERS")
|
|
224
244
|
.split(/\r?\n/u)
|
|
225
245
|
.map((line) => line.trim());
|
|
@@ -306,7 +326,7 @@ export function renderWorkflowCatalog(policy) {
|
|
|
306
326
|
"",
|
|
307
327
|
"Public workflows own the consumer API. Component workflows own reusable multi-job topology. Self workflows own Buildchain repository automation. Actions own execution steps; JS adapters and Rust/WASM own implementation.",
|
|
308
328
|
"",
|
|
309
|
-
"Names use `public-<category>-<purpose>.yml`, `.<category>-<purpose>.yml`, and `
|
|
329
|
+
"Names use `public-<category>-<purpose>.yml`, `.<category>-<purpose>.yml`, and the generated self callers `buildchain.yml` and `buildchain-recover.yml`, with categories `build`, `release`, and `ops`. The primary build API and backbone use `build.yml` and `.build.yml`.",
|
|
310
330
|
"",
|
|
311
331
|
"Register ownership before adding a workflow. `pnpm run check:workflows` validates source, calls, required CI integration and independent review ownership. `pnpm run generate:workflows` regenerates this catalog.",
|
|
312
332
|
"",
|
|
@@ -330,22 +350,22 @@ export function renderWorkflowCatalog(policy) {
|
|
|
330
350
|
}
|
|
331
351
|
|
|
332
352
|
function validateEntryOwnership(entry, errors) {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
353
|
+
for (const field of ["summary", "owner", "rationale"])
|
|
354
|
+
if (typeof entry[field] !== "string" || !entry[field].trim())
|
|
355
|
+
errors.push(`${entry.id}: missing ${field}`);
|
|
356
|
+
if (!["active", "preview"].includes(entry.status))
|
|
357
|
+
errors.push(`${entry.id}: unsupported lifecycle status`);
|
|
358
|
+
for (const field of ["compatibility", "migration", "retiredAlias"])
|
|
359
|
+
if (field in entry)
|
|
360
|
+
errors.push(
|
|
361
|
+
`${entry.id}: ${field} is not an executable architecture contract`,
|
|
362
|
+
);
|
|
363
|
+
if (
|
|
364
|
+
!["reusable", "dispatch-service", "repository"].includes(entry.invocation)
|
|
365
|
+
)
|
|
366
|
+
errors.push(`${entry.id}: invalid invocation`);
|
|
367
|
+
if ((entry.role === "self") !== (entry.invocation === "repository"))
|
|
368
|
+
errors.push(`${entry.id}: role and invocation disagree`);
|
|
369
|
+
if (entry.role === "component" && entry.invocation !== "reusable")
|
|
370
|
+
errors.push(`${entry.id}: component must be reusable`);
|
|
351
371
|
}
|
|
@@ -10,25 +10,29 @@ try {
|
|
|
10
10
|
await build({
|
|
11
11
|
entry: {
|
|
12
12
|
"release-discussion": "packages/core/release/discussion/reader-entry.js",
|
|
13
|
+
"business-attempt": "packages/core/workflow/attempt/reader-entry.js",
|
|
13
14
|
},
|
|
14
15
|
outDir: output,
|
|
15
16
|
format: ["cjs"],
|
|
16
17
|
target: "node24",
|
|
17
18
|
platform: "node",
|
|
18
19
|
splitting: false,
|
|
20
|
+
noExternal: ["smol-toml"],
|
|
19
21
|
sourcemap: false,
|
|
20
22
|
minify: false,
|
|
21
23
|
clean: false,
|
|
22
24
|
outExtension: () => ({ js: ".cjs" }),
|
|
23
25
|
silent: true,
|
|
24
26
|
});
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
if (check)
|
|
28
|
+
for (const name of ["release-discussion", "business-attempt"]) {
|
|
29
|
+
if (
|
|
30
|
+
!fs
|
|
31
|
+
.readFileSync(path.join(output, `${name}.cjs`))
|
|
32
|
+
.equals(fs.readFileSync(`dist/readers/${name}.cjs`))
|
|
33
|
+
)
|
|
34
|
+
throw new Error(`Historical ${name} reader bundle is stale`);
|
|
35
|
+
}
|
|
32
36
|
} finally {
|
|
33
37
|
if (check) fs.rmSync(output, { recursive: true, force: true });
|
|
34
38
|
}
|