@kungfu-tech/buildchain 2.0.15-alpha.1 → 2.0.15

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
@@ -106,8 +106,8 @@ Buildchain v2 ships these active surfaces:
106
106
  manifests, publish-gate source locks, resolved release manifests, and
107
107
  aggregate build summaries;
108
108
  - `.github/workflows/.web-surface.yml` as the reusable site/app surface for PR
109
- preview plans, closed-PR preview cleanup plans, push-main staging promotion
110
- plans, and explicit production environment gates;
109
+ preview plans and optional apply, closed-PR preview cleanup apply, push-main
110
+ staging apply, and explicit production environment gates;
111
111
  - `actions/run-lifecycle` for callers that need the same lifecycle/manifest
112
112
  contract inside their own workflows;
113
113
  - governance-closed self-promotion through `Buildchain Ref Promotion`.
@@ -236,6 +236,18 @@ stop in `finalizing` and output `finalization-needed=true`. A later run can
236
236
  resume from the same transaction state and complete ref movement without
237
237
  republishing matching artifacts.
238
238
 
239
+ If finalization fails after an exact Git tag, a channel branch, or dev/alpha
240
+ sync ref has already moved, the next run reads the durable `finalizing` state
241
+ and continues from the recorded transaction. The current workflow SHA may be a
242
+ version-state merge commit that contains or corresponds to the transaction's
243
+ `release_material_sha`; it does not have to equal the original `source_sha` or
244
+ the transaction `release_sha`. Exact tags are accepted when they already point
245
+ at the transaction release/material SHA or the finalized channel head. Floating
246
+ channel tags and dev/alpha refs are then retried idempotently, and the
247
+ transaction is marked `complete` only after those public refs are consistent.
248
+ An exact tag at an unrelated SHA is still a material conflict and blocks
249
+ recovery.
250
+
239
251
  If finalization fails after an exact Git tag is created, the next run reads the
240
252
  durable `finalizing` state, verifies the exact tag points at the recorded
241
253
  release SHA, and retries the remaining floating refs. An exact tag at a
@@ -285,10 +285,55 @@ The reusable workflow maps GitHub events to Buildchain web-surface semantics:
285
285
  | `push` to `main` | validate, build, verify, and plan `staging` from the merged `main` SHA |
286
286
  | `workflow_dispatch` with `production-approved = true` | plan `production` and enter the configured GitHub Environment gate |
287
287
 
288
- The workflow deliberately plans and emits manifests by default. Callers that
289
- want live AWS mutation should invoke `deploy-apply` / `cleanup-apply` from a
290
- controlled deploy job with scoped credentials. This keeps production from being
291
- an implicit side effect of merging to `main`.
288
+ The workflow deliberately plans and emits manifests by default. Live mutation is
289
+ opt-in per channel:
290
+
291
+ ```yaml
292
+ permissions:
293
+ contents: read
294
+ id-token: write
295
+ pull-requests: write
296
+
297
+ jobs:
298
+ web-surface:
299
+ uses: kungfu-systems/buildchain/.github/workflows/.web-surface.yml@v2
300
+ with:
301
+ buildchain-ref: v2
302
+ build-command: pnpm run build
303
+ verify-command: pnpm run check
304
+ artifact-path: dist
305
+ preview-apply: true
306
+ preview-cleanup-apply: true
307
+ preview-aws-role-arn: arn:aws:iam::123456789012:role/site-preview-github-actions
308
+ staging-apply: true
309
+ staging-aws-role-arn: arn:aws:iam::123456789012:role/site-staging-github-actions
310
+ production-apply: false
311
+ production-aws-role-arn: arn:aws:iam::123456789012:role/site-production-github-actions
312
+ production-environment: production
313
+ ```
314
+
315
+ When enabled, Buildchain owns the full release apply state machine:
316
+
317
+ - PR preview deploys run `deploy-apply --dry-run false` with the preview role
318
+ and update a single idempotent PR comment.
319
+ - Closed PR cleanup runs `cleanup-apply --dry-run false` with the preview role
320
+ only.
321
+ - Pushes to `main` run staging `deploy-apply --dry-run false` with the staging
322
+ role.
323
+ - Production runs only on `workflow_dispatch` when both `production-approved`
324
+ and `production-apply` are true, and the job is gated by the configured GitHub
325
+ Environment.
326
+
327
+ Callers must grant `id-token: write` for OIDC role assumption. Preview comments
328
+ also need `pull-requests: write`. The AWS roles remain caller-owned and should
329
+ be scoped by channel: preview can mutate only preview resources, staging can
330
+ mutate only staging resources, and production can mutate only production
331
+ resources.
332
+
333
+ Apply mode fails closed when the deploy config still contains placeholder AWS
334
+ targets such as `pending-preview-distribution`. Planning can use placeholders
335
+ for dry-run-only design work, but live apply requires concrete bucket and
336
+ CloudFront distribution identifiers.
292
337
 
293
338
  ## Site Repository Shape
294
339
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "2.0.15-alpha.1",
3
+ "version": "2.0.15",
4
4
  "private": false,
5
5
  "description": "Kungfu Buildchain reusable workflows, release governance, and CLI tooling.",
6
6
  "repository": "https://github.com/kungfu-systems/buildchain",
@@ -223,6 +223,31 @@ function appliedStatus({ dryRun, operations, noOp = false }) {
223
223
  return dryRun ? "planned" : "applied";
224
224
  }
225
225
 
226
+ function isPlaceholderValue(value) {
227
+ const normalized = String(value || "").trim().toLowerCase();
228
+ return (
229
+ normalized === "" ||
230
+ normalized === "pending" ||
231
+ normalized.startsWith("pending-") ||
232
+ normalized.startsWith("pending_") ||
233
+ normalized.includes("placeholder") ||
234
+ normalized.includes("example.")
235
+ );
236
+ }
237
+
238
+ function assertConcreteAwsDeployConfig({ deployConfig, channel, operation }) {
239
+ const bucket = deployConfig.bucket || deployConfig.target || "";
240
+ const distribution = deployConfig.cloudfront_distribution || deployConfig.distribution || "";
241
+ if (isPlaceholderValue(bucket)) {
242
+ throw new Error(`web-surface ${operation} apply requires concrete deploy.${channel}.bucket or deploy.${channel}.target`);
243
+ }
244
+ if (isPlaceholderValue(distribution)) {
245
+ throw new Error(
246
+ `web-surface ${operation} apply requires concrete deploy.${channel}.cloudfront_distribution or deploy.${channel}.distribution`,
247
+ );
248
+ }
249
+ }
250
+
226
251
  function deployManifestKey(deployConfig, manifest) {
227
252
  return joinS3Key(manifestPrefixFor(deployConfig), `${manifest.alias || manifest.channel}.json`);
228
253
  }
@@ -485,6 +510,13 @@ export function applyWebSurfaceDeploy({
485
510
  if (resolvedPlan.adapter !== "aws-s3-cloudfront") {
486
511
  throw new Error(`web-surface deploy apply does not support adapter: ${resolvedPlan.adapter}`);
487
512
  }
513
+ if (!dryRun) {
514
+ assertConcreteAwsDeployConfig({
515
+ deployConfig,
516
+ channel: resolvedPlan.channel,
517
+ operation: "deploy",
518
+ });
519
+ }
488
520
  const bucket = deployConfig.bucket || deployConfig.target || "";
489
521
  const artifactRoot = path.resolve(cwd, resolvedPlan.artifact.path);
490
522
  const objectPrefix = objectPrefixFor(deployConfig, resolvedPlan.manifest.alias || resolvedPlan.manifest.channel);
@@ -669,6 +701,13 @@ export function applyWebSurfaceCleanup({
669
701
  if (cleanup.adapter !== "aws-s3-cloudfront") {
670
702
  throw new Error(`web-surface cleanup apply does not support adapter: ${cleanup.adapter}`);
671
703
  }
704
+ if (!dryRun) {
705
+ assertConcreteAwsDeployConfig({
706
+ deployConfig,
707
+ channel: cleanup.channel,
708
+ operation: "cleanup",
709
+ });
710
+ }
672
711
  const bucket = deployConfig.bucket || deployConfig.target || "";
673
712
  const operations = cleanup.entries.flatMap((entry) => {
674
713
  const entryOperations = [
@@ -125,6 +125,7 @@ export function webSurfaceCli() {
125
125
  "web-surface-apply-mode": result.applyMode,
126
126
  "web-surface-apply-status": result.status,
127
127
  "web-surface-manifest-json": JSON.stringify(result.manifest),
128
+ "web-surface-apply-result-json": JSON.stringify(result),
128
129
  });
129
130
  assertApplySucceeded(result);
130
131
  return result;