@kungfu-tech/buildchain 4.1.2 → 4.1.3-alpha.1

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.
Files changed (64) hide show
  1. package/CONTRIBUTING.md +7 -4
  2. package/architecture/action-taxonomy.json +4 -0
  3. package/architecture/agent-change-map.md +44 -0
  4. package/architecture/internal-capabilities.json +56 -0
  5. package/architecture/maintainability-debt.json +2 -1
  6. package/architecture/maintainability-policy.json +4 -4
  7. package/architecture/release-topology.json +14 -1
  8. package/architecture/universal-workflow-bootstrap.json +13 -2
  9. package/architecture/universal-workflow-capability-policy.json +14 -8
  10. package/contracts/promotion-invocation-v1.schema.json +6 -3
  11. package/contracts/promotion-request-v1.schema.json +6 -3
  12. package/contracts/release-discussion-v1.schema.json +143 -0
  13. package/dist/readers/release-discussion.cjs +207 -0
  14. package/dist/site/buildchain-contract.json +15 -11
  15. package/dist/site/buildchain-site.json +64 -8
  16. package/dist/site/capability-registry.json +3 -3
  17. package/dist/site/kfd-claims.json +95 -9
  18. package/dist/site/kfd-upstream-aggregate.json +1 -1
  19. package/dist/site/manual-registry.json +1 -1
  20. package/dist/site/node-api-registry.json +83 -1
  21. package/dist/site/page-registry.json +59 -3
  22. package/dist/site/public-surface-audit.json +53 -7
  23. package/dist/site/publication-registry.json +4 -4
  24. package/dist/site/release-provenance.json +2 -1
  25. package/dist/site/site-manifest.json +5 -5
  26. package/dist/site/workflow-registry.json +79 -7
  27. package/docs/node-api-reference.md +9 -0
  28. package/docs/release-discussions.md +193 -0
  29. package/package.json +10 -6
  30. package/packages/core/providers/github/discussions/materials.js +157 -0
  31. package/packages/core/providers/github/discussions/transport.js +126 -0
  32. package/packages/core/publication/binary/action.js +21 -7
  33. package/packages/core/release/discussion/actions.js +65 -0
  34. package/packages/core/release/discussion/binary.js +103 -0
  35. package/packages/core/release/discussion/checkpoints.js +282 -0
  36. package/packages/core/release/discussion/envelope.js +142 -0
  37. package/packages/core/release/discussion/evidence.js +51 -0
  38. package/packages/core/release/discussion/presentation.js +92 -0
  39. package/packages/core/release/discussion/publication.js +123 -0
  40. package/packages/core/release/discussion/qualification.js +122 -0
  41. package/packages/core/release/discussion/reader-entry.js +11 -0
  42. package/packages/core/release/discussion/reader.js +177 -0
  43. package/packages/core/release/discussion/recovery.js +132 -0
  44. package/packages/core/release/discussion/session.js +115 -0
  45. package/packages/core/release/discussion/store.js +166 -0
  46. package/packages/core/release/discussion/threads.js +83 -0
  47. package/packages/core/release/github-release.js +3 -1
  48. package/packages/core/release/promote-candidate/action.js +86 -6
  49. package/packages/core/release/promote-candidate/preparation.js +106 -0
  50. package/packages/core/release/promote-candidate/product-provider.js +4 -1
  51. package/packages/core/release/promote-candidate/provider-settlement.js +36 -23
  52. package/packages/core/release/promote-candidate/transaction.js +55 -97
  53. package/packages/core/release/promotion/candidate.js +11 -2
  54. package/packages/core/release/promotion/qualification.js +5 -2
  55. package/packages/core/release/promotion-request.js +8 -4
  56. package/packages/core/workflow/engine/execution.js +5 -1
  57. package/packages/core/workflow/engine/provider-context.js +2 -0
  58. package/packages/core/workflow/engine/release-observation.js +1 -0
  59. package/packages/core/workflow/engine/release-promotion.js +38 -26
  60. package/packages/core/workflow/universal-workflow-bootstrap.js +1 -0
  61. package/scripts/build-release-discussion-reader.mjs +34 -0
  62. package/scripts/inventory/binary.mjs +1 -1
  63. package/scripts/maintainability-metrics.mjs +1 -0
  64. package/scripts/site-capability-metadata.mjs +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "4.1.2",
3
+ "version": "4.1.3-alpha.1",
4
4
  "private": false,
5
5
  "description": "Buildchain Release Passport, release governance, CLI toolkit, and site facts.",
6
6
  "repository": "https://github.com/kungfu-systems/buildchain",
@@ -122,7 +122,8 @@
122
122
  "./publication-rehearsal": "./packages/core/publication/publication-rehearsal.js",
123
123
  "./publication-qualification": "./packages/core/publication/publication-qualification.js",
124
124
  "./adopter-delivery": "./packages/core/adoption/adopter-delivery.js",
125
- "./cross-platform-adopter-qualification": "./packages/core/adoption/cross-platform-adopter-qualification.js"
125
+ "./cross-platform-adopter-qualification": "./packages/core/adoption/cross-platform-adopter-qualification.js",
126
+ "./release-discussion-reader": "./packages/core/release/discussion/reader.js"
126
127
  },
127
128
  "files": [
128
129
  "AGENTS.md",
@@ -142,7 +143,8 @@
142
143
  "docs/*.md",
143
144
  "fixtures/*/README.md",
144
145
  "scripts/",
145
- "actions/*/*/*/README.md"
146
+ "actions/*/*/*/README.md",
147
+ "dist/readers/"
146
148
  ],
147
149
  "engines": {
148
150
  "node": ">=22.14.0"
@@ -155,7 +157,7 @@
155
157
  "scripts": {
156
158
  "precheck:static": "prettier --check architecture/decisions/0003-two-phase-delivery-warrant.md architecture/delivery-authority-parity.json contracts/dev-delivery-authority-v2.schema.json dist/site/schemas/dev-delivery-authority-v2.schema.json docs/dev-delivery-qualification-landing-adr.md packages/core/dev-delivery/dev-delivery-authority-candidate.js packages/core/dev-delivery/dev-delivery-authority-evidence.js packages/core/dev-delivery/dev-delivery-authority-landing.js packages/core/dev-delivery/dev-delivery-authority-observation.js packages/core/dev-delivery/dev-delivery-authority-qualification.js packages/core/dev-delivery/dev-delivery-authority-settlement.js packages/core/dev-delivery/dev-delivery-authority-state.js packages/core/dev-delivery/dev-delivery-execution-failure.js packages/core/dev-delivery/dev-delivery-execution-transfer.js packages/core/dev-delivery/dev-delivery-landing-readback.js packages/core/dev-delivery/dev-delivery-landing-terminal-evidence.js packages/core/dev-delivery/dev-delivery-landing-testing-port.js packages/core/dev-delivery/dev-delivery-native-execution.js packages/core/dev-delivery/dev-delivery-native-proof.js packages/core/dev-delivery/dev-delivery-process-boundary.js packages/core/dev-delivery/dev-delivery-warrant-qualification.js packages/core/dev-delivery/dev-delivery-warrant-state.js packages/core/dev-delivery/commands/dev-delivery-authority-command-adapters.mjs packages/core/dev-delivery/commands/dev-delivery-authority-provider.mjs packages/core/dev-delivery/commands/dev-delivery-authority.mjs packages/core/dev-delivery/native/execution.js packages/core/dev-delivery/commands/dev-delivery-process-boundary.mjs packages/core/dev-delivery/native/transactions.js packages/core/dev-delivery/commands/dev-delivery-two-phase.mjs packages/core/dev-delivery/commands/dev-delivery-warrant-options.mjs packages/core/dev-delivery/warrant/persistence.js templates/native-dev-delivery.yml",
157
159
  "operator:windows-jit": "bash packages/core/providers/commands/aws-windows-jit-operator.sh",
158
- "check": "pnpm run check:layout && node scripts/check-runtime-entry.mjs && pnpm run check:implementation-naming && pnpm run check:static && node scripts/check-ci-lane-change-budget.mjs && node scripts/check-public-dogfood-contract.mjs && node scripts/check-floating-consumer-policy-contract.mjs && node scripts/check-release-topology.mjs && node scripts/check-universal-workflow-bootstrap.mjs && pnpm run check:bridge && pnpm run check:contracts:rust && pnpm run check:wasm && node scripts/check-maintainability.mjs && node scripts/check-internal-architecture.mjs && node scripts/generate-agent-change-map.mjs && node scripts/check-inventory.mjs && node scripts/check-core-mechanism-inventory.mjs && node scripts/check-runtime-semantic-closure.mjs && node scripts/check-capability-parity-inventory.mjs && node scripts/check-release-tail-contract.mjs && node scripts/publication-rehearsal-fixture.mjs --check && node packages/core/governance/commands/architecture.mjs validate && node scripts/warrant-shadow-plan.mjs validate && pnpm run check:site && pnpm run check:workflows && pnpm run test:unit && pnpm run check:golden-path && node scripts/check-action-bundles.mjs",
160
+ "check": "pnpm run check:discussion-reader && pnpm run check:layout && node scripts/check-runtime-entry.mjs && pnpm run check:implementation-naming && pnpm run check:static && node scripts/check-ci-lane-change-budget.mjs && node scripts/check-public-dogfood-contract.mjs && node scripts/check-floating-consumer-policy-contract.mjs && node scripts/check-release-topology.mjs && node scripts/check-universal-workflow-bootstrap.mjs && pnpm run check:bridge && pnpm run check:contracts:rust && pnpm run check:wasm && node scripts/check-maintainability.mjs && node scripts/check-internal-architecture.mjs && node scripts/generate-agent-change-map.mjs && node scripts/check-inventory.mjs && node scripts/check-core-mechanism-inventory.mjs && node scripts/check-runtime-semantic-closure.mjs && node scripts/check-capability-parity-inventory.mjs && node scripts/check-release-tail-contract.mjs && node scripts/publication-rehearsal-fixture.mjs --check && node packages/core/governance/commands/architecture.mjs validate && node scripts/warrant-shadow-plan.mjs validate && pnpm run check:site && pnpm run check:workflows && pnpm run test:unit && pnpm run check:golden-path && node scripts/check-action-bundles.mjs",
159
161
  "check:static": "node scripts/check-javascript-syntax.mjs && node scripts/check-source-bindings.mjs && prettier --check architecture/*.json contracts/release-tail-provider-bindings-v1.schema.json contracts/v4-canonical-contracts-v1.schema.json contracts/v4-compatibility-facts-v1.schema.json contracts/v4-delivery-warrant-semantic-diff-report-v1.schema.json contracts/v4-delivery-warrant-trace-v1.schema.json contracts/v4-floating-consumer-policy-receipt-v1.schema.json contracts/runtime-resume-lineage-v2.schema.json contracts/v4-tail-reseal-v1.schema.json contracts/v4-host-contract-v1.schema.json contracts/v4-provider-operation-journal-v1.schema.json contracts/v4-release-activation-shadow-v1.schema.json contracts/v4-stable-publication-fence-v1.schema.json contracts/v4-partial-mutation-recovery-v1.schema.json contracts/v4-stage-capsule-v1.schema.json contracts/v4-stage-capsule-store-v1.schema.json contracts/v4-stage-capsule-resume-v1.schema.json contracts/release-tail-capabilities-v1.schema.json contracts/fixtures/release-tail-capabilities-v1/*.json contracts/fixtures/v4-delivery-warrant-trace-v1/*.json contracts/fixtures/v4-floating-consumer-policy-v1/*.json contracts/fixtures/runtime-resume-lineage-v2/*.json contracts/fixtures/v4-tail-reseal-v1/*.json contracts/fixtures/v4-compatibility-facts-v1/*.json contracts/fixtures/v4-provider-operation-journal-v1/*.json contracts/fixtures/v4-provider-readback-idempotency-v1/*.json contracts/fixtures/v4-release-activation-shadow-v1/*.json contracts/fixtures/v4-stable-publication-fence-v1/*.json contracts/fixtures/v4-partial-mutation-recovery-v1/*.json contracts/fixtures/v4-stage-capsule-v1/*.json contracts/fixtures/v4-stage-capsule-store-v1/*.json contracts/fixtures/v4-stage-capsule-resume-v1/*.json packages/core/contracts/command-registry.mjs packages/core/governance/cli/compatibility-facts.mjs packages/core/contracts/canonical-contracts.js packages/core/contracts/kungfu-temporal-fact.js packages/core/governance/buildchain-compatibility-fact.js packages/core/governance/buildchain-compatibility-authority.js packages/core/governance/buildchain-compatibility-proof.js packages/core/consumer/floating-consumer-evidence.js packages/core/consumer/floating-consumer-policy.js packages/core/consumer/floating-consumer-release-passport.js packages/core/release/recovery/lineage.js packages/core/release/tail-reseal-contract.js packages/core/release/tail-reseal-github.js packages/core/release/tail-reseal-receipt.js packages/core/release/tail-reseal.js packages/core/consumer/runtime-selector-persistence.js packages/core/release/provider-operation-journal.js packages/core/release/provider-readback-idempotency.js packages/core/release/release-activation-shadow.js packages/core/release/stable-publication-fence.js packages/core/release/partial-mutation-recovery-qualification.js packages/core/build/stage-capsule.js packages/core/build/stage-capsule-store.js packages/core/build/stage-capsule-local-store.js packages/core/build/platform-stage-checkpoints.js packages/core/build/stage-capsule-resume-planner.js packages/core/build/stage-capsule-qualification.js packages/core/build/stage-capsule-qualification-campaign.js packages/core/dev-delivery/delivery-warrant-fixture-runner.js packages/core/dev-delivery/delivery-warrant-read-candidate.js packages/core/dev-delivery/delivery-warrant-semantic-diff-gate.js packages/core/dev-delivery/delivery-warrant-shadow-adapter.js scripts/check-core-mechanism-inventory.mjs scripts/check-runtime-semantic-closure.mjs scripts/check-release-tail-contract.mjs scripts/check-javascript-syntax.mjs scripts/check-maintainability.mjs packages/core/dev-delivery/commands/dev-pr-prequeue-guard.mjs scripts/check-public-dogfood-contract.mjs scripts/maintainability-metrics.mjs scripts/maintainability-domain-metrics.mjs scripts/maintainability-governance.mjs scripts/generate-agent-change-map.mjs scripts/public-reference.mjs scripts/site-reference-registry.mjs scripts/generate-public-reference.mjs packages/core/governance/commands/architecture.mjs packages/core/runtime/commands/bridge-bootstrap.mjs scripts/bridge-evidence.mjs packages/core/consumer/commands/consumer-policy.mjs packages/core/runtime/commands/host-adapter.mjs scripts/platform-stage-checkpoint-rehearsal.mjs scripts/stage-capsule-resume-rehearsal.mjs packages/core/build/commands/stage-capsule-qualification.mjs scripts/tail-reseal-macos-rehearsal.mjs packages/core/release/commands/tail-reseal.mjs scripts/warrant-shadow-plan.mjs scripts/verify-golden-path.mjs tests/runtime-entry-provider.test.mjs tests/command-registry.test.mjs tests/core-mechanism-inventory.test.mjs tests/release-tail-contract.test.mjs tests/maintainability.test.mjs tests/maintainability-governance.test.mjs tests/promotion-responsibilities.test.mjs tests/public-reference.test.mjs tests/architecture.test.mjs tests/bridge.test.mjs tests/canonical-contracts.test.mjs tests/delivery-warrant-fixture-runner.test.mjs tests/delivery-warrant-read-candidate.test.mjs tests/floating-consumer-policy.test.mjs tests/runtime-resume-lineage.test.mjs tests/runtime-semantic-closure.test.mjs tests/tail-reseal.test.mjs tests/tail-reseal-parity.test.mjs tests/compatibility-facts.test.mjs tests/platform-stage-checkpoints.test.mjs tests/provider-operation-journal.test.mjs tests/provider-readback-idempotency.test.mjs tests/public-dogfood-contract.test.mjs tests/release-activation-shadow.test.mjs tests/stable-publication-fence.test.mjs tests/partial-mutation-recovery-qualification.test.mjs tests/stage-capsule.test.mjs tests/stage-capsule-store.test.mjs tests/stage-capsule-resume-planner.test.mjs tests/stage-capsule-qualification.test.mjs tests/warrant-shadow-plan.test.mjs",
160
162
  "check:bridge": "cargo fmt --manifest-path crates/buildchain-host-bridge/Cargo.toml -- --check && cargo clippy --locked --manifest-path crates/buildchain-host-bridge/Cargo.toml -- -D warnings && cargo test --locked --manifest-path crates/buildchain-host-bridge/Cargo.toml",
161
163
  "check:contracts": "pnpm run check:contracts:rust && node --test tests/canonical-contracts.test.mjs tests/delivery-warrant-fixture-runner.test.mjs tests/delivery-warrant-read-candidate.test.mjs tests/domain-wasm.test.mjs tests/floating-consumer-policy-contract.test.mjs tests/floating-consumer-policy.test.mjs tests/runtime-resume-lineage.test.mjs tests/tail-reseal.test.mjs tests/tail-reseal-parity.test.mjs tests/compatibility-facts.test.mjs tests/publication-rehearsal.test.mjs tests/platform-stage-checkpoints.test.mjs tests/provider-operation-journal.test.mjs tests/provider-readback-idempotency.test.mjs tests/public-dogfood-contract.test.mjs tests/release-activation-shadow.test.mjs tests/stable-publication-fence.test.mjs tests/partial-mutation-recovery-qualification.test.mjs tests/stage-capsule.test.mjs tests/stage-capsule-store.test.mjs tests/stage-capsule-resume-planner.test.mjs tests/stage-capsule-qualification.test.mjs",
@@ -170,7 +172,7 @@
170
172
  "check:site": "pnpm run check:next-development && pnpm run check:reference && node scripts/generate-site-bundle.mjs --check",
171
173
  "check:golden-path": "node scripts/verify-golden-path.mjs",
172
174
  "test:unit": "node --test tests/*.test.mjs",
173
- "build": "pnpm -r --filter \"./actions/**\" build",
175
+ "build": "pnpm run build:discussion-reader && pnpm -r --filter \"./actions/**\" build",
174
176
  "binary:build": "node scripts/build-standalone-binary.mjs",
175
177
  "package": "npm pack --dry-run --json --registry=https://registry.npmjs.org/",
176
178
  "pack:check": "npm pack --dry-run --json --registry=https://registry.npmjs.org/",
@@ -180,7 +182,9 @@
180
182
  "check:contracts:rust": "cargo fmt --manifest-path crates/buildchain-domain-contracts/Cargo.toml -- --check && cargo clippy --locked --manifest-path crates/buildchain-domain-contracts/Cargo.toml -- -D warnings && cargo test --locked --manifest-path crates/buildchain-domain-contracts/Cargo.toml",
181
183
  "generate:workflows": "node scripts/generate-workflow-taxonomy.mjs && node scripts/generate-bootstrap-admission.mjs",
182
184
  "check:implementation-naming": "node scripts/check-implementation-naming.mjs",
183
- "check:layout": "node scripts/check-code-layout.mjs"
185
+ "check:layout": "node scripts/check-code-layout.mjs",
186
+ "build:discussion-reader": "node scripts/build-release-discussion-reader.mjs",
187
+ "check:discussion-reader": "node scripts/build-release-discussion-reader.mjs --check"
184
188
  },
185
189
  "devDependencies": {
186
190
  "@actions/expressions": "0.3.61",
@@ -0,0 +1,157 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ export function materialDigest(bytes) {
4
+ return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
5
+ }
6
+
7
+ // Draft release assets retain immutable recovery bytes outside the source Git
8
+ // object database. Only the Discussion record selects a committed manifest.
9
+ export function discussionMaterials({ octokit, repository, intentId }) {
10
+ const [owner, repo] = repository.split("/");
11
+ const tag = `buildchain-records/${intentId.replace("sha256:", "")}`;
12
+ const repos = octokit.rest.repos;
13
+ let retainedArchive;
14
+ async function findArchive() {
15
+ let pages = 0;
16
+ const releases = await octokit.paginate(
17
+ repos.listReleases,
18
+ { owner, repo, per_page: 100 },
19
+ (response) => {
20
+ if (++pages > 100)
21
+ throw new Error("Material archive pagination exceeded its bound");
22
+ return response.data.filter((release) => release.tag_name === tag);
23
+ },
24
+ );
25
+ const matches = releases.filter((release) => release.tag_name === tag);
26
+ if (matches.length > 1)
27
+ throw new Error("Ambiguous transaction material archive");
28
+ return matches[0];
29
+ }
30
+ async function archive() {
31
+ if (retainedArchive) return retainedArchive;
32
+ let release = await findArchive();
33
+ if (!release) {
34
+ try {
35
+ release = (
36
+ await repos.createRelease({
37
+ owner,
38
+ repo,
39
+ tag_name: tag,
40
+ // Storage archives use the repository default; candidate identity is
41
+ // retained in the material manifest, never in a storage Git ref.
42
+ name: `Buildchain transaction materials ${intentId}`,
43
+ body: "Immutable recovery material. The associated Discussion owns transaction state.",
44
+ draft: true,
45
+ prerelease: true,
46
+ })
47
+ ).data;
48
+ } catch (error) {
49
+ release = await findArchive();
50
+ if (!release)
51
+ throw new Error("Material archive creation outcome is unknown", {
52
+ cause: error,
53
+ });
54
+ }
55
+ }
56
+ if (!release.draft || release.tag_name !== tag)
57
+ throw new Error("Transaction material archive identity mismatch");
58
+ retainedArchive = release;
59
+ return release;
60
+ }
61
+ async function read(handle) {
62
+ const response = await repos.getReleaseAsset({
63
+ owner,
64
+ repo,
65
+ asset_id: handle.id,
66
+ headers: { accept: "application/octet-stream" },
67
+ });
68
+ const bytes = Buffer.from(response.data);
69
+ if (bytes.length !== handle.size || materialDigest(bytes) !== handle.digest)
70
+ throw new Error(
71
+ "Retained transaction material failed integrity verification",
72
+ );
73
+ return bytes;
74
+ }
75
+ async function put(bytes, descriptor = {}) {
76
+ const extension =
77
+ descriptor.name?.match(/\.([a-zA-Z0-9]{1,8})$/u)?.[1] || "";
78
+ if (bytes.length > 256 * 1024 * 1024)
79
+ throw new Error("Transaction material exceeds the supported file bound");
80
+ const digest = materialDigest(bytes),
81
+ name = digest.replace(":", "-") + (extension ? `.${extension}` : "");
82
+ const release = await archive();
83
+ const find = async () => {
84
+ const assets = await octokit.paginate(repos.listReleaseAssets, {
85
+ owner,
86
+ repo,
87
+ release_id: release.id,
88
+ per_page: 100,
89
+ });
90
+ const matches = assets.filter((asset) => asset.name === name);
91
+ if (matches.length > 1)
92
+ throw new Error("Ambiguous immutable transaction material");
93
+ return matches[0];
94
+ };
95
+ let asset = await find();
96
+ if (!asset) {
97
+ try {
98
+ asset = (
99
+ await repos.uploadReleaseAsset({
100
+ owner,
101
+ repo,
102
+ release_id: release.id,
103
+ name,
104
+ data: bytes,
105
+ headers: {
106
+ "content-type":
107
+ descriptor.mediaType || "application/octet-stream",
108
+ },
109
+ })
110
+ ).data;
111
+ } catch (error) {
112
+ asset = await find();
113
+ if (!asset)
114
+ throw new Error("Transaction material upload outcome is unknown", {
115
+ cause: error,
116
+ });
117
+ }
118
+ }
119
+ const handle = {
120
+ id: asset.id,
121
+ digest,
122
+ size: bytes.length,
123
+ ...(asset.browser_download_url
124
+ ? { downloadUrl: asset.browser_download_url }
125
+ : {}),
126
+ ...(release.html_url ? { archiveUrl: release.html_url } : {}),
127
+ };
128
+ await read(handle);
129
+ return handle;
130
+ }
131
+ async function diagnose(operation, effect) {
132
+ try {
133
+ return await effect();
134
+ } catch (error) {
135
+ let cause = error;
136
+ for (let depth = 0; depth < 4 && cause.cause; depth++)
137
+ cause = cause.cause;
138
+ const details = [
139
+ operation,
140
+ cause.name,
141
+ cause.status,
142
+ cause.request?.method,
143
+ cause.response?.data?.errors?.[0]?.code,
144
+ cause.code,
145
+ ].filter(Boolean);
146
+ error.code = `discussion-material-${details
147
+ .join("-")
148
+ .replace(/[^a-zA-Z0-9-]/gu, "-")
149
+ .slice(0, 140)}`;
150
+ throw error;
151
+ }
152
+ }
153
+ return {
154
+ put: (bytes, descriptor) => diagnose("put", () => put(bytes, descriptor)),
155
+ read: (handle) => diagnose("read", () => read(handle)),
156
+ };
157
+ }
@@ -0,0 +1,126 @@
1
+ const fields =
2
+ "id number url body author { __typename login ... on Node { id } } lastEditedAt repository { nameWithOwner }";
3
+ const commentFields =
4
+ "id url body author { __typename login ... on Node { id } } lastEditedAt replyTo { id }";
5
+
6
+ export function discussionTransport(graphql) {
7
+ async function request(query, variables) {
8
+ try {
9
+ return await graphql(query, variables);
10
+ } catch (error) {
11
+ const code =
12
+ error.errors?.[0]?.extensions?.code ||
13
+ error.errors?.[0]?.type ||
14
+ error.status ||
15
+ "unknown";
16
+ error.code = `discussion-provider-${String(code).replace(/[^a-zA-Z0-9-]/gu, "-")}`;
17
+ throw error;
18
+ }
19
+ }
20
+ async function repository(repository) {
21
+ const [owner, name] = repository.split("/");
22
+ const result = await request(
23
+ `
24
+ query ($owner: String!, $name: String!) {
25
+ viewer {
26
+ login
27
+ id
28
+ }
29
+ repository(owner: $owner, name: $name) {
30
+ id
31
+ hasDiscussionsEnabled
32
+ discussionCategories(first: 25) {
33
+ nodes {
34
+ id
35
+ name
36
+ slug
37
+ }
38
+ }
39
+ }
40
+ }
41
+ `,
42
+ { owner, name },
43
+ );
44
+ if (!result.repository?.hasDiscussionsEnabled)
45
+ throw new Error(
46
+ `Enable Discussions in ${repository} before release execution`,
47
+ );
48
+ return { ...result.repository, viewer: result.viewer };
49
+ }
50
+ async function list(repository, categoryId, after = null) {
51
+ const [owner, name] = repository.split("/");
52
+ const result = await request(
53
+ `query($owner:String!,$name:String!,$category:ID!,$after:String){repository(owner:$owner,name:$name){discussions(first:100,after:$after,categoryId:$category,orderBy:{field:CREATED_AT,direction:ASC}){nodes{${fields}} pageInfo{hasNextPage endCursor}}}}`,
54
+ { owner, name, category: categoryId, after },
55
+ );
56
+ return result.repository.discussions;
57
+ }
58
+ async function get(id) {
59
+ const result = await request(
60
+ `query($id:ID!){node(id:$id){... on Discussion{${fields}}}}`,
61
+ { id },
62
+ );
63
+ if (!result.node?.repository)
64
+ throw new Error("Release Discussion was not found");
65
+ return result.node;
66
+ }
67
+ async function comments(id, after = null) {
68
+ const result = await request(
69
+ `query($id:ID!,$after:String){node(id:$id){... on Discussion{comments(first:100,after:$after){nodes{${commentFields} replies{totalCount}} pageInfo{hasNextPage endCursor}}}}}`,
70
+ { id, after },
71
+ );
72
+ if (!result.node?.comments)
73
+ throw new Error("Release Discussion comments are unavailable");
74
+ return result.node.comments;
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
+ }
85
+ async function create({ repositoryId, categoryId, title, body }) {
86
+ const result = await request(
87
+ `mutation($input:CreateDiscussionInput!){createDiscussion(input:$input){discussion{${fields}}}}`,
88
+ { input: { repositoryId, categoryId, title, body } },
89
+ );
90
+ return result.createDiscussion.discussion;
91
+ }
92
+ async function append(id, body, replyToId = null) {
93
+ const result = await request(
94
+ `mutation($input:AddDiscussionCommentInput!){addDiscussionComment(input:$input){comment{${commentFields}}}}`,
95
+ {
96
+ input: { discussionId: id, body, ...(replyToId ? { replyToId } : {}) },
97
+ },
98
+ );
99
+ return result.addDiscussionComment.comment;
100
+ }
101
+ return { repository, list, get, comments, replies, create, append };
102
+ }
103
+
104
+ export async function collectDiscussionPages(readPage, limit = 100) {
105
+ let cursor = null;
106
+ const nodes = [];
107
+ const cursors = new Set();
108
+ for (let page = 0; page < limit; page++) {
109
+ const result = await readPage(cursor);
110
+ if (!Array.isArray(result?.nodes) || !result.pageInfo)
111
+ throw new Error("Incomplete Discussion page");
112
+ nodes.push(...result.nodes);
113
+ if (Buffer.byteLength(JSON.stringify(nodes)) > 32 * 1024 * 1024)
114
+ throw new Error(
115
+ "Discussion read exceeds its byte bound; refusing an incomplete view",
116
+ );
117
+ if (!result.pageInfo.hasNextPage) return nodes;
118
+ cursor = result.pageInfo.endCursor;
119
+ if (!cursor || cursors.has(cursor))
120
+ throw new Error("Discussion pagination did not advance");
121
+ cursors.add(cursor);
122
+ }
123
+ throw new Error(
124
+ "Discussion page limit exceeded; refusing an incomplete transaction view",
125
+ );
126
+ }
@@ -1,3 +1,6 @@
1
+ import { getOctokit } from "@actions/github";
2
+ import { observeBinaryDistribution } from "../../release/discussion/binary.js";
3
+ import { selectedRecordRuntime } from "../../release/discussion/session.js";
1
4
  import path from "node:path";
2
5
  import { verifyCheckoutIdentity } from "../../runtime/checkout-identity.js";
3
6
  import { releaseAssetClient } from "../../providers/github/release-assets.js";
@@ -13,17 +16,28 @@ export async function publishBinaryAssetsAction(core, env) {
13
16
  const client = releaseAssetClient(env.GITHUB_REPOSITORY, {
14
17
  token: core.getInput("token", { required: true }),
15
18
  });
16
- const result = await publishBinaryAssets(
19
+ const result = await observeBinaryDistribution(
17
20
  {
18
- workspace,
21
+ client,
22
+ octokit: getOctokit(core.getInput("token", { required: true })),
19
23
  repository: env.GITHUB_REPOSITORY,
20
- sourceSha,
21
24
  tag: core.getInput("tag", { required: true }),
22
- capability: JSON.parse(
23
- core.getInput("capability-json", { required: true }),
24
- ),
25
+ runtime: selectedRecordRuntime(env),
26
+ writer: `${env.GITHUB_RUN_ID}:${env.GITHUB_RUN_ATTEMPT}:${env.GITHUB_JOB}`,
25
27
  },
26
- client,
28
+ () =>
29
+ publishBinaryAssets(
30
+ {
31
+ workspace,
32
+ repository: env.GITHUB_REPOSITORY,
33
+ sourceSha,
34
+ tag: core.getInput("tag", { required: true }),
35
+ capability: JSON.parse(
36
+ core.getInput("capability-json", { required: true }),
37
+ ),
38
+ },
39
+ client,
40
+ ),
27
41
  );
28
42
  core.info(
29
43
  `Verified ${result.assets.length} immutable binary assets for ${result.tag}`,
@@ -0,0 +1,65 @@
1
+ import { discussionStatus } from "./reader.js";
2
+ import * as github from "@actions/github";
3
+ import { openReleaseSession, selectedRecordRuntime } from "./session.js";
4
+ import { releaseDiscussionStore } from "./store.js";
5
+ import { discussionTransport } from "../../providers/github/discussions/transport.js";
6
+ import { createProgress } from "./envelope.js";
7
+
8
+ function actionStore(core) {
9
+ return releaseDiscussionStore(
10
+ discussionTransport(
11
+ github.getOctokit(core.getInput("token", { required: true })).graphql,
12
+ ),
13
+ );
14
+ }
15
+
16
+ export async function openDiscussionAction(core, env) {
17
+ const result = await openReleaseSession({
18
+ graphql: github.getOctokit(core.getInput("token", { required: true }))
19
+ .graphql,
20
+ repository: env.GITHUB_REPOSITORY,
21
+ key: core.getInput("intent-key", { required: true }),
22
+ source: JSON.parse(core.getInput("source-json", { required: true })),
23
+ expectedNodes: JSON.parse(
24
+ core.getInput("expected-nodes-json", { required: true }),
25
+ ),
26
+ runtime: selectedRecordRuntime(env),
27
+ attempt: `${env.GITHUB_RUN_ID}:${env.GITHUB_RUN_ATTEMPT}`,
28
+ discussionId: core.getInput("discussion-id"),
29
+ predecessor: core.getInput("predecessor"),
30
+ dryRun: core.getBooleanInput("dry-run"),
31
+ });
32
+ core.setOutput("session-json", JSON.stringify(result.session));
33
+ core.setOutput("discussion-id", result.session.discussion?.id || "");
34
+ core.setOutput("discussion-url", result.session.discussion?.url || "");
35
+ }
36
+
37
+ export async function recordDiscussionAction(core, env) {
38
+ const session = JSON.parse(core.getInput("session-json", { required: true }));
39
+ if (session.dryRun) return;
40
+ const record = createProgress({
41
+ intent: session.intent,
42
+ runtime: selectedRecordRuntime(env),
43
+ attempt: session.attempt,
44
+ writer: `${env.GITHUB_RUN_ID}:${env.GITHUB_RUN_ATTEMPT}:${env.GITHUB_JOB}`,
45
+ predecessor: session.predecessor,
46
+ node: core.getInput("node", { required: true }),
47
+ status: core.getInput("status", { required: true }),
48
+ sequence: Number(core.getInput("sequence")),
49
+ payload: JSON.parse(core.getInput("payload-json") || "{}"),
50
+ });
51
+ const result = await actionStore(core).append(session, record);
52
+ core.setOutput("record-id", result.id);
53
+ }
54
+
55
+ export async function inspectDiscussionAction(core) {
56
+ const session = JSON.parse(core.getInput("session-json", { required: true }));
57
+ if (session.dryRun) {
58
+ core.setOutput("status", "dry-run");
59
+ return;
60
+ }
61
+ const result = await actionStore(core).read(session);
62
+ core.setOutput("status", result.status);
63
+ core.setOutput("handoff-json", JSON.stringify(result.handoff || {}));
64
+ core.setOutput("state-json", JSON.stringify(discussionStatus(result)));
65
+ }
@@ -0,0 +1,103 @@
1
+ import { releaseCheckpoints } from "./checkpoints.js";
2
+ import { discussionTransport } from "../../providers/github/discussions/transport.js";
3
+ import { releaseDiscussionStore } from "./store.js";
4
+ import { createProgress, decodeRecord } from "./envelope.js";
5
+
6
+ // An independent distribution workflow joins the existing intent after resolving
7
+ // its public release locator. Capturing the attempt before execution keeps a late
8
+ // result on its original attempt when another workflow starts recovery.
9
+ export async function observeBinaryDistribution(
10
+ { client, octokit, repository, tag, runtime, writer },
11
+ effect,
12
+ ) {
13
+ const release = await client.release(tag);
14
+ const assets = release.assets.filter(
15
+ ({ name }) => name === "buildchain.release-transaction.json",
16
+ );
17
+ if (assets.length !== 1)
18
+ throw new Error(
19
+ "Binary distribution requires one release transaction locator",
20
+ );
21
+ const locator = JSON.parse(client.assetBytes(assets[0]));
22
+ if (
23
+ locator.schema !== "buildchain.release-locator/v1" ||
24
+ locator.repository !== repository ||
25
+ `v${locator.version}` !== tag
26
+ )
27
+ throw new Error("Binary transaction locator does not match its release");
28
+ const transport = discussionTransport(octokit.graphql);
29
+ const discussion = await transport.get(locator.discussionId);
30
+ const intent = decodeRecord(discussion.body);
31
+ if (
32
+ intent?.id !== locator.intent ||
33
+ intent.repository !== repository ||
34
+ discussion.author?.__typename !== "Bot" ||
35
+ discussion.author.login !== "github-actions"
36
+ )
37
+ throw new Error(
38
+ "Binary transaction intent does not match the consumer workflow",
39
+ );
40
+ const store = releaseDiscussionStore(transport);
41
+ const session = { discussion, intent, writerId: discussion.author.id };
42
+ const state = await store.read(session);
43
+ if (!state.attempt || !intent.expectedNodes.includes("binary-distribution"))
44
+ throw new Error(
45
+ "Binary distribution was not declared by this release intent",
46
+ );
47
+ const attemptRecord = state.records.find(
48
+ (record) => record.attempt === state.attempt,
49
+ );
50
+ Object.assign(session, {
51
+ attempt: state.attempt,
52
+ predecessor: attemptRecord.predecessor,
53
+ runtime,
54
+ writer,
55
+ });
56
+ const sequence =
57
+ Math.max(
58
+ -1,
59
+ ...state.records
60
+ .filter(
61
+ (record) =>
62
+ record.kind === "progress" &&
63
+ record.attempt === state.attempt &&
64
+ record.node === "binary-distribution",
65
+ )
66
+ .map((record) => record.sequence),
67
+ ) + 1;
68
+ const record = (status, offset, payload = {}) =>
69
+ store.append(
70
+ session,
71
+ createProgress({
72
+ intent,
73
+ runtime,
74
+ writer,
75
+ attempt: state.attempt,
76
+ predecessor: attemptRecord.predecessor,
77
+ node: "binary-distribution",
78
+ status,
79
+ sequence: sequence + offset,
80
+ payload,
81
+ }),
82
+ );
83
+ await record("running", 0);
84
+ let result;
85
+ try {
86
+ result = await effect();
87
+ } catch (error) {
88
+ try {
89
+ await record("failure", 1, {
90
+ code: String(error.code || "execution-failed"),
91
+ });
92
+ await releaseCheckpoints({ session, store, octokit }).diagnostics(
93
+ "binary-distribution",
94
+ error.code,
95
+ );
96
+ } catch (recordError) {
97
+ error.discussionRecordingError = recordError.message;
98
+ }
99
+ throw error;
100
+ }
101
+ await record("success", 1, { tag, assets: result.assets.length });
102
+ return result;
103
+ }