@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,162 @@
|
|
|
1
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
2
|
+
import { object, relativePath } from "../../consumer/contract/shape.js";
|
|
3
|
+
import { materializePipelineVersion } from "./version.js";
|
|
4
|
+
|
|
5
|
+
export function pipelineVersionMaterialPaths(policy, configPath) {
|
|
6
|
+
const primary = [...new Set(policy.files.map(({ path }) => path))];
|
|
7
|
+
const derived = policy.derived_files || [];
|
|
8
|
+
const paths = [...primary, ...derived];
|
|
9
|
+
if (
|
|
10
|
+
!paths.length ||
|
|
11
|
+
paths.length > 128 ||
|
|
12
|
+
new Set(paths).size !== paths.length
|
|
13
|
+
)
|
|
14
|
+
throw new Error(
|
|
15
|
+
"Version material requires a bounded disjoint path inventory",
|
|
16
|
+
);
|
|
17
|
+
for (const file of paths) {
|
|
18
|
+
relativePath(file, "version material path");
|
|
19
|
+
if (
|
|
20
|
+
file.includes("*") ||
|
|
21
|
+
file === configPath ||
|
|
22
|
+
[
|
|
23
|
+
".buildchain/contract-lock.json",
|
|
24
|
+
".buildchain/alpha-contract-lock.json",
|
|
25
|
+
].includes(file) ||
|
|
26
|
+
/^\.(?:git|github)\//u.test(file)
|
|
27
|
+
)
|
|
28
|
+
throw new Error(
|
|
29
|
+
"Version material cannot rewrite its source contract or workflow authority",
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return paths.sort();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function checkFiles(files, paths) {
|
|
36
|
+
if (recordDigest(Object.keys(files).sort()) !== recordDigest(paths))
|
|
37
|
+
throw new Error("Version regeneration changed its declared file inventory");
|
|
38
|
+
let total = 0;
|
|
39
|
+
for (const file of paths) {
|
|
40
|
+
if (
|
|
41
|
+
typeof files[file] !== "string" ||
|
|
42
|
+
Buffer.byteLength(files[file]) > 4 * 1024 * 1024
|
|
43
|
+
)
|
|
44
|
+
throw new Error("Version regeneration requires bounded text files");
|
|
45
|
+
total += Buffer.byteLength(files[file]);
|
|
46
|
+
}
|
|
47
|
+
if (total > 64 * 1024 * 1024)
|
|
48
|
+
throw new Error(
|
|
49
|
+
"Version regeneration exceeds its complete material byte limit",
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// This is an observation from a credentialless product job. A provider adapter
|
|
54
|
+
// must qualify its exact producer execution before passing it to collection.
|
|
55
|
+
export function pipelineVersionRegenerationResult(
|
|
56
|
+
preparation,
|
|
57
|
+
platform,
|
|
58
|
+
files,
|
|
59
|
+
) {
|
|
60
|
+
const paths = pipelineVersionMaterialPaths(
|
|
61
|
+
preparation.versionPolicy,
|
|
62
|
+
preparation.source.configPath,
|
|
63
|
+
);
|
|
64
|
+
checkFiles(files, paths);
|
|
65
|
+
if (!preparation.platforms.includes(platform))
|
|
66
|
+
throw new Error("Version regeneration platform was not declared");
|
|
67
|
+
const body = {
|
|
68
|
+
schema: "buildchain.pipeline-version-regeneration/v1",
|
|
69
|
+
preparationRoot: preparation.root,
|
|
70
|
+
source: preparation.source,
|
|
71
|
+
platform,
|
|
72
|
+
version: preparation.version,
|
|
73
|
+
files,
|
|
74
|
+
};
|
|
75
|
+
return { ...body, root: recordDigest(body) };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function qualifiedFiles(preparation, result, paths) {
|
|
79
|
+
object(result, [
|
|
80
|
+
"schema",
|
|
81
|
+
"preparationRoot",
|
|
82
|
+
"source",
|
|
83
|
+
"platform",
|
|
84
|
+
"version",
|
|
85
|
+
"files",
|
|
86
|
+
"root",
|
|
87
|
+
]);
|
|
88
|
+
const expected = pipelineVersionRegenerationResult(
|
|
89
|
+
preparation,
|
|
90
|
+
result.platform,
|
|
91
|
+
result.files,
|
|
92
|
+
);
|
|
93
|
+
if (recordDigest(result) !== recordDigest(expected))
|
|
94
|
+
throw new Error(
|
|
95
|
+
"Version regeneration changed its preparation, source, version or result root",
|
|
96
|
+
);
|
|
97
|
+
checkFiles(result.files, paths);
|
|
98
|
+
return result.files;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function collectPipelineVersionMaterial(preparation, files, results) {
|
|
102
|
+
const policy = preparation.versionPolicy;
|
|
103
|
+
const paths = pipelineVersionMaterialPaths(
|
|
104
|
+
policy,
|
|
105
|
+
preparation.source.configPath,
|
|
106
|
+
);
|
|
107
|
+
checkFiles(files, paths);
|
|
108
|
+
const expectedPlatforms = [...new Set(preparation.platforms)].sort();
|
|
109
|
+
const actualPlatforms = results.map(({ platform }) => platform).sort();
|
|
110
|
+
if (
|
|
111
|
+
recordDigest(actualPlatforms) !== recordDigest(expectedPlatforms) ||
|
|
112
|
+
!actualPlatforms.length
|
|
113
|
+
)
|
|
114
|
+
throw new Error(
|
|
115
|
+
"Version regeneration requires every declared platform exactly once",
|
|
116
|
+
);
|
|
117
|
+
const material = materializePipelineVersion(
|
|
118
|
+
policy,
|
|
119
|
+
files,
|
|
120
|
+
preparation.version,
|
|
121
|
+
);
|
|
122
|
+
const primary = new Set(policy.files.map(({ path }) => path));
|
|
123
|
+
const expected = {
|
|
124
|
+
...files,
|
|
125
|
+
...Object.fromEntries(
|
|
126
|
+
material.changes.map(({ path, content }) => [path, content]),
|
|
127
|
+
),
|
|
128
|
+
};
|
|
129
|
+
const derived = new Map();
|
|
130
|
+
for (const result of results) {
|
|
131
|
+
const observed = qualifiedFiles(preparation, result, paths);
|
|
132
|
+
for (const file of paths) {
|
|
133
|
+
if (primary.has(file)) {
|
|
134
|
+
if (observed[file] !== expected[file])
|
|
135
|
+
throw new Error(
|
|
136
|
+
"Product commands changed bytes outside the planned version fields",
|
|
137
|
+
);
|
|
138
|
+
} else if (observed[file] !== files[file]) {
|
|
139
|
+
if (derived.has(file) && derived.get(file) !== observed[file])
|
|
140
|
+
throw new Error(
|
|
141
|
+
"Platforms produced conflicting derived version bytes",
|
|
142
|
+
);
|
|
143
|
+
derived.set(file, observed[file]);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const changes = [...derived].map(([path, content]) => ({
|
|
148
|
+
path,
|
|
149
|
+
before: recordDigest(files[path]),
|
|
150
|
+
content,
|
|
151
|
+
after: recordDigest(content),
|
|
152
|
+
}));
|
|
153
|
+
return {
|
|
154
|
+
...material,
|
|
155
|
+
changes: [...material.changes, ...changes].sort((left, right) =>
|
|
156
|
+
left.path.localeCompare(right.path),
|
|
157
|
+
),
|
|
158
|
+
regenerationRoots: results
|
|
159
|
+
.toSorted((left, right) => left.platform.localeCompare(right.platform))
|
|
160
|
+
.map(({ root }) => root),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
6
|
+
|
|
7
|
+
function git(cwd, ...args) {
|
|
8
|
+
return execFileSync("git", ["-C", cwd, ...args], {
|
|
9
|
+
encoding: "utf8",
|
|
10
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function fileState(cwd, file) {
|
|
15
|
+
const absolute = path.join(cwd, file);
|
|
16
|
+
if (fs.realpathSync(path.dirname(absolute)) !== path.dirname(absolute))
|
|
17
|
+
throw new Error(
|
|
18
|
+
"Version source parent directories cannot become symbolic links",
|
|
19
|
+
);
|
|
20
|
+
const stat = fs.lstatSync(absolute);
|
|
21
|
+
const bytes = stat.isSymbolicLink()
|
|
22
|
+
? Buffer.from(fs.readlinkSync(absolute))
|
|
23
|
+
: stat.isFile()
|
|
24
|
+
? fs.readFileSync(absolute)
|
|
25
|
+
: null;
|
|
26
|
+
if (!bytes)
|
|
27
|
+
throw new Error(
|
|
28
|
+
"Version preparation requires regular tracked source or symbolic links",
|
|
29
|
+
);
|
|
30
|
+
return {
|
|
31
|
+
kind: stat.isSymbolicLink() ? "link" : "file",
|
|
32
|
+
executable:
|
|
33
|
+
process.platform === "win32" ? null : Boolean(stat.mode & 0o111),
|
|
34
|
+
digest: createHash("sha256").update(bytes).digest("hex"),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function guardPipelineVersionSource(cwd, source, allowedPaths) {
|
|
39
|
+
const entries = git(cwd, "ls-tree", "-r", "-z", source.commit)
|
|
40
|
+
.split("\0")
|
|
41
|
+
.filter(Boolean);
|
|
42
|
+
const files = entries.map((entry) => {
|
|
43
|
+
const match =
|
|
44
|
+
/^(?:100644|100755|120000) blob [0-9a-f]{40}\t([\s\S]+)$/u.exec(entry);
|
|
45
|
+
if (!match)
|
|
46
|
+
throw new Error(
|
|
47
|
+
"Version preparation cannot rewrite a source containing unresolved submodules",
|
|
48
|
+
);
|
|
49
|
+
return match[1];
|
|
50
|
+
});
|
|
51
|
+
const before = new Map(files.map((file) => [file, fileState(cwd, file)]));
|
|
52
|
+
return () => {
|
|
53
|
+
const current = git(cwd, "rev-parse", "HEAD", "HEAD^{tree}")
|
|
54
|
+
.trim()
|
|
55
|
+
.split(/\r?\n/u);
|
|
56
|
+
if (recordDigest(current) !== recordDigest([source.commit, source.tree]))
|
|
57
|
+
throw new Error(
|
|
58
|
+
"Version product commands changed their admitted Git HEAD or tree",
|
|
59
|
+
);
|
|
60
|
+
for (const [file, expected] of before) {
|
|
61
|
+
const current = fileState(cwd, file);
|
|
62
|
+
if (allowedPaths.includes(file)) current.digest = expected.digest;
|
|
63
|
+
if (recordDigest(current) !== recordDigest(expected))
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Version product commands changed undeclared source bytes: ${file}`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { parse, stringify } from "smol-toml";
|
|
2
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
3
|
+
|
|
4
|
+
function document(file, bytes) {
|
|
5
|
+
const text = Buffer.isBuffer(bytes) ? bytes.toString("utf8") : bytes;
|
|
6
|
+
return file.format === "json" ? JSON.parse(text) : parse(text);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function field(value, key) {
|
|
10
|
+
const names = key.split(".");
|
|
11
|
+
let parent = value;
|
|
12
|
+
for (const name of names.slice(0, -1)) {
|
|
13
|
+
if (
|
|
14
|
+
["__proto__", "prototype", "constructor"].includes(name) ||
|
|
15
|
+
!Object.hasOwn(parent, name) ||
|
|
16
|
+
!parent[name] ||
|
|
17
|
+
typeof parent[name] !== "object"
|
|
18
|
+
)
|
|
19
|
+
throw new Error(
|
|
20
|
+
"Version field does not resolve to an owned document key",
|
|
21
|
+
);
|
|
22
|
+
parent = parent[name];
|
|
23
|
+
}
|
|
24
|
+
const name = names.at(-1);
|
|
25
|
+
if (
|
|
26
|
+
["__proto__", "prototype", "constructor"].includes(name) ||
|
|
27
|
+
!Object.hasOwn(parent, name) ||
|
|
28
|
+
typeof parent[name] !== "string"
|
|
29
|
+
)
|
|
30
|
+
throw new Error("Version field must be an existing string");
|
|
31
|
+
return { parent, name };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function readPipelineVersion(policy, files) {
|
|
35
|
+
const versions = policy.files.map((file) => {
|
|
36
|
+
if (!Object.hasOwn(files, file.path))
|
|
37
|
+
throw new Error(`Missing declared version file: ${file.path}`);
|
|
38
|
+
const { parent, name } = field(document(file, files[file.path]), file.key);
|
|
39
|
+
return parent[name];
|
|
40
|
+
});
|
|
41
|
+
if (!versions.length || new Set(versions).size !== 1)
|
|
42
|
+
throw new Error(
|
|
43
|
+
"All declared version fields must identify the same candidate version",
|
|
44
|
+
);
|
|
45
|
+
return versions[0];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function materializePipelineVersion(policy, files, version) {
|
|
49
|
+
const before = readPipelineVersion(policy, files);
|
|
50
|
+
if (policy.strategy === "anchored" && before !== version)
|
|
51
|
+
throw new Error("Buildchain cannot rewrite anchored version authority");
|
|
52
|
+
const documents = new Map();
|
|
53
|
+
for (const file of policy.files) {
|
|
54
|
+
if (!documents.has(file.path))
|
|
55
|
+
documents.set(file.path, {
|
|
56
|
+
file,
|
|
57
|
+
value: document(file, files[file.path]),
|
|
58
|
+
});
|
|
59
|
+
const entry = documents.get(file.path);
|
|
60
|
+
if (entry.file.format !== file.format)
|
|
61
|
+
throw new Error("One version document cannot have conflicting formats");
|
|
62
|
+
const { parent, name } = field(entry.value, file.key);
|
|
63
|
+
parent[name] = version;
|
|
64
|
+
}
|
|
65
|
+
const changes = [...documents].flatMap(([filePath, { file, value }]) => {
|
|
66
|
+
if (before === version) return [];
|
|
67
|
+
const content =
|
|
68
|
+
file.format === "json"
|
|
69
|
+
? `${JSON.stringify(value, null, 2)}\n`
|
|
70
|
+
: stringify(value);
|
|
71
|
+
return [
|
|
72
|
+
{
|
|
73
|
+
path: filePath,
|
|
74
|
+
before: recordDigest(String(files[filePath])),
|
|
75
|
+
content,
|
|
76
|
+
after: recordDigest(content),
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
});
|
|
80
|
+
return { before, version, changes };
|
|
81
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { resumePipelineSession } from "../../workflow/pipeline/session.js";
|
|
2
|
+
import { pipelinePublicationJournal } from "./journal.js";
|
|
3
|
+
import { retainedPipelineStableWait } from "./stable-wait.js";
|
|
4
|
+
|
|
5
|
+
export async function claimPipelinePublicationWorker(
|
|
6
|
+
attempt,
|
|
7
|
+
publisherSha,
|
|
8
|
+
host,
|
|
9
|
+
) {
|
|
10
|
+
const { run: execution } = await host.runs.read(host.runId, host.runAttempt);
|
|
11
|
+
const definitions = (execution.referenced_workflows || []).filter((entry) =>
|
|
12
|
+
entry.path?.startsWith(
|
|
13
|
+
"kungfu-systems/buildchain/.github/workflows/.release-pipeline-products.yml@",
|
|
14
|
+
),
|
|
15
|
+
);
|
|
16
|
+
if (
|
|
17
|
+
!/^[0-9a-f]{40}$/u.test(publisherSha || "") ||
|
|
18
|
+
definitions.length !== 1 ||
|
|
19
|
+
definitions[0].sha !== publisherSha
|
|
20
|
+
)
|
|
21
|
+
throw new Error(
|
|
22
|
+
"Publisher definition is not the exact hosted reusable workflow",
|
|
23
|
+
);
|
|
24
|
+
const session = await resumePipelineSession({ ...host, attempt }, host);
|
|
25
|
+
const observed = session.observed;
|
|
26
|
+
if (observed.phases.merge?.payload.state !== "success")
|
|
27
|
+
throw new Error(
|
|
28
|
+
"Product publication requires completed protected integration",
|
|
29
|
+
);
|
|
30
|
+
if (observed.status === "complete") return null;
|
|
31
|
+
const phase = observed.missing[0];
|
|
32
|
+
if (!["publish", "distribution", "next-development"].includes(phase))
|
|
33
|
+
throw new Error("Attempt is not at product publication");
|
|
34
|
+
const prior = observed.history
|
|
35
|
+
.at(-1)
|
|
36
|
+
.events.filter((event) =>
|
|
37
|
+
event.payload.materials.some((material) =>
|
|
38
|
+
material.id.startsWith("publication/worker/"),
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
.at(-1)?.payload.writer;
|
|
42
|
+
if (
|
|
43
|
+
prior &&
|
|
44
|
+
(prior.runId !== String(host.runId) ||
|
|
45
|
+
prior.runAttempt !== String(host.runAttempt))
|
|
46
|
+
) {
|
|
47
|
+
const { run } = await host.runs.read(
|
|
48
|
+
Number(prior.runId),
|
|
49
|
+
Number(prior.runAttempt),
|
|
50
|
+
);
|
|
51
|
+
// A time-only wait releases publication ownership before its outer run ends.
|
|
52
|
+
// The next worker still claims the exact native head under the publication lock.
|
|
53
|
+
if (
|
|
54
|
+
run.status !== "completed" &&
|
|
55
|
+
!(await retainedPipelineStableWait(observed, host))
|
|
56
|
+
)
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const journal = pipelinePublicationJournal(session, host);
|
|
60
|
+
const claim = {
|
|
61
|
+
schema: "buildchain.pipeline-publication-worker/v1",
|
|
62
|
+
attempt,
|
|
63
|
+
runId: host.runId,
|
|
64
|
+
runAttempt: host.runAttempt,
|
|
65
|
+
};
|
|
66
|
+
await journal.record("publication/worker", claim, {
|
|
67
|
+
phase,
|
|
68
|
+
expectedHead: observed.head,
|
|
69
|
+
});
|
|
70
|
+
await journal.fence();
|
|
71
|
+
return { session, journal, phase };
|
|
72
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readNpmPackResult } from "./npm/pack-result.js";
|
|
1
2
|
import crypto from "node:crypto";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import os from "node:os";
|
|
@@ -564,8 +565,7 @@ function packPublication({ buildRoot, packageDir, source, toolchain }) {
|
|
|
564
565
|
label: "npm pack reproducibility candidate",
|
|
565
566
|
},
|
|
566
567
|
);
|
|
567
|
-
const
|
|
568
|
-
const result = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
568
|
+
const result = readNpmPackResult(output);
|
|
569
569
|
if (!result?.filename || !result?.integrity || !result?.shasum) {
|
|
570
570
|
throw new Error(
|
|
571
571
|
"npm pack reproducibility candidate did not report filename, integrity, and shasum",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { releaseCheckpoints } from "./checkpoints.js";
|
|
1
2
|
import { discussionTransport } from "../../providers/github/discussions/transport.js";
|
|
2
3
|
import { releaseDiscussionStore } from "./store.js";
|
|
3
4
|
import { createProgress, decodeRecord } from "./envelope.js";
|
|
@@ -46,6 +47,12 @@ export async function observeBinaryDistribution(
|
|
|
46
47
|
const attemptRecord = state.records.find(
|
|
47
48
|
(record) => record.attempt === state.attempt,
|
|
48
49
|
);
|
|
50
|
+
Object.assign(session, {
|
|
51
|
+
attempt: state.attempt,
|
|
52
|
+
predecessor: attemptRecord.predecessor,
|
|
53
|
+
runtime,
|
|
54
|
+
writer,
|
|
55
|
+
});
|
|
49
56
|
const sequence =
|
|
50
57
|
Math.max(
|
|
51
58
|
-1,
|
|
@@ -82,6 +89,10 @@ export async function observeBinaryDistribution(
|
|
|
82
89
|
await record("failure", 1, {
|
|
83
90
|
code: String(error.code || "execution-failed"),
|
|
84
91
|
});
|
|
92
|
+
await releaseCheckpoints({ session, store, octokit }).diagnostics(
|
|
93
|
+
"binary-distribution",
|
|
94
|
+
error.code,
|
|
95
|
+
);
|
|
85
96
|
} catch (recordError) {
|
|
86
97
|
error.discussionRecordingError = recordError.message;
|
|
87
98
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
retainAttachment,
|
|
3
|
+
diagnosticReport,
|
|
4
|
+
diagnosticLog,
|
|
5
|
+
} from "./evidence.js";
|
|
1
6
|
import fs from "node:fs";
|
|
2
7
|
import path from "node:path";
|
|
3
8
|
import { createProgress, recordDigest } from "./envelope.js";
|
|
@@ -10,7 +15,11 @@ export function releaseCheckpoints({ session, store, octokit }) {
|
|
|
10
15
|
repository: session.intent.repository,
|
|
11
16
|
intentId: session.intent.id,
|
|
12
17
|
});
|
|
13
|
-
async function checkpoint(
|
|
18
|
+
async function checkpoint(
|
|
19
|
+
node,
|
|
20
|
+
value,
|
|
21
|
+
{ attachments = [], label = value.schema || "Recovery checkpoint" } = {},
|
|
22
|
+
) {
|
|
14
23
|
const observed = await store.read(session);
|
|
15
24
|
const records = observed.records.filter(
|
|
16
25
|
(record) =>
|
|
@@ -21,20 +30,29 @@ export function releaseCheckpoints({ session, store, octokit }) {
|
|
|
21
30
|
const root = recordDigest(value);
|
|
22
31
|
const duplicate = records.find((record) => record.payload.root === root);
|
|
23
32
|
if (duplicate) return duplicate;
|
|
24
|
-
const
|
|
33
|
+
const sequence =
|
|
34
|
+
Math.max(-1, ...records.map((record) => record.sequence)) + 1;
|
|
35
|
+
const handle = await retainAttachment(materials, {
|
|
36
|
+
name: `${node}-checkpoint-${sequence}.json`,
|
|
37
|
+
mediaType: "application/json",
|
|
38
|
+
bytes: Buffer.from(JSON.stringify(value, null, 2) + "\n"),
|
|
39
|
+
});
|
|
25
40
|
const record = createProgress({
|
|
26
41
|
intent: session.intent,
|
|
27
42
|
runtime: session.runtime,
|
|
28
43
|
attempt: session.attempt,
|
|
44
|
+
writer: session.writer || session.attempt,
|
|
29
45
|
predecessor: session.predecessor,
|
|
30
46
|
kind: "checkpoint",
|
|
31
47
|
node,
|
|
32
48
|
status: "running",
|
|
33
|
-
sequence
|
|
49
|
+
sequence,
|
|
34
50
|
payload: {
|
|
35
51
|
schema: "buildchain.release-checkpoint/v1",
|
|
36
52
|
root,
|
|
37
53
|
material: handle,
|
|
54
|
+
label,
|
|
55
|
+
attachments: [handle, ...attachments],
|
|
38
56
|
},
|
|
39
57
|
});
|
|
40
58
|
await store.append(session, record);
|
|
@@ -69,7 +87,30 @@ export function releaseCheckpoints({ session, store, octokit }) {
|
|
|
69
87
|
throw new Error("Unsupported retained publication reader manifest");
|
|
70
88
|
return materials.read(manifest.reader);
|
|
71
89
|
}
|
|
72
|
-
|
|
90
|
+
async function diagnostics(node, code) {
|
|
91
|
+
const report = diagnosticReport(
|
|
92
|
+
session,
|
|
93
|
+
await store.read(session),
|
|
94
|
+
node,
|
|
95
|
+
code,
|
|
96
|
+
);
|
|
97
|
+
const log = await retainAttachment(materials, {
|
|
98
|
+
name: `${node}-diagnostics.log`,
|
|
99
|
+
mediaType: "text/plain",
|
|
100
|
+
bytes: diagnosticLog(report),
|
|
101
|
+
});
|
|
102
|
+
return checkpoint(node, report, {
|
|
103
|
+
attachments: [log],
|
|
104
|
+
label: `Execution diagnostics: ${report.code}`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
checkpoint,
|
|
109
|
+
readCheckpoint,
|
|
110
|
+
materials,
|
|
111
|
+
publicationReader,
|
|
112
|
+
diagnostics,
|
|
113
|
+
};
|
|
73
114
|
}
|
|
74
115
|
|
|
75
116
|
function assertNoSymlink(file, base) {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Evidence is declared transaction data, never an implicit upload of workspace
|
|
2
|
+
// files, environment variables, raw exceptions or authenticated HTTP responses.
|
|
3
|
+
export async function retainAttachment(materials, { name, mediaType, bytes }) {
|
|
4
|
+
if (
|
|
5
|
+
!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,159}$/u.test(name) ||
|
|
6
|
+
!["application/json", "text/plain", "application/octet-stream"].includes(
|
|
7
|
+
mediaType,
|
|
8
|
+
)
|
|
9
|
+
)
|
|
10
|
+
throw new Error("Invalid transaction attachment descriptor");
|
|
11
|
+
const handle = await materials.put(bytes, { name, mediaType });
|
|
12
|
+
if (!handle.downloadUrl)
|
|
13
|
+
throw new Error("Retained attachment is missing its provider download URL");
|
|
14
|
+
return { name, mediaType, ...handle };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function diagnosticReport(session, state, node, code) {
|
|
18
|
+
return {
|
|
19
|
+
schema: "buildchain.release-diagnostics/v1",
|
|
20
|
+
intent: session.intent.id,
|
|
21
|
+
attempt: session.attempt,
|
|
22
|
+
node,
|
|
23
|
+
code: String(code || "execution-failed")
|
|
24
|
+
.replace(/[^a-zA-Z0-9._-]/gu, "-")
|
|
25
|
+
.slice(0, 160),
|
|
26
|
+
events: state.records
|
|
27
|
+
.filter((record) => record.attempt === session.attempt)
|
|
28
|
+
.map(({ id, node, kind, status, sequence, writer }) => ({
|
|
29
|
+
id,
|
|
30
|
+
node,
|
|
31
|
+
kind,
|
|
32
|
+
status,
|
|
33
|
+
sequence,
|
|
34
|
+
writer,
|
|
35
|
+
})),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function diagnosticLog(report) {
|
|
40
|
+
return Buffer.from(
|
|
41
|
+
[
|
|
42
|
+
`Release intent ${report.intent}`,
|
|
43
|
+
`Attempt ${report.attempt}: ${report.node} ${report.code}`,
|
|
44
|
+
...report.events.map(
|
|
45
|
+
(event) =>
|
|
46
|
+
`${event.node} ${event.kind} ${event.sequence} ${event.status} writer=${event.writer} record=${event.id}`,
|
|
47
|
+
),
|
|
48
|
+
"",
|
|
49
|
+
].join("\n"),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { isAttemptRoot } from "./threads.js";
|
|
2
|
+
|
|
3
|
+
const text = (value) =>
|
|
4
|
+
String(value ?? "")
|
|
5
|
+
.replace(
|
|
6
|
+
/[&<>\[\]`*_\\|]/gu,
|
|
7
|
+
(character) => `&#${character.charCodeAt(0)};`,
|
|
8
|
+
)
|
|
9
|
+
.replace(/[\r\n]/gu, " ");
|
|
10
|
+
const code = (value) => `\`${String(value).replace(/[`\r\n]/gu, " ")}\``;
|
|
11
|
+
|
|
12
|
+
function runLink(repository, writer) {
|
|
13
|
+
const match = /^(\d+):(\d+)(?::.*)?$/u.exec(writer);
|
|
14
|
+
return match
|
|
15
|
+
? `[${text(writer)}](https://github.com/${repository}/actions/runs/${match[1]}/attempts/${match[2]})`
|
|
16
|
+
: code(writer);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function runtimeLink(runtime) {
|
|
20
|
+
return `[${runtime.repository}@${runtime.sha.slice(0, 12)}](https://github.com/${runtime.repository}/commit/${runtime.sha})`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function renderIntent(intent) {
|
|
24
|
+
return [
|
|
25
|
+
`## ${intent.source?.qualification ? "Qualification" : "Release"}: ${text(intent.key)}`,
|
|
26
|
+
`Repository: ${code(intent.repository)}`,
|
|
27
|
+
"Each top-level transaction comment owns one attempt. Its replies contain node results, recovery checkpoints and downloadable evidence. Recovery attempts link to their predecessors.",
|
|
28
|
+
`Expected nodes: ${intent.expectedNodes.map(code).join(" → ")}`,
|
|
29
|
+
`Intent: ${code(intent.id)}`,
|
|
30
|
+
].join("\n\n");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function attachmentRows(record, repository) {
|
|
34
|
+
return (record.payload?.attachments || []).map((attachment) => {
|
|
35
|
+
const url = new URL(attachment.downloadUrl);
|
|
36
|
+
if (
|
|
37
|
+
url.protocol !== "https:" ||
|
|
38
|
+
url.hostname !== "github.com" ||
|
|
39
|
+
!url.pathname.startsWith(`/${repository}/releases/download/`) ||
|
|
40
|
+
url.search ||
|
|
41
|
+
url.hash
|
|
42
|
+
)
|
|
43
|
+
throw new Error(
|
|
44
|
+
"Attachment download must belong to the consumer repository",
|
|
45
|
+
);
|
|
46
|
+
return `| [${text(attachment.name)}](${url.href}) | ${text(attachment.mediaType)} | ${attachment.size} | ${code(attachment.digest)} |`;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function renderEvent(record, intent, state) {
|
|
51
|
+
if (isAttemptRoot(record)) {
|
|
52
|
+
const predecessor = state.roots.get(record.predecessor);
|
|
53
|
+
return [
|
|
54
|
+
`## Attempt ${text(record.attempt)}`,
|
|
55
|
+
`Execution: ${runLink(intent.repository, record.writer)} \nRuntime: ${runtimeLink(record.runtime)}`,
|
|
56
|
+
record.predecessor
|
|
57
|
+
? `Resumes: ${predecessor?.url ? `[${text(record.predecessor)}](${predecessor.url})` : code(record.predecessor)}`
|
|
58
|
+
: "Initial execution of this release intent.",
|
|
59
|
+
`Nodes: ${intent.expectedNodes.map(code).join(" → ")}`,
|
|
60
|
+
"Results and evidence are appended below. This root preserves the original execution context.",
|
|
61
|
+
].join("\n\n");
|
|
62
|
+
}
|
|
63
|
+
const label =
|
|
64
|
+
record.kind === "checkpoint"
|
|
65
|
+
? `checkpoint ${record.sequence}`
|
|
66
|
+
: record.status;
|
|
67
|
+
const lines = [
|
|
68
|
+
`### ${text(record.node)} · ${label}`,
|
|
69
|
+
`Attempt: ${code(record.attempt)} · Writer: ${runLink(intent.repository, record.writer)}`,
|
|
70
|
+
`Runtime: ${runtimeLink(record.runtime)}`,
|
|
71
|
+
];
|
|
72
|
+
if (record.payload?.label) lines.push(text(record.payload.label));
|
|
73
|
+
if (record.payload?.code)
|
|
74
|
+
lines.push(`Failure code: ${code(record.payload.code)}`);
|
|
75
|
+
const attachments = attachmentRows(record, intent.repository);
|
|
76
|
+
if (attachments.length)
|
|
77
|
+
lines.push(
|
|
78
|
+
"| Attachment | Type | Bytes | SHA-256 |\n| --- | --- | ---: | --- |\n" +
|
|
79
|
+
attachments.join("\n"),
|
|
80
|
+
"Retained files use the transaction material archive. Access requires repository permission to read draft release assets.",
|
|
81
|
+
);
|
|
82
|
+
const { attachments: _attachments, ...details } = record.payload || {};
|
|
83
|
+
if (Object.keys(details).length) {
|
|
84
|
+
const json = JSON.stringify(details, null, 2);
|
|
85
|
+
// HTML escaping keeps payload strings from closing the presentation container.
|
|
86
|
+
lines.push(
|
|
87
|
+
`<details><summary>Record details</summary><pre>${json.slice(0, 6000).replace(/[&<>]/gu, (c) => ({ "&": "&", "<": "<", ">": ">" })[c])}${json.length > 6000 ? "\n… See the retained JSON attachment for the complete material." : ""}</pre></details>`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
lines.push(`Record: ${code(record.id)}`);
|
|
91
|
+
return lines.join("\n\n");
|
|
92
|
+
}
|
|
@@ -40,8 +40,10 @@ export async function publishWithDiscussion(
|
|
|
40
40
|
observe = () => {},
|
|
41
41
|
},
|
|
42
42
|
) {
|
|
43
|
+
let retained;
|
|
43
44
|
const journal = await openReleaseSession({
|
|
44
45
|
graphql: octokit.graphql,
|
|
46
|
+
onFailure: (node, error) => retained.diagnostics(node, error.code),
|
|
45
47
|
repository: request.repository,
|
|
46
48
|
key: request.version,
|
|
47
49
|
source: { version: request.version },
|
|
@@ -54,7 +56,7 @@ export async function publishWithDiscussion(
|
|
|
54
56
|
request["publish-transaction-override"],
|
|
55
57
|
),
|
|
56
58
|
});
|
|
57
|
-
|
|
59
|
+
retained = releaseCheckpoints({
|
|
58
60
|
session: journal.session,
|
|
59
61
|
store: journal.store,
|
|
60
62
|
octokit,
|