@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,89 @@
|
|
|
1
|
+
import { setTimeout } from "node:timers/promises";
|
|
2
|
+
import {
|
|
3
|
+
canonicalJson,
|
|
4
|
+
recordDigest,
|
|
5
|
+
} from "../../release/discussion/envelope.js";
|
|
6
|
+
import { readBusinessAttempt } from "./reader.js";
|
|
7
|
+
|
|
8
|
+
export const ATTEMPT_JOURNAL = "buildchain.attempt-journal/v1";
|
|
9
|
+
|
|
10
|
+
export function journalSnapshot(intent, records) {
|
|
11
|
+
readBusinessAttempt({ intent, records });
|
|
12
|
+
const body = { schema: ATTEMPT_JOURNAL, intent, records };
|
|
13
|
+
return { ...body, root: recordDigest(body) };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function validateJournal(snapshot, intent) {
|
|
17
|
+
if (
|
|
18
|
+
snapshot?.schema !== ATTEMPT_JOURNAL ||
|
|
19
|
+
canonicalJson(snapshot.intent) !== canonicalJson(intent) ||
|
|
20
|
+
canonicalJson(snapshot) !==
|
|
21
|
+
canonicalJson(journalSnapshot(intent, snapshot.records))
|
|
22
|
+
)
|
|
23
|
+
throw new Error("Attempt journal identity or content root drift");
|
|
24
|
+
return snapshot;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// The provider atomically appends a single-parent Git commit with force=false.
|
|
28
|
+
// Discussion is a projection of these admitted bytes, never a competing writer.
|
|
29
|
+
export function atomicAttemptJournal(provider, intent) {
|
|
30
|
+
readBusinessAttempt({ intent, records: [] });
|
|
31
|
+
async function read() {
|
|
32
|
+
const loaded = await provider.read(intent);
|
|
33
|
+
const snapshot = loaded
|
|
34
|
+
? validateJournal(loaded.snapshot, intent)
|
|
35
|
+
: journalSnapshot(intent, []);
|
|
36
|
+
return {
|
|
37
|
+
commit: loaded?.commit || "",
|
|
38
|
+
snapshot,
|
|
39
|
+
...readBusinessAttempt(snapshot),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
async function append(record, expectedHead) {
|
|
43
|
+
const observed = await read();
|
|
44
|
+
if (observed.snapshot.records.some((event) => event.id === record.id)) {
|
|
45
|
+
// Never acknowledge an invalid body merely because its ID was copied.
|
|
46
|
+
const prior = observed.snapshot.records.find(
|
|
47
|
+
(event) => event.id === record.id,
|
|
48
|
+
);
|
|
49
|
+
if (canonicalJson(prior) !== canonicalJson(record))
|
|
50
|
+
throw new Error("Conflicting attempt record bytes");
|
|
51
|
+
return observed;
|
|
52
|
+
}
|
|
53
|
+
if ((observed.head || "") !== expectedHead)
|
|
54
|
+
throw new Error("Stale attempt head; reobserve before writing");
|
|
55
|
+
if (record.node === "attempt") {
|
|
56
|
+
if (record.predecessor !== (observed.attempt || ""))
|
|
57
|
+
throw new Error("Recovery must succeed the current attempt");
|
|
58
|
+
} else if (record.attempt !== observed.attempt)
|
|
59
|
+
throw new Error("A superseded attempt cannot append new results");
|
|
60
|
+
const snapshot = journalSnapshot(intent, [
|
|
61
|
+
...observed.snapshot.records,
|
|
62
|
+
record,
|
|
63
|
+
]);
|
|
64
|
+
let failure;
|
|
65
|
+
try {
|
|
66
|
+
await provider.append({
|
|
67
|
+
intent,
|
|
68
|
+
snapshot,
|
|
69
|
+
expectedCommit: observed.commit,
|
|
70
|
+
});
|
|
71
|
+
} catch (error) {
|
|
72
|
+
failure = error;
|
|
73
|
+
}
|
|
74
|
+
// A lost response can follow a committed append. Reconcile the immutable
|
|
75
|
+
// record in live history; never retry the old parent with force=true.
|
|
76
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
77
|
+
if (attempt) await setTimeout(1000);
|
|
78
|
+
const result = await read();
|
|
79
|
+
if (
|
|
80
|
+
result.snapshot.records.some(
|
|
81
|
+
(event) => canonicalJson(event) === canonicalJson(record),
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
throw failure || new Error("Attempt append missing from provider readback");
|
|
87
|
+
}
|
|
88
|
+
return { read, append };
|
|
89
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { object, text, choice } from "../../consumer/contract/shape.js";
|
|
3
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
4
|
+
|
|
5
|
+
export const MATERIAL_KINDS = [
|
|
6
|
+
"artifact",
|
|
7
|
+
"checkpoint",
|
|
8
|
+
"receipt",
|
|
9
|
+
"passport",
|
|
10
|
+
"provider-readback",
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export function materialReference(value, attempt, repository = "") {
|
|
14
|
+
object(
|
|
15
|
+
value,
|
|
16
|
+
["id", "kind", "digest", "bytes", "url", "generation", "producerAttempt"],
|
|
17
|
+
[],
|
|
18
|
+
"material",
|
|
19
|
+
);
|
|
20
|
+
text(value.id, "material.id", /^[a-z][a-z0-9._/-]*$/u);
|
|
21
|
+
choice(value.kind, MATERIAL_KINDS, "material.kind");
|
|
22
|
+
text(value.digest, "material.digest", /^sha256:[0-9a-f]{64}$/u);
|
|
23
|
+
if (!Number.isSafeInteger(value.bytes) || value.bytes < 1)
|
|
24
|
+
throw new Error("Invalid material byte size");
|
|
25
|
+
const url = new URL(value.url);
|
|
26
|
+
if (
|
|
27
|
+
url.protocol !== "https:" ||
|
|
28
|
+
url.username ||
|
|
29
|
+
url.password ||
|
|
30
|
+
url.search ||
|
|
31
|
+
url.hash ||
|
|
32
|
+
!["github.com", "api.github.com"].includes(url.hostname)
|
|
33
|
+
)
|
|
34
|
+
throw new Error(
|
|
35
|
+
"Material must use a permanent GitHub URL without credentials or signed queries",
|
|
36
|
+
);
|
|
37
|
+
if (
|
|
38
|
+
repository &&
|
|
39
|
+
!(url.hostname === "github.com"
|
|
40
|
+
? url.pathname.startsWith(`/${repository}/releases/download/`) ||
|
|
41
|
+
url.pathname.startsWith(`/${repository}/actions/runs/`)
|
|
42
|
+
: url.pathname.startsWith(`/repos/${repository}/actions/artifacts/`) ||
|
|
43
|
+
url.pathname.startsWith(`/repos/${repository}/releases/assets/`))
|
|
44
|
+
)
|
|
45
|
+
throw new Error("Material belongs to another consumer repository");
|
|
46
|
+
if (
|
|
47
|
+
value.generation !== attempt.generation ||
|
|
48
|
+
value.producerAttempt !== attempt.id
|
|
49
|
+
)
|
|
50
|
+
throw new Error(
|
|
51
|
+
"Material is not qualified for this exact attempt and generation",
|
|
52
|
+
);
|
|
53
|
+
return { ...value };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function verifyMaterialBytes(reference, bytes) {
|
|
57
|
+
const digest = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
58
|
+
if (bytes.length !== reference.bytes || digest !== reference.digest)
|
|
59
|
+
throw new Error(`Material integrity mismatch: ${reference.id}`);
|
|
60
|
+
return { id: reference.id, digest, bytes: bytes.length };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function indexMaterials(index, references, attempt) {
|
|
64
|
+
for (const reference of references) {
|
|
65
|
+
materialReference(reference, attempt);
|
|
66
|
+
const old = index[reference.id];
|
|
67
|
+
if (old && recordDigest(old) !== recordDigest(reference))
|
|
68
|
+
throw new Error(`Immutable material identity conflict: ${reference.id}`);
|
|
69
|
+
index[reference.id] = { ...reference };
|
|
70
|
+
}
|
|
71
|
+
return index;
|
|
72
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { readBusinessAttempt } from "./reader.js";
|
|
3
|
+
export { readBusinessAttempt } from "./reader.js";
|
|
4
|
+
|
|
5
|
+
// Captured, provider-authenticated JSON only. No live ref lookup or effects.
|
|
6
|
+
if (typeof require !== "undefined" && require.main === module) {
|
|
7
|
+
const state = readBusinessAttempt(JSON.parse(fs.readFileSync(0, "utf8")));
|
|
8
|
+
const { status, reason, attempt, generation, missing, recovery } = state;
|
|
9
|
+
process.stdout.write(
|
|
10
|
+
`${JSON.stringify({ status, reason, attempt, generation, missing, recovery })}\n`,
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canonicalJson,
|
|
3
|
+
encodeRecord,
|
|
4
|
+
recordDigest,
|
|
5
|
+
} from "../../release/discussion/envelope.js";
|
|
6
|
+
import { readReleaseDiscussion } from "../../release/discussion/reader.js";
|
|
7
|
+
import { pipelineIntent } from "./identity.js";
|
|
8
|
+
import { validateAttemptRecord } from "./records.js";
|
|
9
|
+
import { indexMaterials } from "./materials.js";
|
|
10
|
+
|
|
11
|
+
export const MAX_ATTEMPT_RECORDS = 10_000;
|
|
12
|
+
const MAX_HISTORY_BYTES = 32 * 1024 * 1024;
|
|
13
|
+
|
|
14
|
+
function validateIntent(intent) {
|
|
15
|
+
const source = intent.source;
|
|
16
|
+
if (source?.kind !== "pipeline") throw new Error("Not a pipeline intent");
|
|
17
|
+
const expected = pipelineIntent({
|
|
18
|
+
...source,
|
|
19
|
+
repository: intent.repository,
|
|
20
|
+
phases: intent.expectedNodes,
|
|
21
|
+
runtime: intent.runtime,
|
|
22
|
+
});
|
|
23
|
+
if (recordDigest(expected) !== recordDigest(intent))
|
|
24
|
+
throw new Error("Pipeline intent identity mismatch");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function foldAttempt(events, intent) {
|
|
28
|
+
events.sort((a, b) => a.sequence - b.sequence);
|
|
29
|
+
const roots = events.filter((record) => record.node === "attempt");
|
|
30
|
+
if (roots.length !== 1 || roots[0] !== events[0])
|
|
31
|
+
throw new Error("Attempt requires exactly one initial root");
|
|
32
|
+
const phases = Object.create(null),
|
|
33
|
+
materials = Object.create(null),
|
|
34
|
+
keys = new Set(),
|
|
35
|
+
runs = new Map();
|
|
36
|
+
let previous = null;
|
|
37
|
+
for (const record of events) {
|
|
38
|
+
const value = record.payload;
|
|
39
|
+
if (
|
|
40
|
+
value.identity.id !== roots[0].payload.identity.id ||
|
|
41
|
+
value.generation.id !== roots[0].payload.generation.id
|
|
42
|
+
)
|
|
43
|
+
throw new Error("Attempt changed its immutable generation");
|
|
44
|
+
if (
|
|
45
|
+
record.sequence !== (previous ? previous.sequence + 1 : 0) ||
|
|
46
|
+
value.previous !== (previous?.id || "")
|
|
47
|
+
)
|
|
48
|
+
throw new Error(
|
|
49
|
+
"Attempt record chain has a gap, fork or missing predecessor",
|
|
50
|
+
);
|
|
51
|
+
if (keys.has(value.eventKey))
|
|
52
|
+
throw new Error("Conflicting duplicate event key");
|
|
53
|
+
keys.add(value.eventKey);
|
|
54
|
+
if (record.node !== "attempt") {
|
|
55
|
+
const before = intent.expectedNodes.slice(
|
|
56
|
+
0,
|
|
57
|
+
intent.expectedNodes.indexOf(record.node),
|
|
58
|
+
);
|
|
59
|
+
if (before.some((phase) => phases[phase]?.payload.state !== "success"))
|
|
60
|
+
throw new Error("Phase predecessor has not succeeded");
|
|
61
|
+
const old = phases[record.node];
|
|
62
|
+
if (
|
|
63
|
+
old &&
|
|
64
|
+
["success", "failure", "cancelled", "superseded"].includes(
|
|
65
|
+
old.payload.state,
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
throw new Error(
|
|
69
|
+
"Terminal phase history is immutable; recover in a successor attempt",
|
|
70
|
+
);
|
|
71
|
+
phases[record.node] = record;
|
|
72
|
+
}
|
|
73
|
+
indexMaterials(materials, value.materials, value.identity);
|
|
74
|
+
runs.set(record.writer, value.writer);
|
|
75
|
+
previous = record;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
identity: roots[0].payload.identity,
|
|
79
|
+
generation: roots[0].payload.generation,
|
|
80
|
+
head: previous.id,
|
|
81
|
+
events,
|
|
82
|
+
phases,
|
|
83
|
+
materials,
|
|
84
|
+
runs: [...runs.values()],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// This projection is evidence navigation, never a Warrant or publication authorization.
|
|
89
|
+
export function readBusinessAttempt({ intent, records }) {
|
|
90
|
+
validateIntent(intent);
|
|
91
|
+
if (!Array.isArray(records) || records.length > MAX_ATTEMPT_RECORDS)
|
|
92
|
+
throw new Error("Attempt record count exceeds its bound");
|
|
93
|
+
if (Buffer.byteLength(canonicalJson(records)) > MAX_HISTORY_BYTES)
|
|
94
|
+
throw new Error("Attempt history byte bound exceeded");
|
|
95
|
+
const unique = new Map(),
|
|
96
|
+
grouped = new Map();
|
|
97
|
+
for (const record of records) {
|
|
98
|
+
validateAttemptRecord(record, intent);
|
|
99
|
+
unique.set(record.id, record);
|
|
100
|
+
}
|
|
101
|
+
for (const record of unique.values()) {
|
|
102
|
+
const events = grouped.get(record.attempt) || [];
|
|
103
|
+
events.push(record);
|
|
104
|
+
grouped.set(record.attempt, events);
|
|
105
|
+
}
|
|
106
|
+
const lineage = readReleaseDiscussion({
|
|
107
|
+
body: encodeRecord(intent),
|
|
108
|
+
records: [...unique.values()],
|
|
109
|
+
});
|
|
110
|
+
const history = lineage.attempts.map((id) =>
|
|
111
|
+
foldAttempt(grouped.get(id), intent),
|
|
112
|
+
);
|
|
113
|
+
const current = history.at(-1);
|
|
114
|
+
if (!current)
|
|
115
|
+
return {
|
|
116
|
+
intent,
|
|
117
|
+
status: "pending",
|
|
118
|
+
reason: "not-admitted",
|
|
119
|
+
attempt: "",
|
|
120
|
+
missing: intent.expectedNodes,
|
|
121
|
+
history,
|
|
122
|
+
recovery: null,
|
|
123
|
+
};
|
|
124
|
+
const missing = intent.expectedNodes.filter(
|
|
125
|
+
(phase) => current.phases[phase]?.payload.state !== "success",
|
|
126
|
+
);
|
|
127
|
+
const states = Object.values(current.phases).map((record) => record.payload);
|
|
128
|
+
const stopped = states.find((value) =>
|
|
129
|
+
["failure", "cancelled", "superseded"].includes(value.state),
|
|
130
|
+
);
|
|
131
|
+
const waiting = states.find((value) => value.state === "waiting");
|
|
132
|
+
const status =
|
|
133
|
+
stopped?.state ||
|
|
134
|
+
(missing.length ? (waiting ? "waiting" : "running") : "complete");
|
|
135
|
+
return {
|
|
136
|
+
intent,
|
|
137
|
+
attempt: current.identity.id,
|
|
138
|
+
generation: current.generation.id,
|
|
139
|
+
status,
|
|
140
|
+
reason: stopped?.reason || waiting?.reason || status,
|
|
141
|
+
head: current.head,
|
|
142
|
+
missing,
|
|
143
|
+
phases: current.phases,
|
|
144
|
+
materials: current.materials,
|
|
145
|
+
runs: current.runs,
|
|
146
|
+
history,
|
|
147
|
+
recovery: status === "complete" ? null : { attempt: current.identity.id },
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createProgress,
|
|
3
|
+
recordDigest,
|
|
4
|
+
encodeRecord,
|
|
5
|
+
} from "../../release/discussion/envelope.js";
|
|
6
|
+
import { object, text, choice, unique } from "../../consumer/contract/shape.js";
|
|
7
|
+
import { providerWriter, validateAttempt } from "./identity.js";
|
|
8
|
+
import { materialReference } from "./materials.js";
|
|
9
|
+
|
|
10
|
+
export const ATTEMPT_EVENT = "buildchain.business-attempt-event/v1";
|
|
11
|
+
export const STATES = [
|
|
12
|
+
"running",
|
|
13
|
+
"waiting",
|
|
14
|
+
"success",
|
|
15
|
+
"failure",
|
|
16
|
+
"cancelled",
|
|
17
|
+
"superseded",
|
|
18
|
+
];
|
|
19
|
+
const ENVELOPE_STATUS = {
|
|
20
|
+
running: "running",
|
|
21
|
+
waiting: "running",
|
|
22
|
+
success: "success",
|
|
23
|
+
failure: "failure",
|
|
24
|
+
cancelled: "cancelled",
|
|
25
|
+
superseded: "cancelled",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function attemptRecord({
|
|
29
|
+
intent,
|
|
30
|
+
attempt,
|
|
31
|
+
generation,
|
|
32
|
+
writer,
|
|
33
|
+
runtime,
|
|
34
|
+
previous = null,
|
|
35
|
+
eventKey,
|
|
36
|
+
phase,
|
|
37
|
+
state,
|
|
38
|
+
reason = "",
|
|
39
|
+
materials = [],
|
|
40
|
+
}) {
|
|
41
|
+
validateAttempt(attempt, intent, generation);
|
|
42
|
+
const writerId = providerWriter(writer, intent.repository);
|
|
43
|
+
choice(state, STATES, "state");
|
|
44
|
+
text(eventKey, "eventKey");
|
|
45
|
+
if (
|
|
46
|
+
eventKey.length > 240 ||
|
|
47
|
+
typeof reason !== "string" ||
|
|
48
|
+
reason.length > 2000
|
|
49
|
+
)
|
|
50
|
+
throw new Error("Attempt event text exceeds its bound");
|
|
51
|
+
if (!previous && (phase !== "attempt" || state !== "running"))
|
|
52
|
+
throw new Error("Open an attempt root before phase progress");
|
|
53
|
+
if (
|
|
54
|
+
previous &&
|
|
55
|
+
(previous.attempt !== attempt.id ||
|
|
56
|
+
previous.payload.generation.id !== generation.id)
|
|
57
|
+
)
|
|
58
|
+
throw new Error(
|
|
59
|
+
"Record predecessor belongs to another attempt or generation",
|
|
60
|
+
);
|
|
61
|
+
if (!Array.isArray(materials) || materials.length > 100)
|
|
62
|
+
throw new Error("Attempt event material bound exceeded");
|
|
63
|
+
unique(
|
|
64
|
+
materials.map((item) => item.id),
|
|
65
|
+
"materials",
|
|
66
|
+
);
|
|
67
|
+
materials.forEach((item) =>
|
|
68
|
+
materialReference(item, attempt, intent.repository),
|
|
69
|
+
);
|
|
70
|
+
return createProgress({
|
|
71
|
+
intent,
|
|
72
|
+
attempt: attempt.id,
|
|
73
|
+
predecessor: attempt.predecessor,
|
|
74
|
+
writer: writerId,
|
|
75
|
+
runtime,
|
|
76
|
+
node: phase,
|
|
77
|
+
status: ENVELOPE_STATUS[state],
|
|
78
|
+
sequence: previous ? previous.sequence + 1 : 0,
|
|
79
|
+
payload: {
|
|
80
|
+
schema: ATTEMPT_EVENT,
|
|
81
|
+
identity: attempt,
|
|
82
|
+
generation,
|
|
83
|
+
writer: { ...writer },
|
|
84
|
+
previous: previous?.id || "",
|
|
85
|
+
eventKey,
|
|
86
|
+
state,
|
|
87
|
+
reason,
|
|
88
|
+
materials: structuredClone(materials),
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function validateAttemptRecord(record, intent) {
|
|
94
|
+
const value = record.payload;
|
|
95
|
+
object(
|
|
96
|
+
value,
|
|
97
|
+
[
|
|
98
|
+
"schema",
|
|
99
|
+
"identity",
|
|
100
|
+
"generation",
|
|
101
|
+
"writer",
|
|
102
|
+
"previous",
|
|
103
|
+
"eventKey",
|
|
104
|
+
"state",
|
|
105
|
+
"reason",
|
|
106
|
+
"materials",
|
|
107
|
+
],
|
|
108
|
+
[],
|
|
109
|
+
"attemptEvent",
|
|
110
|
+
);
|
|
111
|
+
if (value.schema !== ATTEMPT_EVENT)
|
|
112
|
+
throw new Error("Attempt event requires its historical runtime reader");
|
|
113
|
+
validateAttempt(value.identity, intent, value.generation);
|
|
114
|
+
const { id, ...content } = record;
|
|
115
|
+
if (
|
|
116
|
+
id !== recordDigest(content) ||
|
|
117
|
+
record.attempt !== value.identity.id ||
|
|
118
|
+
record.intent !== intent.id ||
|
|
119
|
+
record.predecessor !== value.identity.predecessor
|
|
120
|
+
)
|
|
121
|
+
throw new Error("Attempt record identity mismatch");
|
|
122
|
+
if (providerWriter(value.writer, intent.repository) !== record.writer)
|
|
123
|
+
throw new Error("Attempt provider writer mismatch");
|
|
124
|
+
choice(value.state, STATES, "attemptEvent.state");
|
|
125
|
+
if (record.status !== ENVELOPE_STATUS[value.state])
|
|
126
|
+
throw new Error("Attempt record status mismatch");
|
|
127
|
+
if (value.previous)
|
|
128
|
+
text(value.previous, "previous", /^sha256:[0-9a-f]{64}$/u);
|
|
129
|
+
const expected = attemptRecord({
|
|
130
|
+
intent,
|
|
131
|
+
attempt: value.identity,
|
|
132
|
+
generation: value.generation,
|
|
133
|
+
writer: value.writer,
|
|
134
|
+
runtime: record.runtime,
|
|
135
|
+
previous: value.previous
|
|
136
|
+
? {
|
|
137
|
+
id: value.previous,
|
|
138
|
+
attempt: record.attempt,
|
|
139
|
+
payload: value,
|
|
140
|
+
sequence: record.sequence - 1,
|
|
141
|
+
}
|
|
142
|
+
: null,
|
|
143
|
+
eventKey: value.eventKey,
|
|
144
|
+
phase: record.node,
|
|
145
|
+
state: value.state,
|
|
146
|
+
reason: value.reason,
|
|
147
|
+
materials: value.materials,
|
|
148
|
+
});
|
|
149
|
+
if (recordDigest(expected) !== recordDigest(record))
|
|
150
|
+
throw new Error("Invalid attempt record envelope");
|
|
151
|
+
encodeRecord(record);
|
|
152
|
+
return record;
|
|
153
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { releaseDiscussionStore } from "../../release/discussion/store.js";
|
|
2
|
+
import { validateAttemptRecord } from "./records.js";
|
|
3
|
+
import { readBusinessAttempt } from "./reader.js";
|
|
4
|
+
|
|
5
|
+
// exclusive.run owns the repository/intent-wide writer scope, not merely this
|
|
6
|
+
// process. assertOwner rechecks the live provider fence before every mutation.
|
|
7
|
+
// No default mutex: an in-memory lock cannot serialize independent workflows.
|
|
8
|
+
export function businessAttemptStore(transport, exclusive, options = {}) {
|
|
9
|
+
if (typeof exclusive?.run !== "function")
|
|
10
|
+
throw new Error("Attempt writes require an exclusive provider writer");
|
|
11
|
+
const journal = releaseDiscussionStore(transport, options);
|
|
12
|
+
async function read(session) {
|
|
13
|
+
const observed = await journal.read(session);
|
|
14
|
+
return { ...observed, ...readBusinessAttempt(observed) };
|
|
15
|
+
}
|
|
16
|
+
async function scoped(intent, operation) {
|
|
17
|
+
return exclusive.run(
|
|
18
|
+
{ repository: intent.repository, intent: intent.id },
|
|
19
|
+
async (owner) => {
|
|
20
|
+
if (typeof owner?.assertOwner !== "function")
|
|
21
|
+
throw new Error("Missing live writer fence");
|
|
22
|
+
await owner.assertOwner();
|
|
23
|
+
return operation(owner);
|
|
24
|
+
},
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
async function initialize(request) {
|
|
28
|
+
readBusinessAttempt({ intent: request.intent, records: [] });
|
|
29
|
+
return scoped(request.intent, async (owner) => {
|
|
30
|
+
const repository = await transport.repository(request.intent.repository);
|
|
31
|
+
if (repository.id !== request.intent.source.repositoryId)
|
|
32
|
+
throw new Error("Intent repository identity changed");
|
|
33
|
+
const fenced = { ...transport };
|
|
34
|
+
for (const name of ["create", "append"])
|
|
35
|
+
fenced[name] = async (...args) => {
|
|
36
|
+
await owner.assertOwner();
|
|
37
|
+
return transport[name](...args);
|
|
38
|
+
};
|
|
39
|
+
return releaseDiscussionStore(fenced, options).initialize(request);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
async function append(session, record, expectedHead) {
|
|
43
|
+
return scoped(session.intent, async (owner) => {
|
|
44
|
+
validateAttemptRecord(record, session.intent);
|
|
45
|
+
const observed = await read(session);
|
|
46
|
+
const existing = observed.records.find((event) => event.id === record.id);
|
|
47
|
+
if (existing) return observed;
|
|
48
|
+
if ((observed.head || "") !== expectedHead)
|
|
49
|
+
throw new Error("Stale attempt head; reobserve before writing");
|
|
50
|
+
if (record.node === "attempt") {
|
|
51
|
+
if (record.predecessor !== (observed.attempt || ""))
|
|
52
|
+
throw new Error("Recovery must succeed the current attempt");
|
|
53
|
+
} else if (record.attempt !== observed.attempt)
|
|
54
|
+
throw new Error("A superseded attempt cannot append new results");
|
|
55
|
+
readBusinessAttempt({
|
|
56
|
+
intent: observed.intent,
|
|
57
|
+
records: [...observed.records, record],
|
|
58
|
+
});
|
|
59
|
+
await owner.assertOwner();
|
|
60
|
+
const fenced = {
|
|
61
|
+
...transport,
|
|
62
|
+
append: async (...args) => {
|
|
63
|
+
await owner.assertOwner();
|
|
64
|
+
return transport.append(...args);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
await releaseDiscussionStore(fenced, options).append(session, record);
|
|
68
|
+
const result = await read(session);
|
|
69
|
+
if (result.head !== record.id)
|
|
70
|
+
throw new Error("Attempt append readback has a conflicting writer");
|
|
71
|
+
return result;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return { initialize, read, append };
|
|
75
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { normalInputs } from "../../consumer/contract/entries.js";
|
|
5
|
+
import { compileConsumerPlan } from "../../consumer/contract/plan.js";
|
|
6
|
+
import { buildPipelineProducts } from "../pipeline/build.js";
|
|
7
|
+
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
if (
|
|
10
|
+
args.length !== 4 ||
|
|
11
|
+
args[0] !== "--platform" ||
|
|
12
|
+
args[2] !== "--config-path"
|
|
13
|
+
)
|
|
14
|
+
throw new Error(
|
|
15
|
+
"usage: pipeline-build --platform <platform> --config-path <TOML>",
|
|
16
|
+
);
|
|
17
|
+
const { configPath } = normalInputs({ "config-path": args[3] });
|
|
18
|
+
const cwd = fs.realpathSync(process.cwd());
|
|
19
|
+
const config = fs.realpathSync(path.join(cwd, configPath));
|
|
20
|
+
if (!config.startsWith(`${cwd}${path.sep}`))
|
|
21
|
+
throw new Error("Candidate TOML escapes its checkout");
|
|
22
|
+
const plan = compileConsumerPlan(fs.readFileSync(config, "utf8"));
|
|
23
|
+
await buildPipelineProducts({ cwd, plan, platform: args[1] });
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function pipelineActionOutputs(core, result) {
|
|
2
|
+
core.setOutput("operation", result.operation);
|
|
3
|
+
core.setOutput("context", JSON.stringify(result.context || {}));
|
|
4
|
+
core.setOutput(
|
|
5
|
+
"matrix",
|
|
6
|
+
JSON.stringify({ include: result.context?.platforms || [] }),
|
|
7
|
+
);
|
|
8
|
+
core.setOutput("request", JSON.stringify(result.request || {}));
|
|
9
|
+
core.setOutput("attempt", result.attempt || result.context?.attempt || "");
|
|
10
|
+
core.info(result.reason || result.operation);
|
|
11
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { pipelineActionOutputs } from "./action-output.js";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { normalInputs } from "../../consumer/contract/entries.js";
|
|
5
|
+
import { pipelineHost } from "./host.js";
|
|
6
|
+
import { controlPipeline } from "./controller.js";
|
|
7
|
+
import { buildPipelineSource } from "./build.js";
|
|
8
|
+
import { wakePipelineStableWait } from "../../publication/pipeline/stable-wait.js";
|
|
9
|
+
|
|
10
|
+
export async function controlPipelineAction(core, env) {
|
|
11
|
+
const { configPath } = normalInputs({
|
|
12
|
+
"config-path": core.getInput("config-path") || undefined,
|
|
13
|
+
});
|
|
14
|
+
const host = await pipelineHost(core, env, "Buildchain pipeline controller");
|
|
15
|
+
const result = await controlPipeline(
|
|
16
|
+
env.GITHUB_EVENT_NAME,
|
|
17
|
+
JSON.parse(fs.readFileSync(env.GITHUB_EVENT_PATH, "utf8")),
|
|
18
|
+
{ "config-path": configPath },
|
|
19
|
+
host,
|
|
20
|
+
);
|
|
21
|
+
pipelineActionOutputs(core, result);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function buildPipelineAction(core, env) {
|
|
25
|
+
const context = JSON.parse(core.getInput("context", { required: true }));
|
|
26
|
+
const result = await buildPipelineSource({
|
|
27
|
+
cwd: path.join(env.GITHUB_WORKSPACE, ".buildchain/product"),
|
|
28
|
+
source: context.source,
|
|
29
|
+
platform: core.getInput("platform", { required: true }),
|
|
30
|
+
environment: env,
|
|
31
|
+
});
|
|
32
|
+
core.info(
|
|
33
|
+
`Verified ${result.products.length} products on ${result.platform}`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function wakePipelineAction(core, env) {
|
|
38
|
+
const waiting = core.getInput("stable-wait");
|
|
39
|
+
const host = await pipelineHost(
|
|
40
|
+
core,
|
|
41
|
+
env,
|
|
42
|
+
waiting ? "Recheck stable publication" : "Record delivery attempt",
|
|
43
|
+
);
|
|
44
|
+
if (waiting) {
|
|
45
|
+
const wait = JSON.parse(waiting);
|
|
46
|
+
if (wait.attempt !== core.getInput("attempt", { required: true }))
|
|
47
|
+
throw new Error("Stable wake changed its exact business attempt");
|
|
48
|
+
core.info(JSON.stringify(await wakePipelineStableWait(wait, host)));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
await host.wake(core.getInput("attempt", { required: true }));
|
|
52
|
+
}
|