@kungfu-tech/buildchain 2.4.10-alpha.0 → 2.4.10-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -70,11 +70,16 @@ and rebuilds every action bundle.
70
70
  - Bugs, feature requests, questions, and documentation issues go through GitHub
71
71
  issues; security vulnerabilities use private vulnerability reporting - see
72
72
  [`SECURITY.md`](SECURITY.md).
73
+ - Brand, hosted-service, and upstream-provider boundaries are documented in
74
+ [`TRADEMARK.md`](TRADEMARK.md), [`ACCEPTABLE_USE.md`](ACCEPTABLE_USE.md), and
75
+ [`PROVIDER_COMPLIANCE.md`](PROVIDER_COMPLIANCE.md).
73
76
 
74
77
  ## Ground rules
75
78
 
76
79
  - Never include secrets, credentials, tokens, or private logs in code, commits,
77
80
  issues, or pull requests.
81
+ - Do not build or document official release integrations that bypass provider
82
+ protections, hide credential boundaries, or forge release evidence.
78
83
  - Keep generated action bundles in sync with source changes.
79
84
  - Keep documentation in sync with behavior, especially release governance and
80
85
  reusable workflow contracts.
package/LICENSE-POLICY.md CHANGED
@@ -38,12 +38,28 @@ Apache-2.0 grants copyright and patent permissions. It does not grant trademark
38
38
  rights. Names, logos, domain names, and product marks such as "Kungfu" and
39
39
  "Buildchain" may be governed by separate brand guidelines.
40
40
 
41
+ See [TRADEMARK.md](TRADEMARK.md) for the official project mark and fork identity
42
+ boundary.
43
+
41
44
  ## Hosted and commercial services
42
45
 
43
46
  The open source license covers this repository. Hosted services, team features,
44
47
  enterprise support, managed deployments, commercial connectors, or other
45
48
  services offered by the project maintainers may use separate terms.
46
49
 
50
+ See [ACCEPTABLE_USE.md](ACCEPTABLE_USE.md) for acceptable use of official hosted,
51
+ managed, or maintainer-operated services.
52
+
53
+ ## Upstream provider integrations
54
+
55
+ Official Buildchain integrations should use documented provider APIs, workflow
56
+ surfaces, package-registry flows, cloud APIs, OIDC, and least-privilege
57
+ credentials. They should not bypass provider protections, hide credential
58
+ boundaries, or forge release evidence.
59
+
60
+ See [PROVIDER_COMPLIANCE.md](PROVIDER_COMPLIANCE.md) for the official provider
61
+ integration posture.
62
+
47
63
  ## Third-party software
48
64
 
49
65
  Buildchain depends on third-party software. Source dependencies are declared in
package/README.md CHANGED
@@ -85,6 +85,19 @@ const report = verifyBuildchainLogEvents({
85
85
  The package also ships `dist/site/` as the Buildchain-owned fact source for
86
86
  `buildchain.libkungfu.dev`.
87
87
 
88
+ ## Project Governance
89
+
90
+ - [`LICENSE-POLICY.md`](LICENSE-POLICY.md) explains the Apache-2.0 project
91
+ license, DCO-based contributions, and third-party notice boundary.
92
+ - [`TRADEMARK.md`](TRADEMARK.md) explains official project marks and fork
93
+ identity boundaries.
94
+ - [`ACCEPTABLE_USE.md`](ACCEPTABLE_USE.md) explains acceptable use of official
95
+ services and maintainer-operated infrastructure.
96
+ - [`PROVIDER_COMPLIANCE.md`](PROVIDER_COMPLIANCE.md) explains the official
97
+ posture for GitHub, npm, cloud, credential, release evidence, and other
98
+ provider integrations.
99
+ - [`SECURITY.md`](SECURITY.md) explains private vulnerability reporting.
100
+
88
101
  Native build consumers can import the diagnostics toolkit instead of copying
89
102
  repository-local probes:
90
103
 
package/SECURITY.md CHANGED
@@ -35,6 +35,13 @@ Security reports may cover:
35
35
  - local file access, path traversal, or unsafe archive handling;
36
36
  - credential, token, or private data exposure.
37
37
 
38
+ Service-abuse, provider-compliance, credential-handling, misleading official
39
+ identity, or compromised release-evidence reports may also be
40
+ security-sensitive. Use private vulnerability reporting when public disclosure
41
+ would expose credentials, provider account details, user data, or an exploitable
42
+ bypass. See `ACCEPTABLE_USE.md`, `PROVIDER_COMPLIANCE.md`, and `TRADEMARK.md`
43
+ for the related policy boundaries.
44
+
38
45
  ## Public disclosure
39
46
 
40
47
  Please allow maintainers time to investigate and prepare a fix before public
package/docs/MAP.md CHANGED
@@ -43,6 +43,7 @@ running artifact), *use* (consume / extend) - and a **status**:
43
43
  | How can a consumer workflow report a Buildchain-owned failure back to Buildchain? | [`consumer-issue-reporting.md`](consumer-issue-reporting.md) + [`../actions/report-buildchain-issue/README.md`](../actions/report-buildchain-issue/README.md) | use | stable |
44
44
  | What do the fixture repositories demonstrate? | [`../fixtures/libnode-shaped/README.md`](../fixtures/libnode-shaped/README.md), [`../fixtures/publish-transaction-shaped/README.md`](../fixtures/publish-transaction-shaped/README.md), [`../fixtures/web-surface-shaped/README.md`](../fixtures/web-surface-shaped/README.md) | verify | stable |
45
45
  | What license and contribution terms apply? | [`../LICENSE`](../LICENSE) + [`../LICENSE-POLICY.md`](../LICENSE-POLICY.md) | use | stable |
46
+ | What trademark, official-service, and provider-compliance boundaries apply? | [`../TRADEMARK.md`](../TRADEMARK.md) + [`../ACCEPTABLE_USE.md`](../ACCEPTABLE_USE.md) + [`../PROVIDER_COMPLIANCE.md`](../PROVIDER_COMPLIANCE.md) | use | stable |
46
47
  | How do I report a vulnerability? | [`../SECURITY.md`](../SECURITY.md) | use | stable |
47
48
 
48
49
  ## Also asking about
@@ -88,6 +89,10 @@ running artifact), *use* (consume / extend) - and a **status**:
88
89
  [`site-bundle-contract.md`](site-bundle-contract.md).
89
90
  - **sites / web previews / staging / production gates** ->
90
91
  [`web-surface-deployments.md`](web-surface-deployments.md).
92
+ - **trademark / fork / official service / provider compliance / release
93
+ evidence boundary** -> [`../TRADEMARK.md`](../TRADEMARK.md),
94
+ [`../ACCEPTABLE_USE.md`](../ACCEPTABLE_USE.md), and
95
+ [`../PROVIDER_COMPLIANCE.md`](../PROVIDER_COMPLIANCE.md).
91
96
 
92
97
  ## How this map is maintained
93
98
 
package/docs/cli.md CHANGED
@@ -365,10 +365,14 @@ transaction that promotes release refs:
365
365
  workflow and do not publish.
366
366
 
367
367
  The promotion workflow uses npm Trusted Publishing through GitHub Actions OIDC.
368
- It runs on a GitHub-hosted runner with `id-token: write`, generates the
369
- version-state commit, runs `lifecycle.verify`, runs `lifecycle.publish`, writes
370
- Buildchain publish evidence, validates that evidence, and only then moves exact
371
- tags and floating refs.
368
+ It runs on a GitHub-hosted runner with `id-token: write`, but it does not
369
+ manually run the release-candidate resolver or promote action. Buildchain's own
370
+ dogfood path calls the declarative `release-candidate-promote.yml` wrapper with
371
+ channel, target ref/SHA, PR-stage workflow, artifact, status-check, and passport
372
+ inputs. The wrapper generates the version-state commit, runs
373
+ `lifecycle.verify`, runs `lifecycle.publish`, writes Buildchain publish
374
+ evidence, validates that evidence, and only then moves exact tags and floating
375
+ refs.
372
376
 
373
377
  ```bash
374
378
  node scripts/npm-publish-transaction.mjs
@@ -64,4 +64,15 @@ Buildchain-owned promotion workflow resolves the matching same-repository
64
64
  merged channel PR and downloads its PR-stage RC passport automatically before
65
65
  promotion starts. The consumer wrapper defaults to a PR-stage workflow file
66
66
  named `build.yml` with display name `Build`, and filters the RC passport and
67
- build summary by the configured `artifact-name` before promotion.
67
+ build summary by the configured `artifact-name` before promotion. It also
68
+ downloads payload artifacts from the same PR-stage run, validates the required
69
+ payload count, passes downloaded platform manifests into the release passport,
70
+ and either forwards an explicit `publish-required-artifacts-json` value or
71
+ generates one before calling `promote-buildchain-ref`. The default npm path
72
+ generates that requirement list from the downloaded `.tgz` payloads themselves:
73
+ Buildchain reads `package/package.json` inside each tarball for the real scoped
74
+ package name and version, computes npm-style `sha512-...` integrity over the
75
+ tarball bytes, marks `publish-package-main` as `role: main`, and marks every
76
+ other package as `role: platform`. Consumer workflows therefore stay
77
+ declarative and do not need their own artifact download or publish-evidence
78
+ generation scripts.
@@ -59,7 +59,9 @@ Buildchain implements the same governance loop with:
59
59
 
60
60
  - `.github/workflows/release-verify.yml` for PR verification;
61
61
  - `.github/workflows/buildchain-ref-promotion.yml` for post-verify ref
62
- promotion;
62
+ promotion; this workflow dogfoods the declarative
63
+ `release-candidate-promote.yml` wrapper and does not hand-wire resolver,
64
+ artifact download, publish-gate, or promote action steps;
63
65
  - `actions/promote-buildchain-ref` for branch, tag, version-state, and
64
66
  governance checks;
65
67
  - package-manager adapters that can update version state for pnpm, npm, and
@@ -305,9 +307,10 @@ When debugging or extending release behavior, read in this order:
305
307
  1. `docs/release-flow.md`
306
308
  2. `.github/workflows/release-verify.yml`
307
309
  3. `.github/workflows/buildchain-ref-promotion.yml`
308
- 4. `actions/promote-buildchain-ref/README.md`
309
- 5. `actions/promote-buildchain-ref/src/`
310
- 6. `docs/migration-inventory.md`
310
+ 4. `.github/workflows/release-candidate-promote.yml`
311
+ 5. `actions/promote-buildchain-ref/README.md`
312
+ 6. `actions/promote-buildchain-ref/src/`
313
+ 7. `docs/migration-inventory.md`
311
314
 
312
315
  That path gives the policy first, the workflow trigger second, and the action
313
316
  implementation last.
@@ -377,12 +377,14 @@ The passport records two separate source identities:
377
377
  commit used for publish authority.
378
378
 
379
379
  The reusable promote wrapper resolves the merged PR, finds exactly one matching
380
- PR-stage release-candidate artifact, downloads it, compares the built tree with
381
- the promotion channel tree, locks `publish-gate/{alpha,release,major}` to the
382
- promotion channel commit, and then calls `actions/promote-buildchain-ref` with
380
+ PR-stage release-candidate artifact, downloads it with the build summary and
381
+ payload artifacts from the same PR-stage run, validates the payload count,
382
+ compares the built tree with the promotion channel tree, locks
383
+ `publish-gate/{alpha,release,major}` to the promotion channel commit, and then
384
+ calls `actions/promote-buildchain-ref` with
383
385
  `promote-only-release-candidate: "true"`. It does not call `.build.yml`, does
384
- not create a matrix, and must fail before publish if the RC evidence is missing
385
- or ambiguous.
386
+ not create a matrix, and must fail before publish if the RC evidence or payload
387
+ set is missing or ambiguous.
386
388
 
387
389
  ```yaml
388
390
  jobs:
@@ -400,8 +402,26 @@ jobs:
400
402
  publish-target: npm
401
403
  runner-preset: github-hosted
402
404
  trusted-publishing: true
405
+ required-status-check: check
406
+ required-artifact-count: 3
407
+ publish-dist-tag: alpha
408
+ publish-package-set-order: platforms-first-main-last
409
+ publish-package-main: "@kungfu-tech/libnode"
410
+ release-passport-product-name: Libnode
403
411
  ```
404
412
 
413
+ `publish-required-artifacts-json` can still be passed explicitly for custom
414
+ publish targets. For the default `publish-artifact-kind: npm` path, consumers do
415
+ not download artifacts or run repository scripts to build publish evidence. The
416
+ wrapper downloads the PR-stage payload artifacts, finds the downloaded `.tgz`
417
+ packages, reads each tarball's `package/package.json` for the real scoped
418
+ package name and version, computes the npm `sha512-...` integrity from the
419
+ tarball bytes, marks the package matching `publish-package-main` as `role:
420
+ main`, marks the rest as `role: platform`, and passes the generated
421
+ `publish-required-artifacts-json` to `promote-buildchain-ref` before any publish
422
+ side effect. Downloaded platform manifests are still passed into the release
423
+ passport unless `release-passport-platform-manifest-paths` is set explicitly.
424
+
405
425
  Custom publish jobs can also repeat the channel-ref preflight:
406
426
 
407
427
  ```yaml
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "2.4.10-alpha.0",
3
+ "version": "2.4.10-alpha.2",
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",
@@ -200,13 +200,23 @@ for (const forbiddenSnippet of [
200
200
  }
201
201
  for (const requiredSnippet of [
202
202
  "id-token: write",
203
- "registry-url: \"https://registry.npmjs.org/\"",
204
- "publish-transaction: \"true\"",
203
+ "actions: read",
204
+ "uses: ./.github/workflows/release-candidate-promote.yml",
205
+ "target-sha: ${{ github.event.workflow_run.head_sha || inputs.sha || github.sha }}",
206
+ "publish-required-artifacts-json: \"[]\"",
205
207
  ]) {
206
208
  if (!buildchainRefPromotionWorkflow.includes(requiredSnippet)) {
207
209
  throw new Error(`buildchain ref promotion workflow missing npm transaction snippet: ${requiredSnippet}`);
208
210
  }
209
211
  }
212
+ for (const forbiddenSnippet of [
213
+ "run: node scripts/release-candidate-resolver.mjs",
214
+ "uses: ./actions/promote-buildchain-ref",
215
+ ]) {
216
+ if (buildchainRefPromotionWorkflow.includes(forbiddenSnippet)) {
217
+ throw new Error(`buildchain ref promotion workflow must use the declarative wrapper, found manual snippet: ${forbiddenSnippet}`);
218
+ }
219
+ }
210
220
  for (const requiredSnippet of [
211
221
  "distTag || (pkg.version.includes(\"-\") ? \"alpha\" : \"latest\")",
212
222
  "\"publish\", \"--dry-run\", \"--access\", \"public\"",
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import crypto from "node:crypto";
2
3
  import fs from "node:fs";
3
4
  import os from "node:os";
4
5
  import path from "node:path";
@@ -118,6 +119,173 @@ function outputPath(filePath) {
118
119
  return relative.startsWith("../") || relative === ".." ? filePath : relative;
119
120
  }
120
121
 
122
+ function splitPatterns(value = "") {
123
+ return String(value || "")
124
+ .split(/\r?\n|,/)
125
+ .map((entry) => entry.trim())
126
+ .filter(Boolean);
127
+ }
128
+
129
+ function escapeRegExp(value) {
130
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
131
+ }
132
+
133
+ function artifactPatternToRegExp(pattern) {
134
+ return new RegExp(`^${String(pattern).split("*").map(escapeRegExp).join(".*")}$`);
135
+ }
136
+
137
+ export function selectPayloadArtifacts({
138
+ artifacts = [],
139
+ artifactName = "",
140
+ sourceSha = "",
141
+ patterns = [],
142
+ } = {}) {
143
+ const prefix = String(artifactName || "").trim();
144
+ const sha = assertSha(sourceSha, "sourceSha");
145
+ const active = artifacts.filter((artifact) => !artifact.expired);
146
+ const excludedNames = new Set([
147
+ `${prefix}-release-candidate-${sha}`,
148
+ `${prefix}-summary-${sha}`,
149
+ `${prefix}-diagnostics-summary-${sha}`,
150
+ ]);
151
+ const effectivePatterns = splitPatterns(patterns).length
152
+ ? splitPatterns(patterns)
153
+ : [`${prefix}-manifest-*-${sha}`];
154
+ const matchers = effectivePatterns.map(artifactPatternToRegExp);
155
+ return active
156
+ .filter((artifact) => !excludedNames.has(String(artifact.name || "")))
157
+ .filter((artifact) => matchers.some((matcher) => matcher.test(String(artifact.name || ""))))
158
+ .sort((left, right) => String(left.name || "").localeCompare(String(right.name || "")));
159
+ }
160
+
161
+ function findDownloadedFiles(root, filename) {
162
+ const matches = [];
163
+ const stack = fs.existsSync(root) ? [root] : [];
164
+ while (stack.length) {
165
+ const current = stack.pop();
166
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
167
+ const fullPath = path.join(current, entry.name);
168
+ if (entry.isDirectory()) {
169
+ stack.push(fullPath);
170
+ } else if (entry.name === filename) {
171
+ matches.push(fullPath);
172
+ }
173
+ }
174
+ }
175
+ return matches.sort();
176
+ }
177
+
178
+ function findDownloadedFilesByExtension(root, extensions = []) {
179
+ const normalizedExtensions = extensions.map((extension) => String(extension || "").toLowerCase());
180
+ const matches = [];
181
+ const stack = fs.existsSync(root) ? [root] : [];
182
+ while (stack.length) {
183
+ const current = stack.pop();
184
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
185
+ const fullPath = path.join(current, entry.name);
186
+ if (entry.isDirectory()) {
187
+ stack.push(fullPath);
188
+ continue;
189
+ }
190
+ const lowerName = entry.name.toLowerCase();
191
+ if (normalizedExtensions.some((extension) => lowerName.endsWith(extension))) {
192
+ matches.push(fullPath);
193
+ }
194
+ }
195
+ }
196
+ return matches.sort();
197
+ }
198
+
199
+ function packageNameFromArtifactPath(filePath) {
200
+ const basename = path.basename(String(filePath || ""));
201
+ return basename
202
+ .replace(/\.tgz$/i, "")
203
+ .replace(/\.tar\.gz$/i, "")
204
+ .replace(/\.zip$/i, "");
205
+ }
206
+
207
+ function npmIntegrity(filePath) {
208
+ return `sha512-${crypto.createHash("sha512").update(fs.readFileSync(filePath)).digest("base64")}`;
209
+ }
210
+
211
+ function readNpmPackageJsonFromTarball(tarballPath) {
212
+ const candidates = ["package/package.json", "./package/package.json"];
213
+ const errors = [];
214
+ for (const candidate of candidates) {
215
+ try {
216
+ return JSON.parse(execFileSync("tar", ["-xOf", tarballPath, candidate], { encoding: "utf8" }));
217
+ } catch (error) {
218
+ errors.push(error.stderr?.toString?.().trim() || error.message);
219
+ }
220
+ }
221
+ throw new Error(`npm package tarball ${tarballPath} does not contain package/package.json: ${errors.filter(Boolean).join("; ")}`);
222
+ }
223
+
224
+ export function readNpmPackageArtifact({
225
+ tarballPath,
226
+ mainPackage = "",
227
+ kind = "npm",
228
+ } = {}) {
229
+ const packageJson = readNpmPackageJsonFromTarball(tarballPath);
230
+ const name = String(packageJson.name || "").trim();
231
+ const version = String(packageJson.version || "").trim();
232
+ if (!name || !version) {
233
+ throw new Error(`npm package tarball ${tarballPath || "<empty>"} package.json must include name and version`);
234
+ }
235
+ const integrity = npmIntegrity(tarballPath);
236
+ return {
237
+ kind,
238
+ name,
239
+ ref: version,
240
+ digest: integrity,
241
+ integrity,
242
+ role: mainPackage && name === mainPackage ? "main" : "platform",
243
+ };
244
+ }
245
+
246
+ export function generatePublishRequiredArtifacts({
247
+ manifests = [],
248
+ version = "",
249
+ kind = "npm",
250
+ tarballPaths = [],
251
+ mainPackage = "",
252
+ } = {}) {
253
+ if (String(kind || "") === "npm" && tarballPaths.length > 0) {
254
+ const artifacts = tarballPaths
255
+ .map((tarballPath) => readNpmPackageArtifact({ tarballPath, mainPackage, kind }))
256
+ .sort((left, right) => `${left.role}:${left.name}`.localeCompare(`${right.role}:${right.name}`));
257
+ const seen = new Set();
258
+ for (const artifact of artifacts) {
259
+ const key = `${artifact.name}@${artifact.ref}`;
260
+ if (seen.has(key)) {
261
+ throw new Error(`duplicate npm package tarball for ${key}`);
262
+ }
263
+ seen.add(key);
264
+ }
265
+ return artifacts;
266
+ }
267
+ const ref = String(version || "").trim();
268
+ if (!ref) {
269
+ return [];
270
+ }
271
+ return manifests.flatMap((manifest) => {
272
+ const platform = manifest.platform?.id || manifest.platformId || "";
273
+ const files = Array.isArray(manifest.files) ? manifest.files : [];
274
+ return files
275
+ .filter((file) => file?.sha256)
276
+ .map((file) => ({
277
+ kind,
278
+ name: packageNameFromArtifactPath(file.path || file.name || manifest.artifactName || platform),
279
+ ref,
280
+ digest: String(file.sha256).startsWith("sha256:")
281
+ ? String(file.sha256)
282
+ : `sha256:${file.sha256}`,
283
+ role: "platform",
284
+ platform,
285
+ }));
286
+ });
287
+ }
288
+
121
289
  export function selectReleaseCandidateArtifacts({ artifacts = [], artifactName = "" }) {
122
290
  const expectedPrefix = String(artifactName || "").trim();
123
291
  const active = artifacts.filter((artifact) => !artifact.expired);
@@ -170,6 +338,10 @@ export async function resolveReleaseCandidateArtifacts({
170
338
  workflowFile = DEFAULT_WORKFLOW_FILE,
171
339
  workflowName = "Build Surface Fixture",
172
340
  artifactName = "",
341
+ artifactPatterns = "",
342
+ requiredArtifactCount = 0,
343
+ publishArtifactKind = "npm",
344
+ publishPackageMain = "",
173
345
  outputDir = ".buildchain/release-candidate",
174
346
  fetchImpl = globalThis.fetch,
175
347
  download = true,
@@ -221,6 +393,16 @@ export async function resolveReleaseCandidateArtifacts({
221
393
  artifacts: Array.isArray(artifactResponse.artifacts) ? artifactResponse.artifacts : [],
222
394
  artifactName,
223
395
  });
396
+ const payloadArtifacts = selectPayloadArtifacts({
397
+ artifacts: Array.isArray(artifactResponse.artifacts) ? artifactResponse.artifacts : [],
398
+ artifactName: selected.prefix,
399
+ sourceSha: selected.sourceSha,
400
+ patterns: artifactPatterns,
401
+ });
402
+ const minimumPayloadCount = Number(requiredArtifactCount || 0);
403
+ if (minimumPayloadCount > 0 && payloadArtifacts.length < minimumPayloadCount) {
404
+ throw new Error(`expected at least ${minimumPayloadCount} PR-stage payload artifacts, found ${payloadArtifacts.length}`);
405
+ }
224
406
  const result = {
225
407
  enabled: true,
226
408
  repository: repoInfo.fullName,
@@ -240,6 +422,7 @@ export async function resolveReleaseCandidateArtifacts({
240
422
  artifacts: {
241
423
  passport: selected.passport.name,
242
424
  summary: selected.summary.name,
425
+ payloads: payloadArtifacts.map((artifact) => artifact.name),
243
426
  artifactName: selected.prefix,
244
427
  sourceSha: selected.sourceSha,
245
428
  },
@@ -252,6 +435,7 @@ export async function resolveReleaseCandidateArtifacts({
252
435
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "buildchain-rc-"));
253
436
  const passportDir = path.join(resolvedOutput, "passport");
254
437
  const summaryDir = path.join(resolvedOutput, "summary");
438
+ const payloadDir = path.join(resolvedOutput, "payloads");
255
439
  const passportZip = path.join(tempDir, "passport.zip");
256
440
  const summaryZip = path.join(tempDir, "summary.zip");
257
441
  await githubDownload({
@@ -268,6 +452,18 @@ export async function resolveReleaseCandidateArtifacts({
268
452
  outputPath: summaryZip,
269
453
  path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/artifacts/${selected.summary.id}/zip`,
270
454
  });
455
+ for (const artifact of payloadArtifacts) {
456
+ const safeName = String(artifact.name || `artifact-${artifact.id}`).replace(/[^A-Za-z0-9._-]/g, "_");
457
+ const payloadZip = path.join(tempDir, `${safeName}.zip`);
458
+ await githubDownload({
459
+ apiUrl,
460
+ token,
461
+ fetchImpl,
462
+ outputPath: payloadZip,
463
+ path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/artifacts/${artifact.id}/zip`,
464
+ });
465
+ unzip(payloadZip, path.join(payloadDir, safeName));
466
+ }
271
467
  unzip(passportZip, passportDir);
272
468
  unzip(summaryZip, summaryDir);
273
469
  fs.rmSync(tempDir, { recursive: true, force: true });
@@ -277,14 +473,43 @@ export async function resolveReleaseCandidateArtifacts({
277
473
  throw new Error("downloaded release-candidate artifacts did not contain release-candidate-passport.json and build-summary.json");
278
474
  }
279
475
  const passport = JSON.parse(fs.readFileSync(passportPath, "utf8"));
476
+ const platformManifestPaths = findDownloadedFiles(payloadDir, "manifest.json");
477
+ const npmTarballPaths = publishArtifactKind === "npm"
478
+ ? findDownloadedFilesByExtension(payloadDir, [".tgz"])
479
+ : [];
480
+ const downloadedRequiredArtifactCount = publishArtifactKind === "npm"
481
+ ? npmTarballPaths.length
482
+ : platformManifestPaths.length;
483
+ if (minimumPayloadCount > 0 && downloadedRequiredArtifactCount < minimumPayloadCount) {
484
+ const noun = publishArtifactKind === "npm" ? "npm package tarballs" : "platform manifests";
485
+ throw new Error(`expected at least ${minimumPayloadCount} downloaded ${noun}, found ${downloadedRequiredArtifactCount}`);
486
+ }
487
+ const manifests = platformManifestPaths.map((manifestPath) => JSON.parse(fs.readFileSync(manifestPath, "utf8")));
488
+ const generatedRequiredArtifacts = generatePublishRequiredArtifacts({
489
+ manifests,
490
+ version: passport.target?.version || "",
491
+ kind: publishArtifactKind,
492
+ tarballPaths: npmTarballPaths,
493
+ mainPackage: publishPackageMain,
494
+ });
495
+ const requiredArtifactsPath = path.join(resolvedOutput, "publish-required-artifacts.json");
496
+ fs.writeFileSync(requiredArtifactsPath, `${JSON.stringify(generatedRequiredArtifacts, null, 2)}\n`);
280
497
  return {
281
498
  ...result,
282
499
  paths: {
283
500
  passport: outputPath(passportPath),
284
501
  buildSummary: outputPath(buildSummaryPath),
502
+ payloads: outputPath(payloadDir),
503
+ platformManifests: platformManifestPaths.map(outputPath),
504
+ npmTarballs: npmTarballPaths.map(outputPath),
505
+ publishRequiredArtifacts: outputPath(requiredArtifactsPath),
285
506
  },
286
507
  version: passport.target?.version || "",
287
508
  candidateHash: passport.candidateHash || "",
509
+ payloadCount: payloadArtifacts.length,
510
+ platformManifestCount: platformManifestPaths.length,
511
+ npmTarballCount: npmTarballPaths.length,
512
+ publishRequiredArtifacts: generatedRequiredArtifacts,
288
513
  };
289
514
  }
290
515
 
@@ -296,6 +521,10 @@ export async function resolveReleaseCandidateArtifactsCli() {
296
521
  workflowFile: env("BUILDCHAIN_RC_WORKFLOW_FILE", DEFAULT_WORKFLOW_FILE),
297
522
  workflowName: env("BUILDCHAIN_RC_WORKFLOW_NAME", ""),
298
523
  artifactName: env("BUILDCHAIN_ARTIFACT_NAME"),
524
+ artifactPatterns: env("BUILDCHAIN_ARTIFACT_PATTERNS"),
525
+ requiredArtifactCount: env("BUILDCHAIN_REQUIRED_ARTIFACT_COUNT", "0"),
526
+ publishArtifactKind: env("BUILDCHAIN_PUBLISH_ARTIFACT_KIND", "npm"),
527
+ publishPackageMain: env("BUILDCHAIN_PUBLISH_PACKAGE_MAIN"),
299
528
  outputDir: env("BUILDCHAIN_RC_OUTPUT_DIR", ".buildchain/release-candidate"),
300
529
  });
301
530
  writeGitHubOutputs({
@@ -306,6 +535,14 @@ export async function resolveReleaseCandidateArtifactsCli() {
306
535
  "release-candidate-source-sha": result.artifacts?.sourceSha || "",
307
536
  "release-candidate-artifact": result.artifacts?.passport || "",
308
537
  "release-candidate-build-summary-artifact": result.artifacts?.summary || "",
538
+ "release-candidate-payload-artifacts": (result.artifacts?.payloads || []).join(","),
539
+ "release-candidate-payload-dir": result.paths?.payloads || "",
540
+ "release-candidate-platform-manifest-paths": (result.paths?.platformManifests || []).join(","),
541
+ "release-candidate-platform-manifest-count": String(result.platformManifestCount || 0),
542
+ "release-candidate-npm-tarball-paths": (result.paths?.npmTarballs || []).join(","),
543
+ "release-candidate-npm-tarball-count": String(result.npmTarballCount || 0),
544
+ "publish-required-artifacts-json": JSON.stringify(result.publishRequiredArtifacts || []),
545
+ "publish-required-artifacts-path": result.paths?.publishRequiredArtifacts || "",
309
546
  "release-candidate-run-id": result.run?.id || "",
310
547
  "release-candidate-run-url": result.run?.url || "",
311
548
  "release-candidate-pr": result.pullRequest?.number ? String(result.pullRequest.number) : "",