@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,142 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { bindConsumerSource } from "../../consumer/contract/identity.js";
|
|
6
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
7
|
+
import { consumerCommandSession } from "../../runtime/consumer-shell.js";
|
|
8
|
+
import { createNativeChildEnvironment } from "../../dev-delivery/native/execution.js";
|
|
9
|
+
|
|
10
|
+
function inspectTrackedBytes(cwd, tree) {
|
|
11
|
+
const root = fs.realpathSync(cwd);
|
|
12
|
+
const entries = execFileSync(
|
|
13
|
+
"git",
|
|
14
|
+
["-C", root, "ls-tree", "-r", "-z", tree],
|
|
15
|
+
{
|
|
16
|
+
encoding: "utf8",
|
|
17
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
18
|
+
},
|
|
19
|
+
)
|
|
20
|
+
.split("\0")
|
|
21
|
+
.filter(Boolean);
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
const match =
|
|
24
|
+
/^(100644|100755|120000) blob ([0-9a-f]{40})\t([\s\S]+)$/u.exec(entry);
|
|
25
|
+
if (!match) throw new Error("Build source requires resolved tracked files");
|
|
26
|
+
const [, mode, blob, file] = match;
|
|
27
|
+
const absolute = path.join(root, file);
|
|
28
|
+
if (fs.realpathSync(path.dirname(absolute)) !== path.dirname(absolute))
|
|
29
|
+
throw new Error(`Build source parent became a symbolic link: ${file}`);
|
|
30
|
+
const stat = fs.lstatSync(absolute);
|
|
31
|
+
const link = mode === "120000";
|
|
32
|
+
if (link ? !stat.isSymbolicLink() : !stat.isFile())
|
|
33
|
+
throw new Error(`Build tracked source kind changed: ${file}`);
|
|
34
|
+
if (
|
|
35
|
+
!link &&
|
|
36
|
+
process.platform !== "win32" &&
|
|
37
|
+
Boolean(stat.mode & 0o111) !== (mode === "100755")
|
|
38
|
+
)
|
|
39
|
+
throw new Error(`Build tracked source executable mode changed: ${file}`);
|
|
40
|
+
const bytes = link
|
|
41
|
+
? fs.readlinkSync(absolute, { encoding: "buffer" })
|
|
42
|
+
: fs.readFileSync(absolute);
|
|
43
|
+
const actual = createHash("sha1")
|
|
44
|
+
.update(`blob ${bytes.length}\0`)
|
|
45
|
+
.update(bytes)
|
|
46
|
+
.digest("hex");
|
|
47
|
+
if (actual !== blob)
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Build tracked source bytes differ from the admitted tree: ${file}`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function inspectPipelineSource(cwd, expected) {
|
|
55
|
+
const git = (...args) =>
|
|
56
|
+
execFileSync("git", ["-C", cwd, ...args], {
|
|
57
|
+
encoding: "utf8",
|
|
58
|
+
maxBuffer: 1024 * 1024,
|
|
59
|
+
}).trim();
|
|
60
|
+
if (
|
|
61
|
+
git("rev-parse", "HEAD") !== expected.commit ||
|
|
62
|
+
git("rev-parse", "HEAD^{tree}") !== expected.tree
|
|
63
|
+
)
|
|
64
|
+
throw new Error("Build checkout does not match admitted source");
|
|
65
|
+
const configFile = fs.realpathSync(path.join(cwd, expected.configPath));
|
|
66
|
+
if (!configFile.startsWith(`${fs.realpathSync(cwd)}${path.sep}`))
|
|
67
|
+
throw new Error("Consumer TOML escapes the source checkout");
|
|
68
|
+
const { identity, plan } = bindConsumerSource(
|
|
69
|
+
{
|
|
70
|
+
repository: expected.repository,
|
|
71
|
+
commit: expected.commit,
|
|
72
|
+
tree: expected.tree,
|
|
73
|
+
configPath: expected.configPath,
|
|
74
|
+
configBlob: git("rev-parse", `HEAD:${expected.configPath}`),
|
|
75
|
+
},
|
|
76
|
+
fs.readFileSync(configFile),
|
|
77
|
+
);
|
|
78
|
+
if (recordDigest(identity) !== recordDigest(expected))
|
|
79
|
+
throw new Error("Build TOML changed after source admission");
|
|
80
|
+
if (git("status", "--porcelain", "--untracked-files=no"))
|
|
81
|
+
throw new Error(
|
|
82
|
+
"Build source has tracked modifications at its verification boundary",
|
|
83
|
+
);
|
|
84
|
+
// Index flags and local Git filters cannot qualify different product bytes.
|
|
85
|
+
inspectTrackedBytes(cwd, expected.tree);
|
|
86
|
+
return plan;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function productDirectory(cwd, product) {
|
|
90
|
+
const directory = fs.realpathSync(
|
|
91
|
+
path.resolve(cwd, product.directory || "."),
|
|
92
|
+
);
|
|
93
|
+
const root = fs.realpathSync(cwd);
|
|
94
|
+
if (directory !== root && !directory.startsWith(`${root}${path.sep}`))
|
|
95
|
+
throw new Error("Product directory escapes its source checkout");
|
|
96
|
+
return directory;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// This node executes only product-owned build/test commands, on a read-only
|
|
100
|
+
// credential job. Its output is an observation; independent seal/qualification
|
|
101
|
+
// jobs must bind provider completion and exact artifacts before granting reuse.
|
|
102
|
+
export async function buildPipelineSource(
|
|
103
|
+
{ cwd, source, platform, environment = process.env },
|
|
104
|
+
{ inspect = inspectPipelineSource, session = consumerCommandSession } = {},
|
|
105
|
+
) {
|
|
106
|
+
const plan = inspect(cwd, source);
|
|
107
|
+
const result = await buildPipelineProducts(
|
|
108
|
+
{ cwd, plan, platform, environment },
|
|
109
|
+
session,
|
|
110
|
+
);
|
|
111
|
+
inspect(cwd, source);
|
|
112
|
+
return {
|
|
113
|
+
schema: "buildchain.pipeline-build-observation/v1",
|
|
114
|
+
source,
|
|
115
|
+
...result,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function buildPipelineProducts(
|
|
120
|
+
{ cwd, plan, platform, environment = process.env },
|
|
121
|
+
session = consumerCommandSession,
|
|
122
|
+
) {
|
|
123
|
+
const products = plan.products.filter((product) =>
|
|
124
|
+
product.platforms.includes(platform),
|
|
125
|
+
);
|
|
126
|
+
if (!products.length)
|
|
127
|
+
throw new Error("No products declared for the selected platform");
|
|
128
|
+
const results = [];
|
|
129
|
+
for (const product of products) {
|
|
130
|
+
const commands = session(createNativeChildEnvironment(environment));
|
|
131
|
+
const directory = productDirectory(cwd, product);
|
|
132
|
+
for (const phase of ["install", "build", "verify"]) {
|
|
133
|
+
for (const script of product[phase] || [])
|
|
134
|
+
await commands.run({ script, cwd: directory, strict: true });
|
|
135
|
+
}
|
|
136
|
+
results.push({ product: product.id, outcome: "success" });
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
platform,
|
|
140
|
+
products: results,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
2
|
+
|
|
3
|
+
function cancellationEvent(reason) {
|
|
4
|
+
if (reason === "source-generation-changed") return "synchronize";
|
|
5
|
+
if (reason === "pull-request-dequeued") return "dequeued";
|
|
6
|
+
if (reason === "pull-request-closed") return "closed";
|
|
7
|
+
return "cancelled";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function exactCandidate(fresh, id) {
|
|
11
|
+
const candidate = fresh.queue.candidates.find(
|
|
12
|
+
(entry) => entry.candidateId === id,
|
|
13
|
+
);
|
|
14
|
+
if (
|
|
15
|
+
!candidate ||
|
|
16
|
+
candidate.sourceHead !== fresh.current.generation.source.commit ||
|
|
17
|
+
candidate.pullRequestNumber !== fresh.current.intent.source.pullRequest
|
|
18
|
+
)
|
|
19
|
+
throw new Error("Cancellation candidate source identity drift");
|
|
20
|
+
return candidate;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function applyCleanup(fresh, readback, service) {
|
|
24
|
+
const { decision } = readback;
|
|
25
|
+
const candidate = exactCandidate(fresh, readback.candidateId);
|
|
26
|
+
const request = {
|
|
27
|
+
expectedOldStateRoot: fresh.queue.stateRoot,
|
|
28
|
+
pullRequestNumber: candidate.pullRequestNumber,
|
|
29
|
+
expectedSourceHead: candidate.sourceHead,
|
|
30
|
+
eventAction: cancellationEvent(decision.reason),
|
|
31
|
+
evidenceRoot: recordDigest(readback),
|
|
32
|
+
outcome:
|
|
33
|
+
decision.reason === "pull-request-dequeued" ? "dequeued" : "cancelled",
|
|
34
|
+
reason: decision.reason,
|
|
35
|
+
execute: true,
|
|
36
|
+
};
|
|
37
|
+
if (decision.operation === "cancel-queued")
|
|
38
|
+
return service.cancelQueued({
|
|
39
|
+
...request,
|
|
40
|
+
candidateId: candidate.candidateId,
|
|
41
|
+
observedSourceHead:
|
|
42
|
+
readback.live.observedHead || readback.live.source?.commit,
|
|
43
|
+
});
|
|
44
|
+
if (
|
|
45
|
+
!candidate.terminal &&
|
|
46
|
+
(fresh.queue.activeWarrant?.candidateId !== candidate.candidateId ||
|
|
47
|
+
fresh.worker?.status !== "completed")
|
|
48
|
+
)
|
|
49
|
+
throw new Error(
|
|
50
|
+
"Active cancellation requires exact terminal worker readback",
|
|
51
|
+
);
|
|
52
|
+
const warrant =
|
|
53
|
+
fresh.queue.activeWarrant?.candidateId === candidate.candidateId
|
|
54
|
+
? fresh.queue.activeWarrant
|
|
55
|
+
: null;
|
|
56
|
+
return service.settle({
|
|
57
|
+
...request,
|
|
58
|
+
...(warrant
|
|
59
|
+
? {
|
|
60
|
+
fencingToken: warrant.fencingToken,
|
|
61
|
+
leaseGeneration: warrant.generation,
|
|
62
|
+
}
|
|
63
|
+
: {}),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function finishCleanup(fresh, result, readbackMaterial, ports) {
|
|
68
|
+
const receipt = await ports.materials.retain(
|
|
69
|
+
`cancellation/receipt-${result.receiptRoot.slice(7)}`,
|
|
70
|
+
result,
|
|
71
|
+
);
|
|
72
|
+
if (
|
|
73
|
+
result.observation.activeWarrant?.candidateId === fresh.decision.candidateId
|
|
74
|
+
)
|
|
75
|
+
return { status: "waiting", reason: "warrant-retained" };
|
|
76
|
+
const prior = fresh.current.phases[fresh.phase];
|
|
77
|
+
if (prior?.payload.state === "cancelled") {
|
|
78
|
+
if (
|
|
79
|
+
prior.payload.reason !== fresh.decision.reason ||
|
|
80
|
+
!prior.payload.materials.some(
|
|
81
|
+
(material) => recordDigest(material) === recordDigest(readbackMaterial),
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
throw new Error(
|
|
85
|
+
"Terminal cancellation projection has different evidence",
|
|
86
|
+
);
|
|
87
|
+
} else
|
|
88
|
+
await ports.progress.progress({
|
|
89
|
+
attempt: fresh.attempt,
|
|
90
|
+
phase: fresh.phase,
|
|
91
|
+
state: "cancelled",
|
|
92
|
+
eventKey: `cancelled:${result.receiptRoot}`,
|
|
93
|
+
reason: fresh.decision.reason,
|
|
94
|
+
materials: [readbackMaterial, receipt],
|
|
95
|
+
});
|
|
96
|
+
if (result.receipt.successorWake)
|
|
97
|
+
await ports.wake(result.receipt.successorWake);
|
|
98
|
+
return { status: "cancelled", receiptRoot: result.receiptRoot };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// The pending readback is committed to the attempt before the domain mutation.
|
|
102
|
+
// Recovery can therefore prove a completed effect even if its response was lost.
|
|
103
|
+
export async function cancelPipelineCandidate({ attempt, decision }, ports) {
|
|
104
|
+
const fresh = await ports.observe();
|
|
105
|
+
if (
|
|
106
|
+
fresh.attempt !== attempt ||
|
|
107
|
+
recordDigest(fresh.decision) !== recordDigest(decision)
|
|
108
|
+
)
|
|
109
|
+
throw new Error("Pipeline cancellation changed during admission");
|
|
110
|
+
if (decision.operation === "stop-worker") {
|
|
111
|
+
await ports.progress.requestStop(decision.reason);
|
|
112
|
+
return { status: "waiting", reason: "provider-worker-stop-required" };
|
|
113
|
+
}
|
|
114
|
+
if (!["cancel-queued", "settle-cancelled"].includes(decision.operation))
|
|
115
|
+
throw new Error("Pipeline cancellation requires an exact cleanup decision");
|
|
116
|
+
const candidate = exactCandidate(fresh, decision.candidateId);
|
|
117
|
+
const readback = {
|
|
118
|
+
schema: "buildchain.pipeline-cancellation-readback/v1",
|
|
119
|
+
attempt,
|
|
120
|
+
generation: fresh.current.generation.id,
|
|
121
|
+
decision,
|
|
122
|
+
live: fresh.live,
|
|
123
|
+
worker: fresh.worker || null,
|
|
124
|
+
candidateId: candidate.candidateId,
|
|
125
|
+
stateRoot: fresh.queue.stateRoot,
|
|
126
|
+
};
|
|
127
|
+
const evidenceRoot = recordDigest(readback);
|
|
128
|
+
const reference = await ports.materials.retain(
|
|
129
|
+
`cancellation/readback-${evidenceRoot.slice(7)}`,
|
|
130
|
+
readback,
|
|
131
|
+
"provider-readback",
|
|
132
|
+
);
|
|
133
|
+
await ports.progress.progress({
|
|
134
|
+
attempt,
|
|
135
|
+
phase: fresh.phase,
|
|
136
|
+
state: "waiting",
|
|
137
|
+
eventKey: `cancellation-request:${evidenceRoot}`,
|
|
138
|
+
reason: `pipeline-stop-requested:${decision.reason}`,
|
|
139
|
+
materials: [reference],
|
|
140
|
+
});
|
|
141
|
+
const result = await applyCleanup(fresh, readback, ports.service);
|
|
142
|
+
return finishCleanup(fresh, result, reference, ports);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function recoverPipelineCancellation(fresh, ports) {
|
|
146
|
+
const candidate = exactCandidate(fresh, fresh.decision.candidateId);
|
|
147
|
+
if (
|
|
148
|
+
!["cancelled", "dequeued"].includes(candidate.status) ||
|
|
149
|
+
!candidate.terminal?.evidenceRoot
|
|
150
|
+
)
|
|
151
|
+
throw new Error(
|
|
152
|
+
"Cancellation recovery requires terminal provider evidence",
|
|
153
|
+
);
|
|
154
|
+
const id = `cancellation/readback-${candidate.terminal.evidenceRoot.slice(7)}`;
|
|
155
|
+
const reference = fresh.current.materials[id];
|
|
156
|
+
if (!reference)
|
|
157
|
+
throw new Error("Cancellation intent was not retained before its effect");
|
|
158
|
+
const readback = await ports.materials.read(reference);
|
|
159
|
+
if (
|
|
160
|
+
recordDigest(readback) !== candidate.terminal.evidenceRoot ||
|
|
161
|
+
readback.attempt !== fresh.attempt ||
|
|
162
|
+
readback.generation !== fresh.current.generation.id ||
|
|
163
|
+
readback.candidateId !== candidate.candidateId
|
|
164
|
+
)
|
|
165
|
+
throw new Error("Cancellation recovery retained evidence drift");
|
|
166
|
+
const recovered = { ...fresh, decision: readback.decision };
|
|
167
|
+
const result = await applyCleanup(recovered, readback, ports.service);
|
|
168
|
+
return finishCleanup(recovered, result, reference, ports);
|
|
169
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
2
|
+
import { pipelineBuildEvidence } from "./build-evidence.js";
|
|
3
|
+
|
|
4
|
+
async function record(session, phase, state, receipt, host, reason = "") {
|
|
5
|
+
const observed = await session.journal.read();
|
|
6
|
+
if (observed.attempt !== session.observed.attempt)
|
|
7
|
+
throw new Error("Channel receipt belongs to a superseded attempt");
|
|
8
|
+
const root = recordDigest(receipt);
|
|
9
|
+
const eventKey = `${phase}:${state}:${root}`;
|
|
10
|
+
const store = host.materialStore(session);
|
|
11
|
+
const prior = observed.history
|
|
12
|
+
.at(-1)
|
|
13
|
+
.events.find((event) => event.payload.eventKey === eventKey);
|
|
14
|
+
if (prior) {
|
|
15
|
+
if (
|
|
16
|
+
prior.node !== phase ||
|
|
17
|
+
prior.payload.state !== state ||
|
|
18
|
+
prior.payload.reason !== reason ||
|
|
19
|
+
prior.payload.materials.length !== 1 ||
|
|
20
|
+
recordDigest(await store.read(prior.payload.materials[0])) !== root
|
|
21
|
+
)
|
|
22
|
+
throw new Error("Channel retry changed its immutable receipt");
|
|
23
|
+
return observed;
|
|
24
|
+
}
|
|
25
|
+
const reference = await store.retain(
|
|
26
|
+
`${phase}/${root.slice(7)}`,
|
|
27
|
+
receipt,
|
|
28
|
+
"provider-readback",
|
|
29
|
+
);
|
|
30
|
+
return session.progress.progress({
|
|
31
|
+
attempt: observed.attempt,
|
|
32
|
+
phase,
|
|
33
|
+
state,
|
|
34
|
+
eventKey,
|
|
35
|
+
reason,
|
|
36
|
+
materials: [reference],
|
|
37
|
+
expectedHead: observed.head,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function channelMerge(session, admission, host) {
|
|
42
|
+
const observed = await session.journal.read();
|
|
43
|
+
const current = { ...observed.history.at(-1), intent: session.intent };
|
|
44
|
+
if (current.phases.merge?.payload.state !== "success") {
|
|
45
|
+
const integration = await host.integration.observe(current);
|
|
46
|
+
await record(session, "merge", "success", integration, host);
|
|
47
|
+
}
|
|
48
|
+
if (host.terminalOnly) {
|
|
49
|
+
await host.wake(observed.attempt);
|
|
50
|
+
return {
|
|
51
|
+
operation: "wait",
|
|
52
|
+
reason: "publication-requires-a-normal-dispatch",
|
|
53
|
+
attempt: observed.attempt,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// Product publication is a separate typed domain stage. This operation keeps
|
|
57
|
+
// the business attempt open until its published byte/readback receipts exist.
|
|
58
|
+
return {
|
|
59
|
+
operation: "publish",
|
|
60
|
+
attempt: observed.attempt,
|
|
61
|
+
source: admission.live.source,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function controlPipelineChannel(session, admission, inputs, host) {
|
|
66
|
+
const observed = await session.journal.read();
|
|
67
|
+
const current = { ...observed.history.at(-1), intent: session.intent };
|
|
68
|
+
const { live } = admission;
|
|
69
|
+
if (live.merged) return channelMerge(session, admission, host);
|
|
70
|
+
if (
|
|
71
|
+
live.state !== "open" ||
|
|
72
|
+
!live.source ||
|
|
73
|
+
live.routeEnabled === false ||
|
|
74
|
+
live.targetBranch !== session.intent.source.targetBranch ||
|
|
75
|
+
live.baseCommit !== current.generation.baseCommit
|
|
76
|
+
) {
|
|
77
|
+
await record(
|
|
78
|
+
session,
|
|
79
|
+
observed.missing[0],
|
|
80
|
+
"superseded",
|
|
81
|
+
live,
|
|
82
|
+
host,
|
|
83
|
+
"channel-source-no-longer-admitted",
|
|
84
|
+
);
|
|
85
|
+
return {
|
|
86
|
+
operation: "successor",
|
|
87
|
+
reason: "channel-source-no-longer-admitted",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (host.terminalOnly)
|
|
91
|
+
return { operation: "wait", reason: "terminal-event-cannot-execute" };
|
|
92
|
+
const qualification = await host.qualifyChannel(
|
|
93
|
+
live.source,
|
|
94
|
+
live.targetBranch,
|
|
95
|
+
);
|
|
96
|
+
const build = await pipelineBuildEvidence(session, host);
|
|
97
|
+
if (!build) return { operation: "build", admission };
|
|
98
|
+
if (build.run.status !== "completed" || build.run.conclusion !== "success")
|
|
99
|
+
return { operation: "wait", reason: "source-workflow-not-qualified" };
|
|
100
|
+
const policy = await host.policy.observe(
|
|
101
|
+
current,
|
|
102
|
+
admission.protectedPlan.review,
|
|
103
|
+
);
|
|
104
|
+
if (!policy.review || !policy.checksPassing || !live.ready || live.draft)
|
|
105
|
+
return { operation: "wait", reason: "protected-channel-gates-pending" };
|
|
106
|
+
if (!current.phases.review)
|
|
107
|
+
await record(session, "review", "success", policy, host);
|
|
108
|
+
const queue = await host.queue.getMergeQueueState(
|
|
109
|
+
session.intent.source.targetBranch,
|
|
110
|
+
);
|
|
111
|
+
if (!queue.enabled)
|
|
112
|
+
throw new Error("Protected channel merge queue is disabled");
|
|
113
|
+
const entry = queue.entries.find(
|
|
114
|
+
(item) => item.pullRequestNumber === live.pullRequest,
|
|
115
|
+
);
|
|
116
|
+
if (entry) {
|
|
117
|
+
if (entry.pullRequestHeadSha !== live.source.commit)
|
|
118
|
+
throw new Error("Protected queue source drift");
|
|
119
|
+
return { operation: "wait", reason: "channel-in-protected-merge-queue" };
|
|
120
|
+
}
|
|
121
|
+
await record(
|
|
122
|
+
session,
|
|
123
|
+
"merge",
|
|
124
|
+
"waiting",
|
|
125
|
+
{
|
|
126
|
+
schema: "buildchain.pipeline-channel-enqueue/v1",
|
|
127
|
+
qualification,
|
|
128
|
+
policy,
|
|
129
|
+
queue,
|
|
130
|
+
},
|
|
131
|
+
host,
|
|
132
|
+
);
|
|
133
|
+
const again = await host.source.observeIntent(
|
|
134
|
+
session.intent,
|
|
135
|
+
current.generation,
|
|
136
|
+
inputs,
|
|
137
|
+
);
|
|
138
|
+
if (recordDigest(again.live) !== recordDigest(live))
|
|
139
|
+
throw new Error("Channel changed before queue admission");
|
|
140
|
+
await host.queue.enqueuePullRequest({
|
|
141
|
+
pullRequestId: policy.pr.id,
|
|
142
|
+
expectedHeadOid: live.source.commit,
|
|
143
|
+
});
|
|
144
|
+
const readback = await host.queue.getMergeQueueState(
|
|
145
|
+
session.intent.source.targetBranch,
|
|
146
|
+
);
|
|
147
|
+
if (
|
|
148
|
+
!readback.entries.some(
|
|
149
|
+
(item) =>
|
|
150
|
+
item.pullRequestNumber === live.pullRequest &&
|
|
151
|
+
item.pullRequestHeadSha === live.source.commit,
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
throw new Error("Channel queue admission requires exact provider readback");
|
|
155
|
+
await record(session, "merge", "waiting", readback, host);
|
|
156
|
+
return { operation: "wait", reason: "channel-enqueued" };
|
|
157
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
2
|
+
import { selectProductPublicationIntent } from "../../release/product-publication.js";
|
|
3
|
+
import { pipelineExpectedProducts } from "../../publication/pipeline/plan.js";
|
|
4
|
+
import { githubPipelineVersion } from "../../providers/github/pipeline-version.js";
|
|
5
|
+
|
|
6
|
+
// This read-only qualification must precede both required checks and queue
|
|
7
|
+
// admission. Publication repeats the Rust-owned decision after protected merge.
|
|
8
|
+
export async function qualifyPipelineChannelSource(source, branch, host) {
|
|
9
|
+
if (branch.startsWith("dev/")) return null;
|
|
10
|
+
const admitted = await host.source.source(source.commit, source.configPath);
|
|
11
|
+
if (recordDigest(admitted.identity) !== recordDigest(source))
|
|
12
|
+
throw new Error("Channel qualification source identity drift");
|
|
13
|
+
const { plan } = admitted;
|
|
14
|
+
const routes = plan.channels.filter(
|
|
15
|
+
(route) => route.to === branch && route.operation !== "develop",
|
|
16
|
+
);
|
|
17
|
+
if (routes.length !== 1)
|
|
18
|
+
throw new Error("Channel qualification requires one publication route");
|
|
19
|
+
const route = routes[0];
|
|
20
|
+
const from = `${route.operation === "alpha" ? "dev" : "alpha"}/${branch.split("/").slice(1).join("/")}`;
|
|
21
|
+
if (["alpha", "stable"].includes(route.operation) && route.from !== from)
|
|
22
|
+
throw new Error("Channel source and target version lines must match");
|
|
23
|
+
const version = await githubPipelineVersion(
|
|
24
|
+
host.request,
|
|
25
|
+
host.repository,
|
|
26
|
+
).inspect(source, plan.version);
|
|
27
|
+
const selection = selectProductPublicationIntent({
|
|
28
|
+
channel: route.operation === "alpha" ? "alpha" : "stable",
|
|
29
|
+
targetRef: branch,
|
|
30
|
+
sourceSha: source.commit,
|
|
31
|
+
sourceTimestamp: version.sourceTimestamp,
|
|
32
|
+
repository: source.repository,
|
|
33
|
+
artifactKind: "custom",
|
|
34
|
+
requiredArtifactsRoot: recordDigest(pipelineExpectedProducts(plan)),
|
|
35
|
+
candidateVersion: version.version,
|
|
36
|
+
observedVersions: [],
|
|
37
|
+
});
|
|
38
|
+
if (
|
|
39
|
+
plan.version.strategy === "anchored" &&
|
|
40
|
+
selection.version !== version.version
|
|
41
|
+
)
|
|
42
|
+
throw new Error("Anchored channel source requires a materialized version");
|
|
43
|
+
const body = {
|
|
44
|
+
schema: "buildchain.pipeline-channel-source-qualification/v1",
|
|
45
|
+
source,
|
|
46
|
+
route,
|
|
47
|
+
contractRoot: recordDigest(plan),
|
|
48
|
+
version: version.version,
|
|
49
|
+
versionFilesRoot: recordDigest(version.files),
|
|
50
|
+
selection,
|
|
51
|
+
};
|
|
52
|
+
return { ...body, root: recordDigest(body) };
|
|
53
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { pipelineEvent } from "./events.js";
|
|
2
|
+
import { selectPipelineSession } from "./selection.js";
|
|
3
|
+
import { beginPipelineBuild } from "./build-control.js";
|
|
4
|
+
import { controlPipelineDelivery } from "./delivery-control.js";
|
|
5
|
+
import {
|
|
6
|
+
githubPipelineEvents,
|
|
7
|
+
githubPipelineQueueExit,
|
|
8
|
+
} from "../../providers/github/pipeline-events.js";
|
|
9
|
+
import { pipelinePlatforms } from "./platforms.js";
|
|
10
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
11
|
+
|
|
12
|
+
async function groupBuild(event, inputs, host) {
|
|
13
|
+
const queue = await host.queue.getMergeQueueState(event.branch);
|
|
14
|
+
if (
|
|
15
|
+
!queue.enabled ||
|
|
16
|
+
!queue.entries.some(
|
|
17
|
+
(entry) =>
|
|
18
|
+
entry.headSha === event.commit && entry.baseSha === event.baseCommit,
|
|
19
|
+
)
|
|
20
|
+
)
|
|
21
|
+
throw new Error(
|
|
22
|
+
"Merge-group execution is not in the exact protected provider queue",
|
|
23
|
+
);
|
|
24
|
+
const admitted = await host.source.source(
|
|
25
|
+
event.commit,
|
|
26
|
+
inputs["config-path"],
|
|
27
|
+
);
|
|
28
|
+
const context = {
|
|
29
|
+
schema: "buildchain.pipeline-group-build-context/v1",
|
|
30
|
+
source: admitted.identity,
|
|
31
|
+
branch: event.branch,
|
|
32
|
+
baseCommit: event.baseCommit,
|
|
33
|
+
runId: host.runId,
|
|
34
|
+
runAttempt: host.runAttempt,
|
|
35
|
+
platforms: pipelinePlatforms(admitted.plan),
|
|
36
|
+
};
|
|
37
|
+
return { operation: "build", context };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function controlPipeline(name, payload, inputs, host) {
|
|
41
|
+
const event = pipelineEvent(name, payload, host.repository);
|
|
42
|
+
host.terminalOnly = event.terminalOnly;
|
|
43
|
+
host.eventAction = payload.action || "";
|
|
44
|
+
if (event.kind === "ignore")
|
|
45
|
+
return { operation: "wait", reason: event.reason };
|
|
46
|
+
if (event.kind === "branch") {
|
|
47
|
+
const events = githubPipelineEvents(
|
|
48
|
+
host.request,
|
|
49
|
+
host.provider,
|
|
50
|
+
host.repository,
|
|
51
|
+
);
|
|
52
|
+
const attempts = await events.branch(event.branch, event.commit);
|
|
53
|
+
for (const attempt of attempts) await host.wake(attempt);
|
|
54
|
+
return {
|
|
55
|
+
operation: "wait",
|
|
56
|
+
reason: "existing-channel-intents-woken",
|
|
57
|
+
count: attempts.length,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (event.kind === "merge-group") return groupBuild(event, inputs, host);
|
|
61
|
+
const selected = await selectPipelineSession(event, payload, inputs, host);
|
|
62
|
+
if (!selected)
|
|
63
|
+
return { operation: "wait", reason: "no-admitted-pipeline-intent" };
|
|
64
|
+
const { session, admission } = selected;
|
|
65
|
+
const current = session.observed.history.at(-1);
|
|
66
|
+
if (
|
|
67
|
+
current.identity.requestKey.startsWith("recover:") &&
|
|
68
|
+
recordDigest(current.events[0].runtime) !== recordDigest(host.runtime)
|
|
69
|
+
) {
|
|
70
|
+
await host.wake(current.identity.id);
|
|
71
|
+
return {
|
|
72
|
+
operation: "wait",
|
|
73
|
+
reason: "admitted-recovery-runtime-continuation-required",
|
|
74
|
+
attempt: current.identity.id,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (
|
|
78
|
+
["failure", "cancelled", "superseded", "complete"].includes(
|
|
79
|
+
session.observed.status,
|
|
80
|
+
)
|
|
81
|
+
) {
|
|
82
|
+
await host.notifyTerminal?.(session);
|
|
83
|
+
await host.project(session);
|
|
84
|
+
return {
|
|
85
|
+
operation: "wait",
|
|
86
|
+
reason: `attempt-${session.observed.status}`,
|
|
87
|
+
attempt: session.observed.attempt,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (
|
|
91
|
+
admission.live.source &&
|
|
92
|
+
host.selection.source.sha !== admission.live.source.commit
|
|
93
|
+
) {
|
|
94
|
+
await host.wake(session.observed.attempt);
|
|
95
|
+
return {
|
|
96
|
+
operation: "wait",
|
|
97
|
+
reason: "source-generation-runtime-selection-required",
|
|
98
|
+
attempt: session.observed.attempt,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (session.intent.expectedNodes.includes("warrant"))
|
|
102
|
+
host.queueExit ||= githubPipelineQueueExit(
|
|
103
|
+
host.github.graphql,
|
|
104
|
+
host.repository,
|
|
105
|
+
);
|
|
106
|
+
const operation = session.intent.expectedNodes.includes("warrant")
|
|
107
|
+
? await controlPipelineDelivery(session, inputs, host)
|
|
108
|
+
: await host.channel(session, admission, inputs);
|
|
109
|
+
if (operation.operation === "build") {
|
|
110
|
+
const context = await beginPipelineBuild(
|
|
111
|
+
session,
|
|
112
|
+
operation.admission || admission,
|
|
113
|
+
host,
|
|
114
|
+
);
|
|
115
|
+
await host.project(session);
|
|
116
|
+
if (!context)
|
|
117
|
+
return { operation: "wait", reason: "existing-build-execution-retained" };
|
|
118
|
+
return { operation: "build", context };
|
|
119
|
+
}
|
|
120
|
+
await host.project(session);
|
|
121
|
+
if (operation.operation === "successor") {
|
|
122
|
+
// Closed or retargeted PRs do not authorize a new attempt on the old route.
|
|
123
|
+
// A later normal event re-admits a lawful source after the old candidate ends.
|
|
124
|
+
await host.wake(session.observed.attempt);
|
|
125
|
+
return {
|
|
126
|
+
operation: "wait",
|
|
127
|
+
reason: operation.reason,
|
|
128
|
+
attempt: session.observed.attempt,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (operation.operation === "settle") {
|
|
132
|
+
const settled = await host.settle(
|
|
133
|
+
session,
|
|
134
|
+
operation.fresh,
|
|
135
|
+
operation.delivery,
|
|
136
|
+
);
|
|
137
|
+
await host.project(session);
|
|
138
|
+
return settled;
|
|
139
|
+
}
|
|
140
|
+
return operation;
|
|
141
|
+
}
|