@kungfu-tech/buildchain 4.0.1-alpha.2 → 4.0.1-alpha.21
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/actions/release-tail/README.md +8 -0
- package/architecture/ci-lane-change-budget.json +187 -0
- package/architecture/decisions/0002-next-development-transition.md +101 -0
- package/architecture/decisions/0003-two-phase-delivery-warrant.md +152 -0
- package/architecture/internal-capabilities.json +211 -8
- package/architecture/maintainability-policy.json +113 -28
- package/architecture/release-tail-contract-inventory.json +5 -2
- package/architecture/v3-core-mechanism-inventory.json +91 -0
- package/architecture/v4-adopter-delivery-parity.json +32 -5
- package/architecture/v4-capability-state-machine-manifest.json +73 -0
- package/architecture/v4-compatibility-facts-parity.json +77 -0
- package/architecture/v4-delivery-authority-parity.json +250 -0
- package/architecture/v4-delivery-warrant-shadow-fixtures.json +41 -9
- package/architecture/v4-floating-consumer-policy.json +6 -0
- package/architecture/v4-next-development-parity.json +192 -0
- package/architecture/v4-publication-rehearsal-parity.json +84 -0
- package/architecture/v4-release-train-parity.json +1 -0
- package/architecture/v4-tail-reseal-parity.json +150 -0
- package/bin/buildchain.mjs +32 -3
- package/bin/internal/adopter-delivery-cli.mjs +70 -0
- package/bin/internal/command-registry.mjs +3 -0
- package/bin/internal/compatibility-facts-cli.mjs +101 -0
- package/contracts/dev-delivery-authority-v2.schema.json +662 -0
- package/contracts/fixtures/next-development-transition-v1/anchored-manual-waiting.json +47 -0
- package/contracts/fixtures/next-development-transition-v1/semver-auto-planned.json +47 -0
- package/contracts/fixtures/next-development-transition-v1/version-model-cases.json +40 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/archive-template.json +19 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/bootstrap-positive.json +93 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/gate-positive.json +29 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/gate-unknown-selector.json +26 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/offline-vectors.json +50 -0
- package/contracts/fixtures/v4-compatibility-facts-v1/shared.json +1129 -0
- package/contracts/fixtures/v4-floating-consumer-policy-v1/cases.json +7 -1
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/artifacts/product.bin +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/config/buildchain.toml +5 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/release-activation.json +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/signed-channel.json +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/evidence/qualification.json +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/candidate.json +5 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/release-passport.json +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/capsule.json +707 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/offline-vectors.json +27 -0
- package/contracts/fixtures/v4-tail-reseal-v1/valid.json +229 -0
- package/contracts/next-development-request-v1.schema.json +66 -0
- package/contracts/next-development-transition-v1.schema.json +292 -0
- package/contracts/v4-adopter-delivery-v1.schema.json +81 -0
- package/contracts/v4-compatibility-facts-v1.schema.json +230 -0
- package/contracts/v4-floating-consumer-policy-receipt-v1.schema.json +4 -2
- package/contracts/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
- package/contracts/v4-tail-reseal-v1.schema.json +276 -0
- package/dist/site/agent-index.json +5 -0
- package/dist/site/artifact-schemas.json +10 -0
- package/dist/site/buildchain-contract.json +1475 -27
- package/dist/site/buildchain-site.json +627 -31
- package/dist/site/capability-registry.json +17 -14
- package/dist/site/cli-registry.json +205 -3
- package/dist/site/kfd-claims.json +490 -16
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +52 -6
- package/dist/site/node-api-registry.json +14736 -8851
- package/dist/site/page-registry.json +593 -21
- package/dist/site/public-surface-audit.json +837 -48
- package/dist/site/publication-authority-registry.json +44 -8
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +24 -0
- package/dist/site/schemas/dev-delivery-authority-v2.schema.json +662 -0
- package/dist/site/schemas/release-tail-capabilities-v1.schema.json +199 -0
- package/dist/site/schemas/v4-adopter-delivery-v1.schema.json +81 -0
- package/dist/site/schemas/v4-compatibility-facts-v1.schema.json +230 -0
- package/dist/site/schemas/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
- package/dist/site/site-manifest.json +34 -10
- package/dist/site/workflow-registry.json +160 -10
- package/docs/MAP.md +8 -2
- package/docs/cli-reference.md +404 -13
- package/docs/cli.md +8 -0
- package/docs/dev-alpha-candidate-patrol.md +8 -0
- package/docs/dev-delivery-qualification-landing-adr.md +251 -0
- package/docs/dev-delivery-warrant.md +351 -38
- package/docs/next-development-transition.md +118 -0
- package/docs/node-api-reference.md +646 -293
- package/docs/release-tail-provider-plane.md +30 -0
- package/docs/v4-adopter-delivery.md +133 -0
- package/docs/v4-compatibility-facts.md +91 -0
- package/docs/v4-publication-rehearsal.md +130 -0
- package/docs/v4-tail-reseal.md +73 -0
- package/docs/versioning.md +4 -3
- package/package.json +34 -6
- package/packages/core/adopter-delivery-passport.js +259 -0
- package/packages/core/adopter-delivery-vectors.js +158 -0
- package/packages/core/adopter-delivery-vectors.json +77 -0
- package/packages/core/buildchain-agent-manuals.js +3 -0
- package/packages/core/buildchain-compatibility-authority.js +336 -0
- package/packages/core/buildchain-compatibility-fact.js +394 -0
- package/packages/core/buildchain-compatibility-facts.json +217 -0
- package/packages/core/buildchain-compatibility-proof.js +566 -0
- package/packages/core/buildchain-config.js +84 -0
- package/packages/core/buildchain-contract.js +96 -143
- package/packages/core/buildchain-delivery-bootstrap.js +240 -0
- package/packages/core/buildchain-delivery-infrastructure.js +164 -0
- package/packages/core/buildchain-delivery-self-dogfood.js +395 -0
- package/packages/core/buildchain-publication-authority.js +1 -0
- package/packages/core/channel-candidate.js +8 -0
- package/packages/core/channel-promotion-baseline.js +55 -0
- package/packages/core/ci-lane-change-budget.js +247 -0
- package/packages/core/dev-alpha-candidate-selection.js +10 -2
- package/packages/core/dev-delivery-authority-candidate.js +270 -0
- package/packages/core/dev-delivery-authority-evidence.js +146 -0
- package/packages/core/dev-delivery-authority-landing.js +461 -0
- package/packages/core/dev-delivery-authority-observation.js +48 -0
- package/packages/core/dev-delivery-authority-qualification.js +591 -0
- package/packages/core/dev-delivery-authority-settlement.js +213 -0
- package/packages/core/dev-delivery-authority-state.js +583 -0
- package/packages/core/dev-delivery-candidate-identity.js +13 -0
- package/packages/core/dev-delivery-contract-surface.js +76 -0
- package/packages/core/dev-delivery-execution-failure.js +133 -0
- package/packages/core/dev-delivery-execution-transfer.js +592 -0
- package/packages/core/dev-delivery-landing-admission-core.js +119 -0
- package/packages/core/dev-delivery-landing-readback.js +598 -0
- package/packages/core/dev-delivery-landing-terminal-evidence.js +271 -0
- package/packages/core/dev-delivery-landing-testing-port.js +6 -0
- package/packages/core/dev-delivery-native-execution.js +110 -0
- package/packages/core/dev-delivery-native-proof.js +562 -0
- package/packages/core/dev-delivery-process-boundary.js +563 -0
- package/packages/core/dev-delivery-provider-attempt.js +127 -0
- package/packages/core/dev-delivery-provider-heartbeat.js +382 -0
- package/packages/core/dev-delivery-warrant-cancellation.js +1 -0
- package/packages/core/dev-delivery-warrant-qualification.js +145 -0
- package/packages/core/dev-delivery-warrant-settlement.js +237 -36
- package/packages/core/dev-delivery-warrant-state.js +587 -0
- package/packages/core/dev-delivery-warrant.js +328 -370
- package/packages/core/github-governance-authority.js +1 -0
- package/packages/core/index.js +5 -0
- package/packages/core/kungfu-temporal-fact.js +557 -0
- package/packages/core/legacy-kfd-adopter-driver.js +167 -0
- package/packages/core/next-development-candidate-reservation.js +186 -0
- package/packages/core/next-development-controller.js +728 -0
- package/packages/core/next-development-projection.js +287 -0
- package/packages/core/next-development-transition.js +738 -0
- package/packages/core/paper-agent-entry.js +11 -5
- package/packages/core/paper.js +16 -5
- package/packages/core/published-delivery-authority.js +266 -0
- package/packages/core/release-candidate-recovery.js +70 -15
- package/packages/core/release-candidate.js +2 -2
- package/packages/core/release-passport.js +36 -9
- package/packages/core/v4-adopter-delivery-parity.js +3 -3
- package/packages/core/v4-adopter-delivery.js +359 -0
- package/packages/core/v4-canonical-contracts.js +5 -0
- package/packages/core/v4-delivery-warrant-read-candidate.js +10 -3
- package/packages/core/v4-floating-consumer-evidence.js +18 -4
- package/packages/core/v4-floating-consumer-policy.js +143 -20
- package/packages/core/v4-floating-consumer-release-passport.js +32 -5
- package/packages/core/v4-publication-rehearsal-capsule.js +468 -0
- package/packages/core/v4-publication-rehearsal-provider-bindings.js +212 -0
- package/packages/core/v4-publication-rehearsal.js +481 -0
- package/packages/core/v4-tail-reseal-contract.js +26 -0
- package/packages/core/v4-tail-reseal-github.js +178 -0
- package/packages/core/v4-tail-reseal-receipt.js +225 -0
- package/packages/core/v4-tail-reseal.js +594 -0
- package/packages/core/workflow-yaml-contract.js +24 -2
- package/scripts/buildchain-cli-help.mjs +41 -2
- package/scripts/check-ci-lane-change-budget.mjs +100 -0
- package/scripts/check-inventory.mjs +14 -3
- package/scripts/check-v4-floating-consumer-policy-contract.mjs +23 -4
- package/scripts/dev-alpha-candidate-patrol.mjs +22 -1
- package/scripts/dev-delivery-authority-command-adapters.mjs +206 -0
- package/scripts/dev-delivery-authority-provider.mjs +28 -0
- package/scripts/dev-delivery-authority.mjs +490 -0
- package/scripts/dev-delivery-native-run.mjs +177 -0
- package/scripts/dev-delivery-process-boundary.mjs +260 -0
- package/scripts/dev-delivery-proof.mjs +67 -2
- package/scripts/dev-delivery-provider-heartbeat.mjs +215 -0
- package/scripts/dev-delivery-source-proof-replay.mjs +153 -0
- package/scripts/dev-delivery-source-proof-reuse.mjs +13 -97
- package/scripts/dev-delivery-two-phase-resume.mjs +345 -0
- package/scripts/dev-delivery-two-phase.mjs +573 -0
- package/scripts/dev-delivery-warrant-options.mjs +266 -0
- package/scripts/dev-delivery-warrant-store.mjs +231 -0
- package/scripts/dev-delivery-warrant.mjs +232 -194
- package/scripts/dev-pr-auto-merge.mjs +1 -1
- package/scripts/dev-pr-delivery-warrant.mjs +11 -0
- package/scripts/dev-qualification-patrol.mjs +9 -9
- package/scripts/dispatch-artifact-signing-authority.mjs +1 -1
- package/scripts/generate-channel-promotion-workflow.mjs +2 -2
- package/scripts/generate-next-development-guidance.mjs +49 -0
- package/scripts/generate-release-candidate-passport.mjs +152 -0
- package/scripts/generate-site-bundle.mjs +69 -4
- package/scripts/init-repo.mjs +22 -4
- package/scripts/next-development-self-dogfood-harness.mjs +460 -0
- package/scripts/next-development-self-dogfood.mjs +597 -0
- package/scripts/next-development-transition.mjs +47 -0
- package/scripts/npm-publish-transaction.mjs +2 -2
- package/scripts/promotion-channel-router.mjs +4 -4
- package/scripts/release-candidate-anchor-provenance.mjs +99 -0
- package/scripts/release-tail.mjs +51 -3
- package/scripts/release-train-self-dogfood.mjs +94 -92
- package/scripts/resume-from-candidate-run.mjs +557 -30
- package/scripts/site-capability-metadata.mjs +41 -0
- package/scripts/stable-candidate-qualification.mjs +9 -10
- package/scripts/v4-consumer-policy.mjs +5 -5
- package/scripts/v4-publication-rehearsal-fixture.mjs +144 -0
- package/scripts/v4-tail-reseal-macos-rehearsal.mjs +134 -0
- package/scripts/v4-tail-reseal.mjs +416 -0
- package/templates/native-dev-delivery.yml +143 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
devDeliveryClone as clone,
|
|
6
|
+
devDeliveryContentRoot,
|
|
7
|
+
devDeliveryExactRoot as exactRoot,
|
|
8
|
+
devDeliveryExactSha as exactSha,
|
|
9
|
+
devDeliveryPositiveInteger as positiveInteger,
|
|
10
|
+
devDeliveryProtectedBase as protectedBase,
|
|
11
|
+
devDeliveryRepository as repository,
|
|
12
|
+
devDeliveryText as normalizedText,
|
|
13
|
+
devDeliveryTimestamp as timestamp,
|
|
14
|
+
} from "./dev-delivery-common.js";
|
|
15
|
+
import { assertDevDeliveryGitHubHostedRunner } from "./dev-delivery-provider-attempt.js";
|
|
16
|
+
|
|
17
|
+
export const PROVIDER_FINALIZER_BOUNDARY_SCHEMA =
|
|
18
|
+
"kungfu.buildchain.provider-finalizer-boundary/v1";
|
|
19
|
+
export const PROVIDER_FAILURE_SETTLEMENT_SCHEMA =
|
|
20
|
+
"kungfu.buildchain.provider-failure-settlement/v1";
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
createNativeExecutionTransfer,
|
|
24
|
+
NATIVE_EXECUTION_TRANSFER_SCHEMA,
|
|
25
|
+
stageNativeExecutionTransfer,
|
|
26
|
+
verifyNativeExecutionTransfer,
|
|
27
|
+
} from "./dev-delivery-execution-transfer.js";
|
|
28
|
+
|
|
29
|
+
const PROVIDER_CREDENTIAL_NAME =
|
|
30
|
+
/(?:^(?:ACTIONS_ID_TOKEN_REQUEST_TOKEN|ACTIONS_RUNTIME_TOKEN|AWS_SESSION_TOKEN|AWS_WEB_IDENTITY_TOKEN_FILE|AZURE_FEDERATED_TOKEN_FILE|GH_TOKEN|GITHUB_TOKEN|GOOGLE_GHA_CREDS_PATH|KUNGFU_GITHUB_TOKEN|NPM_TOKEN|SSH_AUTH_SOCK)$|(?:^|_)(?:(?:api|access|auth|publish|release|provider|promotion|governance|approval|mutation|write)_?(?:key|token)|authorization|credentials?|password|secret|private_key|client_secret|signing_key)(?:_|$))/iu;
|
|
31
|
+
const HOSTED_RUNNER_BOUNDARY = "github-actions-runner-worker/v1";
|
|
32
|
+
const GITHUB_HOSTED_LINUX_WORKER =
|
|
33
|
+
/^\/home\/runner\/(?:runners|actions-runner\/cached)\/(\d+\.\d+\.\d+)\/bin\/Runner\.Worker$/u;
|
|
34
|
+
function text(value, label) {
|
|
35
|
+
const normalized = normalizedText(value);
|
|
36
|
+
if (!normalized) throw new Error(`${label} is required`);
|
|
37
|
+
return normalized;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isCredentialVariableName(name) {
|
|
41
|
+
return PROVIDER_CREDENTIAL_NAME.test(String(name || "").trim());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readProcFile(procRoot, pid, name, readFileSync) {
|
|
45
|
+
const file = path.posix.join(procRoot, String(pid), name);
|
|
46
|
+
return ancestryRead(readFileSync, file, "utf8");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function ancestryRead(reader, file, ...args) {
|
|
50
|
+
try {
|
|
51
|
+
return reader(file, ...args);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`credential ancestry is unreadable at ${file}: ${error.code || error.message}`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function processCredentialNames(procRoot, pid, readFileSync) {
|
|
60
|
+
return readProcFile(procRoot, pid, "environ", readFileSync)
|
|
61
|
+
.split("\0")
|
|
62
|
+
.filter((entry) => {
|
|
63
|
+
const separator = entry.indexOf("=");
|
|
64
|
+
const name = entry.slice(0, separator);
|
|
65
|
+
const value = entry.slice(separator + 1);
|
|
66
|
+
return (
|
|
67
|
+
separator > 0 &&
|
|
68
|
+
Boolean(value) &&
|
|
69
|
+
!(
|
|
70
|
+
name === "BUILDCHAIN_CREDENTIAL_ANCESTRY_BOUNDARY" &&
|
|
71
|
+
value === HOSTED_RUNNER_BOUNDARY
|
|
72
|
+
) &&
|
|
73
|
+
isCredentialVariableName(name)
|
|
74
|
+
);
|
|
75
|
+
})
|
|
76
|
+
.map((entry) => entry.slice(0, entry.indexOf("=")));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function processParentPid(procRoot, pid, readFileSync) {
|
|
80
|
+
const status = readProcFile(procRoot, pid, "status", readFileSync);
|
|
81
|
+
const match = status.match(/^PPid:\s+(\d+)$/mu);
|
|
82
|
+
if (!match)
|
|
83
|
+
throw new Error(`credential ancestry has no PPid for process ${pid}`);
|
|
84
|
+
return Number(match[1]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function assertHostedLinuxController(environment) {
|
|
88
|
+
const expected = {
|
|
89
|
+
GITHUB_ACTIONS: "true",
|
|
90
|
+
RUNNER_ENVIRONMENT: "github-hosted",
|
|
91
|
+
RUNNER_OS: "Linux",
|
|
92
|
+
BUILDCHAIN_CREDENTIAL_ANCESTRY_BOUNDARY: HOSTED_RUNNER_BOUNDARY,
|
|
93
|
+
};
|
|
94
|
+
const exact = Object.entries(expected).every(
|
|
95
|
+
([name, value]) => environment[name] === value,
|
|
96
|
+
);
|
|
97
|
+
if (!exact || !/^GitHub Actions(?: \d+)?$/u.test(environment.RUNNER_NAME)) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
"credential ancestry requires the explicit GitHub-hosted Linux runner boundary",
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function hostedBoundary(procRoot, pid, command, readlinkSync) {
|
|
105
|
+
if (
|
|
106
|
+
command.length !== 4 ||
|
|
107
|
+
command[1] !== "spawnclient" ||
|
|
108
|
+
!/^\d+$/u.test(command[2] || "") ||
|
|
109
|
+
!/^\d+$/u.test(command[3] || "")
|
|
110
|
+
) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const link = path.posix.join(procRoot, String(pid), "exe");
|
|
114
|
+
const executable = ancestryRead(readlinkSync, link);
|
|
115
|
+
if (
|
|
116
|
+
!GITHUB_HOSTED_LINUX_WORKER.test(executable) ||
|
|
117
|
+
path.posix.basename(command[0]) !== path.posix.basename(executable)
|
|
118
|
+
) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`credential ancestry found an untrusted Runner.Worker: executable=${JSON.stringify(executable)} argv0=${JSON.stringify(command[0])}`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return { pid, kind: HOSTED_RUNNER_BOUNDARY, executable };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function inspectCredentiallessProcessAncestry({
|
|
127
|
+
pid = process.pid,
|
|
128
|
+
platform = process.platform,
|
|
129
|
+
procRoot = "/proc",
|
|
130
|
+
readFileSync = fs.readFileSync,
|
|
131
|
+
readlinkSync = fs.readlinkSync,
|
|
132
|
+
environment = process.env,
|
|
133
|
+
} = {}) {
|
|
134
|
+
if (platform !== "linux")
|
|
135
|
+
throw new Error("credential ancestry requires Linux /proc evidence");
|
|
136
|
+
assertHostedLinuxController(environment);
|
|
137
|
+
const exposed = [];
|
|
138
|
+
const traversed = [];
|
|
139
|
+
const visited = new Set();
|
|
140
|
+
let current = positiveInteger(pid, "process pid");
|
|
141
|
+
while (true) {
|
|
142
|
+
if (visited.has(current))
|
|
143
|
+
throw new Error(`credential ancestry cycle at process ${current}`);
|
|
144
|
+
visited.add(current);
|
|
145
|
+
traversed.push(current);
|
|
146
|
+
const names = processCredentialNames(procRoot, current, readFileSync);
|
|
147
|
+
if (names.length > 0)
|
|
148
|
+
exposed.push({ pid: current, names: [...new Set(names)].sort() });
|
|
149
|
+
const command = readProcFile(procRoot, current, "cmdline", readFileSync)
|
|
150
|
+
.split("\0")
|
|
151
|
+
.filter(Boolean);
|
|
152
|
+
const boundary = hostedBoundary(procRoot, current, command, readlinkSync);
|
|
153
|
+
if (boundary)
|
|
154
|
+
return { ok: exposed.length === 0, boundary, exposed, traversed };
|
|
155
|
+
const parent = processParentPid(procRoot, current, readFileSync);
|
|
156
|
+
if (!Number.isInteger(parent) || parent <= 1)
|
|
157
|
+
throw new Error("credential ancestry escaped before trusted boundary");
|
|
158
|
+
current = parent;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function assertCredentiallessProcessAncestry(options = {}) {
|
|
163
|
+
const inspection = inspectCredentiallessProcessAncestry(options);
|
|
164
|
+
if (!inspection.ok) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`write-capable credential-like variable found in process ancestry: ${inspection.exposed
|
|
167
|
+
.map(({ pid, names }) => `${pid}:${names.join(",")}`)
|
|
168
|
+
.join(" ")}`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
return inspection;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function devDeliveryProviderJobByName(
|
|
175
|
+
jobs,
|
|
176
|
+
expectedName,
|
|
177
|
+
{ allowMissing = false } = {},
|
|
178
|
+
) {
|
|
179
|
+
const exactName = text(expectedName, "provider job name");
|
|
180
|
+
const suffix = ` / ${exactName}`;
|
|
181
|
+
const matches = jobs.filter((job) => {
|
|
182
|
+
const actualName = String(job.name || "");
|
|
183
|
+
return (
|
|
184
|
+
actualName === exactName ||
|
|
185
|
+
(actualName.endsWith(suffix) && actualName.length > suffix.length)
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
if (matches.length === 0 && allowMissing) return null;
|
|
189
|
+
if (matches.length !== 1) {
|
|
190
|
+
throw new Error(`expected exactly one provider job named ${exactName}`);
|
|
191
|
+
}
|
|
192
|
+
return matches[0];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function assertHostedJob(job, runAttempt, label) {
|
|
196
|
+
if (Number(job.run_attempt) !== runAttempt)
|
|
197
|
+
throw new Error(`${label} run attempt mismatch`);
|
|
198
|
+
if (!job.runner_name) throw new Error(`${label} runner name is missing`);
|
|
199
|
+
assertDevDeliveryGitHubHostedRunner({
|
|
200
|
+
runnerGroupName: job.runner_group_name,
|
|
201
|
+
runnerLabels: job.labels,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function createNativeExecutionSealBinding({
|
|
206
|
+
nativeContext,
|
|
207
|
+
sealJob,
|
|
208
|
+
sealRunnerName,
|
|
209
|
+
sealRunnerEnvironment,
|
|
210
|
+
sealRunnerOs,
|
|
211
|
+
sealRunnerArch,
|
|
212
|
+
observedAt,
|
|
213
|
+
} = {}) {
|
|
214
|
+
if (sealRunnerEnvironment !== "github-hosted") {
|
|
215
|
+
throw new Error(
|
|
216
|
+
"native execution seal requires a GitHub-hosted fresh runner",
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
const native = clone(nativeContext || {});
|
|
220
|
+
const nativeRunId = positiveInteger(native.workflowRunId, "workflow run id");
|
|
221
|
+
const nativeRunAttempt = positiveInteger(
|
|
222
|
+
native.workflowRunAttempt,
|
|
223
|
+
"workflow run attempt",
|
|
224
|
+
);
|
|
225
|
+
const nativeCompletedAt = timestamp(
|
|
226
|
+
native.evidenceCompletedAt,
|
|
227
|
+
"native evidence completion time",
|
|
228
|
+
);
|
|
229
|
+
const sealedAt = timestamp(observedAt, "seal observation time");
|
|
230
|
+
if (
|
|
231
|
+
text(native.job) === text(sealJob) ||
|
|
232
|
+
text(native.runnerName) === text(sealRunnerName)
|
|
233
|
+
) {
|
|
234
|
+
throw new Error("native and seal job or runner identities match");
|
|
235
|
+
}
|
|
236
|
+
if (
|
|
237
|
+
native.schema !== "kungfu.buildchain.native-job-context/v1" ||
|
|
238
|
+
native.runnerEnvironment !== "github-hosted" ||
|
|
239
|
+
!["succeeded", "failed"].includes(native.outcome)
|
|
240
|
+
) {
|
|
241
|
+
throw new Error("native job context is not a hosted terminal outcome");
|
|
242
|
+
}
|
|
243
|
+
if (Date.parse(nativeCompletedAt) >= Date.parse(sealedAt)) {
|
|
244
|
+
throw new Error("native evidence completion and seal ordering is invalid");
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
producer: {
|
|
248
|
+
workflowRunId: nativeRunId,
|
|
249
|
+
workflowRunAttempt: nativeRunAttempt,
|
|
250
|
+
job: text(native.job),
|
|
251
|
+
runnerEnvironment: native.runnerEnvironment,
|
|
252
|
+
runnerName: text(native.runnerName),
|
|
253
|
+
runnerOs: text(native.runnerOs),
|
|
254
|
+
runnerArch: text(native.runnerArch),
|
|
255
|
+
},
|
|
256
|
+
sealer: {
|
|
257
|
+
job: text(sealJob),
|
|
258
|
+
runnerEnvironment: sealRunnerEnvironment,
|
|
259
|
+
runnerName: sealRunnerName,
|
|
260
|
+
runnerOs: text(sealRunnerOs, "seal runner OS"),
|
|
261
|
+
runnerArch: text(sealRunnerArch, "seal runner architecture"),
|
|
262
|
+
},
|
|
263
|
+
nativeOutcome: native.outcome,
|
|
264
|
+
nativeCompletedAt,
|
|
265
|
+
sealedAt,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function failureSettlementBinding(executionTransfer, nativeJobId, sealJobId) {
|
|
270
|
+
if (!executionTransfer.failure || !executionTransfer.failureSettlement) {
|
|
271
|
+
throw new Error(
|
|
272
|
+
"failed execution transfer lacks verified failure evidence",
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
schema: PROVIDER_FAILURE_SETTLEMENT_SCHEMA,
|
|
277
|
+
outcome: "terminal-failure",
|
|
278
|
+
evidenceRoot: executionTransfer.failure.evidenceRoot,
|
|
279
|
+
reason: executionTransfer.failure.reason,
|
|
280
|
+
warrantStateRoot: executionTransfer.warrant.stateRoot,
|
|
281
|
+
candidateId: executionTransfer.warrant.candidateId,
|
|
282
|
+
fencingToken: executionTransfer.warrant.fencingToken,
|
|
283
|
+
leaseGeneration: executionTransfer.warrant.generation,
|
|
284
|
+
pullRequestNumber: executionTransfer.warrant.pullRequestNumber,
|
|
285
|
+
sourceHead: executionTransfer.warrant.sourceHead,
|
|
286
|
+
transferRoot: executionTransfer.transferRoot,
|
|
287
|
+
nativeJobId,
|
|
288
|
+
sealJobId,
|
|
289
|
+
nativeJobConclusion: "failure",
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function failureBoundaryFields(executionTransfer, nativeJobId, sealJobId) {
|
|
294
|
+
return executionTransfer.outcome === "failed"
|
|
295
|
+
? {
|
|
296
|
+
failureSettlement: failureSettlementBinding(
|
|
297
|
+
executionTransfer,
|
|
298
|
+
nativeJobId,
|
|
299
|
+
sealJobId,
|
|
300
|
+
),
|
|
301
|
+
}
|
|
302
|
+
: {};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function createProviderFinalizerBoundary({
|
|
306
|
+
jobs,
|
|
307
|
+
executionTransfer,
|
|
308
|
+
workflowRunId,
|
|
309
|
+
workflowRunAttempt,
|
|
310
|
+
nativeJobName,
|
|
311
|
+
sealJobName,
|
|
312
|
+
finalizerJobName,
|
|
313
|
+
finalizerRunnerName,
|
|
314
|
+
finalizerRunnerEnvironment,
|
|
315
|
+
pullRequestReadback,
|
|
316
|
+
baseRefReadback,
|
|
317
|
+
observedAt,
|
|
318
|
+
} = {}) {
|
|
319
|
+
if (finalizerRunnerEnvironment !== "github-hosted") {
|
|
320
|
+
throw new Error("provider finalizer requires a GitHub-hosted fresh runner");
|
|
321
|
+
}
|
|
322
|
+
const runId = positiveInteger(workflowRunId, "workflow run id");
|
|
323
|
+
const runAttempt = positiveInteger(
|
|
324
|
+
workflowRunAttempt,
|
|
325
|
+
"workflow run attempt",
|
|
326
|
+
);
|
|
327
|
+
const entries = Array.isArray(jobs) ? jobs : jobs?.jobs;
|
|
328
|
+
if (!Array.isArray(entries))
|
|
329
|
+
throw new Error("provider jobs readback is required");
|
|
330
|
+
if (
|
|
331
|
+
executionTransfer.producer.workflowRunId !== runId ||
|
|
332
|
+
executionTransfer.producer.workflowRunAttempt !== runAttempt
|
|
333
|
+
) {
|
|
334
|
+
throw new Error("native execution transfer workflow identity mismatch");
|
|
335
|
+
}
|
|
336
|
+
const nativeJob = devDeliveryProviderJobByName(entries, nativeJobName);
|
|
337
|
+
const sealJob = devDeliveryProviderJobByName(entries, sealJobName);
|
|
338
|
+
const finalizerJob = devDeliveryProviderJobByName(entries, finalizerJobName);
|
|
339
|
+
const nativeJobId = positiveInteger(nativeJob.id, "native provider job id");
|
|
340
|
+
const finalizerJobId = positiveInteger(
|
|
341
|
+
finalizerJob.id,
|
|
342
|
+
"finalizer provider job id",
|
|
343
|
+
);
|
|
344
|
+
const sealJobId = positiveInteger(sealJob.id, "seal provider job id");
|
|
345
|
+
const jobIds = [nativeJobId, sealJobId, finalizerJobId];
|
|
346
|
+
const runnerNames = [
|
|
347
|
+
nativeJob.runner_name,
|
|
348
|
+
sealJob.runner_name,
|
|
349
|
+
finalizerJob.runner_name,
|
|
350
|
+
];
|
|
351
|
+
if (new Set(jobIds).size !== 3 || new Set(runnerNames).size !== 3) {
|
|
352
|
+
throw new Error(
|
|
353
|
+
"native, seal, and finalizer identities are not pairwise distinct",
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
if (nativeJob.runner_name !== executionTransfer.producer.runnerName) {
|
|
357
|
+
throw new Error("native runner live readback does not match transfer");
|
|
358
|
+
}
|
|
359
|
+
if (
|
|
360
|
+
executionTransfer.producer.job !== "native-execution" ||
|
|
361
|
+
executionTransfer.sealer.job !== "seal-native-execution" ||
|
|
362
|
+
sealJob.runner_name !== executionTransfer.sealer.runnerName
|
|
363
|
+
) {
|
|
364
|
+
throw new Error("native or seal live job identity does not match transfer");
|
|
365
|
+
}
|
|
366
|
+
if (finalizerJob.runner_name !== finalizerRunnerName) {
|
|
367
|
+
throw new Error("finalizer runner live readback does not match context");
|
|
368
|
+
}
|
|
369
|
+
assertHostedJob(nativeJob, runAttempt, "native");
|
|
370
|
+
assertHostedJob(sealJob, runAttempt, "seal");
|
|
371
|
+
assertHostedJob(finalizerJob, runAttempt, "finalizer");
|
|
372
|
+
const expectedConclusion =
|
|
373
|
+
executionTransfer.outcome === "succeeded" ? "success" : "failure";
|
|
374
|
+
if (
|
|
375
|
+
nativeJob.status !== "completed" ||
|
|
376
|
+
nativeJob.conclusion !== expectedConclusion
|
|
377
|
+
) {
|
|
378
|
+
throw new Error(
|
|
379
|
+
"native provider job is not terminal with the transferred outcome",
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
if (sealJob.status !== "completed" || sealJob.conclusion !== "success") {
|
|
383
|
+
throw new Error("provider seal job is not successfully completed");
|
|
384
|
+
}
|
|
385
|
+
if (finalizerJob.status !== "in_progress") {
|
|
386
|
+
throw new Error("provider finalizer job is not the live in-progress job");
|
|
387
|
+
}
|
|
388
|
+
const transferredAt = timestamp(
|
|
389
|
+
executionTransfer.completedAt,
|
|
390
|
+
"native transfer completion time",
|
|
391
|
+
);
|
|
392
|
+
const nativeStartedAt = timestamp(
|
|
393
|
+
nativeJob.started_at,
|
|
394
|
+
"native provider job start time",
|
|
395
|
+
);
|
|
396
|
+
const nativeCompletedAt = timestamp(
|
|
397
|
+
nativeJob.completed_at,
|
|
398
|
+
"native provider job completion time",
|
|
399
|
+
);
|
|
400
|
+
const sealStartedAt = timestamp(sealJob.started_at, "seal job start time");
|
|
401
|
+
const sealCompletedAt = timestamp(
|
|
402
|
+
sealJob.completed_at,
|
|
403
|
+
"seal job completion time",
|
|
404
|
+
);
|
|
405
|
+
const finalizerStartedAt = timestamp(
|
|
406
|
+
finalizerJob.started_at,
|
|
407
|
+
"finalizer provider job start time",
|
|
408
|
+
);
|
|
409
|
+
const boundaryObservedAt = timestamp(observedAt, "boundary observation time");
|
|
410
|
+
if (
|
|
411
|
+
Date.parse(nativeStartedAt) >= Date.parse(nativeCompletedAt) ||
|
|
412
|
+
Date.parse(transferredAt) > Date.parse(nativeCompletedAt) ||
|
|
413
|
+
Date.parse(nativeCompletedAt) >= Date.parse(sealStartedAt) ||
|
|
414
|
+
Date.parse(sealStartedAt) >= Date.parse(sealCompletedAt) ||
|
|
415
|
+
Date.parse(executionTransfer.sealedAt) > Date.parse(sealCompletedAt) ||
|
|
416
|
+
Date.parse(sealCompletedAt) >= Date.parse(finalizerStartedAt) ||
|
|
417
|
+
Date.parse(boundaryObservedAt) < Date.parse(finalizerStartedAt)
|
|
418
|
+
) {
|
|
419
|
+
throw new Error(
|
|
420
|
+
"provider finalizer did not start after ordered native and seal completion",
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
const expectedRepository = repository(executionTransfer.warrant.repository);
|
|
424
|
+
const expectedProtectedBase = protectedBase(
|
|
425
|
+
executionTransfer.warrant.protectedBase,
|
|
426
|
+
);
|
|
427
|
+
const expectedPullRequest = positiveInteger(
|
|
428
|
+
executionTransfer.warrant.pullRequestNumber,
|
|
429
|
+
"Warrant pull request number",
|
|
430
|
+
);
|
|
431
|
+
if (
|
|
432
|
+
Number(pullRequestReadback?.number) !== expectedPullRequest ||
|
|
433
|
+
pullRequestReadback?.state !== "open" ||
|
|
434
|
+
pullRequestReadback?.head?.sha !== executionTransfer.warrant.sourceHead
|
|
435
|
+
) {
|
|
436
|
+
throw new Error("live pull request readback is stale or mismatched");
|
|
437
|
+
}
|
|
438
|
+
if (pullRequestReadback?.base?.ref !== expectedProtectedBase) {
|
|
439
|
+
throw new Error("live pull request protected base is stale or mismatched");
|
|
440
|
+
}
|
|
441
|
+
if (pullRequestReadback?.base?.repo?.full_name !== expectedRepository) {
|
|
442
|
+
throw new Error("live pull request repository is stale or mismatched");
|
|
443
|
+
}
|
|
444
|
+
if (
|
|
445
|
+
baseRefReadback?.ref !== `refs/heads/${expectedProtectedBase}` ||
|
|
446
|
+
baseRefReadback?.object?.type !== "commit"
|
|
447
|
+
) {
|
|
448
|
+
throw new Error("live protected base ref readback is stale or mismatched");
|
|
449
|
+
}
|
|
450
|
+
const protectedBaseSha = exactSha(
|
|
451
|
+
baseRefReadback.object.sha,
|
|
452
|
+
"live protected base SHA",
|
|
453
|
+
);
|
|
454
|
+
const body = {
|
|
455
|
+
schema: PROVIDER_FINALIZER_BOUNDARY_SCHEMA,
|
|
456
|
+
workflowRunId: runId,
|
|
457
|
+
workflowRunAttempt: runAttempt,
|
|
458
|
+
nativeJob: {
|
|
459
|
+
id: nativeJobId,
|
|
460
|
+
name: nativeJob.name,
|
|
461
|
+
runnerName: nativeJob.runner_name,
|
|
462
|
+
startedAt: nativeStartedAt,
|
|
463
|
+
completedAt: nativeCompletedAt,
|
|
464
|
+
conclusion: nativeJob.conclusion,
|
|
465
|
+
},
|
|
466
|
+
sealJob: {
|
|
467
|
+
id: sealJobId,
|
|
468
|
+
name: sealJob.name,
|
|
469
|
+
runnerName: sealJob.runner_name,
|
|
470
|
+
startedAt: sealStartedAt,
|
|
471
|
+
completedAt: sealCompletedAt,
|
|
472
|
+
conclusion: sealJob.conclusion,
|
|
473
|
+
},
|
|
474
|
+
finalizerJob: {
|
|
475
|
+
id: finalizerJobId,
|
|
476
|
+
name: finalizerJob.name,
|
|
477
|
+
runnerName: finalizerJob.runner_name,
|
|
478
|
+
startedAt: finalizerStartedAt,
|
|
479
|
+
status: finalizerJob.status,
|
|
480
|
+
},
|
|
481
|
+
providerSource: {
|
|
482
|
+
repository: expectedRepository,
|
|
483
|
+
pullRequestNumber: expectedPullRequest,
|
|
484
|
+
sourceHead: executionTransfer.warrant.sourceHead,
|
|
485
|
+
protectedBase: expectedProtectedBase,
|
|
486
|
+
protectedBaseSha,
|
|
487
|
+
pullRequestState: "open",
|
|
488
|
+
},
|
|
489
|
+
transferRoot: exactRoot(executionTransfer.transferRoot, "transfer root"),
|
|
490
|
+
separation: "pairwise-distinct-github-hosted-native-seal-finalizer",
|
|
491
|
+
observedAt: boundaryObservedAt,
|
|
492
|
+
...failureBoundaryFields(executionTransfer, nativeJobId, sealJobId),
|
|
493
|
+
};
|
|
494
|
+
return { ...body, boundaryRoot: devDeliveryContentRoot(body) };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export function verifyProviderFailureSettlementBinding(
|
|
498
|
+
boundaryInput,
|
|
499
|
+
executionTransfer,
|
|
500
|
+
expected = {},
|
|
501
|
+
) {
|
|
502
|
+
const boundary = clone(boundaryInput || {});
|
|
503
|
+
const boundaryRoot = exactRoot(
|
|
504
|
+
boundary.boundaryRoot,
|
|
505
|
+
"provider finalizer boundary root",
|
|
506
|
+
);
|
|
507
|
+
delete boundary.boundaryRoot;
|
|
508
|
+
if (boundary.schema !== PROVIDER_FINALIZER_BOUNDARY_SCHEMA) {
|
|
509
|
+
throw new Error("provider finalizer boundary schema is unsupported");
|
|
510
|
+
}
|
|
511
|
+
if (devDeliveryContentRoot(boundary) !== boundaryRoot) {
|
|
512
|
+
throw new Error("provider finalizer boundary root drift");
|
|
513
|
+
}
|
|
514
|
+
if (executionTransfer.outcome !== "failed") {
|
|
515
|
+
throw new Error("provider failure settlement requires a failed transfer");
|
|
516
|
+
}
|
|
517
|
+
if (boundary.transferRoot !== executionTransfer.transferRoot) {
|
|
518
|
+
throw new Error("provider failure settlement transfer root mismatch");
|
|
519
|
+
}
|
|
520
|
+
const settlement = failureSettlementBinding(
|
|
521
|
+
executionTransfer,
|
|
522
|
+
positiveInteger(boundary.nativeJob?.id, "native provider job id"),
|
|
523
|
+
positiveInteger(boundary.sealJob?.id, "seal provider job id"),
|
|
524
|
+
);
|
|
525
|
+
if (
|
|
526
|
+
JSON.stringify(boundary.failureSettlement) !== JSON.stringify(settlement)
|
|
527
|
+
) {
|
|
528
|
+
throw new Error("provider failure settlement boundary binding mismatch");
|
|
529
|
+
}
|
|
530
|
+
if (
|
|
531
|
+
boundary.nativeJob?.conclusion !== "failure" ||
|
|
532
|
+
boundary.providerSource?.pullRequestNumber !==
|
|
533
|
+
settlement.pullRequestNumber ||
|
|
534
|
+
boundary.providerSource?.sourceHead !== settlement.sourceHead ||
|
|
535
|
+
boundary.providerSource?.protectedBase !==
|
|
536
|
+
executionTransfer.warrant.protectedBase
|
|
537
|
+
) {
|
|
538
|
+
throw new Error("provider failure settlement live-state binding mismatch");
|
|
539
|
+
}
|
|
540
|
+
const observed = {
|
|
541
|
+
boundaryRoot,
|
|
542
|
+
finalizerBoundaryRoot: boundaryRoot,
|
|
543
|
+
transferRoot: settlement.transferRoot,
|
|
544
|
+
evidenceRoot: settlement.evidenceRoot,
|
|
545
|
+
nativeJobId: settlement.nativeJobId,
|
|
546
|
+
sealJobId: settlement.sealJobId,
|
|
547
|
+
warrantStateRoot: settlement.warrantStateRoot,
|
|
548
|
+
candidateId: settlement.candidateId,
|
|
549
|
+
fencingToken: settlement.fencingToken,
|
|
550
|
+
leaseGeneration: settlement.leaseGeneration,
|
|
551
|
+
pullRequestNumber: settlement.pullRequestNumber,
|
|
552
|
+
sourceHead: settlement.sourceHead,
|
|
553
|
+
};
|
|
554
|
+
for (const [field, value] of Object.entries(expected)) {
|
|
555
|
+
if (value !== undefined && observed[field] !== value) {
|
|
556
|
+
throw new Error(`provider failure settlement ${field} mismatch`);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return {
|
|
560
|
+
...settlement,
|
|
561
|
+
finalizerBoundaryRoot: boundaryRoot,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import {
|
|
2
|
+
devDeliveryExactRoot as exactRoot,
|
|
3
|
+
devDeliveryExactSha as exactSha,
|
|
4
|
+
devDeliveryPositiveInteger as positiveInteger,
|
|
5
|
+
devDeliveryProtectedBase as protectedBase,
|
|
6
|
+
devDeliveryRepository as repository,
|
|
7
|
+
devDeliveryText as text,
|
|
8
|
+
} from "./dev-delivery-common.js";
|
|
9
|
+
|
|
10
|
+
export const DEV_DELIVERY_PROVIDER_ATTEMPT_SCHEMA =
|
|
11
|
+
"kungfu.buildchain.github-landing-provider-attempt/v1";
|
|
12
|
+
|
|
13
|
+
function workflowPath(value) {
|
|
14
|
+
const normalized = text(value);
|
|
15
|
+
if (!/^\.github\/workflows\/[^/]+\.ya?ml$/u.test(normalized)) {
|
|
16
|
+
throw new Error("provider workflow path must name a root workflow");
|
|
17
|
+
}
|
|
18
|
+
return normalized;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function workflowRef(value, path) {
|
|
22
|
+
const normalized = text(value);
|
|
23
|
+
if (
|
|
24
|
+
!new RegExp(
|
|
25
|
+
`^[^/\\s]+/[^/\\s]+/${path.replaceAll(".", "\\.")}@refs/(?:heads|tags)/[^\\s]+$`,
|
|
26
|
+
"u",
|
|
27
|
+
).test(normalized)
|
|
28
|
+
) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
"provider workflow ref must bind repository, path, and an exact heads or tags ref",
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return normalized;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function labels(input) {
|
|
37
|
+
if (!Array.isArray(input) || input.length === 0) {
|
|
38
|
+
throw new Error("provider runner labels must be a non-empty array");
|
|
39
|
+
}
|
|
40
|
+
const normalized = [...new Set(input.map(text).filter(Boolean))].sort();
|
|
41
|
+
if (normalized.length === 0) {
|
|
42
|
+
throw new Error("provider runner labels must contain exact labels");
|
|
43
|
+
}
|
|
44
|
+
return normalized;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function assertDevDeliveryGitHubHostedRunner({
|
|
48
|
+
runnerGroupName,
|
|
49
|
+
runnerLabels,
|
|
50
|
+
} = {}) {
|
|
51
|
+
const group = text(runnerGroupName);
|
|
52
|
+
const exactLabels = labels(runnerLabels);
|
|
53
|
+
if (
|
|
54
|
+
group !== "GitHub Actions" ||
|
|
55
|
+
exactLabels.some((label) => label.toLowerCase() === "self-hosted")
|
|
56
|
+
) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
"provider authority requires the GitHub-hosted runner group",
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return { runnerGroupName: group, runnerLabels: exactLabels };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function normalizeDevDeliveryProviderAttempt(input = {}, expected = {}) {
|
|
65
|
+
if (input.schema !== DEV_DELIVERY_PROVIDER_ATTEMPT_SCHEMA) {
|
|
66
|
+
throw new Error("Landing provider attempt schema is unsupported");
|
|
67
|
+
}
|
|
68
|
+
const exactWorkflowPath = workflowPath(input.workflowPath);
|
|
69
|
+
const attempt = {
|
|
70
|
+
schema: DEV_DELIVERY_PROVIDER_ATTEMPT_SCHEMA,
|
|
71
|
+
repository: repository(input.repository),
|
|
72
|
+
workflowId: positiveInteger(input.workflowId, "provider workflow id"),
|
|
73
|
+
workflowPath: exactWorkflowPath,
|
|
74
|
+
workflowRef: workflowRef(input.workflowRef, exactWorkflowPath),
|
|
75
|
+
workflowSha: exactSha(input.workflowSha, "provider workflow SHA"),
|
|
76
|
+
event: text(input.event),
|
|
77
|
+
runId: positiveInteger(input.runId, "provider run id"),
|
|
78
|
+
runAttempt: positiveInteger(input.runAttempt, "provider run attempt"),
|
|
79
|
+
jobId: positiveInteger(input.jobId, "provider job id"),
|
|
80
|
+
jobName: text(input.jobName),
|
|
81
|
+
jobRole: text(input.jobRole),
|
|
82
|
+
runnerId: positiveInteger(input.runnerId, "provider runner id"),
|
|
83
|
+
runnerName: text(input.runnerName),
|
|
84
|
+
runnerGroupId: positiveInteger(
|
|
85
|
+
input.runnerGroupId,
|
|
86
|
+
"provider runner group id",
|
|
87
|
+
),
|
|
88
|
+
runnerGroupName: text(input.runnerGroupName),
|
|
89
|
+
runnerLabels: labels(input.runnerLabels),
|
|
90
|
+
sourceHead: exactSha(input.sourceHead, "provider source head"),
|
|
91
|
+
mergeGroupHead: exactSha(input.mergeGroupHead, "provider merge-group head"),
|
|
92
|
+
protectedBase: protectedBase(input.protectedBase),
|
|
93
|
+
};
|
|
94
|
+
if (attempt.event !== "merge_group") {
|
|
95
|
+
throw new Error("Landing provider attempt event must be merge_group");
|
|
96
|
+
}
|
|
97
|
+
if (!attempt.jobName) throw new Error("provider job name is required");
|
|
98
|
+
if (attempt.jobRole !== "landing-authority") {
|
|
99
|
+
throw new Error("provider job role must be landing-authority");
|
|
100
|
+
}
|
|
101
|
+
if (!attempt.runnerName || !attempt.runnerGroupName) {
|
|
102
|
+
throw new Error("provider runner identity is incomplete");
|
|
103
|
+
}
|
|
104
|
+
assertDevDeliveryGitHubHostedRunner(attempt);
|
|
105
|
+
for (const [field, value] of Object.entries(expected)) {
|
|
106
|
+
if (value !== undefined && attempt[field] !== value) {
|
|
107
|
+
throw new Error(`Landing provider attempt ${field} mismatch`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return attempt;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function normalizeDevDeliveryTerminalProviderEvidence(input = {}) {
|
|
114
|
+
const evidence = {};
|
|
115
|
+
if (input.providerAttempt) {
|
|
116
|
+
evidence.providerAttempt = normalizeDevDeliveryProviderAttempt(
|
|
117
|
+
input.providerAttempt,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
if (input.providerTerminalReadbackRoot) {
|
|
121
|
+
evidence.providerTerminalReadbackRoot = exactRoot(
|
|
122
|
+
input.providerTerminalReadbackRoot,
|
|
123
|
+
"providerTerminalReadbackRoot",
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return evidence;
|
|
127
|
+
}
|