@kungfu-tech/buildchain 4.1.3-alpha.0 → 4.1.3-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +41 -75
- package/CONTRIBUTING.md +19 -26
- package/architecture/action-taxonomy.json +24 -0
- package/architecture/agent-change-map.md +272 -12
- package/architecture/capability-state-machine-manifest.json +1 -4
- package/architecture/ci-lane-change-budget.json +190 -807
- package/architecture/core-mechanisms.json +3 -6
- package/architecture/decisions/0005-minimal-consumer-contract.md +122 -0
- package/architecture/decisions/0006-business-attempt-journal.md +89 -0
- package/architecture/decisions/0007-hosted-pipeline-controller.md +106 -0
- package/architecture/decisions/0008-pipeline-product-publication.md +120 -0
- package/architecture/decisions/0009-pipeline-version-preparation.md +186 -0
- package/architecture/internal-capabilities.json +371 -9
- package/architecture/maintainability-debt.json +50 -34
- package/architecture/maintainability-policy.json +8 -8
- package/architecture/minimal-consumer-migration.json +5670 -0
- package/architecture/platform-stage-checkpoints.json +31 -7
- package/architecture/product-upstreams.json +20 -0
- package/architecture/release-topology.json +331 -74
- package/architecture/stage-capsule-qualification.json +3 -1
- package/architecture/universal-workflow-bootstrap.json +15 -1
- package/architecture/universal-workflow-capability-policy.json +9 -9
- package/architecture/universal-workflow-fault-campaign.json +3 -1
- package/architecture/workflow-taxonomy.json +77 -328
- package/contracts/release-discussion-v1.schema.json +3 -0
- package/dist/readers/business-attempt.cjs +809 -0
- package/dist/readers/release-discussion.cjs +4 -0
- package/dist/site/buildchain-contract.json +16 -68
- package/dist/site/buildchain-site.json +47 -27
- package/dist/site/capability-registry.json +4 -4
- package/dist/site/controller-registry.json +1 -47
- package/dist/site/kfd-claims.json +499 -494
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +3 -3
- package/dist/site/node-api-registry.json +56 -56
- package/dist/site/page-registry.json +40 -20
- package/dist/site/public-surface-audit.json +286 -349
- package/dist/site/publication-authority-registry.json +203 -523
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +7 -7
- package/dist/site/workflow-registry.json +590 -940
- package/docs/code-organization.md +9 -6
- package/docs/node-api-reference.md +103 -103
- package/docs/release-discussions.md +44 -5
- package/docs/release-governance.md +10 -12
- package/docs/runtime-entry.md +73 -5
- package/docs/runtime-train-validation.md +39 -11
- package/docs/stable-candidate-patrol.md +6 -5
- package/docs/workflow-catalog.md +9 -32
- package/package.json +4 -3
- package/packages/core/adoption/buildchain-kfd-claims.js +7 -7
- package/packages/core/build/standalone/build.js +7 -1
- package/packages/core/consumer/buildchain-config.js +2 -21
- package/packages/core/consumer/contract/entries.js +85 -0
- package/packages/core/consumer/contract/examples.js +96 -0
- package/packages/core/consumer/contract/identity.js +65 -0
- package/packages/core/consumer/contract/inspection.js +45 -0
- package/packages/core/consumer/contract/plan.js +149 -0
- package/packages/core/consumer/contract/products.js +119 -0
- package/packages/core/consumer/contract/reader.js +47 -0
- package/packages/core/consumer/contract/shape.js +46 -0
- package/packages/core/dev-delivery/candidate/admission.js +9 -5
- package/packages/core/dev-delivery/candidate/reservation.js +11 -0
- package/packages/core/dev-delivery/candidate/source-paths.js +12 -4
- package/packages/core/dev-delivery/commands/project-cut-merge-queue-admission.mjs +47 -20
- package/packages/core/dev-delivery/dev-delivery-provider-heartbeat.js +18 -15
- package/packages/core/dev-delivery/native/actions.js +9 -1
- package/packages/core/dev-delivery/native/heartbeat-action.js +16 -0
- package/packages/core/dev-delivery/native/heartbeat.js +6 -4
- package/packages/core/dev-delivery/native/process-entry.js +19 -0
- package/packages/core/dev-delivery/queue/landing-action.js +53 -0
- package/packages/core/dev-delivery/warrant/failure-settlement.js +8 -6
- package/packages/core/dev-delivery/warrant/service.js +7 -0
- package/packages/core/governance/buildchain-publication-authority.js +8 -23
- package/packages/core/governance/github-governance-authority.js +1 -0
- package/packages/core/observability/controller-evidence.js +0 -8
- package/packages/core/paper/operations/bootstrap.js +2 -0
- package/packages/core/paper/paper-npm-bootstrap.js +2 -2
- package/packages/core/providers/github/attempt-index.js +60 -0
- package/packages/core/providers/github/attempt-journal.js +154 -0
- package/packages/core/providers/github/discussions/materials.js +35 -11
- package/packages/core/providers/github/discussions/transport.js +16 -5
- package/packages/core/providers/github/pipeline-checkout.js +49 -0
- package/packages/core/providers/github/pipeline-entry-runs.js +210 -0
- package/packages/core/providers/github/pipeline-events.js +131 -0
- package/packages/core/providers/github/pipeline-integration.js +95 -0
- package/packages/core/providers/github/pipeline-policy.js +240 -0
- package/packages/core/providers/github/pipeline-product-payload.js +50 -0
- package/packages/core/providers/github/pipeline-product-release.js +198 -0
- package/packages/core/providers/github/pipeline-publication-artifacts.js +153 -0
- package/packages/core/providers/github/pipeline-release-evidence.js +30 -0
- package/packages/core/providers/github/pipeline-run-entry.js +86 -0
- package/packages/core/providers/github/pipeline-runs.js +132 -0
- package/packages/core/providers/github/pipeline-source.js +195 -0
- package/packages/core/providers/github/pipeline-stable-baseline.js +86 -0
- package/packages/core/providers/github/pipeline-stable-source.js +372 -0
- package/packages/core/providers/github/pipeline-version-artifacts.js +91 -0
- package/packages/core/providers/github/pipeline-version-readback.js +117 -0
- package/packages/core/providers/github/pipeline-version.js +199 -0
- package/packages/core/providers/github/pipeline-worker.js +108 -0
- package/packages/core/providers/github/release-evidence-assets.js +97 -0
- package/packages/core/publication/candidate/registry-hydration.js +2 -2
- package/packages/core/publication/npm/pack-preview.js +2 -2
- package/packages/core/publication/npm/pack-result.js +31 -0
- package/packages/core/publication/npm/package.js +3 -6
- package/packages/core/publication/npm/pipeline-channel.js +36 -0
- package/packages/core/publication/npm/pipeline-provider.js +137 -0
- package/packages/core/publication/npm/registry.js +12 -9
- package/packages/core/publication/pipeline/actions.js +116 -0
- package/packages/core/publication/pipeline/apply.js +189 -0
- package/packages/core/publication/pipeline/build-segments.js +107 -0
- package/packages/core/publication/pipeline/capsules.js +92 -0
- package/packages/core/publication/pipeline/context.js +39 -0
- package/packages/core/publication/pipeline/development-anchor.js +91 -0
- package/packages/core/publication/pipeline/development-current.js +63 -0
- package/packages/core/publication/pipeline/development-pr.js +112 -0
- package/packages/core/publication/pipeline/development-proof.js +76 -0
- package/packages/core/publication/pipeline/development-transition.js +112 -0
- package/packages/core/publication/pipeline/distribution.js +130 -0
- package/packages/core/publication/pipeline/documents.js +192 -0
- package/packages/core/publication/pipeline/effects.js +192 -0
- package/packages/core/publication/pipeline/files.js +66 -0
- package/packages/core/publication/pipeline/imported-materials.js +32 -0
- package/packages/core/publication/pipeline/journal.js +122 -0
- package/packages/core/publication/pipeline/next-development.js +117 -0
- package/packages/core/publication/pipeline/pack.js +180 -0
- package/packages/core/publication/pipeline/package-policy.js +22 -0
- package/packages/core/publication/pipeline/plan.js +136 -0
- package/packages/core/publication/pipeline/prepare.js +139 -0
- package/packages/core/publication/pipeline/qualification.js +175 -0
- package/packages/core/publication/pipeline/qualify.js +104 -0
- package/packages/core/publication/pipeline/recovery-admission.js +122 -0
- package/packages/core/publication/pipeline/recovery-build-download.js +80 -0
- package/packages/core/publication/pipeline/recovery-build-plan.js +122 -0
- package/packages/core/publication/pipeline/recovery-build-readback.js +68 -0
- package/packages/core/publication/pipeline/recovery-capsules.js +54 -0
- package/packages/core/publication/pipeline/recovery-import.js +80 -0
- package/packages/core/publication/pipeline/recovery-inspection.js +218 -0
- package/packages/core/publication/pipeline/recovery-materials.js +104 -0
- package/packages/core/publication/pipeline/recovery-plan.js +46 -0
- package/packages/core/publication/pipeline/recovery-prepare.js +73 -0
- package/packages/core/publication/pipeline/recovery-qualification.js +128 -0
- package/packages/core/publication/pipeline/recovery-readback.js +56 -0
- package/packages/core/publication/pipeline/recovery-signing.js +58 -0
- package/packages/core/publication/pipeline/sealed-products.js +79 -0
- package/packages/core/publication/pipeline/settle.js +42 -0
- package/packages/core/publication/pipeline/signing.js +140 -0
- package/packages/core/publication/pipeline/source-plan.js +83 -0
- package/packages/core/publication/pipeline/stable-entry.js +219 -0
- package/packages/core/publication/pipeline/stable-products.js +317 -0
- package/packages/core/publication/pipeline/stable-wait.js +170 -0
- package/packages/core/publication/pipeline/stable.js +82 -0
- package/packages/core/publication/pipeline/version-actions.js +93 -0
- package/packages/core/publication/pipeline/version-build.js +130 -0
- package/packages/core/publication/pipeline/version-context.js +143 -0
- package/packages/core/publication/pipeline/version-material.js +36 -0
- package/packages/core/publication/pipeline/version-preparation.js +69 -0
- package/packages/core/publication/pipeline/version-regeneration.js +162 -0
- package/packages/core/publication/pipeline/version-source-guard.js +69 -0
- package/packages/core/publication/pipeline/version.js +81 -0
- package/packages/core/publication/pipeline/worker.js +72 -0
- package/packages/core/publication/publication-reproducibility.js +2 -2
- package/packages/core/release/discussion/binary.js +11 -0
- package/packages/core/release/discussion/checkpoints.js +45 -4
- package/packages/core/release/discussion/envelope.js +1 -0
- package/packages/core/release/discussion/evidence.js +51 -0
- package/packages/core/release/discussion/presentation.js +92 -0
- package/packages/core/release/discussion/publication.js +3 -1
- package/packages/core/release/discussion/qualification.js +1 -0
- package/packages/core/release/discussion/reader.js +9 -0
- package/packages/core/release/discussion/session.js +2 -0
- package/packages/core/release/discussion/store.js +28 -31
- package/packages/core/release/discussion/threads.js +83 -0
- package/packages/core/release/next-development-transition.js +15 -15
- package/packages/core/release/promote-candidate/product-provider-adapters.js +4 -3
- package/packages/core/release/promote-ref/internal/npm-existing-evidence.js +2 -1
- package/packages/core/release/promote-ref/internal/publish-command.js +16 -5
- package/packages/core/release/stable-release-gate.js +11 -0
- package/packages/core/runtime/buildchain-domain.wasm +0 -0
- package/packages/core/runtime/domain-wasm-artifact.js +2 -2
- package/packages/core/runtime/entry/actions.js +27 -25
- package/packages/core/runtime/entry/attempt.js +25 -0
- package/packages/core/runtime/entry/selection.js +14 -0
- package/packages/core/runtime/entry/source.js +62 -0
- package/packages/core/workflow/attempt/identity.js +170 -0
- package/packages/core/workflow/attempt/journal.js +89 -0
- package/packages/core/workflow/attempt/materials.js +72 -0
- package/packages/core/workflow/attempt/reader-entry.js +12 -0
- package/packages/core/workflow/attempt/reader.js +149 -0
- package/packages/core/workflow/attempt/records.js +153 -0
- package/packages/core/workflow/attempt/store.js +75 -0
- package/packages/core/workflow/commands/pipeline-build.mjs +23 -0
- package/packages/core/workflow/pipeline/action-output.js +11 -0
- package/packages/core/workflow/pipeline/actions.js +52 -0
- package/packages/core/workflow/pipeline/build-control.js +140 -0
- package/packages/core/workflow/pipeline/build-evidence.js +210 -0
- package/packages/core/workflow/pipeline/build-qualification.js +145 -0
- package/packages/core/workflow/pipeline/build-result.js +78 -0
- package/packages/core/workflow/pipeline/build.js +142 -0
- package/packages/core/workflow/pipeline/cancellation.js +169 -0
- package/packages/core/workflow/pipeline/channel-control.js +157 -0
- package/packages/core/workflow/pipeline/channel-source.js +53 -0
- package/packages/core/workflow/pipeline/controller.js +141 -0
- package/packages/core/workflow/pipeline/delivery-control.js +249 -0
- package/packages/core/workflow/pipeline/delivery-observation.js +124 -0
- package/packages/core/workflow/pipeline/delivery-request.js +198 -0
- package/packages/core/workflow/pipeline/events.js +84 -0
- package/packages/core/workflow/pipeline/fence.js +42 -0
- package/packages/core/workflow/pipeline/group-control.js +59 -0
- package/packages/core/workflow/pipeline/guard-build.js +74 -0
- package/packages/core/workflow/pipeline/guard.js +158 -0
- package/packages/core/workflow/pipeline/host.js +144 -0
- package/packages/core/workflow/pipeline/integration-status.js +113 -0
- package/packages/core/workflow/pipeline/materials.js +58 -0
- package/packages/core/workflow/pipeline/notifications.js +36 -0
- package/packages/core/workflow/pipeline/parent-notification.js +27 -0
- package/packages/core/workflow/pipeline/platforms.js +21 -0
- package/packages/core/workflow/pipeline/progress.js +108 -0
- package/packages/core/workflow/pipeline/projection.js +20 -0
- package/packages/core/workflow/pipeline/reconcile.js +194 -0
- package/packages/core/workflow/pipeline/record-action.js +27 -0
- package/packages/core/workflow/pipeline/recorder-status.js +69 -0
- package/packages/core/workflow/pipeline/recovery-action.js +30 -0
- package/packages/core/workflow/pipeline/recovery-admission.js +122 -0
- package/packages/core/workflow/pipeline/recovery-build-control.js +95 -0
- package/packages/core/workflow/pipeline/recovery-build-evidence.js +71 -0
- package/packages/core/workflow/pipeline/recovery-build.js +121 -0
- package/packages/core/workflow/pipeline/recovery-controller.js +91 -0
- package/packages/core/workflow/pipeline/recovery-integration.js +79 -0
- package/packages/core/workflow/pipeline/recovery-merge-proof.js +90 -0
- package/packages/core/workflow/pipeline/recovery-ownership-settlement.js +150 -0
- package/packages/core/workflow/pipeline/recovery-ownership.js +91 -0
- package/packages/core/workflow/pipeline/recovery-plan.js +126 -0
- package/packages/core/workflow/pipeline/recovery-runtime.js +68 -0
- package/packages/core/workflow/pipeline/recovery-session.js +129 -0
- package/packages/core/workflow/pipeline/recovery-transition.js +135 -0
- package/packages/core/workflow/pipeline/recovery-unrecorded-build.js +74 -0
- package/packages/core/workflow/pipeline/runtime-source.js +111 -0
- package/packages/core/workflow/pipeline/selection.js +111 -0
- package/packages/core/workflow/pipeline/session.js +72 -0
- package/packages/core/workflow/pipeline/settlement.js +137 -0
- package/packages/core/workflow/pipeline/wake.js +38 -0
- package/packages/core/workflow/pipeline/web-status.js +108 -0
- package/packages/core/workflow/workflow-taxonomy.mjs +67 -47
- package/scripts/build-release-discussion-reader.mjs +11 -7
- package/scripts/check-inventory.mjs +7 -70
- package/scripts/check-pipeline-publication-topology.mjs +50 -0
- package/scripts/check-public-dogfood-contract.mjs +38 -51
- package/scripts/check-release-topology.mjs +15 -8
- package/scripts/check-self-consumer-contract.mjs +133 -0
- package/scripts/check-universal-workflow-bootstrap.mjs +4 -6
- package/scripts/generate-minimal-consumer-contract.mjs +155 -0
- package/scripts/generate-site-bundle.mjs +1 -1
- package/scripts/inventory/binary.mjs +9 -19
- package/scripts/inventory/promotion.mjs +2 -13
- package/scripts/prepare-product-toolchain.mjs +10 -0
- package/scripts/verify-golden-path.mjs +5 -1
- package/scripts/verify-product-platform.mjs +65 -0
- package/scripts/verify-standalone-product.mjs +42 -0
- package/templates/minimal-consumer/binary/.buildchain/buildchain.toml +45 -0
- package/templates/minimal-consumer/binary/.github/workflows/buildchain-recover.yml +30 -0
- package/templates/minimal-consumer/binary/.github/workflows/buildchain.yml +28 -0
- package/templates/minimal-consumer/binary/package.json +6 -0
- package/templates/minimal-consumer/binary/src/build.mjs +5 -0
- package/templates/minimal-consumer/binary/src/hello.c +2 -0
- package/templates/minimal-consumer/binary/src/verify.mjs +3 -0
- package/templates/minimal-consumer/npm/.buildchain/buildchain.toml +46 -0
- package/templates/minimal-consumer/npm/.github/workflows/buildchain-recover.yml +30 -0
- package/templates/minimal-consumer/npm/.github/workflows/buildchain.yml +28 -0
- package/templates/minimal-consumer/npm/package.json +6 -0
- package/templates/minimal-consumer/npm/src/build.mjs +5 -0
- package/templates/minimal-consumer/npm/src/verify.mjs +3 -0
- package/templates/minimal-consumer/paper/.buildchain/buildchain.toml +45 -0
- package/templates/minimal-consumer/paper/.github/workflows/buildchain-recover.yml +30 -0
- package/templates/minimal-consumer/paper/.github/workflows/buildchain.yml +28 -0
- package/templates/minimal-consumer/paper/package.json +6 -0
- package/templates/minimal-consumer/paper/src/build.mjs +12 -0
- package/templates/minimal-consumer/paper/src/verify.mjs +3 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Immutable lookup pointers are derived from admitted journal commits. The
|
|
2
|
+
// canonical intent journal must contain the selected root before a pointer can
|
|
3
|
+
// resolve; an orphan commit or pointer cannot create attempt authority.
|
|
4
|
+
export function githubAttemptIndex(request, journal, repository) {
|
|
5
|
+
if (!/^[\w.-]+\/[\w.-]+$/u.test(repository || ""))
|
|
6
|
+
throw new Error("Invalid attempt lookup repository");
|
|
7
|
+
const base = `/repos/${repository}`;
|
|
8
|
+
function ref(attempt) {
|
|
9
|
+
if (!/^attempt-[0-9a-f]{64}$/u.test(attempt || ""))
|
|
10
|
+
throw new Error("Invalid business attempt selector");
|
|
11
|
+
return `buildchain/attempt-index/${attempt}`;
|
|
12
|
+
}
|
|
13
|
+
function root(snapshot, attempt) {
|
|
14
|
+
const roots = snapshot.records.filter(
|
|
15
|
+
(record) => record.attempt === attempt && record.node === "attempt",
|
|
16
|
+
);
|
|
17
|
+
if (roots.length !== 1)
|
|
18
|
+
throw new Error("Lookup does not identify one admitted attempt root");
|
|
19
|
+
return roots[0];
|
|
20
|
+
}
|
|
21
|
+
async function resolve(attempt) {
|
|
22
|
+
const pointer = await request(`${base}/git/ref/heads/${ref(attempt)}`, {
|
|
23
|
+
allow404: true,
|
|
24
|
+
});
|
|
25
|
+
if (!pointer) throw new Error("Business attempt lookup is not retained");
|
|
26
|
+
const indexed = await journal.readCommit(repository, pointer.object?.sha);
|
|
27
|
+
const selected = root(indexed.snapshot, attempt);
|
|
28
|
+
const current = await journal.read(indexed.snapshot.intent);
|
|
29
|
+
if (!current || root(current.snapshot, attempt).id !== selected.id)
|
|
30
|
+
throw new Error(
|
|
31
|
+
"Lookup attempt is absent from the authoritative journal",
|
|
32
|
+
);
|
|
33
|
+
return { ...current, selectedAttempt: attempt };
|
|
34
|
+
}
|
|
35
|
+
async function retain(intent, attempt) {
|
|
36
|
+
if (intent.repository !== repository)
|
|
37
|
+
throw new Error("Attempt lookup repository drift");
|
|
38
|
+
const current = await journal.read(intent);
|
|
39
|
+
if (!current) throw new Error("Cannot index an unadmitted attempt");
|
|
40
|
+
const expected = root(current.snapshot, attempt);
|
|
41
|
+
let failure;
|
|
42
|
+
try {
|
|
43
|
+
await request(`${base}/git/refs`, {
|
|
44
|
+
method: "POST",
|
|
45
|
+
body: { ref: `refs/heads/${ref(attempt)}`, sha: current.commit },
|
|
46
|
+
});
|
|
47
|
+
} catch (error) {
|
|
48
|
+
failure = error;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const result = await resolve(attempt);
|
|
52
|
+
if (root(result.snapshot, attempt).id !== expected.id)
|
|
53
|
+
throw new Error("Conflicting business attempt lookup");
|
|
54
|
+
return result;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
throw failure || error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { retain, resolve };
|
|
60
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
canonicalJson,
|
|
4
|
+
recordDigest,
|
|
5
|
+
} from "../../release/discussion/envelope.js";
|
|
6
|
+
import { validateJournal } from "../../workflow/attempt/journal.js";
|
|
7
|
+
|
|
8
|
+
const PATH = "attempt.json";
|
|
9
|
+
const LIMIT = 34 * 1024 * 1024;
|
|
10
|
+
|
|
11
|
+
function exactSha(value) {
|
|
12
|
+
if (!/^[0-9a-f]{40}$/u.test(value || ""))
|
|
13
|
+
throw new Error("Attempt journal requires an exact Git SHA");
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function githubAttemptJournal(request) {
|
|
18
|
+
function coordinates(intent) {
|
|
19
|
+
if (
|
|
20
|
+
!/^[\w.-]+\/[\w.-]+$/u.test(intent.repository) ||
|
|
21
|
+
!/^sha256:[0-9a-f]{64}$/u.test(intent.id)
|
|
22
|
+
)
|
|
23
|
+
throw new Error("Invalid attempt journal coordinates");
|
|
24
|
+
return {
|
|
25
|
+
base: `/repos/${intent.repository}`,
|
|
26
|
+
ref: `buildchain/attempts/${intent.id.slice(7)}`,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
async function verifyRepository(intent, base) {
|
|
30
|
+
const repository = await request(base);
|
|
31
|
+
if (repository.node_id !== intent.source.repositoryId)
|
|
32
|
+
throw new Error("Attempt repository identity changed");
|
|
33
|
+
}
|
|
34
|
+
async function read(intent) {
|
|
35
|
+
const { base, ref } = coordinates(intent);
|
|
36
|
+
await verifyRepository(intent, base);
|
|
37
|
+
const pointer = await request(`${base}/git/ref/heads/${ref}`, {
|
|
38
|
+
allow404: true,
|
|
39
|
+
});
|
|
40
|
+
if (!pointer) return null;
|
|
41
|
+
if (pointer.object?.type !== "commit")
|
|
42
|
+
throw new Error("Attempt journal ref is not a commit");
|
|
43
|
+
const result = await readCommit(intent.repository, pointer.object.sha);
|
|
44
|
+
validateJournal(result.snapshot, intent);
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
async function readCommit(repository, commitSha) {
|
|
48
|
+
if (!/^[\w.-]+\/[\w.-]+$/u.test(repository || ""))
|
|
49
|
+
throw new Error("Invalid attempt journal repository");
|
|
50
|
+
const base = `/repos/${repository}`;
|
|
51
|
+
const sha = exactSha(commitSha);
|
|
52
|
+
const commit = await request(`${base}/git/commits/${sha}`);
|
|
53
|
+
const tree = await request(
|
|
54
|
+
`${base}/git/trees/${exactSha(commit.tree?.sha)}`,
|
|
55
|
+
);
|
|
56
|
+
if (
|
|
57
|
+
tree.truncated ||
|
|
58
|
+
tree.tree?.length !== 1 ||
|
|
59
|
+
tree.tree[0].path !== PATH ||
|
|
60
|
+
tree.tree[0].type !== "blob" ||
|
|
61
|
+
tree.tree[0].mode !== "100644"
|
|
62
|
+
)
|
|
63
|
+
throw new Error("Invalid attempt journal tree");
|
|
64
|
+
const blobSha = exactSha(tree.tree[0].sha);
|
|
65
|
+
const blob = await request(`${base}/git/blobs/${blobSha}`);
|
|
66
|
+
if (
|
|
67
|
+
blob.encoding !== "base64" ||
|
|
68
|
+
!Number.isSafeInteger(blob.size) ||
|
|
69
|
+
blob.size < 0 ||
|
|
70
|
+
blob.size > LIMIT ||
|
|
71
|
+
typeof blob.content !== "string" ||
|
|
72
|
+
blob.content.length > LIMIT * 2
|
|
73
|
+
)
|
|
74
|
+
throw new Error("Invalid or oversized attempt journal blob");
|
|
75
|
+
const bytes = Buffer.from(blob.content, "base64");
|
|
76
|
+
const digest = createHash("sha1")
|
|
77
|
+
.update(`blob ${bytes.length}\0`)
|
|
78
|
+
.update(bytes)
|
|
79
|
+
.digest("hex");
|
|
80
|
+
if (bytes.length !== blob.size || digest !== blobSha)
|
|
81
|
+
throw new Error("Attempt journal blob integrity mismatch");
|
|
82
|
+
const snapshot = JSON.parse(bytes.toString("utf8"));
|
|
83
|
+
if (snapshot.intent?.repository !== repository)
|
|
84
|
+
throw new Error("Attempt journal snapshot belongs to another repository");
|
|
85
|
+
validateJournal(snapshot, snapshot.intent);
|
|
86
|
+
await verifyRepository(snapshot.intent, base);
|
|
87
|
+
return { commit: sha, snapshot };
|
|
88
|
+
}
|
|
89
|
+
async function append({ intent, snapshot, expectedCommit }) {
|
|
90
|
+
validateJournal(snapshot, intent);
|
|
91
|
+
const { base, ref } = coordinates(intent);
|
|
92
|
+
await verifyRepository(intent, base);
|
|
93
|
+
const bytes = `${canonicalJson(snapshot)}\n`;
|
|
94
|
+
if (Buffer.byteLength(bytes) > LIMIT)
|
|
95
|
+
throw new Error("Attempt journal byte bound exceeded");
|
|
96
|
+
const blob = await request(`${base}/git/blobs`, {
|
|
97
|
+
method: "POST",
|
|
98
|
+
body: { content: bytes, encoding: "utf-8" },
|
|
99
|
+
});
|
|
100
|
+
const tree = await request(`${base}/git/trees`, {
|
|
101
|
+
method: "POST",
|
|
102
|
+
body: {
|
|
103
|
+
tree: [
|
|
104
|
+
{ path: PATH, mode: "100644", type: "blob", sha: exactSha(blob.sha) },
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
const commit = await request(`${base}/git/commits`, {
|
|
109
|
+
method: "POST",
|
|
110
|
+
body: {
|
|
111
|
+
message: `chore(attempt): append ${snapshot.root}`,
|
|
112
|
+
tree: exactSha(tree.sha),
|
|
113
|
+
parents: expectedCommit ? [exactSha(expectedCommit)] : [],
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
const sha = exactSha(commit.sha);
|
|
117
|
+
if (expectedCommit)
|
|
118
|
+
await request(`${base}/git/refs/heads/${ref}`, {
|
|
119
|
+
method: "PATCH",
|
|
120
|
+
body: { sha, force: false },
|
|
121
|
+
});
|
|
122
|
+
else
|
|
123
|
+
await request(`${base}/git/refs`, {
|
|
124
|
+
method: "POST",
|
|
125
|
+
body: { ref: `refs/heads/${ref}`, sha },
|
|
126
|
+
});
|
|
127
|
+
return { commit: sha };
|
|
128
|
+
}
|
|
129
|
+
async function lookup(repository, pullRequest, targetBranch) {
|
|
130
|
+
if (
|
|
131
|
+
!Number.isSafeInteger(pullRequest) ||
|
|
132
|
+
pullRequest < 1 ||
|
|
133
|
+
!/^(?:dev|alpha|release|publish-gate)\/[A-Za-z0-9/._-]+$/u.test(
|
|
134
|
+
targetBranch || "",
|
|
135
|
+
) ||
|
|
136
|
+
targetBranch.includes("..")
|
|
137
|
+
)
|
|
138
|
+
throw new Error("Invalid pipeline intent lookup");
|
|
139
|
+
const id = recordDigest({
|
|
140
|
+
repository,
|
|
141
|
+
key: `pr-${pullRequest}:${targetBranch}`,
|
|
142
|
+
});
|
|
143
|
+
const { base, ref } = coordinates({ repository, id });
|
|
144
|
+
const pointer = await request(`${base}/git/ref/heads/${ref}`, {
|
|
145
|
+
allow404: true,
|
|
146
|
+
});
|
|
147
|
+
if (!pointer) return null;
|
|
148
|
+
const result = await readCommit(repository, pointer.object?.sha);
|
|
149
|
+
if (result.snapshot.intent.id !== id)
|
|
150
|
+
throw new Error("Pipeline intent lookup drift");
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
return { read, append, readCommit, lookup };
|
|
154
|
+
}
|
|
@@ -6,9 +6,17 @@ export function materialDigest(bytes) {
|
|
|
6
6
|
|
|
7
7
|
// Draft release assets retain immutable recovery bytes outside the source Git
|
|
8
8
|
// object database. Only the Discussion record selects a committed manifest.
|
|
9
|
-
export function discussionMaterials({
|
|
9
|
+
export function discussionMaterials({
|
|
10
|
+
octokit,
|
|
11
|
+
repository,
|
|
12
|
+
intentId,
|
|
13
|
+
partition = "",
|
|
14
|
+
authorityDescription = "The associated Discussion owns transaction state.",
|
|
15
|
+
}) {
|
|
10
16
|
const [owner, repo] = repository.split("/");
|
|
11
|
-
|
|
17
|
+
if (partition && !/^[1-9][0-9]*-[1-9][0-9]*-[1-9][0-9]*$/u.test(partition))
|
|
18
|
+
throw new Error("Invalid material archive writer partition");
|
|
19
|
+
const tag = `buildchain-records/${intentId.replace("sha256:", "")}${partition ? `/${partition}` : ""}`;
|
|
12
20
|
const repos = octokit.rest.repos;
|
|
13
21
|
let retainedArchive;
|
|
14
22
|
async function findArchive() {
|
|
@@ -27,8 +35,7 @@ export function discussionMaterials({ octokit, repository, intentId }) {
|
|
|
27
35
|
throw new Error("Ambiguous transaction material archive");
|
|
28
36
|
return matches[0];
|
|
29
37
|
}
|
|
30
|
-
async function
|
|
31
|
-
if (retainedArchive) return retainedArchive;
|
|
38
|
+
async function createArchive() {
|
|
32
39
|
let release = await findArchive();
|
|
33
40
|
if (!release) {
|
|
34
41
|
try {
|
|
@@ -40,7 +47,7 @@ export function discussionMaterials({ octokit, repository, intentId }) {
|
|
|
40
47
|
// Storage archives use the repository default; candidate identity is
|
|
41
48
|
// retained in the material manifest, never in a storage Git ref.
|
|
42
49
|
name: `Buildchain transaction materials ${intentId}`,
|
|
43
|
-
body:
|
|
50
|
+
body: `Immutable recovery material. ${authorityDescription}`,
|
|
44
51
|
draft: true,
|
|
45
52
|
prerelease: true,
|
|
46
53
|
})
|
|
@@ -55,9 +62,15 @@ export function discussionMaterials({ octokit, repository, intentId }) {
|
|
|
55
62
|
}
|
|
56
63
|
if (!release.draft || release.tag_name !== tag)
|
|
57
64
|
throw new Error("Transaction material archive identity mismatch");
|
|
58
|
-
retainedArchive = release;
|
|
59
65
|
return release;
|
|
60
66
|
}
|
|
67
|
+
function archive() {
|
|
68
|
+
// Concurrent puts in one writer share the same creation, including readback.
|
|
69
|
+
return (retainedArchive ||= createArchive().catch((error) => {
|
|
70
|
+
retainedArchive = undefined;
|
|
71
|
+
throw error;
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
61
74
|
async function read(handle) {
|
|
62
75
|
const response = await repos.getReleaseAsset({
|
|
63
76
|
owner,
|
|
@@ -72,11 +85,13 @@ export function discussionMaterials({ octokit, repository, intentId }) {
|
|
|
72
85
|
);
|
|
73
86
|
return bytes;
|
|
74
87
|
}
|
|
75
|
-
async function put(bytes) {
|
|
88
|
+
async function put(bytes, descriptor = {}) {
|
|
89
|
+
const extension =
|
|
90
|
+
descriptor.name?.match(/\.([a-zA-Z0-9]{1,8})$/u)?.[1] || "";
|
|
76
91
|
if (bytes.length > 256 * 1024 * 1024)
|
|
77
92
|
throw new Error("Transaction material exceeds the supported file bound");
|
|
78
93
|
const digest = materialDigest(bytes),
|
|
79
|
-
name = digest.replace(":", "-");
|
|
94
|
+
name = digest.replace(":", "-") + (extension ? `.${extension}` : "");
|
|
80
95
|
const release = await archive();
|
|
81
96
|
const find = async () => {
|
|
82
97
|
const assets = await octokit.paginate(repos.listReleaseAssets, {
|
|
@@ -101,7 +116,8 @@ export function discussionMaterials({ octokit, repository, intentId }) {
|
|
|
101
116
|
name,
|
|
102
117
|
data: bytes,
|
|
103
118
|
headers: {
|
|
104
|
-
"content-type":
|
|
119
|
+
"content-type":
|
|
120
|
+
descriptor.mediaType || "application/octet-stream",
|
|
105
121
|
},
|
|
106
122
|
})
|
|
107
123
|
).data;
|
|
@@ -113,7 +129,15 @@ export function discussionMaterials({ octokit, repository, intentId }) {
|
|
|
113
129
|
});
|
|
114
130
|
}
|
|
115
131
|
}
|
|
116
|
-
const handle = {
|
|
132
|
+
const handle = {
|
|
133
|
+
id: asset.id,
|
|
134
|
+
digest,
|
|
135
|
+
size: bytes.length,
|
|
136
|
+
...(asset.browser_download_url
|
|
137
|
+
? { downloadUrl: asset.browser_download_url }
|
|
138
|
+
: {}),
|
|
139
|
+
...(release.html_url ? { archiveUrl: release.html_url } : {}),
|
|
140
|
+
};
|
|
117
141
|
await read(handle);
|
|
118
142
|
return handle;
|
|
119
143
|
}
|
|
@@ -140,7 +164,7 @@ export function discussionMaterials({ octokit, repository, intentId }) {
|
|
|
140
164
|
}
|
|
141
165
|
}
|
|
142
166
|
return {
|
|
143
|
-
put: (bytes) => diagnose("put", () => put(bytes)),
|
|
167
|
+
put: (bytes, descriptor) => diagnose("put", () => put(bytes, descriptor)),
|
|
144
168
|
read: (handle) => diagnose("read", () => read(handle)),
|
|
145
169
|
};
|
|
146
170
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const fields =
|
|
2
2
|
"id number url body author { __typename login ... on Node { id } } lastEditedAt repository { nameWithOwner }";
|
|
3
3
|
const commentFields =
|
|
4
|
-
"id url body author { __typename login ... on Node { id } } lastEditedAt";
|
|
4
|
+
"id url body author { __typename login ... on Node { id } } lastEditedAt replyTo { id }";
|
|
5
5
|
|
|
6
6
|
export function discussionTransport(graphql) {
|
|
7
7
|
async function request(query, variables) {
|
|
@@ -66,13 +66,22 @@ export function discussionTransport(graphql) {
|
|
|
66
66
|
}
|
|
67
67
|
async function comments(id, after = null) {
|
|
68
68
|
const result = await request(
|
|
69
|
-
`query($id:ID!,$after:String){node(id:$id){... on Discussion{comments(first:100,after:$after){nodes{${commentFields}} pageInfo{hasNextPage endCursor}}}}}`,
|
|
69
|
+
`query($id:ID!,$after:String){node(id:$id){... on Discussion{comments(first:100,after:$after){nodes{${commentFields} replies{totalCount}} pageInfo{hasNextPage endCursor}}}}}`,
|
|
70
70
|
{ id, after },
|
|
71
71
|
);
|
|
72
72
|
if (!result.node?.comments)
|
|
73
73
|
throw new Error("Release Discussion comments are unavailable");
|
|
74
74
|
return result.node.comments;
|
|
75
75
|
}
|
|
76
|
+
async function replies(id, after = null) {
|
|
77
|
+
const result = await request(
|
|
78
|
+
`query($id:ID!,$after:String){node(id:$id){... on DiscussionComment{replies(first:100,after:$after){nodes{${commentFields}} pageInfo{hasNextPage endCursor}}}}}`,
|
|
79
|
+
{ id, after },
|
|
80
|
+
);
|
|
81
|
+
if (!result.node?.replies)
|
|
82
|
+
throw new Error("Release Discussion replies are unavailable");
|
|
83
|
+
return result.node.replies;
|
|
84
|
+
}
|
|
76
85
|
async function create({ repositoryId, categoryId, title, body }) {
|
|
77
86
|
const result = await request(
|
|
78
87
|
`mutation($input:CreateDiscussionInput!){createDiscussion(input:$input){discussion{${fields}}}}`,
|
|
@@ -80,14 +89,16 @@ export function discussionTransport(graphql) {
|
|
|
80
89
|
);
|
|
81
90
|
return result.createDiscussion.discussion;
|
|
82
91
|
}
|
|
83
|
-
async function append(id, body) {
|
|
92
|
+
async function append(id, body, replyToId = null) {
|
|
84
93
|
const result = await request(
|
|
85
94
|
`mutation($input:AddDiscussionCommentInput!){addDiscussionComment(input:$input){comment{${commentFields}}}}`,
|
|
86
|
-
{
|
|
95
|
+
{
|
|
96
|
+
input: { discussionId: id, body, ...(replyToId ? { replyToId } : {}) },
|
|
97
|
+
},
|
|
87
98
|
);
|
|
88
99
|
return result.addDiscussionComment.comment;
|
|
89
100
|
}
|
|
90
|
-
return { repository, list, get, comments, create, append };
|
|
101
|
+
return { repository, list, get, comments, replies, create, append };
|
|
91
102
|
}
|
|
92
103
|
|
|
93
104
|
export async function collectDiscussionPages(readPage, limit = 100) {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { githubAuthEnv } from "../source-checkout/auth.js";
|
|
6
|
+
import { createNativeChildEnvironment } from "../../dev-delivery/native/execution.js";
|
|
7
|
+
|
|
8
|
+
// Read-only Git object access for credentialed source qualification. This is a
|
|
9
|
+
// bare repository: no consumer checkout, hooks or build scripts are executed.
|
|
10
|
+
export async function withPipelineSourceObjects(
|
|
11
|
+
{ repository, sourceHead, baseCommit, token },
|
|
12
|
+
operation,
|
|
13
|
+
) {
|
|
14
|
+
if (
|
|
15
|
+
!/^[\w.-]+\/[\w.-]+$/u.test(repository || "") ||
|
|
16
|
+
![sourceHead, baseCommit].every((sha) => /^[0-9a-f]{40}$/u.test(sha || ""))
|
|
17
|
+
)
|
|
18
|
+
throw new Error(
|
|
19
|
+
"Pipeline qualification requires exact repository source coordinates",
|
|
20
|
+
);
|
|
21
|
+
const directory = fs.mkdtempSync(
|
|
22
|
+
path.join(os.tmpdir(), "buildchain-pipeline-source-"),
|
|
23
|
+
);
|
|
24
|
+
const environment = {
|
|
25
|
+
...createNativeChildEnvironment(process.env),
|
|
26
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
27
|
+
...githubAuthEnv(token),
|
|
28
|
+
};
|
|
29
|
+
const git = (args) =>
|
|
30
|
+
execFileSync("git", ["-C", directory, ...args], {
|
|
31
|
+
env: environment,
|
|
32
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
33
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
34
|
+
});
|
|
35
|
+
try {
|
|
36
|
+
git(["init", "--bare", "--quiet"]);
|
|
37
|
+
git([
|
|
38
|
+
"fetch",
|
|
39
|
+
"--quiet",
|
|
40
|
+
"--no-tags",
|
|
41
|
+
`https://github.com/${repository}.git`,
|
|
42
|
+
sourceHead,
|
|
43
|
+
baseCommit,
|
|
44
|
+
]);
|
|
45
|
+
return await operation(directory);
|
|
46
|
+
} finally {
|
|
47
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { runtimeSelector } from "../../runtime/entry/selection.js";
|
|
3
|
+
import { pipelineRunEntry, readPipelineCaller } from "./pipeline-run-entry.js";
|
|
4
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
5
|
+
|
|
6
|
+
export async function pipelineEntryRuns(host, publishedAt) {
|
|
7
|
+
const created = encodeURIComponent(
|
|
8
|
+
`>=${new Date(publishedAt).toISOString()}`,
|
|
9
|
+
);
|
|
10
|
+
const prefix = `/repos/${host.repository}/actions/workflows/buildchain.yml/runs`;
|
|
11
|
+
const runs = [],
|
|
12
|
+
ids = new Set();
|
|
13
|
+
for (let page = 1; page <= 10; page++) {
|
|
14
|
+
const result = await host.request(
|
|
15
|
+
`${prefix}?event=pull_request&status=completed&created=${created}&per_page=100&page=${page}`,
|
|
16
|
+
{ allow404: true },
|
|
17
|
+
);
|
|
18
|
+
if (!result && page === 1) return [];
|
|
19
|
+
if (
|
|
20
|
+
!Array.isArray(result?.workflow_runs) ||
|
|
21
|
+
result.workflow_runs.length > 100
|
|
22
|
+
)
|
|
23
|
+
throw new Error("Published entry run inventory is incomplete");
|
|
24
|
+
for (const run of result.workflow_runs) {
|
|
25
|
+
if (
|
|
26
|
+
!Number.isSafeInteger(run.id) ||
|
|
27
|
+
run.id < 1 ||
|
|
28
|
+
ids.has(run.id) ||
|
|
29
|
+
!Number.isSafeInteger(run.run_attempt) ||
|
|
30
|
+
run.run_attempt < 1 ||
|
|
31
|
+
!Number.isFinite(Date.parse(run.created_at))
|
|
32
|
+
)
|
|
33
|
+
throw new Error(
|
|
34
|
+
"Published entry run inventory has ambiguous coordinates",
|
|
35
|
+
);
|
|
36
|
+
ids.add(run.id);
|
|
37
|
+
runs.push(run);
|
|
38
|
+
}
|
|
39
|
+
if (result.workflow_runs.length < 100)
|
|
40
|
+
return runs.sort(
|
|
41
|
+
(a, b) =>
|
|
42
|
+
Date.parse(b.created_at) - Date.parse(a.created_at) || b.id - a.id,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
throw new Error(
|
|
46
|
+
"Published entry run inventory exceeds its complete-read bound",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function sourceLock(host, source, channel) {
|
|
51
|
+
const pathname =
|
|
52
|
+
channel === "v4-alpha"
|
|
53
|
+
? ".buildchain/alpha-contract-lock.json"
|
|
54
|
+
: ".buildchain/contract-lock.json";
|
|
55
|
+
const file = await host.request(
|
|
56
|
+
`/repos/${host.repository}/contents/${pathname}?ref=${source.commit}`,
|
|
57
|
+
{ allow404: true },
|
|
58
|
+
);
|
|
59
|
+
if (!file) return { value: undefined, evidence: null };
|
|
60
|
+
if (
|
|
61
|
+
file.type !== "file" ||
|
|
62
|
+
file.encoding !== "base64" ||
|
|
63
|
+
!Number.isSafeInteger(file.size) ||
|
|
64
|
+
file.size < 1 ||
|
|
65
|
+
file.size > 1024 * 1024 ||
|
|
66
|
+
typeof file.content !== "string" ||
|
|
67
|
+
file.content.length > 2 * 1024 * 1024
|
|
68
|
+
)
|
|
69
|
+
throw new Error("Published entry runtime lock exceeds its source boundary");
|
|
70
|
+
const bytes = Buffer.from(file.content, "base64");
|
|
71
|
+
const blob = createHash("sha1")
|
|
72
|
+
.update(`blob ${bytes.length}\0`)
|
|
73
|
+
.update(bytes)
|
|
74
|
+
.digest("hex");
|
|
75
|
+
if (bytes.length !== file.size || blob !== file.sha)
|
|
76
|
+
throw new Error("Published entry runtime lock bytes changed");
|
|
77
|
+
const value = JSON.parse(
|
|
78
|
+
new TextDecoder("utf-8", { fatal: true }).decode(bytes),
|
|
79
|
+
);
|
|
80
|
+
if (
|
|
81
|
+
value.schema === "buildchain.consumer-contract-lock/v2" &&
|
|
82
|
+
value.configDigest !== source.configDigest
|
|
83
|
+
)
|
|
84
|
+
throw new Error(
|
|
85
|
+
"Published entry runtime lock belongs to another consumer configuration",
|
|
86
|
+
);
|
|
87
|
+
return {
|
|
88
|
+
value,
|
|
89
|
+
evidence: {
|
|
90
|
+
path: pathname,
|
|
91
|
+
blob,
|
|
92
|
+
digest: `sha256:${createHash("sha256").update(bytes).digest("hex")}`,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function pipelineEntryRun(run, plan, source, host) {
|
|
98
|
+
if (
|
|
99
|
+
run.event !== "pull_request" ||
|
|
100
|
+
run.status !== "completed" ||
|
|
101
|
+
run.repository?.full_name !== host.repository ||
|
|
102
|
+
run.head_repository?.full_name !== host.repository ||
|
|
103
|
+
run.path?.split("@")[0] !== ".github/workflows/buildchain.yml" ||
|
|
104
|
+
!Number.isFinite(Date.parse(run.created_at)) ||
|
|
105
|
+
Date.parse(run.created_at) < Date.parse(source.candidate.publishedAt)
|
|
106
|
+
)
|
|
107
|
+
return null;
|
|
108
|
+
const entry = pipelineRunEntry(run);
|
|
109
|
+
if (entry.recovery || entry.definition.sha !== source.candidate.sha)
|
|
110
|
+
return null;
|
|
111
|
+
const consumer = await host.source.source(
|
|
112
|
+
run.head_sha,
|
|
113
|
+
plan.source.configPath,
|
|
114
|
+
);
|
|
115
|
+
if (recordDigest(consumer.plan) !== plan.contractRoot) return null;
|
|
116
|
+
if (
|
|
117
|
+
consumer.identity.commit !== run.head_sha ||
|
|
118
|
+
consumer.identity.configPath !== plan.source.configPath ||
|
|
119
|
+
consumer.identity.repository !== host.repository
|
|
120
|
+
)
|
|
121
|
+
throw new Error("Published entry consumer source changed during readback");
|
|
122
|
+
await readPipelineCaller(
|
|
123
|
+
run,
|
|
124
|
+
consumer.identity.configPath,
|
|
125
|
+
host.request,
|
|
126
|
+
host.repository,
|
|
127
|
+
);
|
|
128
|
+
const lock = await sourceLock(host, consumer.identity, entry.channel);
|
|
129
|
+
const selection = runtimeSelector({
|
|
130
|
+
lock: lock.value,
|
|
131
|
+
workflowSha: entry.definition.sha,
|
|
132
|
+
});
|
|
133
|
+
if (selection.ref !== source.candidate.sha) return null;
|
|
134
|
+
return {
|
|
135
|
+
source: consumer.identity,
|
|
136
|
+
entry: entry.definition,
|
|
137
|
+
selection,
|
|
138
|
+
lock: lock.evidence,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function pipelineEntryCheck(run, host) {
|
|
143
|
+
const checks = [],
|
|
144
|
+
ids = new Set();
|
|
145
|
+
const prefix = `/repos/${host.repository}/commits/${run.head_sha}/check-runs`;
|
|
146
|
+
for (let page = 1; page <= 10; page++) {
|
|
147
|
+
const result = await host.request(
|
|
148
|
+
`${prefix}?check_name=check&filter=all&per_page=100&page=${page}`,
|
|
149
|
+
);
|
|
150
|
+
if (!Array.isArray(result?.check_runs) || result.check_runs.length > 100)
|
|
151
|
+
throw new Error("Published entry check inventory is incomplete");
|
|
152
|
+
for (const check of result.check_runs) {
|
|
153
|
+
if (!Number.isSafeInteger(check.id) || check.id < 1 || ids.has(check.id))
|
|
154
|
+
throw new Error(
|
|
155
|
+
"Published entry check inventory has ambiguous identities",
|
|
156
|
+
);
|
|
157
|
+
ids.add(check.id);
|
|
158
|
+
checks.push(check);
|
|
159
|
+
}
|
|
160
|
+
if (result.check_runs.length < 100)
|
|
161
|
+
return exactCheck(checks, run, host.repository);
|
|
162
|
+
}
|
|
163
|
+
throw new Error(
|
|
164
|
+
"Published entry check inventory exceeds its complete-read bound",
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function exactCheck(checks, run, repository) {
|
|
169
|
+
const url = `https://github.com/${repository}/actions/runs/${run.id}/attempts/${run.run_attempt}`;
|
|
170
|
+
// Actions may replace details_url with the check's own provider URL. Its
|
|
171
|
+
// exact external execution identity still binds the original native receipt.
|
|
172
|
+
const matches = checks.filter(
|
|
173
|
+
(check) =>
|
|
174
|
+
check.name === "check" &&
|
|
175
|
+
(check.details_url === url ||
|
|
176
|
+
(check.details_url ===
|
|
177
|
+
`https://github.com/${repository}/runs/${check.id}` &&
|
|
178
|
+
check.external_id?.endsWith(`:${run.id}:${run.run_attempt}`))),
|
|
179
|
+
);
|
|
180
|
+
if (!matches.length) return null;
|
|
181
|
+
const values = matches.map((check) => {
|
|
182
|
+
const parsed =
|
|
183
|
+
/^buildchain:(attempt-[0-9a-f]{64}):([1-9][0-9]*):([1-9][0-9]*)$/u.exec(
|
|
184
|
+
check.external_id || "",
|
|
185
|
+
);
|
|
186
|
+
if (
|
|
187
|
+
!parsed ||
|
|
188
|
+
Number(parsed[2]) !== run.id ||
|
|
189
|
+
Number(parsed[3]) !== run.run_attempt ||
|
|
190
|
+
check.head_sha !== run.head_sha ||
|
|
191
|
+
check.status !== "completed" ||
|
|
192
|
+
!["success", "failure"].includes(check.conclusion) ||
|
|
193
|
+
check.app?.slug !== "github-actions"
|
|
194
|
+
)
|
|
195
|
+
throw new Error(
|
|
196
|
+
"Published entry check does not bind its exact native attempt",
|
|
197
|
+
);
|
|
198
|
+
return {
|
|
199
|
+
attempt: parsed[1],
|
|
200
|
+
outcome: check.conclusion,
|
|
201
|
+
summary: check.output?.summary || "",
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
if (values.some((value) => recordDigest(value) !== recordDigest(values[0])))
|
|
205
|
+
throw new Error("Published entry checks disagree about the same execution");
|
|
206
|
+
return {
|
|
207
|
+
...values[0],
|
|
208
|
+
ids: matches.map(({ id }) => id).sort((a, b) => a - b),
|
|
209
|
+
};
|
|
210
|
+
}
|