@kungfu-tech/buildchain 2.4.8-alpha.0 → 2.4.8-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 CHANGED
@@ -150,6 +150,9 @@ The active reusable workflow surfaces are:
150
150
 
151
151
  - `.github/workflows/.build.yml` for deterministic multi-platform build and
152
152
  artifact contracts;
153
+ - `.github/workflows/release-candidate-promote.yml` for post-merge
154
+ promote-only publication from a PR-stage release candidate, without a second
155
+ heavy build;
153
156
  - `.github/workflows/.web-surface.yml` for preview, staging, production, and
154
157
  cleanup plans for site/app repositories;
155
158
  - `.github/workflows/buildchain-ref-promotion.yml` for protected release
@@ -168,6 +171,10 @@ uses: kungfu-systems/buildchain/actions/validate-config@v2
168
171
  uses: kungfu-systems/buildchain/.github/workflows/.build.yml@v2
169
172
  ```
170
173
 
174
+ ```yaml
175
+ uses: kungfu-systems/buildchain/.github/workflows/release-candidate-promote.yml@v2
176
+ ```
177
+
171
178
  ## Release Model
172
179
 
173
180
  Buildchain treats a reviewed branch merge as release intent:
@@ -359,6 +359,45 @@ If the branch tip no longer matches the manifest SHA, the publish job must fail
359
359
  closed. Moving a gate branch creates a new publish decision and should produce a
360
360
  new build run.
361
361
 
362
+ ## Release Candidate Promote-Only
363
+
364
+ For native package sets, the PR build is the only heavy build. When a PR targets
365
+ `alpha/<line>` or `release/<line>`, `.build.yml` uploads a release-candidate
366
+ bundle next to the platform artifacts. The bundle contains:
367
+
368
+ - `release-candidate.passport.json`;
369
+ - the aggregate `build-summary.json`;
370
+ - copied platform manifest evidence for the built platforms.
371
+
372
+ The passport records two separate source identities:
373
+
374
+ - `builtSourceSha` / `builtSourceTreeSha`: the PR-stage source that produced the
375
+ artifacts, usually the PR merge ref;
376
+ - `promotionChannelSha` / `promotionChannelTreeSha`: the post-merge channel
377
+ commit used for publish authority.
378
+
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
383
+ `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
+
387
+ ```yaml
388
+ jobs:
389
+ promote:
390
+ uses: kungfu-systems/buildchain/.github/workflows/release-candidate-promote.yml@v2
391
+ with:
392
+ channel: alpha
393
+ target-ref: alpha/v22/v22.22
394
+ artifact-name: libnode
395
+ package-manager: npm
396
+ publish-target: npm
397
+ runner-preset: github-hosted
398
+ trusted-publishing: true
399
+ ```
400
+
362
401
  Custom publish jobs can also repeat the channel-ref preflight:
363
402
 
364
403
  ```yaml
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "2.4.8-alpha.0",
3
+ "version": "2.4.8-alpha.1",
4
4
  "private": false,
5
5
  "description": "Buildchain Release Passport, release governance, CLI toolkit, and site facts.",
6
6
  "repository": "https://github.com/kungfu-systems/buildchain",
@@ -80,6 +80,26 @@ function normalizePlatformEntry(platform = {}, index = 0) {
80
80
  };
81
81
  }
82
82
 
83
+ function inferVersionFromReleaseManifest(buildSummary = {}) {
84
+ const raw = buildSummary.publishSource?.releaseManifest || "";
85
+ if (!raw) {
86
+ return "";
87
+ }
88
+ try {
89
+ const manifest = JSON.parse(raw);
90
+ const versions = [
91
+ ...new Set(
92
+ (manifest.versionFiles || [])
93
+ .map((file) => optionalString(file.version).trim())
94
+ .filter(Boolean),
95
+ ),
96
+ ];
97
+ return versions.length === 1 ? versions[0] : "";
98
+ } catch {
99
+ return "";
100
+ }
101
+ }
102
+
83
103
  export function createReleaseCandidatePassport({
84
104
  repository = "",
85
105
  pullRequest = {},
@@ -96,10 +116,12 @@ export function createReleaseCandidatePassport({
96
116
  } = {}) {
97
117
  const normalizedSummary = buildSummary && typeof buildSummary === "object" ? buildSummary : {};
98
118
  const sourceSha = sourceHeadSha || normalizedSummary.publishSource?.sha || normalizedSummary.git?.sha || "";
119
+ const resolvedTreeHash = optionalString(sourceTreeHash || normalizedSummary.git?.treeSha);
99
120
  const channel = normalizeTargetChannel(targetChannel)
100
121
  || normalizeTargetChannel(normalizedSummary.publishSource?.channel)
101
122
  || normalizeTargetChannel(normalizedSummary.publishGate?.channel)
102
123
  || normalizeTargetChannel(pullRequest.baseRef);
124
+ const resolvedVersion = version || normalizedSummary.publishSource?.consumerVersion || inferVersionFromReleaseManifest(normalizedSummary);
103
125
  const candidate = {
104
126
  schemaVersion: 1,
105
127
  contract: RELEASE_CANDIDATE_PASSPORT_CONTRACT,
@@ -114,13 +136,15 @@ export function createReleaseCandidatePassport({
114
136
  target: {
115
137
  channel: nonEmptyString(channel, "targetChannel"),
116
138
  ref: optionalString(normalizedSummary.publishSource?.ref || normalizedSummary.git?.ref),
117
- version: nonEmptyString(version || normalizedSummary.publishSource?.consumerVersion, "version"),
139
+ version: nonEmptyString(resolvedVersion, "version"),
118
140
  },
119
141
  source: {
120
142
  headSha: nonEmptyString(sourceSha, "sourceHeadSha"),
121
143
  baseSha: optionalString(baseSha),
122
144
  mergeRefSha: optionalString(mergeRefSha || sourceSha),
123
- treeHash: optionalString(sourceTreeHash || normalizedSummary.git?.treeSha),
145
+ treeHash: resolvedTreeHash,
146
+ builtSourceSha: nonEmptyString(mergeRefSha || sourceSha, "builtSourceSha"),
147
+ builtSourceTreeSha: resolvedTreeHash,
124
148
  },
125
149
  buildchain: {
126
150
  ref: optionalString(buildchain.ref || normalizedSummary.runtime?.ref),
@@ -576,6 +576,11 @@ export function createReleasePassport({
576
576
  targetRef,
577
577
  releaseSha,
578
578
  releaseMaterialSha,
579
+ builtSourceSha: optionalString(release.builtSourceSha || release.built_source_sha),
580
+ builtSourceTreeSha: optionalString(release.builtSourceTreeSha || release.built_source_tree_sha),
581
+ promotionChannelSha: optionalString(release.promotionChannelSha || release.promotion_channel_sha),
582
+ promotionChannelTreeSha: optionalString(release.promotionChannelTreeSha || release.promotion_channel_tree_sha),
583
+ treeEquivalent: release.treeEquivalent === undefined ? undefined : Boolean(release.treeEquivalent),
579
584
  publishToolingSha: optionalString(
580
585
  release.publishToolingSha ||
581
586
  release.publish_tooling_sha ||
@@ -28,8 +28,11 @@ const requiredPaths = [
28
28
  "scripts/build-standalone-binary.mjs",
29
29
  "scripts/create-release-bundle.mjs",
30
30
  "scripts/generate-site-bundle.mjs",
31
+ "scripts/generate-release-candidate-passport.mjs",
31
32
  "scripts/npm-publish-dry-run.mjs",
32
33
  "scripts/npm-publish-transaction.mjs",
34
+ "scripts/release-candidate-resolver.mjs",
35
+ "scripts/workflow-friction-report.mjs",
33
36
  "docs/migration-inventory.md",
34
37
  "docs/lifecycle-protocol.md",
35
38
  "docs/ownership.md",
@@ -38,6 +41,7 @@ const requiredPaths = [
38
41
  ".github/actionlint.yaml",
39
42
  ".github/workflows/self-hosted-runner-smoke.yml",
40
43
  ".github/workflows/buildchain-ref-promotion.yml",
44
+ ".github/workflows/release-candidate-promote.yml",
41
45
  ".github/workflows/npm-publish.yml",
42
46
  ".github/workflows/binary-distribution.yml",
43
47
  ".github/workflows/verify.yml",
@@ -23,7 +23,7 @@ export function generateReleaseCandidatePassportCli() {
23
23
  const sourceSha = env("BUILDCHAIN_RC_SOURCE_HEAD_SHA", buildSummary.publishSource?.sha || buildSummary.git?.sha || "");
24
24
  const version = env(
25
25
  "BUILDCHAIN_RC_VERSION",
26
- buildSummary.publishSource?.consumerVersion || `source-${String(sourceSha).slice(0, 12)}`,
26
+ buildSummary.publishSource?.consumerVersion || "",
27
27
  );
28
28
  const passport = createReleaseCandidatePassport({
29
29
  repository: env("GITHUB_REPOSITORY", buildSummary.git?.repository || ""),
@@ -294,6 +294,7 @@ export async function resolveReleaseCandidateArtifactsCli() {
294
294
  "release-candidate-passport-path": result.paths?.passport || "",
295
295
  "release-candidate-build-summary-path": result.paths?.buildSummary || "",
296
296
  "release-candidate-version": result.version || "",
297
+ "release-candidate-source-sha": result.artifacts?.sourceSha || "",
297
298
  "release-candidate-artifact": result.artifacts?.passport || "",
298
299
  "release-candidate-build-summary-artifact": result.artifacts?.summary || "",
299
300
  "release-candidate-run-id": result.run?.id || "",