@kungfu-tech/buildchain 3.0.6 → 3.0.7-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.
- package/README.md +4 -2
- package/actions/promote-buildchain-ref/README.md +10 -0
- package/actions/release-tail/README.md +17 -0
- package/bin/buildchain.mjs +11 -0
- package/bin/internal/command-registry.mjs +2 -0
- package/contracts/auditable-demo-scenario-v1.schema.json +1 -1
- package/contracts/engineering-housekeeper-v1.schema.json +143 -0
- package/contracts/fixtures/engineering-housekeeper-v1/cases.json +68 -0
- package/contracts/fixtures/release-tail-capabilities-v1/kungfu-alpha.json +273 -0
- package/contracts/publication-rehearsal-capsule-v1.schema.json +173 -0
- package/contracts/release-tail-capabilities-v1.schema.json +199 -0
- package/contracts/release-tail-provider-bindings-v1.schema.json +56 -0
- package/dist/site/agent-index.json +3 -0
- package/dist/site/artifact-schemas.json +6 -0
- package/dist/site/buildchain-contract.json +42 -27
- package/dist/site/buildchain-site.json +384 -33
- package/dist/site/capability-registry.json +15 -12
- package/dist/site/cli-registry.json +102 -0
- package/dist/site/controller-registry.json +19 -3
- package/dist/site/kfd-claims.json +359 -17
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +53 -7
- package/dist/site/node-api-registry.json +5882 -3605
- package/dist/site/page-registry.json +348 -21
- package/dist/site/public-surface-audit.json +396 -16
- package/dist/site/publication-authority-registry.json +106 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +7 -0
- package/dist/site/schemas/publication-rehearsal-capsule-v1.schema.json +269 -0
- package/dist/site/schemas/release-tail-capabilities-v1.schema.json +353 -0
- package/dist/site/schemas/release-tail-provider-bindings-v1.schema.json +94 -0
- package/dist/site/site-manifest.json +35 -11
- package/dist/site/workflow-registry.json +204 -10
- package/docs/MAP.md +6 -2
- package/docs/auditable-demo.md +2 -2
- package/docs/cli-reference.md +136 -0
- package/docs/dev-delivery-warrant.md +49 -4
- package/docs/engineering-housekeeper.md +138 -0
- package/docs/lifecycle-protocol.md +4 -2
- package/docs/node-api-reference.md +495 -281
- package/docs/publication-rehearsal.md +94 -0
- package/docs/release-governance.md +17 -2
- package/docs/release-tail-contract.md +160 -0
- package/docs/release-tail-provider-plane.md +120 -0
- package/docs/reusable-build-surface.md +11 -0
- package/package.json +11 -3
- package/packages/core/artifact-signing.js +61 -0
- package/packages/core/buildchain-agent-manuals.js +3 -0
- package/packages/core/buildchain-config.js +66 -6
- package/packages/core/buildchain-kfd-claims.js +1 -1
- package/packages/core/buildchain-publication-authority.js +5 -0
- package/packages/core/controller-evidence.js +2 -1
- package/packages/core/dev-delivery-warrant-cancellation.js +1 -0
- package/packages/core/dev-delivery-warrant-shadow.js +502 -0
- package/packages/core/dev-delivery-warrant.js +15 -6
- package/packages/core/diagnostics.js +8 -3
- package/packages/core/engineering-housekeeper-github-client.js +222 -0
- package/packages/core/engineering-housekeeper-github.js +501 -0
- package/packages/core/engineering-housekeeper.js +259 -0
- package/packages/core/index.js +42 -0
- package/packages/core/kfd-gate.js +45 -15
- package/packages/core/paper-agent-entry.js +11 -5
- package/packages/core/paper-repository.js +1 -0
- package/packages/core/paper-scaffold-content.js +21 -0
- package/packages/core/paper.js +28 -2
- package/packages/core/publication-rehearsal-projection.js +173 -0
- package/packages/core/publication-rehearsal-runtime.js +921 -0
- package/packages/core/release-passport.js +130 -20
- package/packages/core/release-tail-compatibility.js +60 -0
- package/packages/core/release-tail-provider-adapters.js +461 -0
- package/packages/core/release-tail-provider-plane.js +1228 -0
- package/scripts/assemble-publication-artifact-admission.mjs +1 -1
- package/scripts/assemble-self-publication-admission.mjs +2 -2
- package/scripts/audit-publication-control-plane.mjs +1 -1
- package/scripts/auditable-demo-bundle-verification.mjs +2 -3
- package/scripts/auditable-demo-platform.mjs +2 -2
- package/scripts/auditable-demo-renditions.mjs +1 -1
- package/scripts/auditable-demo.mjs +2 -2
- package/scripts/build-contract-core.mjs +8 -3
- package/scripts/build-standalone-binary.mjs +14 -3
- package/scripts/buildchain-cli-help.mjs +13 -0
- package/scripts/check-core-mechanism-inventory.mjs +347 -0
- package/scripts/check-inventory.mjs +10 -8
- package/scripts/check-maintainability.mjs +9 -2
- package/scripts/check-release-tail-contract.mjs +435 -0
- package/scripts/dev-delivery-warrant.mjs +31 -4
- package/scripts/dev-pr-auto-merge.mjs +30 -4
- package/scripts/dev-pr-delivery-warrant.mjs +50 -0
- package/scripts/engineering-housekeeper-workflow.mjs +394 -0
- package/scripts/generate-channel-promotion-workflow.mjs +10 -8
- package/scripts/generate-site-bundle.mjs +47 -4
- package/scripts/init-repo.mjs +26 -2
- package/scripts/inspect-artifact-signing-requests.mjs +6 -0
- package/scripts/materialize-self-release-candidate-version.mjs +137 -0
- package/scripts/publication-commit-evidence.mjs +69 -23
- package/scripts/release-candidate-resolver.mjs +16 -10
- package/scripts/release-tail.mjs +159 -0
- package/scripts/resume-from-candidate-run.mjs +123 -9
- package/scripts/seal-artifact-signing-requests.mjs +6 -0
- package/scripts/site-capability-metadata.mjs +13 -0
- package/scripts/v4-architecture.mjs +600 -0
- package/scripts/web-surface-core.mjs +8 -2
- package/scripts/workflow-call-contract.mjs +184 -5
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { releaseTailRoot } from "./release-tail-provider-plane.js";
|
|
6
|
+
|
|
7
|
+
const ROOT = /^sha256:[0-9a-f]{64}$/u;
|
|
8
|
+
|
|
9
|
+
export class ReleaseTailProviderError extends Error {
|
|
10
|
+
constructor(
|
|
11
|
+
message,
|
|
12
|
+
{ code = "provider-error", classification = "transient" } = {},
|
|
13
|
+
) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "ReleaseTailProviderError";
|
|
16
|
+
this.releaseTailCode = code;
|
|
17
|
+
this.releaseTailClass = classification;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function providerError(message, options) {
|
|
22
|
+
throw new ReleaseTailProviderError(message, options);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function assertEffect(effect, adapter, destinationKinds) {
|
|
26
|
+
if (!effect || effect.adapter !== adapter) {
|
|
27
|
+
providerError(`adapter ${adapter} received an incompatible effect`, {
|
|
28
|
+
code: "adapter-effect-mismatch",
|
|
29
|
+
classification: "conflict",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
if (!destinationKinds.includes(effect.destination?.kind)) {
|
|
33
|
+
providerError(`adapter ${adapter} does not support destination kind`, {
|
|
34
|
+
code: "adapter-destination-mismatch",
|
|
35
|
+
classification: "conflict",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function exactRoot(value, label) {
|
|
41
|
+
const normalized = String(value || "").toLowerCase();
|
|
42
|
+
if (!ROOT.test(normalized)) {
|
|
43
|
+
providerError(`${label} must be a sha256 content root`, {
|
|
44
|
+
code: "provider-root-invalid",
|
|
45
|
+
classification: "conflict",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return normalized;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function fileRoot(filePath) {
|
|
52
|
+
return `sha256:${crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex")}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function safeProviderCode(value, fallback) {
|
|
56
|
+
const normalized = String(value || fallback);
|
|
57
|
+
return /^[a-z0-9][a-z0-9._-]{0,159}$/u.test(normalized)
|
|
58
|
+
? normalized
|
|
59
|
+
: fallback;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function classifyProviderError(error, fallbackCode) {
|
|
63
|
+
if (error instanceof ReleaseTailProviderError) throw error;
|
|
64
|
+
const status = Number(error?.status || error?.response?.status || 0);
|
|
65
|
+
if (status === 409 || status === 412 || status === 422) {
|
|
66
|
+
return new ReleaseTailProviderError(
|
|
67
|
+
"provider rejected an immutable or conditional mutation",
|
|
68
|
+
{
|
|
69
|
+
code: "provider-policy-conflict",
|
|
70
|
+
classification: "conflict",
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return new ReleaseTailProviderError("provider operation failed transiently", {
|
|
75
|
+
code: safeProviderCode(error?.releaseTailCode, fallbackCode),
|
|
76
|
+
classification: "transient",
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function rootedDocument(value) {
|
|
81
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
|
|
82
|
+
return {
|
|
83
|
+
value,
|
|
84
|
+
root: `sha256:${crypto.createHash("sha256").update(value).digest("hex")}`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return { value, root: releaseTailRoot(value) };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function createRootedDocumentAdapter({
|
|
91
|
+
id,
|
|
92
|
+
destinationKinds,
|
|
93
|
+
readDocument,
|
|
94
|
+
resolveDocument,
|
|
95
|
+
commitDocument,
|
|
96
|
+
}) {
|
|
97
|
+
if (
|
|
98
|
+
typeof readDocument !== "function" ||
|
|
99
|
+
typeof resolveDocument !== "function" ||
|
|
100
|
+
typeof commitDocument !== "function"
|
|
101
|
+
) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`${id} requires readDocument, resolveDocument, and commitDocument`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return Object.freeze({
|
|
107
|
+
id,
|
|
108
|
+
async readback(effect) {
|
|
109
|
+
assertEffect(effect, id, destinationKinds);
|
|
110
|
+
let observed;
|
|
111
|
+
try {
|
|
112
|
+
observed = await readDocument(
|
|
113
|
+
effect.destination.locator,
|
|
114
|
+
structuredClone(effect),
|
|
115
|
+
);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
throw classifyProviderError(error, "provider-readback-transient");
|
|
118
|
+
}
|
|
119
|
+
if (observed === undefined || observed === null) {
|
|
120
|
+
return { outcome: "absent", providerCode: "provider-object-absent" };
|
|
121
|
+
}
|
|
122
|
+
const document = rootedDocument(observed);
|
|
123
|
+
return {
|
|
124
|
+
outcome: "observed",
|
|
125
|
+
subjectRoot: effect.subjectRoot,
|
|
126
|
+
targetRoot: document.root,
|
|
127
|
+
evidenceRoots: [document.root],
|
|
128
|
+
providerCode: "provider-object-observed",
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
async apply(effect) {
|
|
132
|
+
assertEffect(effect, id, destinationKinds);
|
|
133
|
+
const resolved = rootedDocument(
|
|
134
|
+
await resolveDocument(structuredClone(effect)),
|
|
135
|
+
);
|
|
136
|
+
if (resolved.root !== effect.targetRoot) {
|
|
137
|
+
providerError(
|
|
138
|
+
"resolved provider document does not match the sealed target root",
|
|
139
|
+
{
|
|
140
|
+
code: "sealed-target-root-mismatch",
|
|
141
|
+
classification: "conflict",
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
await commitDocument({
|
|
147
|
+
locator: effect.destination.locator,
|
|
148
|
+
document: resolved.value,
|
|
149
|
+
operationId: effect.operationId,
|
|
150
|
+
subjectRoot: effect.subjectRoot,
|
|
151
|
+
targetRoot: effect.targetRoot,
|
|
152
|
+
authorityMove: effect.channelPolicy.authorityMove,
|
|
153
|
+
});
|
|
154
|
+
} catch (error) {
|
|
155
|
+
throw classifyProviderError(error, "provider-mutation-transient");
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function createSignedStaticChannelAdapter(options = {}) {
|
|
162
|
+
return createRootedDocumentAdapter({
|
|
163
|
+
id: "signed-static-channel",
|
|
164
|
+
destinationKinds: ["well-known-signed-channel"],
|
|
165
|
+
readDocument: options.readDocument,
|
|
166
|
+
resolveDocument: options.resolveDocument,
|
|
167
|
+
commitDocument: options.commitDocument,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function createSiteReleaseActivationAdapter(options = {}) {
|
|
172
|
+
return createRootedDocumentAdapter({
|
|
173
|
+
id: "site-release-activation",
|
|
174
|
+
destinationKinds: ["production-site", "http-json"],
|
|
175
|
+
readDocument: options.readDocument,
|
|
176
|
+
resolveDocument: options.resolveDocument,
|
|
177
|
+
commitDocument: options.activate,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function createActivationReceiptProjectorAdapter(options = {}) {
|
|
182
|
+
return createRootedDocumentAdapter({
|
|
183
|
+
id: "activation-receipt-projector",
|
|
184
|
+
destinationKinds: ["release-passport-attachment"],
|
|
185
|
+
readDocument: options.readEvidence,
|
|
186
|
+
resolveDocument: options.synthesizeEvidence,
|
|
187
|
+
commitDocument: options.writeEvidence,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function createHttpJsonReadback({ fetchImpl = globalThis.fetch } = {}) {
|
|
192
|
+
if (typeof fetchImpl !== "function")
|
|
193
|
+
throw new Error("HTTP JSON readback requires fetch");
|
|
194
|
+
return async function readHttpJson(locator) {
|
|
195
|
+
let response;
|
|
196
|
+
try {
|
|
197
|
+
response = await fetchImpl(locator, {
|
|
198
|
+
method: "GET",
|
|
199
|
+
headers: { accept: "application/json" },
|
|
200
|
+
});
|
|
201
|
+
} catch (error) {
|
|
202
|
+
throw classifyProviderError(error, "http-network-transient");
|
|
203
|
+
}
|
|
204
|
+
if (response.status === 404) return null;
|
|
205
|
+
if (
|
|
206
|
+
response.status === 401 ||
|
|
207
|
+
response.status === 403 ||
|
|
208
|
+
response.status >= 500
|
|
209
|
+
) {
|
|
210
|
+
throw new ReleaseTailProviderError(
|
|
211
|
+
"HTTP JSON readback is temporarily unavailable",
|
|
212
|
+
{
|
|
213
|
+
code:
|
|
214
|
+
response.status === 401 || response.status === 403
|
|
215
|
+
? "http-credential-transient"
|
|
216
|
+
: "http-provider-transient",
|
|
217
|
+
classification: "transient",
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
if (!response.ok) {
|
|
222
|
+
throw new ReleaseTailProviderError(
|
|
223
|
+
"HTTP JSON readback returned a terminal policy response",
|
|
224
|
+
{
|
|
225
|
+
code: "http-policy-conflict",
|
|
226
|
+
classification: "conflict",
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
return await response.json();
|
|
232
|
+
} catch {
|
|
233
|
+
throw new ReleaseTailProviderError(
|
|
234
|
+
"HTTP JSON readback was not valid JSON",
|
|
235
|
+
{
|
|
236
|
+
code: "http-json-invalid",
|
|
237
|
+
classification: "conflict",
|
|
238
|
+
},
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function parseGitHubReleaseLocator(locator) {
|
|
245
|
+
const match = String(locator || "").match(
|
|
246
|
+
/^github-release:([^/\s]+)\/([^@\s]+)@([^\s]+)$/u,
|
|
247
|
+
);
|
|
248
|
+
if (!match) {
|
|
249
|
+
providerError(
|
|
250
|
+
"GitHub Release locator must be github-release:owner/repo@tag",
|
|
251
|
+
{
|
|
252
|
+
code: "github-release-locator-invalid",
|
|
253
|
+
classification: "conflict",
|
|
254
|
+
},
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
return { owner: match[1], repo: match[2], tag: match[3] };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function normalizedArtifactBindings(effect, resolveArtifact) {
|
|
261
|
+
return effect.artifactRoles
|
|
262
|
+
.map((artifact) => {
|
|
263
|
+
const resolved = resolveArtifact(artifact.role, structuredClone(effect));
|
|
264
|
+
if (!resolved || typeof resolved !== "object") {
|
|
265
|
+
providerError(`artifact role ${artifact.role} did not resolve`, {
|
|
266
|
+
code: "artifact-binding-missing",
|
|
267
|
+
classification: "conflict",
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
const filePath = path.resolve(String(resolved.path || ""));
|
|
271
|
+
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
|
272
|
+
providerError(`artifact role ${artifact.role} is not a file`, {
|
|
273
|
+
code: "artifact-file-missing",
|
|
274
|
+
classification: "conflict",
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
const root = fileRoot(filePath);
|
|
278
|
+
if (
|
|
279
|
+
root !== artifact.root ||
|
|
280
|
+
(resolved.root && exactRoot(resolved.root, "artifact root") !== root)
|
|
281
|
+
) {
|
|
282
|
+
providerError(`artifact role ${artifact.role} changed after sealing`, {
|
|
283
|
+
code: "artifact-root-mismatch",
|
|
284
|
+
classification: "conflict",
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
role: artifact.role,
|
|
289
|
+
root,
|
|
290
|
+
name: String(resolved.name || path.basename(filePath)),
|
|
291
|
+
path: filePath,
|
|
292
|
+
};
|
|
293
|
+
})
|
|
294
|
+
.sort((left, right) => left.role.localeCompare(right.role));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function githubReleaseAssetsTargetRoot({ destination, artifacts }) {
|
|
298
|
+
return releaseTailRoot({
|
|
299
|
+
destination,
|
|
300
|
+
artifacts: artifacts
|
|
301
|
+
.map(({ role, root, name }) => ({
|
|
302
|
+
role,
|
|
303
|
+
root: exactRoot(root, "artifact root"),
|
|
304
|
+
name,
|
|
305
|
+
}))
|
|
306
|
+
.sort((left, right) => left.role.localeCompare(right.role)),
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function createGitHubReleaseAssetsAdapter({
|
|
311
|
+
octokit,
|
|
312
|
+
resolveArtifact,
|
|
313
|
+
} = {}) {
|
|
314
|
+
if (!octokit?.rest?.repos || typeof resolveArtifact !== "function") {
|
|
315
|
+
throw new Error(
|
|
316
|
+
"github-release-assets requires octokit and resolveArtifact",
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
async function releaseAndAssets(effect) {
|
|
320
|
+
const coordinate = parseGitHubReleaseLocator(effect.destination.locator);
|
|
321
|
+
let release;
|
|
322
|
+
try {
|
|
323
|
+
release = (
|
|
324
|
+
await octokit.rest.repos.getReleaseByTag({
|
|
325
|
+
owner: coordinate.owner,
|
|
326
|
+
repo: coordinate.repo,
|
|
327
|
+
tag: coordinate.tag,
|
|
328
|
+
})
|
|
329
|
+
).data;
|
|
330
|
+
} catch (error) {
|
|
331
|
+
if (Number(error?.status || error?.response?.status) === 404) {
|
|
332
|
+
return { coordinate, release: null, assets: [] };
|
|
333
|
+
}
|
|
334
|
+
throw classifyProviderError(error, "github-release-readback-transient");
|
|
335
|
+
}
|
|
336
|
+
let assets;
|
|
337
|
+
try {
|
|
338
|
+
assets =
|
|
339
|
+
(
|
|
340
|
+
await octokit.rest.repos.listReleaseAssets({
|
|
341
|
+
owner: coordinate.owner,
|
|
342
|
+
repo: coordinate.repo,
|
|
343
|
+
release_id: release.id,
|
|
344
|
+
per_page: 100,
|
|
345
|
+
})
|
|
346
|
+
).data || [];
|
|
347
|
+
} catch (error) {
|
|
348
|
+
throw classifyProviderError(error, "github-assets-readback-transient");
|
|
349
|
+
}
|
|
350
|
+
return { coordinate, release, assets };
|
|
351
|
+
}
|
|
352
|
+
return Object.freeze({
|
|
353
|
+
id: "github-release-assets",
|
|
354
|
+
async readback(effect) {
|
|
355
|
+
assertEffect(effect, "github-release-assets", ["github-release"]);
|
|
356
|
+
const expected = normalizedArtifactBindings(effect, resolveArtifact);
|
|
357
|
+
const observed = await releaseAndAssets(effect);
|
|
358
|
+
if (!observed.release) {
|
|
359
|
+
return { outcome: "absent", providerCode: "github-release-absent" };
|
|
360
|
+
}
|
|
361
|
+
const byName = new Map(
|
|
362
|
+
observed.assets.map((asset) => [asset.name, asset]),
|
|
363
|
+
);
|
|
364
|
+
if (expected.some((artifact) => !byName.has(artifact.name))) {
|
|
365
|
+
return {
|
|
366
|
+
outcome: "absent",
|
|
367
|
+
providerCode: "github-release-assets-incomplete",
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
const artifacts = expected.map((artifact) => {
|
|
371
|
+
const asset = byName.get(artifact.name);
|
|
372
|
+
return {
|
|
373
|
+
role: artifact.role,
|
|
374
|
+
name: artifact.name,
|
|
375
|
+
root: String(asset.digest || "").toLowerCase(),
|
|
376
|
+
};
|
|
377
|
+
});
|
|
378
|
+
if (artifacts.some((artifact) => !ROOT.test(artifact.root))) {
|
|
379
|
+
return {
|
|
380
|
+
outcome: "transient",
|
|
381
|
+
providerCode: "github-asset-digest-unavailable",
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
outcome: "observed",
|
|
386
|
+
subjectRoot: effect.subjectRoot,
|
|
387
|
+
targetRoot: githubReleaseAssetsTargetRoot({
|
|
388
|
+
destination: effect.destination,
|
|
389
|
+
artifacts,
|
|
390
|
+
}),
|
|
391
|
+
evidenceRoots: artifacts.map((artifact) => artifact.root),
|
|
392
|
+
providerCode: "github-release-assets-observed",
|
|
393
|
+
};
|
|
394
|
+
},
|
|
395
|
+
async apply(effect) {
|
|
396
|
+
assertEffect(effect, "github-release-assets", ["github-release"]);
|
|
397
|
+
const expected = normalizedArtifactBindings(effect, resolveArtifact);
|
|
398
|
+
const expectedTarget = githubReleaseAssetsTargetRoot({
|
|
399
|
+
destination: effect.destination,
|
|
400
|
+
artifacts: expected,
|
|
401
|
+
});
|
|
402
|
+
if (expectedTarget !== effect.targetRoot) {
|
|
403
|
+
providerError(
|
|
404
|
+
"GitHub Release artifact bindings do not match the sealed target root",
|
|
405
|
+
{
|
|
406
|
+
code: "sealed-target-root-mismatch",
|
|
407
|
+
classification: "conflict",
|
|
408
|
+
},
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
const observed = await releaseAndAssets(effect);
|
|
412
|
+
let release = observed.release;
|
|
413
|
+
if (!release) {
|
|
414
|
+
try {
|
|
415
|
+
release = (
|
|
416
|
+
await octokit.rest.repos.createRelease({
|
|
417
|
+
owner: observed.coordinate.owner,
|
|
418
|
+
repo: observed.coordinate.repo,
|
|
419
|
+
tag_name: observed.coordinate.tag,
|
|
420
|
+
target_commitish: effect.subject.sourceSha,
|
|
421
|
+
name: observed.coordinate.tag,
|
|
422
|
+
draft: false,
|
|
423
|
+
prerelease: effect.subject.channel === "alpha",
|
|
424
|
+
})
|
|
425
|
+
).data;
|
|
426
|
+
} catch (error) {
|
|
427
|
+
throw classifyProviderError(error, "github-release-create-transient");
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const byName = new Map(
|
|
431
|
+
observed.assets.map((asset) => [asset.name, asset]),
|
|
432
|
+
);
|
|
433
|
+
for (const artifact of expected) {
|
|
434
|
+
const existing = byName.get(artifact.name);
|
|
435
|
+
if (existing) {
|
|
436
|
+
if (String(existing.digest || "").toLowerCase() !== artifact.root) {
|
|
437
|
+
providerError(
|
|
438
|
+
`immutable GitHub Release asset collision: ${artifact.name}`,
|
|
439
|
+
{
|
|
440
|
+
code: "github-asset-collision",
|
|
441
|
+
classification: "conflict",
|
|
442
|
+
},
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
await octokit.rest.repos.uploadReleaseAsset({
|
|
449
|
+
owner: observed.coordinate.owner,
|
|
450
|
+
repo: observed.coordinate.repo,
|
|
451
|
+
release_id: release.id,
|
|
452
|
+
name: artifact.name,
|
|
453
|
+
data: fs.readFileSync(artifact.path),
|
|
454
|
+
});
|
|
455
|
+
} catch (error) {
|
|
456
|
+
throw classifyProviderError(error, "github-asset-upload-transient");
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
});
|
|
461
|
+
}
|