@kungfu-tech/buildchain 4.1.2 → 4.1.3-alpha.0
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/CONTRIBUTING.md +7 -4
- package/architecture/action-taxonomy.json +4 -0
- package/architecture/agent-change-map.md +40 -0
- package/architecture/internal-capabilities.json +49 -0
- package/architecture/maintainability-debt.json +2 -1
- package/architecture/maintainability-policy.json +4 -4
- package/architecture/release-topology.json +11 -1
- package/architecture/universal-workflow-bootstrap.json +13 -2
- package/architecture/universal-workflow-capability-policy.json +15 -9
- package/contracts/promotion-invocation-v1.schema.json +6 -3
- package/contracts/promotion-request-v1.schema.json +6 -3
- package/contracts/release-discussion-v1.schema.json +140 -0
- package/dist/readers/release-discussion.cjs +203 -0
- package/dist/site/buildchain-contract.json +15 -11
- package/dist/site/buildchain-site.json +57 -6
- package/dist/site/capability-registry.json +3 -3
- package/dist/site/kfd-claims.json +95 -9
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +1 -1
- package/dist/site/node-api-registry.json +83 -1
- package/dist/site/page-registry.json +54 -3
- package/dist/site/public-surface-audit.json +53 -7
- package/dist/site/publication-registry.json +2 -2
- package/dist/site/release-provenance.json +2 -1
- package/dist/site/site-manifest.json +3 -3
- package/dist/site/workflow-registry.json +79 -7
- package/docs/node-api-reference.md +9 -0
- package/docs/release-discussions.md +154 -0
- package/package.json +10 -6
- package/packages/core/providers/github/discussions/materials.js +146 -0
- package/packages/core/providers/github/discussions/transport.js +115 -0
- package/packages/core/publication/binary/action.js +21 -7
- package/packages/core/release/discussion/actions.js +65 -0
- package/packages/core/release/discussion/binary.js +92 -0
- package/packages/core/release/discussion/checkpoints.js +241 -0
- package/packages/core/release/discussion/envelope.js +141 -0
- package/packages/core/release/discussion/publication.js +121 -0
- package/packages/core/release/discussion/qualification.js +121 -0
- package/packages/core/release/discussion/reader-entry.js +11 -0
- package/packages/core/release/discussion/reader.js +168 -0
- package/packages/core/release/discussion/recovery.js +132 -0
- package/packages/core/release/discussion/session.js +113 -0
- package/packages/core/release/discussion/store.js +169 -0
- package/packages/core/release/github-release.js +3 -1
- package/packages/core/release/promote-candidate/action.js +86 -6
- package/packages/core/release/promote-candidate/preparation.js +106 -0
- package/packages/core/release/promote-candidate/product-provider.js +4 -1
- package/packages/core/release/promote-candidate/provider-settlement.js +36 -23
- package/packages/core/release/promote-candidate/transaction.js +55 -97
- package/packages/core/release/promotion/candidate.js +11 -2
- package/packages/core/release/promotion/qualification.js +5 -2
- package/packages/core/release/promotion-request.js +8 -4
- package/packages/core/workflow/engine/execution.js +5 -1
- package/packages/core/workflow/engine/provider-context.js +2 -0
- package/packages/core/workflow/engine/release-observation.js +1 -0
- package/packages/core/workflow/engine/release-promotion.js +38 -26
- package/packages/core/workflow/universal-workflow-bootstrap.js +1 -0
- package/scripts/build-release-discussion-reader.mjs +34 -0
- package/scripts/inventory/binary.mjs +1 -1
- package/scripts/maintainability-metrics.mjs +1 -0
- package/scripts/site-capability-metadata.mjs +1 -0
|
@@ -0,0 +1,146 @@
|
|
|
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) {
|
|
76
|
+
if (bytes.length > 256 * 1024 * 1024)
|
|
77
|
+
throw new Error("Transaction material exceeds the supported file bound");
|
|
78
|
+
const digest = materialDigest(bytes),
|
|
79
|
+
name = digest.replace(":", "-");
|
|
80
|
+
const release = await archive();
|
|
81
|
+
const find = async () => {
|
|
82
|
+
const assets = await octokit.paginate(repos.listReleaseAssets, {
|
|
83
|
+
owner,
|
|
84
|
+
repo,
|
|
85
|
+
release_id: release.id,
|
|
86
|
+
per_page: 100,
|
|
87
|
+
});
|
|
88
|
+
const matches = assets.filter((asset) => asset.name === name);
|
|
89
|
+
if (matches.length > 1)
|
|
90
|
+
throw new Error("Ambiguous immutable transaction material");
|
|
91
|
+
return matches[0];
|
|
92
|
+
};
|
|
93
|
+
let asset = await find();
|
|
94
|
+
if (!asset) {
|
|
95
|
+
try {
|
|
96
|
+
asset = (
|
|
97
|
+
await repos.uploadReleaseAsset({
|
|
98
|
+
owner,
|
|
99
|
+
repo,
|
|
100
|
+
release_id: release.id,
|
|
101
|
+
name,
|
|
102
|
+
data: bytes,
|
|
103
|
+
headers: {
|
|
104
|
+
"content-type": "application/octet-stream",
|
|
105
|
+
},
|
|
106
|
+
})
|
|
107
|
+
).data;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
asset = await find();
|
|
110
|
+
if (!asset)
|
|
111
|
+
throw new Error("Transaction material upload outcome is unknown", {
|
|
112
|
+
cause: error,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const handle = { id: asset.id, digest, size: bytes.length };
|
|
117
|
+
await read(handle);
|
|
118
|
+
return handle;
|
|
119
|
+
}
|
|
120
|
+
async function diagnose(operation, effect) {
|
|
121
|
+
try {
|
|
122
|
+
return await effect();
|
|
123
|
+
} catch (error) {
|
|
124
|
+
let cause = error;
|
|
125
|
+
for (let depth = 0; depth < 4 && cause.cause; depth++)
|
|
126
|
+
cause = cause.cause;
|
|
127
|
+
const details = [
|
|
128
|
+
operation,
|
|
129
|
+
cause.name,
|
|
130
|
+
cause.status,
|
|
131
|
+
cause.request?.method,
|
|
132
|
+
cause.response?.data?.errors?.[0]?.code,
|
|
133
|
+
cause.code,
|
|
134
|
+
].filter(Boolean);
|
|
135
|
+
error.code = `discussion-material-${details
|
|
136
|
+
.join("-")
|
|
137
|
+
.replace(/[^a-zA-Z0-9-]/gu, "-")
|
|
138
|
+
.slice(0, 140)}`;
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
put: (bytes) => diagnose("put", () => put(bytes)),
|
|
144
|
+
read: (handle) => diagnose("read", () => read(handle)),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
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";
|
|
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}} 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 create({ repositoryId, categoryId, title, body }) {
|
|
77
|
+
const result = await request(
|
|
78
|
+
`mutation($input:CreateDiscussionInput!){createDiscussion(input:$input){discussion{${fields}}}}`,
|
|
79
|
+
{ input: { repositoryId, categoryId, title, body } },
|
|
80
|
+
);
|
|
81
|
+
return result.createDiscussion.discussion;
|
|
82
|
+
}
|
|
83
|
+
async function append(id, body) {
|
|
84
|
+
const result = await request(
|
|
85
|
+
`mutation($input:AddDiscussionCommentInput!){addDiscussionComment(input:$input){comment{${commentFields}}}}`,
|
|
86
|
+
{ input: { discussionId: id, body } },
|
|
87
|
+
);
|
|
88
|
+
return result.addDiscussionComment.comment;
|
|
89
|
+
}
|
|
90
|
+
return { repository, list, get, comments, create, append };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function collectDiscussionPages(readPage, limit = 100) {
|
|
94
|
+
let cursor = null;
|
|
95
|
+
const nodes = [];
|
|
96
|
+
const cursors = new Set();
|
|
97
|
+
for (let page = 0; page < limit; page++) {
|
|
98
|
+
const result = await readPage(cursor);
|
|
99
|
+
if (!Array.isArray(result?.nodes) || !result.pageInfo)
|
|
100
|
+
throw new Error("Incomplete Discussion page");
|
|
101
|
+
nodes.push(...result.nodes);
|
|
102
|
+
if (Buffer.byteLength(JSON.stringify(nodes)) > 32 * 1024 * 1024)
|
|
103
|
+
throw new Error(
|
|
104
|
+
"Discussion read exceeds its byte bound; refusing an incomplete view",
|
|
105
|
+
);
|
|
106
|
+
if (!result.pageInfo.hasNextPage) return nodes;
|
|
107
|
+
cursor = result.pageInfo.endCursor;
|
|
108
|
+
if (!cursor || cursors.has(cursor))
|
|
109
|
+
throw new Error("Discussion pagination did not advance");
|
|
110
|
+
cursors.add(cursor);
|
|
111
|
+
}
|
|
112
|
+
throw new Error(
|
|
113
|
+
"Discussion page limit exceeded; refusing an incomplete transaction view",
|
|
114
|
+
);
|
|
115
|
+
}
|
|
@@ -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
|
|
19
|
+
const result = await observeBinaryDistribution(
|
|
17
20
|
{
|
|
18
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
),
|
|
25
|
+
runtime: selectedRecordRuntime(env),
|
|
26
|
+
writer: `${env.GITHUB_RUN_ID}:${env.GITHUB_RUN_ATTEMPT}:${env.GITHUB_JOB}`,
|
|
25
27
|
},
|
|
26
|
-
|
|
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,92 @@
|
|
|
1
|
+
import { discussionTransport } from "../../providers/github/discussions/transport.js";
|
|
2
|
+
import { releaseDiscussionStore } from "./store.js";
|
|
3
|
+
import { createProgress, decodeRecord } from "./envelope.js";
|
|
4
|
+
|
|
5
|
+
// An independent distribution workflow joins the existing intent after resolving
|
|
6
|
+
// its public release locator. Capturing the attempt before execution keeps a late
|
|
7
|
+
// result on its original attempt when another workflow starts recovery.
|
|
8
|
+
export async function observeBinaryDistribution(
|
|
9
|
+
{ client, octokit, repository, tag, runtime, writer },
|
|
10
|
+
effect,
|
|
11
|
+
) {
|
|
12
|
+
const release = await client.release(tag);
|
|
13
|
+
const assets = release.assets.filter(
|
|
14
|
+
({ name }) => name === "buildchain.release-transaction.json",
|
|
15
|
+
);
|
|
16
|
+
if (assets.length !== 1)
|
|
17
|
+
throw new Error(
|
|
18
|
+
"Binary distribution requires one release transaction locator",
|
|
19
|
+
);
|
|
20
|
+
const locator = JSON.parse(client.assetBytes(assets[0]));
|
|
21
|
+
if (
|
|
22
|
+
locator.schema !== "buildchain.release-locator/v1" ||
|
|
23
|
+
locator.repository !== repository ||
|
|
24
|
+
`v${locator.version}` !== tag
|
|
25
|
+
)
|
|
26
|
+
throw new Error("Binary transaction locator does not match its release");
|
|
27
|
+
const transport = discussionTransport(octokit.graphql);
|
|
28
|
+
const discussion = await transport.get(locator.discussionId);
|
|
29
|
+
const intent = decodeRecord(discussion.body);
|
|
30
|
+
if (
|
|
31
|
+
intent?.id !== locator.intent ||
|
|
32
|
+
intent.repository !== repository ||
|
|
33
|
+
discussion.author?.__typename !== "Bot" ||
|
|
34
|
+
discussion.author.login !== "github-actions"
|
|
35
|
+
)
|
|
36
|
+
throw new Error(
|
|
37
|
+
"Binary transaction intent does not match the consumer workflow",
|
|
38
|
+
);
|
|
39
|
+
const store = releaseDiscussionStore(transport);
|
|
40
|
+
const session = { discussion, intent, writerId: discussion.author.id };
|
|
41
|
+
const state = await store.read(session);
|
|
42
|
+
if (!state.attempt || !intent.expectedNodes.includes("binary-distribution"))
|
|
43
|
+
throw new Error(
|
|
44
|
+
"Binary distribution was not declared by this release intent",
|
|
45
|
+
);
|
|
46
|
+
const attemptRecord = state.records.find(
|
|
47
|
+
(record) => record.attempt === state.attempt,
|
|
48
|
+
);
|
|
49
|
+
const sequence =
|
|
50
|
+
Math.max(
|
|
51
|
+
-1,
|
|
52
|
+
...state.records
|
|
53
|
+
.filter(
|
|
54
|
+
(record) =>
|
|
55
|
+
record.kind === "progress" &&
|
|
56
|
+
record.attempt === state.attempt &&
|
|
57
|
+
record.node === "binary-distribution",
|
|
58
|
+
)
|
|
59
|
+
.map((record) => record.sequence),
|
|
60
|
+
) + 1;
|
|
61
|
+
const record = (status, offset, payload = {}) =>
|
|
62
|
+
store.append(
|
|
63
|
+
session,
|
|
64
|
+
createProgress({
|
|
65
|
+
intent,
|
|
66
|
+
runtime,
|
|
67
|
+
writer,
|
|
68
|
+
attempt: state.attempt,
|
|
69
|
+
predecessor: attemptRecord.predecessor,
|
|
70
|
+
node: "binary-distribution",
|
|
71
|
+
status,
|
|
72
|
+
sequence: sequence + offset,
|
|
73
|
+
payload,
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
await record("running", 0);
|
|
77
|
+
let result;
|
|
78
|
+
try {
|
|
79
|
+
result = await effect();
|
|
80
|
+
} catch (error) {
|
|
81
|
+
try {
|
|
82
|
+
await record("failure", 1, {
|
|
83
|
+
code: String(error.code || "execution-failed"),
|
|
84
|
+
});
|
|
85
|
+
} catch (recordError) {
|
|
86
|
+
error.discussionRecordingError = recordError.message;
|
|
87
|
+
}
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
await record("success", 1, { tag, assets: result.assets.length });
|
|
91
|
+
return result;
|
|
92
|
+
}
|