@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
@@ -0,0 +1,166 @@
1
+ import { readDiscussionThreads, eventParent } from "./threads.js";
2
+ import { renderIntent, renderEvent } from "./presentation.js";
3
+ import { collectDiscussionPages } from "../../providers/github/discussions/transport.js";
4
+ import {
5
+ canonicalJson,
6
+ decodeRecord,
7
+ encodeRecord,
8
+ INTENT_SCHEMA,
9
+ } from "./envelope.js";
10
+ import { readReleaseDiscussion } from "./reader.js";
11
+
12
+ function assertDiscussion(discussion, intent, writerId) {
13
+ if (
14
+ discussion.repository.nameWithOwner.toLowerCase() !==
15
+ intent.repository.toLowerCase()
16
+ )
17
+ throw new Error("Discussion belongs to a different consumer repository");
18
+ if (discussion.author?.id !== writerId || discussion.lastEditedAt)
19
+ throw new Error("Discussion intent has an untrusted author or was edited");
20
+ const existing = decodeRecord(discussion.body);
21
+ if (existing?.id !== intent.id || existing.schema !== INTENT_SCHEMA)
22
+ throw new Error("Discussion contains a different release intent");
23
+ if (
24
+ canonicalJson(existing.expectedNodes) !==
25
+ canonicalJson(intent.expectedNodes) ||
26
+ canonicalJson(existing.source) !== canonicalJson(intent.source)
27
+ )
28
+ throw new Error("Release intent changed during recovery");
29
+ return existing;
30
+ }
31
+
32
+ export function releaseDiscussionStore(
33
+ transport,
34
+ { sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) } = {},
35
+ ) {
36
+ async function observeAfterUnknown(readback, cause) {
37
+ if (
38
+ [401, 403].includes(cause.status) ||
39
+ cause.errors?.some((error) =>
40
+ ["FORBIDDEN", "UNAUTHORIZED"].includes(error.type),
41
+ )
42
+ )
43
+ throw new Error(
44
+ "Consumer workflow cannot write this Discussion; grant discussions: write and permit Announcements creation",
45
+ { cause },
46
+ );
47
+ for (let attempt = 0; attempt < 4; attempt++) {
48
+ const record = await readback();
49
+ if (record) return record;
50
+ if (attempt < 3) await sleep(250 * (attempt + 1));
51
+ }
52
+ throw new Error(
53
+ "Discussion mutation outcome is unknown; retry by reading the same intent before any further write",
54
+ { cause },
55
+ );
56
+ }
57
+ async function initialize({
58
+ intent,
59
+ discussionId = "",
60
+ category = "Announcements",
61
+ dryRun = false,
62
+ }) {
63
+ if (dryRun) return { dryRun: true, intent };
64
+ const repository = await transport.repository(intent.repository);
65
+ const writerId = repository.viewer.id;
66
+ if (discussionId) {
67
+ const discussion = await transport.get(discussionId);
68
+ const original = assertDiscussion(discussion, intent, writerId);
69
+ return { discussion, intent: original, writerId };
70
+ }
71
+ const selected = repository.discussionCategories.nodes.filter(
72
+ (entry) => entry.name === category,
73
+ );
74
+ if (selected.length !== 1)
75
+ throw new Error(
76
+ `Release Discussion category ${category} must exist exactly once`,
77
+ );
78
+ const find = async () => {
79
+ const candidates = await collectDiscussionPages((after) =>
80
+ transport.list(intent.repository, selected[0].id, after),
81
+ );
82
+ const matching = candidates.filter(
83
+ (discussion) =>
84
+ discussion.author?.id === writerId &&
85
+ decodeRecord(discussion.body)?.id === intent.id,
86
+ );
87
+ if (matching.length > 1)
88
+ throw new Error(
89
+ "Multiple Discussions own this release intent; refusing ambiguous recovery",
90
+ );
91
+ if (matching.length) assertDiscussion(matching[0], intent, writerId);
92
+ return matching[0];
93
+ };
94
+ let discussion = await find();
95
+ if (!discussion) {
96
+ try {
97
+ discussion = await transport.create({
98
+ repositoryId: repository.id,
99
+ categoryId: selected[0].id,
100
+ title: intent.source?.qualification
101
+ ? `Buildchain qualification: ${intent.source.qualification}`
102
+ : `Buildchain release: ${intent.key}`,
103
+ body: encodeRecord(intent, renderIntent(intent)),
104
+ });
105
+ } catch (error) {
106
+ discussion = await observeAfterUnknown(find, error);
107
+ }
108
+ }
109
+ const original = assertDiscussion(discussion, intent, writerId);
110
+ return { discussion, intent: original, writerId };
111
+ }
112
+ async function read(session) {
113
+ const discussion = await transport.get(session.discussion.id);
114
+ const intent = assertDiscussion(
115
+ discussion,
116
+ session.intent,
117
+ session.writerId,
118
+ );
119
+ const threads = await readDiscussionThreads(transport, {
120
+ ...session,
121
+ intent,
122
+ discussion,
123
+ });
124
+ return {
125
+ discussion,
126
+ ...threads,
127
+ ...readReleaseDiscussion({
128
+ body: discussion.body,
129
+ records: threads.records,
130
+ }),
131
+ };
132
+ }
133
+ async function append(session, record) {
134
+ if (session.dryRun) return { dryRun: true };
135
+ let observed;
136
+ const find = async () => {
137
+ observed = await read(session);
138
+ const existing = observed.comments.find(
139
+ (comment) => decodeRecord(comment.body)?.id === record.id,
140
+ );
141
+ if (!existing)
142
+ readReleaseDiscussion({
143
+ body: observed.discussion.body,
144
+ records: [...observed.records, record],
145
+ });
146
+ return existing;
147
+ };
148
+ const existing = await find();
149
+ if (existing) return existing;
150
+ const parent = eventParent(observed, record);
151
+ const body = encodeRecord(
152
+ record,
153
+ renderEvent(record, session.intent, observed),
154
+ );
155
+ try {
156
+ await transport.append(session.discussion.id, body, parent);
157
+ } catch (error) {
158
+ return observeAfterUnknown(find, error);
159
+ }
160
+ return observeAfterUnknown(
161
+ find,
162
+ new Error("Discussion append requires provider readback"),
163
+ );
164
+ }
165
+ return { initialize, read, append };
166
+ }
@@ -0,0 +1,83 @@
1
+ import { decodeRecord } from "./envelope.js";
2
+ import { collectDiscussionPages } from "../../providers/github/discussions/transport.js";
3
+
4
+ export function isAttemptRoot(record) {
5
+ return (
6
+ record?.node === "attempt" &&
7
+ record.kind === "progress" &&
8
+ record.status === "running" &&
9
+ record.sequence === 0
10
+ );
11
+ }
12
+
13
+ // Provider placement is checked before the pure reader reduces event semantics.
14
+ // Historical flat records retain their original representation; new writes always
15
+ // use roots and replies. An explicit intent organization makes placement strict.
16
+ export async function readDiscussionThreads(transport, session) {
17
+ const top = await collectDiscussionPages((after) =>
18
+ transport.comments(session.discussion.id, after),
19
+ );
20
+ const comments = [...top];
21
+ for (const root of top) {
22
+ if (root.replies?.totalCount === 0) continue;
23
+ const replies = await collectDiscussionPages((after) =>
24
+ transport.replies(root.id, after),
25
+ );
26
+ for (const reply of replies) {
27
+ if (reply.replyTo?.id !== root.id)
28
+ throw new Error(
29
+ "Discussion reply parent differs from its queried thread",
30
+ );
31
+ }
32
+ comments.push(...replies);
33
+ if (Buffer.byteLength(JSON.stringify(comments)) > 32 * 1024 * 1024)
34
+ throw new Error("Discussion threads exceed the complete-view byte bound");
35
+ }
36
+ const trusted = comments.filter(
37
+ (comment) => comment.author?.id === session.writerId,
38
+ );
39
+ const records = new Map();
40
+ for (const comment of trusted) {
41
+ const record = decodeRecord(comment.body);
42
+ if (!record) continue;
43
+ if (comment.lastEditedAt)
44
+ throw new Error(
45
+ "A transaction record was edited; historical facts are no longer intact",
46
+ );
47
+ records.set(comment.id, record);
48
+ }
49
+ const roots = new Map();
50
+ for (const comment of trusted) {
51
+ const record = records.get(comment.id);
52
+ if (!isAttemptRoot(record)) continue;
53
+ if (comment.replyTo || roots.has(record.attempt))
54
+ throw new Error("Ambiguous or nested Discussion attempt root");
55
+ roots.set(record.attempt, comment);
56
+ }
57
+ for (const comment of trusted) {
58
+ const record = records.get(comment.id);
59
+ if (!record || isAttemptRoot(record)) continue;
60
+ const root = roots.get(record.attempt);
61
+ if (comment.replyTo) {
62
+ if (!root || comment.replyTo.id !== root.id)
63
+ throw new Error(
64
+ "Transaction event is attached to a different attempt root",
65
+ );
66
+ } else if (session.intent.organization === "attempt-threads/v1") {
67
+ throw new Error("Transaction event requires its attempt root reply");
68
+ }
69
+ }
70
+ return { comments: trusted, records: [...records.values()], roots };
71
+ }
72
+
73
+ export function eventParent(state, record) {
74
+ const root = state.roots.get(record.attempt);
75
+ if (isAttemptRoot(record)) {
76
+ if (root)
77
+ throw new Error("Attempt root already exists with different content");
78
+ return null;
79
+ }
80
+ if (!root)
81
+ throw new Error("Open the attempt root before appending its events");
82
+ return root.id;
83
+ }
@@ -302,6 +302,7 @@ export async function publishGitHubReleaseEvidence({
302
302
  declarationPath,
303
303
  qualificationRoot = "",
304
304
  failureAfterCapability = "",
305
+ checkpoint: retainCheckpoint,
305
306
  } = {}) {
306
307
  const assetPaths = collectGitHubReleaseEvidenceAssets({
307
308
  publishEvidencePath,
@@ -349,8 +350,9 @@ export async function publishGitHubReleaseEvidence({
349
350
  }),
350
351
  ...createGitHubProviderAdapters(octokit, materialized.documents),
351
352
  }),
352
- checkpoint: (checkpoint) => {
353
+ checkpoint: async (checkpoint) => {
353
354
  writeReleaseTailTransaction(resolvedStatePath, checkpoint);
355
+ if (retainCheckpoint) await retainCheckpoint(checkpoint);
354
356
  if (
355
357
  failureAfterCapability &&
356
358
  checkpoint.receipts.some(
@@ -1,9 +1,89 @@
1
1
  import * as github from "@actions/github";
2
- import { promoteReleaseCandidate } from "./transaction.js";
2
+ import { publishWithDiscussion } from "../discussion/publication.js";
3
+ import { selectedRecordRuntime } from "../discussion/session.js";
3
4
  export async function candidatePublicationAction(core, env) {
4
- const required = new Set(["candidate-build-summary-path", "required-artifacts-path", "target-ref", "target-sha", "candidate-passport-path", "channel", "product-publication-intent-path", "publication-qualification-path", "publisher-workflow-sha", "repository", "runtime-commit", "runtime-tree", "source-sha", "stage-capsules-path", "tag", "token", "version"]);
5
- const request = Object.fromEntries(["recovery-receipt-path", "candidate-build-summary-path", "candidate-passport-path", "channel", "failure-after-capability", "mutation-token", "product-publication-intent-path", "publication-qualification-path", "publish-artifact-kind", "publish-auth", "publish-command", "publish-dist-tag", "publish-mode", "publish-package-main", "publish-package-set-order", "publisher-workflow-sha", "repository", "required-artifacts-path", "required-status-check", "resume-transaction-id", "runtime-commit", "runtime-tree", "sealed-bundle-manifest", "sealed-bundle-root", "source-sha", "stage-capsules-path", "state-path", "tag", "target-ref", "target-sha", "token", "version"].map(name => [name, core.getInput(name, { required: required.has(name) }).trim()]));
6
- for (const name of ["publish-rematerialize-on-resume", "publish-transaction-override"]) request[name] = core.getBooleanInput(name);
7
- request["artifact-paths"] = core.getMultilineInput("artifact-paths").filter(Boolean);
8
- return promoteReleaseCandidate(request, { octokit: github.getOctokit(request.token), mutationOctokit: github.getOctokit(request["mutation-token"] || request.token), actor: env.GITHUB_ACTOR, runId: env.GITHUB_RUN_ID || "", observe: outputs => { for (const [key, value] of Object.entries(outputs)) core.setOutput(key, value); } });
5
+ const required = new Set([
6
+ "candidate-build-summary-path",
7
+ "required-artifacts-path",
8
+ "target-ref",
9
+ "target-sha",
10
+ "candidate-passport-path",
11
+ "channel",
12
+ "product-publication-intent-path",
13
+ "publication-qualification-path",
14
+ "publisher-workflow-sha",
15
+ "repository",
16
+ "runtime-commit",
17
+ "runtime-tree",
18
+ "source-sha",
19
+ "stage-capsules-path",
20
+ "tag",
21
+ "token",
22
+ "version",
23
+ ]);
24
+ const request = Object.fromEntries(
25
+ [
26
+ "recovery-receipt-path",
27
+ "candidate-build-summary-path",
28
+ "candidate-passport-path",
29
+ "channel",
30
+ "failure-after-capability",
31
+ "mutation-token",
32
+ "product-publication-intent-path",
33
+ "publication-qualification-path",
34
+ "publish-artifact-kind",
35
+ "publish-auth",
36
+ "publish-command",
37
+ "publish-dist-tag",
38
+ "publish-mode",
39
+ "publish-package-main",
40
+ "publish-package-set-order",
41
+ "publisher-workflow-sha",
42
+ "repository",
43
+ "required-artifacts-path",
44
+ "required-status-check",
45
+ "resume-transaction-id",
46
+ "resume-discussion-id",
47
+ "runtime-commit",
48
+ "runtime-tree",
49
+ "sealed-bundle-manifest",
50
+ "sealed-bundle-root",
51
+ "source-sha",
52
+ "stage-capsules-path",
53
+ "state-path",
54
+ "tag",
55
+ "target-ref",
56
+ "target-sha",
57
+ "token",
58
+ "version",
59
+ ].map((name) => [
60
+ name,
61
+ core.getInput(name, { required: required.has(name) }).trim(),
62
+ ]),
63
+ );
64
+ for (const name of [
65
+ "publish-rematerialize-on-resume",
66
+ "publish-transaction-override",
67
+ "standalone-binary-distribution",
68
+ ])
69
+ request[name] = core.getBooleanInput(name);
70
+ request["artifact-paths"] = core
71
+ .getMultilineInput("artifact-paths")
72
+ .filter(Boolean);
73
+ return publishWithDiscussion(request, {
74
+ octokit: github.getOctokit(request.token),
75
+ mutationOctokit: github.getOctokit(
76
+ request["mutation-token"] || request.token,
77
+ ),
78
+ actor: env.GITHUB_ACTOR,
79
+ runId: env.GITHUB_RUN_ID,
80
+ attempt: `${env.GITHUB_RUN_ID}:${env.GITHUB_RUN_ATTEMPT}:${env.GITHUB_ACTION}`,
81
+ runtime: selectedRecordRuntime(env),
82
+ workspace: env.GITHUB_WORKSPACE,
83
+ runtimeRoot: env.BUILDCHAIN_RUNTIME_ROOT,
84
+ observe: (outputs) => {
85
+ for (const [key, value] of Object.entries(outputs))
86
+ core.setOutput(key, value);
87
+ },
88
+ });
9
89
  }
@@ -0,0 +1,106 @@
1
+ import {
2
+ resolveCandidateBuildSummaryPath,
3
+ resolveCandidateProviderInputs,
4
+ resolvePublicationTarget,
5
+ } from "./evidence-inputs.js";
6
+ import {
7
+ assertCandidateEvidenceBinding,
8
+ canonicalChannel,
9
+ observeProtectedPublicationSource,
10
+ } from "./evidence-binding.js";
11
+ import { productProviderRequest } from "./provider-request.js";
12
+ import { createReleaseDocuments } from "./release-documents.js";
13
+ import { read } from "./files.js";
14
+ import { planProductPublication } from "./product-provider.js";
15
+ import path from "node:path";
16
+ export async function prepareCandidatePublication(
17
+ request,
18
+ { octokit, mutationOctokit, actor, runId },
19
+ ) {
20
+ const repository = request["repository"];
21
+ const declaredSourceSha = request["source-sha"];
22
+ const fallbackVersion = request["version"];
23
+ const fallbackTag = request["tag"];
24
+ const channel = request["channel"];
25
+ const expectedTransactionId = request["resume-transaction-id"];
26
+ const candidatePassportPath = request["candidate-passport-path"];
27
+ const buildSummaryPath = resolveCandidateBuildSummaryPath({
28
+ declaredPath: request["candidate-build-summary-path"],
29
+ });
30
+ const candidate = read(candidatePassportPath);
31
+ const stageCapsules = read(request["stage-capsules-path"]);
32
+ const qualification = read(request["publication-qualification-path"]);
33
+ const token = request["token"];
34
+ const publicationTarget = resolvePublicationTarget({
35
+ recoveryReceiptPath: request["recovery-receipt-path"],
36
+ candidate,
37
+ repository,
38
+ channel,
39
+ sourceSha: declaredSourceSha,
40
+ targetRef: request["target-ref"],
41
+ targetSha: request["target-sha"],
42
+ expectedTransactionId,
43
+ });
44
+ const sourceSha = publicationTarget.sourceSha;
45
+ assertCandidateEvidenceBinding({ candidate, stageCapsules, repository });
46
+ const sourceBinding = await observeProtectedPublicationSource({
47
+ octokit,
48
+ repository,
49
+ protectedSourceSha: sourceSha,
50
+ candidate,
51
+ });
52
+ const providerInputs = resolveCandidateProviderInputs({
53
+ recoveryReceiptPath: request["recovery-receipt-path"],
54
+ artifactKind: request["publish-artifact-kind"] || "npm",
55
+ sealedBundleRoot: request["sealed-bundle-root"],
56
+ sealedBundleManifest: request["sealed-bundle-manifest"],
57
+ requiredArtifactsPath: request["required-artifacts-path"],
58
+ publishPackageMain: request["publish-package-main"],
59
+ });
60
+ const providerRequest = productProviderRequest({
61
+ request,
62
+ actor,
63
+ runId,
64
+ octokit,
65
+ mutationOctokit,
66
+ repository,
67
+ targetRef: publicationTarget.targetRef,
68
+ targetSha: publicationTarget.targetSha,
69
+ candidate,
70
+ candidatePassportPath,
71
+ buildSummaryPath,
72
+ qualification,
73
+ providerInputs,
74
+ });
75
+ const publicationPlan = await planProductPublication(providerRequest, {
76
+ fallbackVersion,
77
+ fallbackTag,
78
+ });
79
+ const documents = await createReleaseDocuments({
80
+ request,
81
+ actor,
82
+ runId,
83
+ repository,
84
+ sourceSha,
85
+ fallbackVersion,
86
+ channel,
87
+ candidate,
88
+ stageCapsules,
89
+ qualification,
90
+ sourceBinding,
91
+ publicationPlan,
92
+ publicationIntent: providerRequest.publicationIntent,
93
+ octokit,
94
+ });
95
+ return {
96
+ repository,
97
+ sourceSha,
98
+ token,
99
+ channel,
100
+ qualification,
101
+ providerRequest,
102
+ publicationPlan,
103
+ documents,
104
+ sourceBinding,
105
+ };
106
+ }
@@ -159,7 +159,10 @@ export async function applyProductPublication(request, plan) {
159
159
  });
160
160
  transaction = await executeReleaseTailTransaction(transaction, {
161
161
  adapters: runtime.adapters,
162
- checkpoint: (next) => writeReleaseTailTransaction(statePath, next),
162
+ checkpoint: async (next) => {
163
+ writeReleaseTailTransaction(statePath, next);
164
+ if (request.discussionCheckpoint) await request.discussionCheckpoint("publication", next);
165
+ },
163
166
  });
164
167
  writeReleaseTailTransaction(statePath, transaction);
165
168
  const releaseSha = await runtime.resolveReleaseSha();
@@ -18,12 +18,16 @@ export async function applyAndSettle({
18
18
  providerRequest,
19
19
  publicationPlan,
20
20
  documents,
21
+ observeNode = (_node, effect) => effect(),
21
22
  }) {
23
+ providerRequest.discussionCheckpoint = request.discussionCheckpoint;
22
24
  let productProviderResult;
23
25
  try {
24
- productProviderResult = await applyProductPublication(
25
- providerRequest,
26
- documents.productPublicationPlan,
26
+ productProviderResult = await observeNode("publication", () =>
27
+ applyProductPublication(
28
+ providerRequest,
29
+ documents.productPublicationPlan,
30
+ ),
27
31
  );
28
32
  } catch (error) {
29
33
  if (error.providerProjection)
@@ -37,26 +41,35 @@ export async function applyAndSettle({
37
41
  ".buildchain/release-tail/product-provider-result.json",
38
42
  productProviderResult,
39
43
  );
40
- const result = await publishGitHubReleaseEvidence({
41
- octokit,
42
- repository,
43
- sourceSha: productProviderResult.promotedSha,
44
- version: documents.version,
45
- tag: documents.tag,
46
- channel,
47
- publishEvidencePath: documents.evidencePath,
48
- releasePassportPath: documents.passportPath,
49
- releasePassportOutputDir: path.dirname(documents.passportPath),
50
- additionalAssetPaths: [
51
- ...request["artifact-paths"],
52
- ...(providerRequest.publicationIntent.artifactKind === "oci"
53
- ? [".buildchain/release-tail/oci-publication-readback.json"]
54
- : []),
55
- ],
56
- statePath: request["state-path"] || ".buildchain/release-tail/state.json",
57
- qualificationRoot: qualification.receiptRoot,
58
- failureAfterCapability: request["failure-after-capability"],
59
- });
44
+ const result = await observeNode("github-release", () =>
45
+ publishGitHubReleaseEvidence({
46
+ octokit,
47
+ repository,
48
+ sourceSha: productProviderResult.promotedSha,
49
+ version: documents.version,
50
+ tag: documents.tag,
51
+ channel,
52
+ publishEvidencePath: documents.evidencePath,
53
+ releasePassportPath: documents.passportPath,
54
+ releasePassportOutputDir: path.dirname(documents.passportPath),
55
+ additionalAssetPaths: [
56
+ ...request["artifact-paths"],
57
+ ...(request.discussionReaderPath ? [request.discussionReaderPath] : []),
58
+ ...(request.discussionLocatorPath
59
+ ? [request.discussionLocatorPath]
60
+ : []),
61
+ ...(providerRequest.publicationIntent.artifactKind === "oci"
62
+ ? [".buildchain/release-tail/oci-publication-readback.json"]
63
+ : []),
64
+ ],
65
+ statePath: request["state-path"] || ".buildchain/release-tail/state.json",
66
+ qualificationRoot: qualification.receiptRoot,
67
+ failureAfterCapability: request["failure-after-capability"],
68
+ checkpoint: request.discussionCheckpoint
69
+ ? (state) => request.discussionCheckpoint("github-release", state)
70
+ : undefined,
71
+ }),
72
+ );
60
73
  const releaseReceipt = createReleaseReceipt({
61
74
  schema: RELEASE_RECEIPT_CONTRACT,
62
75
  transactionRoot: documents.releaseTransaction.transactionRoot,