@kungfu-tech/buildchain 2.11.3 → 2.11.4
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/dist/site/buildchain-contract.json +3 -3
- package/dist/site/buildchain-site.json +7 -7
- package/dist/site/kfd-claims.json +2 -2
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +1 -1
- package/dist/site/page-registry.json +2 -2
- package/dist/site/public-surface-audit.json +1 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +5 -5
- package/docs/web-surface-deployments.md +5 -0
- package/package.json +1 -1
- package/scripts/web-surface-core.mjs +42 -1
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"product": {
|
|
5
5
|
"name": "Buildchain",
|
|
6
6
|
"package": "@kungfu-tech/buildchain",
|
|
7
|
-
"version": "2.11.
|
|
7
|
+
"version": "2.11.4",
|
|
8
8
|
"repository": "https://github.com/kungfu-systems/buildchain"
|
|
9
9
|
},
|
|
10
10
|
"majorLine": "v2",
|
|
@@ -434,7 +434,7 @@
|
|
|
434
434
|
"manual entries carry source file digests so downstream sites and agents can detect stale hand-written documentation"
|
|
435
435
|
],
|
|
436
436
|
"breakingDigest": "sha256:7d0d2819e3a3e72989d9c57b5efe9d0bc0a79bc0f2c82a0c7b9d6c5a211a91f2",
|
|
437
|
-
"auditDigest": "sha256:
|
|
437
|
+
"auditDigest": "sha256:1e84dd57b1825f38e03168a4c55a74a587edb8ebe487a07386f1d779d0377ab6"
|
|
438
438
|
},
|
|
439
439
|
{
|
|
440
440
|
"contractVersion": 1,
|
|
@@ -460,5 +460,5 @@
|
|
|
460
460
|
}
|
|
461
461
|
],
|
|
462
462
|
"compatibilityDigest": "sha256:967df7e854fdf582294ddfb6403eff25c00db9518a24051e7d47cbd75527c785",
|
|
463
|
-
"contractDigest": "sha256:
|
|
463
|
+
"contractDigest": "sha256:296b55d3f9b65ee68afc4087b0df23e90724c5f3295cdd4f596775fca1ed978a"
|
|
464
464
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"contract": "kungfu-buildchain-site-bundle",
|
|
4
|
-
"generatedAt": "2026-07-
|
|
5
|
-
"publishedAt": "2026-07-
|
|
4
|
+
"generatedAt": "2026-07-09T21:23:58.798Z",
|
|
5
|
+
"publishedAt": "2026-07-09T21:23:58.798Z",
|
|
6
6
|
"reproducible": true,
|
|
7
7
|
"timestampPolicy": "ci-injected",
|
|
8
8
|
"deterministicInputs": [
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"declared Buildchain surface manifest contract"
|
|
20
20
|
],
|
|
21
21
|
"sourceDateEpoch": "0",
|
|
22
|
-
"sourceRevision": "
|
|
22
|
+
"sourceRevision": "df6967b5d0a5c5c29cbe953c78f0d0a5df673ccf",
|
|
23
23
|
"timestampPolicyDetails": {
|
|
24
24
|
"contract": "kungfu-buildchain-surface-timestamp-policy",
|
|
25
25
|
"timestampFields": [
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"package": {
|
|
39
39
|
"name": "@kungfu-tech/buildchain",
|
|
40
|
-
"version": "2.11.
|
|
40
|
+
"version": "2.11.4",
|
|
41
41
|
"versionSource": "package.json#version"
|
|
42
42
|
},
|
|
43
43
|
"source": {
|
|
@@ -1710,7 +1710,7 @@
|
|
|
1710
1710
|
],
|
|
1711
1711
|
"maturity": "stable",
|
|
1712
1712
|
"sourcePath": "docs/web-surface-deployments.md",
|
|
1713
|
-
"digest": "sha256:
|
|
1713
|
+
"digest": "sha256:b24b9e6f23ad13ccd3a1074a2ecefd26abdd7f848e979dea64b96678f57f0d6d",
|
|
1714
1714
|
"headings": [
|
|
1715
1715
|
{
|
|
1716
1716
|
"level": 1,
|
|
@@ -1793,7 +1793,7 @@
|
|
|
1793
1793
|
"anchor": "boundaries"
|
|
1794
1794
|
}
|
|
1795
1795
|
],
|
|
1796
|
-
"markdown": "# Web-Surface Deployment Contract\n\nBuildchain supports `project.type = \"web-surface\"` for repositories that publish\nsites, docs, product pages, operator consoles, or browser apps. These projects\nneed auditable deployment semantics, but they are not package release lines and\nshould not be forced into `dev/alpha/release` version-state automation.\n\nThe release object for a web surface is:\n\n```text\nsource commit + build artifact + deploy target + channel + deployment manifest\n```\n\nThis keeps the evidence chain clear:\n\n- the source SHA explains what code was built;\n- the artifact hash explains exactly what was deployed;\n- the channel explains who can see it and whether it is promotable;\n- the deploy target and adapter explain where it would be published;\n- the deployment manifest records retention, rollback, security, and secret\n reference metadata.\n\n## Configuration\n\n`.buildchain/buildchain.toml` is the source of truth. Web-surface projects must declare\npreview, staging, and production channels plus a deploy adapter for each.\n\n```toml\nschema = 1\n\n[project]\ntype = \"web-surface\"\nname = \"site-kungfu-tech\"\nsite = \"kungfu-tech\"\n\n[channels.preview]\nurl_pattern = \"https://{alias}.preview.kungfu.tech\"\nvisibility = \"ephemeral\"\nrequires_auth = false\nnoindex = true\n\n[channels.staging]\nurl = \"https://staging.kungfu.tech\"\nvisibility = \"protected\"\naccess_control = \"managed-network\"\nedge_auth = \"none\"\nnoindex = true\npromotable = true\n\n[channels.production]\nurl = \"https://kungfu.tech\"\nvisibility = \"public\"\ncanonical = true\nnoindex = false\n\n[deploy.preview]\nadapter = \"aws-s3-cloudfront\"\nbucket = \"kungfu-tech-preview\"\ncloudfront_distribution = \"E-PREVIEW\"\nartifact_path = \"dist\"\nsecret_refs = [\"AWS_ROLE_ARN\"]\n# Optional. Defaults to \"buildchain\".\n# Use \"external\" when an existing viewer-request CloudFront Function already\n# owns preview alias, surface-prefix, and directory-index routing.\ndirectory_index_rewrite = \"buildchain\"\n```\n\n### Multi-Surface Host Mapping\n\nSome site repositories publish more than one first-class web surface from the\nsame artifact. For example, `site-libkungfu-dev` has a hub plus separate\nhostnames for core, Buildchain, and Kung Fu Decisions. These are not just\nnavigation paths; staging, production preflight, and post-deploy health checks\nmust verify host-level behavior for each surface.\n\nDeclare named surfaces with per-channel URLs:\n\n```toml\n[surfaces.hub]\npath = \"/\"\nproduction_url = \"https://libkungfu.dev\"\nstaging_url = \"https://staging.libkungfu.dev\"\npreview_url_pattern = \"https://{alias}.preview.libkungfu.dev\"\n\n[surfaces.core]\npath = \"/core/\"\nproduction_url = \"https://core.libkungfu.dev\"\nstaging_url = \"https://core.staging.libkungfu.dev\"\npreview_url_pattern = \"https://core-{alias}.preview.libkungfu.dev\"\n\n[surfaces.buildchain]\npath = \"/buildchain/\"\nproduction_url = \"https://buildchain.libkungfu.dev\"\nstaging_url = \"https://buildchain.staging.libkungfu.dev\"\npreview_url_pattern = \"https://buildchain-{alias}.preview.libkungfu.dev\"\n\n[surfaces.kfd]\npath = \"/kfd/\"\nproduction_url = \"https://kfd.libkungfu.dev\"\nstaging_url = \"https://kfd.staging.libkungfu.dev\"\npreview_url_pattern = \"https://kfd-{alias}.preview.libkungfu.dev\"\n```\n\nBuildchain resolves every `(channel, surface)` pair. A preview alias such as\n`pr-12` becomes:\n\n```text\nhub: https://pr-12.preview.libkungfu.dev\ncore: https://core-pr-12.preview.libkungfu.dev\nbuildchain: https://buildchain-pr-12.preview.libkungfu.dev\nkfd: https://kfd-pr-12.preview.libkungfu.dev\n```\n\nWhen `surfaces` is omitted, Buildchain preserves the legacy single-surface\ncontract by creating an implicit `default` surface from the channel URL. When a\nsurface is intentionally path-only, declare it explicitly:\n\n```toml\n[surfaces.docs]\npath = \"/docs/\"\npath_only = true\n```\n\n`path_only = true` is an exception, not the default. Without it, every named\nsurface must declare `preview_url_pattern`, `staging_url`, and\n`production_url`. This makes staging/production mismatches fail during\nvalidation instead of becoming invisible deploy drift.\n\nAdapter strategy remains explicit. The default `aws-s3-cloudfront` plan uses the\nchannel deploy target for every surface, and each binding records its own\nbucket, distribution id, object prefix, manifest key, source path, and URL. A\nchannel can override target details per surface:\n\n```toml\n[deploy.staging.surfaces.core]\nbucket = \"libkungfu-dev-core-staging\"\ncloudfront_distribution = \"E-CORE-STAGING\"\norigin_path = \"/core\"\n```\n\nBuildchain validates these hard constraints:\n\n- `channels.preview.url_pattern` is required and must contain the alias shape\n used by preview deployments.\n- `channels.staging.access_control` must protect staging. Supported modes are\n `managed-network`, `edge-basic-auth`, `oidc`, and `app-auth`.\n- `channels.staging.edge_auth` records whether the edge layer owns auth. Use\n `edge_auth = \"none\"` when staging is protected by managed network controls\n such as WAF/IP allowlists or VPN access.\n- `channels.staging.noindex = true` is required.\n- `channels.production.url` is required.\n- deploy adapters must be declared per channel.\n- named surfaces must declare first-class URLs for every channel unless\n `path_only = true` is explicitly set.\n- secret material must be declared as reference names, such as\n `secret_refs = [\"AWS_ROLE_ARN\"]`; inline secret-like deploy keys are rejected.\n\n### Floating Runtime Contract Lock\n\nWeb-surface repositories can consume the stable Buildchain workflow shell with a\nfloating ref, such as:\n\n```yaml\njobs:\n web:\n uses: kungfu-systems/buildchain/.github/workflows/.web-surface.yml@v2\n with:\n buildchain-contract-lock-path: .buildchain/contract-lock.json\n buildchain-contract-compatibility-policy: major-compatible\n buildchain-contract-drift-issue-mode: compatible-and-breaking\n build-command: pnpm build\n artifact-path: dist\n```\n\nThe caller repository commits `.buildchain/contract-lock.json` after reviewing an\naccepted Buildchain runtime SHA and contract digest. The reusable workflow then\nresolves the floating runtime to an immutable SHA, checks the lock before the\ncaller build command, and applies these rules:\n\n- unchanged lock: continue without feedback;\n- compatible drift: continue, write the drift summary, and open or update a\n caller-repository issue when permissions allow;\n- breaking drift: fail closed before rendering, deployment planning, deploy\n apply, or release publication.\n\nThe caller no longer needs to run `scripts/buildchain-contract-lock.mjs` inside\nits own build command. That check belongs to Buildchain because the actual\ncontract world is stored in the Buildchain runtime ref being used.\n\nSupported adapter names are:\n\n| Adapter | Initial use |\n| --- | --- |\n| `aws-s3-cloudfront` | Static site artifact sync plus CDN invalidation plan |\n| `aws-elastic-beanstalk` | Future dynamic app environment adapter |\n| `aws-ecs-service` | Future dynamic service adapter |\n\nThe channel ontology is independent of the adapter. A future dynamic staging\nenvironment still remains `channel = \"staging\"` with protected/noindex/security\nrequirements.\n\n## Preview Aliases\n\nPreview uses subdomains, not path prefixes:\n\n```text\nhttps://pr-123.preview.kungfu.tech\nhttps://sha-abcdef123456.preview.kungfu.tech\n```\n\nAlias semantics are explicit:\n\n| Alias | Meaning | Mutable | Retention |\n| --- | --- | --- | --- |\n| `pr-123` | Current preview for a pull request | yes | short-lived |\n| `sha-abcdef123456` | Immutable preview for one source SHA | no | longer-lived |\n\nThis allows PR comments to stay stable while preserving immutable evidence for a\nspecific source commit.\n\n## Deployment Manifest\n\nBuildchain emits a manifest with the deployment facts that matter for audit and\nrollback:\n\n```json\n{\n \"schemaVersion\": 1,\n \"contract\": \"kungfu-buildchain-web-surface-deployment\",\n \"site\": \"libkungfu-dev\",\n \"channel\": \"preview\",\n \"alias\": \"sha-abcdef123456\",\n \"url\": \"https://sha-abcdef123456.preview.libkungfu.dev\",\n \"generatedAt\": \"2026-07-01T00:00:00.000Z\",\n \"publishedAt\": \"2026-07-01T00:00:00.000Z\",\n \"reproducible\": true,\n \"timestampPolicy\": \"ci-injected\",\n \"deterministicInputs\": [\n \"web-surface artifact content\",\n \"buildchain.toml web-surface channels/deploy/surfaces\",\n \"sourceSha\",\n \"artifactHash\",\n \"deployment channel\",\n \"deployment alias\"\n ],\n \"sourceRevision\": \"...\",\n \"timestampPolicyDetails\": {\n \"contract\": \"kungfu-buildchain-surface-timestamp-policy\",\n \"timestampFields\": [\"generatedAt\", \"publishedAt\", \"deployedAt\"],\n \"timestampFieldsParticipateInArtifactDigest\": false,\n \"artifactDigestScope\": \"web-surface artifactHash excludes deployment manifest timestamps\"\n },\n \"sourceSha\": \"...\",\n \"artifactHash\": \"...\",\n \"deployTarget\": \"libkungfu-dev-preview\",\n \"adapter\": \"aws-s3-cloudfront\",\n \"deployedAt\": \"2026-07-01T00:00:00.000Z\",\n \"retentionClass\": \"preview-sha-immutable\",\n \"expiresAt\": \"2026-09-29T00:00:00.000Z\",\n \"accessControl\": \"none\",\n \"edgeAuth\": \"none\",\n \"noindex\": true,\n \"secretRefs\": [\"AWS_ROLE_ARN\"],\n \"surfaceBindings\": [\n {\n \"surface\": \"hub\",\n \"channel\": \"preview\",\n \"alias\": \"sha-abcdef123456\",\n \"url\": \"https://sha-abcdef123456.preview.libkungfu.dev\",\n \"sourcePath\": \"/\",\n \"artifactPathPrefix\": \"\",\n \"viewerPathPrefix\": \"/\",\n \"directoryIndex\": \"index.html\",\n \"directoryIndexResolution\": true,\n \"canonicalUrl\": \"https://libkungfu.dev\",\n \"bucket\": \"libkungfu-dev-preview\",\n \"distributionId\": \"E-PREVIEW\",\n \"originPath\": \"\",\n \"objectPrefix\": \"sha-abcdef123456\",\n \"manifestKey\": \".buildchain/deployments/sha-abcdef123456/hub.json\",\n \"routing\": {\n \"contract\": \"kungfu-buildchain-web-surface-path-prefix-rewrite\",\n \"viewerPathPrefix\": \"/\",\n \"artifactPathPrefix\": \"\",\n \"objectPrefix\": \"sha-abcdef123456\",\n \"directoryIndex\": \"index.html\",\n \"directoryIndexResolution\": true\n },\n \"smokeUrls\": [\n {\n \"kind\": \"root\",\n \"requestPath\": \"/\",\n \"url\": \"https://sha-abcdef123456.preview.libkungfu.dev/\",\n \"required\": true\n }\n ],\n \"noindex\": true,\n \"accessControl\": \"none\"\n }\n ]\n}\n```\n\nDynamic adapters can also fill `runtimeId`, `configFingerprint`,\n`healthCheck`, `migrationState`, `rollbackPointer`, and\n`rollbackLimitations`. Buildchain records secret reference names only, never\nsecret values.\n\nThe timestamp policy is shared with package site bundles. Public deployment\nmanifests should expose real workflow generation/publication times while\nseparately declaring why the deployed artifact remains reproducible. For\nweb-surface deployment manifests, `artifactHash` is the static site artifact\ndigest and does not include deployment timestamp fields; the manifest itself\nstill records those fields for human and agent audit.\n\n## Deploy Plans\n\nDeploy planning is the default behavior. It plans the adapter steps and writes\nmanifest JSON, but it does not touch AWS, DNS, CloudFront, or deployment\ncredentials.\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode deploy-plan \\\n --cwd fixtures/web-surface-shaped \\\n --source-sha aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \\\n --alias sha-aaaaaaaaaaaa\n```\n\nFor manifest-only output:\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode manifest \\\n --cwd fixtures/web-surface-shaped \\\n --source-sha aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \\\n --alias pr-123 \\\n --output .buildchain/web-surface-manifest.json\n```\n\nThe CLI emits GitHub outputs when `GITHUB_OUTPUT` is present:\n\n- `web-surface-channel`\n- `web-surface-alias`\n- `web-surface-url`\n- `web-surface-urls-json`\n- `web-surface-artifact-hash`\n- `web-surface-manifest-json`\n\n## Explicit Apply\n\n`deploy-apply` and `cleanup-apply` are explicit execution modes for the\n`aws-s3-cloudfront` static-site adapter. They still default to `--dry-run true`;\nlive AWS mutation requires `--dry-run false`.\n\nDeploy apply syncs the artifact, writes the deployment manifest, and invalidates\nCloudFront when a distribution id is configured:\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode deploy-apply \\\n --cwd fixtures/web-surface-shaped \\\n --channel staging \\\n --source-sha aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \\\n --artifact-path dist \\\n --dry-run false \\\n --output .buildchain/web-surface-staging-apply.json\n```\n\nFor multi-surface sites, each surface host is treated as a root-relative view\nof that surface's artifact path prefix. For example, a `buildchain` surface with\n`path = \"/buildchain/\"` and preview URL\n`https://buildchain-pr-29.preview.libkungfu.dev` syncs the artifact subtree\n`dist/buildchain/` to the preview object prefix `pr-29/buildchain`. A viewer\nrequest for `https://buildchain-pr-29.preview.libkungfu.dev/docs/` therefore\nresolves against the artifact's `dist/buildchain/docs/index.html`, not\n`dist/docs/index.html` and not the hub surface root. The deployment manifest\nrecords this as `routing.contract =\n\"kungfu-buildchain-web-surface-path-prefix-rewrite\"` with\n`viewerPathPrefix = \"/\"`, `artifactPathPrefix = \"buildchain\"`, and\n`directoryIndexResolution = true`.\n\nWhen a surface uses an S3 object prefix, directory-index routing must be handled\nat the viewer-request layer. By default, `directory_index_rewrite =\n\"buildchain\"` makes Buildchain install or update one CloudFront Function per\ndistribution before uploading payloads. The function rewrites any request path\nending in `/` to the corresponding `index.html`, so\n`https://buildchain-pr-29.preview.libkungfu.dev/` resolves to\n`pr-29/buildchain/index.html` and\n`https://buildchain-pr-29.preview.libkungfu.dev/docs/` resolves to\n`pr-29/buildchain/docs/index.html`. This keeps multi-host preview roots\ncompatible with S3 REST origins, where copying alias objects such as\n`pr-29/buildchain` or `pr-29/buildchain/` is not a reliable substitute for an\nedge rewrite.\n\nIf the distribution already has a viewer-request function that owns preview\nalias routing and surface-prefix routing, set `directory_index_rewrite =\n\"external\"` on the deploy channel or surface override. In that mode Buildchain\ndoes not create, update, or attach a generic directory-index function. Instead,\nthe deployment manifest records `directoryIndexRewrite = \"external\"` and\n`directoryIndexStrategy = \"external-viewer-request-function\"`, then the normal\nhealth check still verifies every required root and nested surface URL. This is\nthe correct contract for shared preview distributions such as\n`site-libkungfu-dev`, where a generic function cannot replace the existing\nprefix router.\n\nBuildchain still writes directory-index alias objects during apply as\ncompatibility evidence, but root correctness comes from the viewer-request\nrewrite contract, not from extensionless S3 keys. If Buildchain-managed mode\nfinds a distribution with a different viewer-request function, apply fails\nclosed and records that conflict in the apply result instead of silently serving\n403s. The reusable workflow uploads `buildchain-web-surface-*-diagnostics`\nartifacts containing the apply and health JSON so the failing AWS operation or\nHTTP check is visible from the consumer run.\n\nIt can also execute a previously saved deploy plan. In that mode Buildchain\nrecomputes the local artifact hash before running AWS commands and fails closed\nif the artifact no longer matches the saved plan:\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode deploy-apply \\\n --cwd fixtures/web-surface-shaped \\\n --plan .buildchain/web-surface-staging-plan.json \\\n --dry-run false \\\n --output .buildchain/web-surface-staging-apply.json\n```\n\nCleanup apply deletes preview content, deletes the preview manifest, and\ninvalidates CloudFront:\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode cleanup-apply \\\n --cwd fixtures/web-surface-shaped \\\n --event pull-request-closed \\\n --pull-number 123 \\\n --source-sha aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \\\n --dry-run false \\\n --output .buildchain/web-surface-cleanup-apply.json\n```\n\nCleanup apply can also execute a saved cleanup plan:\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode cleanup-apply \\\n --cwd fixtures/web-surface-shaped \\\n --plan .buildchain/web-surface-cleanup-plan.json \\\n --dry-run false \\\n --output .buildchain/web-surface-cleanup-apply.json\n```\n\nApply output records the channel, alias, source SHA, artifact hash, target\nbucket, object prefix, manifest key, all surface URLs, all surface bindings, CDN\ninvalidation paths, actor/run metadata, and every adapter operation with\n`executed`, `exitCode`, `stdout`, and `stderr`. If an operation fails,\nBuildchain records the failed operation, stops subsequent adapter operations,\nand exits non-zero after writing the result JSON. Buildchain records secret\nreference names only; the runner must provide the AWS CLI and credentials\noutside Buildchain, typically through OIDC and the declared `secret_refs`.\n\n## Production Preflight And Health\n\nProduction promotion is not just `deploy-apply --channel production`. Before a\nlive production apply, the reusable workflow runs:\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode production-preflight \\\n --cwd fixtures/web-surface-shaped \\\n --plan .buildchain/web-surface-production-plan.json \\\n --execute true \\\n --output .buildchain/web-surface-production-preflight.json\n```\n\nThe production preflight checks that:\n\n- `channels.production` is canonical and indexable;\n- every surface has concrete production bucket and CloudFront targets;\n- every production surface URL is HTTPS;\n- the production AWS role can inspect the declared bucket and distribution;\n- CloudFront aliases cover every surface host, including product hosts such as\n `kfd.libkungfu.dev`;\n- DNS resolves for every surface host.\n\nAfter preview, staging, and production apply, the workflow runs:\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode health-check \\\n --cwd fixtures/web-surface-shaped \\\n --result .buildchain/web-surface-production-apply.json \\\n --output .buildchain/web-surface-production-health.json\n```\n\nWhen the apply result includes CloudFront invalidations from the\n`aws-s3-cloudfront` adapter, health check waits for those invalidations to reach\n`Completed` before fetching public smoke URLs. This avoids reporting stale\nCloudFront 403 responses as deployment failures immediately after a successful\nS3 sync and invalidation request.\n\nThe health check fetches every surface root URL and any nested smoke URLs\nrecorded in each surface binding. Nested smoke URLs are derived from nested HTML\nartifact files under the surface path prefix, with directory index resolution\nsuch as `dist/buildchain/docs/index.html` becoming `/docs/` on the buildchain\npreview host. If a surface has no nested HTML route, Buildchain records only\nthe root smoke URL; absence of nested HTML is not a deployment failure. When a\nnested route is present, the check fails closed if a deploy reports success but\nthat child page returns 403 or another unexpected status. Surface root checks\nexpect the apply result to have installed the directory-index rewrite, so a\nmulti-host preview root such as `https://buildchain-pr-29.preview.libkungfu.dev/`\nmust resolve to the surface `index.html`, not the bare prefix directory.\nProduction additionally fails if a response is unreachable, returns an\nunexpected status, or still sends `x-robots-tag: noindex`. The health check also\nverifies that each surface binding recorded a deployment manifest pointer. The\nproduction release passport embeds the deploy plan, apply result, production\npreflight, and health check so a reviewer or agent can audit why the production\nsite changed and whether every declared host and every existing nested route was\nactually covered.\n\nChannels declared with `access_control = \"managed-network\"` use a different\nhealth strategy by default. Buildchain does not require a GitHub-hosted runner\nto fetch a URL that is intentionally reachable only from an approved network.\nInstead, after a live apply the health check uses the deploy role to run S3\n`head-object` checks for each surface manifest and the smoke target object, such\nas the surface `index.html` or a nested `docs/index.html`. The check records\n`healthStrategy = \"s3-object\"` and skips the public HTTP fetch. Dry-run and\nplan-only checks fall back to deployment evidence: each surface must have a\nmanifest key, bucket, object prefix, `sync-static-artifact`, and\n`write-deployment-manifest` evidence, recorded as\n`healthStrategy = \"deployment-evidence\"`. If the workflow is running on a runner\nthat is allowed to reach the managed network, set\n`BUILDCHAIN_WEB_SURFACE_HEALTH_ALLOWED_RUNNER=true` or pass\n`--allowed-managed-network-runner true` to keep the normal HTTP smoke checks.\nSet `BUILDCHAIN_WEB_SURFACE_HEALTH_S3_OBJECTS=false` or pass\n`--managed-network-s3-object-verification false` only when an external channel\npolicy owns managed-network object verification.\n\n## Cleanup Plans\n\nPreview cleanup is an auditable cleanup contract. It can run as a dry-run plan,\nan apply-mode plan, or the explicit `cleanup-apply` executor with preview-only\ncredentials:\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode cleanup-plan \\\n --cwd fixtures/web-surface-shaped \\\n --event pull-request-closed \\\n --pull-number 123 \\\n --aliases pr-123,sha-abcdef123456\n```\n\nThe plan and apply result keep mutable PR aliases and immutable SHA aliases\ndistinct so a caller can expire them with different retention windows. Closed-PR\ncleanup can derive `pr-N` from `--pull-number`, records the event, source SHA,\nactor, run id, preview bucket/prefix, manifest key, and adapter steps, and is an\nauditable no-op when no aliases are requested.\n\n## Reusable Workflow Shape\n\nBuildchain ships `.github/workflows/.web-surface.yml` for repositories that want\nthe standard PR review and promotion flow without copying bespoke glue:\n\n```yaml\njobs:\n web-surface:\n uses: kungfu-systems/buildchain/.github/workflows/.web-surface.yml@v2\n with:\n build-command: npm run build\n verify-command: npm run check\n artifact-path: dist\n```\n\nThe reusable workflow maps GitHub events to Buildchain web-surface semantics:\n\n| Event | Buildchain behavior |\n| --- | --- |\n| `pull_request` opened / synchronized / reopened | validate, build, verify, and plan `preview` for `pr-N` |\n| `pull_request` closed | plan apply-mode cleanup for the `pr-N` preview alias and manifest |\n| `push` to `main` | validate, build, verify, plan and apply `staging` from the merged `main` SHA, then optionally open a production release PR |\n| `push` to `main` from a matching release PR merge | validate the associated release PR, plan `production`, and enter the configured GitHub Environment gate |\n| `workflow_dispatch` with `production-approved = true` | plan `production` and enter the configured GitHub Environment gate |\n\nThe optional `buildchain-ref` input is empty by default. Empty keeps the\nweb-surface run on the stable Buildchain runtime selected by the reusable\nworkflow ref, normally `@v2`. A trusted maintainer can expose a\n`workflow_dispatch` input and pass it through for one-off train validation.\nSee [`runtime-train-validation.md`](runtime-train-validation.md) for the shared\ntrain protocol and notification template:\n\n```yaml\non:\n workflow_dispatch:\n inputs:\n buildchain-ref:\n description: \"Temporary Buildchain runtime ref for trusted manual validation\"\n required: false\n default: \"\"\n\njobs:\n web-surface:\n uses: kungfu-systems/buildchain/.github/workflows/.web-surface.yml@v2\n with:\n buildchain-ref: ${{ inputs.buildchain-ref || '' }}\n build-command: pnpm run build\n verify-command: pnpm run check\n artifact-path: dist\n```\n\nOnly trusted `workflow_dispatch` runs by repository actors with write,\nmaintain, or admin permission may use a non-empty runtime override. Train refs\nsuch as `train/v2/v2.3/site-source-of-truth` are temporary validation refs, not\nstable production dependencies or pending merge targets. They may remain for a\nretention window after release as a fast-use and rollback channel, with old\ntrains handled by periodic Buildchain cleanup. The web-surface deployment\nmanifest records the resolved runtime SHA as `runtimeId` and the stable\nrollback ref as `rollbackPointer`.\n\nThe workflow deliberately plans and emits manifests by default. Live mutation is\nopt-in per channel:\n\n```yaml\npermissions:\n contents: read\n id-token: write\n pull-requests: write\n\njobs:\n web-surface:\n uses: kungfu-systems/buildchain/.github/workflows/.web-surface.yml@v2\n with:\n build-command: pnpm run build\n verify-command: pnpm run check\n artifact-path: dist\n preview-apply: true\n preview-cleanup-apply: true\n preview-aws-role-arn: arn:aws:iam::123456789012:role/site-preview-github-actions\n staging-apply: true\n staging-aws-role-arn: arn:aws:iam::123456789012:role/site-staging-github-actions\n production-apply: false\n production-release-on-main: false\n production-aws-role-arn: arn:aws:iam::123456789012:role/site-production-github-actions\n production-environment: production\n release-feedback-actor-privacy: public\n```\n\nWhen enabled, Buildchain owns the full release apply state machine:\n\n- PR preview deploys run `deploy-apply --dry-run false` with the preview role\n and update a single idempotent PR comment.\n- Closed PR cleanup runs `cleanup-apply --dry-run false` with the preview role\n only.\n- Pushes to `main` run staging `deploy-apply --dry-run false` with the staging\n role, then write a staging release feedback passport artifact and comment the\n associated merged PR with the staging URL, source SHA, artifact identity, run\n URL, and failure context when apply did not complete.\n- When `production-release-on-main=true`, successful staging applies open or\n update a Buildchain-owned release PR from\n `release/<channel>-<short-sha>` to `main`, unless the current push already\n came from a matching release PR merge. The release PR contains one empty\n release-intent commit, carries `production-release-label`, and includes the\n staging URLs, source SHA, artifact hash, and staging release-passport artifact\n link in the PR body.\n- Production release PR handoff is permission-aware. Staging apply and staging\n health remain successful even when the repository or organization has\n GitHub Actions workflow permissions set to read-only. In that case Buildchain\n records `release-pr-status=permission-denied`, uploads the release PR handoff\n summary/body plus staging release passport artifacts, and writes an exact\n manual `gh pr create` command to the step summary. Set\n `fail-on-release-pr-error=true` only when PR creation failure should fail the\n whole workflow.\n- Release pull requests that match the configured production gate get a\n Buildchain review comment with the staging URL and production target, so the\n operator can verify staging from the PR page and use merge as the approval\n action. Consumers do not need to hand-write `gh pr create` or production\n release-intent glue.\n- Production runs when `production-apply` is true and either:\n - a trusted `workflow_dispatch` passes `production-approved=true`; or\n - `production-release-on-main=true` and the `main` push commit is associated\n with exactly one same-repository, merged release pull request matching\n `production-release-label` and `production-release-head-prefix`.\n The production job is then gated by the configured GitHub Environment.\n- Production apply writes a production release feedback passport artifact and\n comments the release PR with the production URL, source SHA, artifact\n identity, run URL, rollback pointer, and failure context when apply did not\n complete.\n\nThe feedback passport records the release responsibility chain:\n\n- human decision actor;\n- trigger actor;\n- runner/execution actor;\n- OIDC/deploy identity reference;\n- decision type and time;\n- source event, PR number, merge commit, and required gate label/head-prefix.\n\n`release-feedback-actor-privacy` controls actor values in the passport and\ncomments. `public` records GitHub actor names, `redacted` records only the actor\nrole, and `private-ref` records a stable private reference hash without exposing\nthe actor name.\n\nFor release-PR publishing, callers opt in explicitly:\n\n```yaml\njobs:\n web-surface:\n uses: kungfu-systems/buildchain/.github/workflows/.web-surface.yml@v2\n with:\n build-command: npm run build\n verify-command: npm run check\n artifact-path: dist\n production-apply: ${{ github.event_name == 'push' && github.ref_name == 'main' }}\n production-release-on-main: true\n production-release-label: buildchain-release\n production-release-head-prefix: release/\n production-release-branch-channel: production\n production-release-pr-mode: auto\n production-aws-role-arn: arn:aws:iam::123456789012:role/site-production-github-actions\n production-environment: production\n```\n\n`production-release-pr-mode` controls the post-staging handoff:\n\n| Mode | Behavior |\n| --- | --- |\n| `auto` | Generate release PR facts, create/update the empty release-intent branch and PR, and label it when token permissions allow. This is the default. |\n| `summary-only` | Generate and upload release PR facts, body, passport evidence, and manual command, but do not call the GitHub PR API. |\n| `disabled` | Record a disabled handoff and skip release PR API calls. |\n\nAutomatic release PR creation normally uses the workflow `github.token`.\nConsumers that cannot enable \"GitHub Actions can create and approve pull\nrequests\" globally should prefer the first-class GitHub App path. Pass the App\nclient id as an input and the private key as a reusable workflow secret; Buildchain\ncreates an installation token inside the release PR job and uses it only for the\nrelease-intent branch, PR, and label operations:\n\n```yaml\nwith:\n production-release-app-client-id: ${{ vars.KUNGFU_RELEASE_APP_CLIENT_ID }}\nsecrets:\n production-release-app-private-key: ${{ secrets.KUNGFU_RELEASE_APP_PRIVATE_KEY }}\n```\n\n`production-release-app-id` remains accepted as a deprecated alias for the input\nname, but the value should be the GitHub App client id. GitHub App numeric App\nIDs and client IDs are distinct, and Buildchain passes the value to\n`actions/create-github-app-token` through its non-deprecated `client-id` input so\nnew runs do not emit the deprecated `app-id` warning.\n\nIf a repository already generates its own narrow token or PAT, it can still pass\nthat through `production-release-pr-token`:\n\n```yaml\nwith:\n production-release-pr-token: ${{ secrets.BUILDCHAIN_RELEASE_PR_TOKEN }}\n```\n\nToken priority is: generated GitHub App installation token,\n`production-release-pr-token`, then `github.token`.\n\nThe merge button becomes the production approval only for a PR that carries the\nrelease label and comes from the configured source-branch prefix. Ordinary pull\nrequests merged into `main` deploy staging and open a release-intent PR; merging\nthat release PR triggers production. A release PR merge push does not open\nanother release PR.\n\nApply-only inputs are validated before the caller build or verification command\nruns. If the current event would run preview, staging, or production apply,\nmissing role inputs or a production apply without `production-approved=true`\non manual dispatch fail immediately instead of spending the build and plan jobs\nfirst.\n\nCallers must grant `id-token: write` for OIDC role assumption. Preview comments\nneed `pull-requests: write`. Automatic release PR creation also needs\n`contents: write`, `pull-requests: write`, and `issues: write` so Buildchain can\ncreate the release branch, write the empty release-intent commit, open or update\nthe PR, and apply the release label. If these permissions are unavailable,\nBuildchain degrades the release handoff instead of marking a successful staging\ndeployment as failed, unless `fail-on-release-pr-error=true`. The AWS roles remain caller-owned and\nshould be scoped by channel: preview can mutate only preview resources, staging\ncan mutate only staging resources, and production can mutate only production\nresources.\n\nApply mode fails closed when the deploy config still contains placeholder AWS\ntargets such as `pending-preview-distribution`. Planning can use placeholders\nfor dry-run-only design work, but live apply requires concrete bucket and\nCloudFront distribution identifiers.\n\n## Site Repository Shape\n\nA site repository can start with:\n\n```toml\nschema = 1\n\n[project]\ntype = \"web-surface\"\nname = \"site-kungfu-tech\"\nsite = \"kungfu-tech\"\n\n[lifecycle.build]\ncommand = \"pnpm run build\"\n\n[lifecycle.verify]\ncommand = \"pnpm run check\"\n```\n\nThen add the channel, deploy, retention, and security declarations shown above.\nThe project may use pnpm, npm, yarn, Vite, Astro, Next static export, Sphinx,\nMkDocs, CMake-generated docs, or another lifecycle command source. Buildchain\nonly needs a deterministic artifact path and the manifest facts.\n\n## Boundaries\n\nBuildchain only performs live AWS mutations in explicit apply modes with\n`--dry-run false`. Production deploys must still be gated by a human-controlled\nworkflow, release, or GitHub Environment. DNS changes, staging auth\nimplementation, CloudFront distribution creation, and credential provisioning\nremain explicitly authorized infrastructure operations outside the web-surface\nartifact apply contract."
|
|
1796
|
+
"markdown": "# Web-Surface Deployment Contract\n\nBuildchain supports `project.type = \"web-surface\"` for repositories that publish\nsites, docs, product pages, operator consoles, or browser apps. These projects\nneed auditable deployment semantics, but they are not package release lines and\nshould not be forced into `dev/alpha/release` version-state automation.\n\nThe release object for a web surface is:\n\n```text\nsource commit + build artifact + deploy target + channel + deployment manifest\n```\n\nThis keeps the evidence chain clear:\n\n- the source SHA explains what code was built;\n- the artifact hash explains exactly what was deployed;\n- the channel explains who can see it and whether it is promotable;\n- the deploy target and adapter explain where it would be published;\n- the deployment manifest records retention, rollback, security, and secret\n reference metadata.\n\n## Configuration\n\n`.buildchain/buildchain.toml` is the source of truth. Web-surface projects must declare\npreview, staging, and production channels plus a deploy adapter for each.\n\n```toml\nschema = 1\n\n[project]\ntype = \"web-surface\"\nname = \"site-kungfu-tech\"\nsite = \"kungfu-tech\"\n\n[channels.preview]\nurl_pattern = \"https://{alias}.preview.kungfu.tech\"\nvisibility = \"ephemeral\"\nrequires_auth = false\nnoindex = true\n\n[channels.staging]\nurl = \"https://staging.kungfu.tech\"\nvisibility = \"protected\"\naccess_control = \"managed-network\"\nedge_auth = \"none\"\nnoindex = true\npromotable = true\n\n[channels.production]\nurl = \"https://kungfu.tech\"\nvisibility = \"public\"\ncanonical = true\nnoindex = false\n\n[deploy.preview]\nadapter = \"aws-s3-cloudfront\"\nbucket = \"kungfu-tech-preview\"\ncloudfront_distribution = \"E-PREVIEW\"\nartifact_path = \"dist\"\nsecret_refs = [\"AWS_ROLE_ARN\"]\n# Optional. Defaults to \"buildchain\".\n# Use \"external\" when an existing viewer-request CloudFront Function already\n# owns preview alias, surface-prefix, and directory-index routing.\ndirectory_index_rewrite = \"buildchain\"\n```\n\n### Multi-Surface Host Mapping\n\nSome site repositories publish more than one first-class web surface from the\nsame artifact. For example, `site-libkungfu-dev` has a hub plus separate\nhostnames for core, Buildchain, and Kung Fu Decisions. These are not just\nnavigation paths; staging, production preflight, and post-deploy health checks\nmust verify host-level behavior for each surface.\n\nDeclare named surfaces with per-channel URLs:\n\n```toml\n[surfaces.hub]\npath = \"/\"\nproduction_url = \"https://libkungfu.dev\"\nstaging_url = \"https://staging.libkungfu.dev\"\npreview_url_pattern = \"https://{alias}.preview.libkungfu.dev\"\n\n[surfaces.core]\npath = \"/core/\"\nproduction_url = \"https://core.libkungfu.dev\"\nstaging_url = \"https://core.staging.libkungfu.dev\"\npreview_url_pattern = \"https://core-{alias}.preview.libkungfu.dev\"\n\n[surfaces.buildchain]\npath = \"/buildchain/\"\nproduction_url = \"https://buildchain.libkungfu.dev\"\nstaging_url = \"https://buildchain.staging.libkungfu.dev\"\npreview_url_pattern = \"https://buildchain-{alias}.preview.libkungfu.dev\"\n\n[surfaces.kfd]\npath = \"/kfd/\"\nproduction_url = \"https://kfd.libkungfu.dev\"\nstaging_url = \"https://kfd.staging.libkungfu.dev\"\npreview_url_pattern = \"https://kfd-{alias}.preview.libkungfu.dev\"\n```\n\nBuildchain resolves every `(channel, surface)` pair. A preview alias such as\n`pr-12` becomes:\n\n```text\nhub: https://pr-12.preview.libkungfu.dev\ncore: https://core-pr-12.preview.libkungfu.dev\nbuildchain: https://buildchain-pr-12.preview.libkungfu.dev\nkfd: https://kfd-pr-12.preview.libkungfu.dev\n```\n\nWhen `surfaces` is omitted, Buildchain preserves the legacy single-surface\ncontract by creating an implicit `default` surface from the channel URL. When a\nsurface is intentionally path-only, declare it explicitly:\n\n```toml\n[surfaces.docs]\npath = \"/docs/\"\npath_only = true\n```\n\n`path_only = true` is an exception, not the default. Without it, every named\nsurface must declare `preview_url_pattern`, `staging_url`, and\n`production_url`. This makes staging/production mismatches fail during\nvalidation instead of becoming invisible deploy drift.\n\nAdapter strategy remains explicit. The default `aws-s3-cloudfront` plan uses the\nchannel deploy target for every surface, and each binding records its own\nbucket, distribution id, object prefix, manifest key, source path, and URL. A\nchannel can override target details per surface:\n\n```toml\n[deploy.staging.surfaces.core]\nbucket = \"libkungfu-dev-core-staging\"\ncloudfront_distribution = \"E-CORE-STAGING\"\norigin_path = \"/core\"\n```\n\nBuildchain validates these hard constraints:\n\n- `channels.preview.url_pattern` is required and must contain the alias shape\n used by preview deployments.\n- `channels.staging.access_control` must protect staging. Supported modes are\n `managed-network`, `edge-basic-auth`, `oidc`, and `app-auth`.\n- `channels.staging.edge_auth` records whether the edge layer owns auth. Use\n `edge_auth = \"none\"` when staging is protected by managed network controls\n such as WAF/IP allowlists or VPN access.\n- `channels.staging.noindex = true` is required.\n- `channels.production.url` is required.\n- deploy adapters must be declared per channel.\n- named surfaces must declare first-class URLs for every channel unless\n `path_only = true` is explicitly set.\n- secret material must be declared as reference names, such as\n `secret_refs = [\"AWS_ROLE_ARN\"]`; inline secret-like deploy keys are rejected.\n\n### Floating Runtime Contract Lock\n\nWeb-surface repositories can consume the stable Buildchain workflow shell with a\nfloating ref, such as:\n\n```yaml\njobs:\n web:\n uses: kungfu-systems/buildchain/.github/workflows/.web-surface.yml@v2\n with:\n buildchain-contract-lock-path: .buildchain/contract-lock.json\n buildchain-contract-compatibility-policy: major-compatible\n buildchain-contract-drift-issue-mode: compatible-and-breaking\n build-command: pnpm build\n artifact-path: dist\n```\n\nThe caller repository commits `.buildchain/contract-lock.json` after reviewing an\naccepted Buildchain runtime SHA and contract digest. The reusable workflow then\nresolves the floating runtime to an immutable SHA, checks the lock before the\ncaller build command, and applies these rules:\n\n- unchanged lock: continue without feedback;\n- compatible drift: continue, write the drift summary, and open or update a\n caller-repository issue when permissions allow;\n- breaking drift: fail closed before rendering, deployment planning, deploy\n apply, or release publication.\n\nThe caller no longer needs to run `scripts/buildchain-contract-lock.mjs` inside\nits own build command. That check belongs to Buildchain because the actual\ncontract world is stored in the Buildchain runtime ref being used.\n\nSupported adapter names are:\n\n| Adapter | Initial use |\n| --- | --- |\n| `aws-s3-cloudfront` | Static site artifact sync plus CDN invalidation plan |\n| `aws-elastic-beanstalk` | Future dynamic app environment adapter |\n| `aws-ecs-service` | Future dynamic service adapter |\n\nThe channel ontology is independent of the adapter. A future dynamic staging\nenvironment still remains `channel = \"staging\"` with protected/noindex/security\nrequirements.\n\n## Preview Aliases\n\nPreview uses subdomains, not path prefixes:\n\n```text\nhttps://pr-123.preview.kungfu.tech\nhttps://sha-abcdef123456.preview.kungfu.tech\n```\n\nAlias semantics are explicit:\n\n| Alias | Meaning | Mutable | Retention |\n| --- | --- | --- | --- |\n| `pr-123` | Current preview for a pull request | yes | short-lived |\n| `sha-abcdef123456` | Immutable preview for one source SHA | no | longer-lived |\n\nThis allows PR comments to stay stable while preserving immutable evidence for a\nspecific source commit.\n\n## Deployment Manifest\n\nBuildchain emits a manifest with the deployment facts that matter for audit and\nrollback:\n\n```json\n{\n \"schemaVersion\": 1,\n \"contract\": \"kungfu-buildchain-web-surface-deployment\",\n \"site\": \"libkungfu-dev\",\n \"channel\": \"preview\",\n \"alias\": \"sha-abcdef123456\",\n \"url\": \"https://sha-abcdef123456.preview.libkungfu.dev\",\n \"generatedAt\": \"2026-07-01T00:00:00.000Z\",\n \"publishedAt\": \"2026-07-01T00:00:00.000Z\",\n \"reproducible\": true,\n \"timestampPolicy\": \"ci-injected\",\n \"deterministicInputs\": [\n \"web-surface artifact content\",\n \"buildchain.toml web-surface channels/deploy/surfaces\",\n \"sourceSha\",\n \"artifactHash\",\n \"deployment channel\",\n \"deployment alias\"\n ],\n \"sourceRevision\": \"...\",\n \"timestampPolicyDetails\": {\n \"contract\": \"kungfu-buildchain-surface-timestamp-policy\",\n \"timestampFields\": [\"generatedAt\", \"publishedAt\", \"deployedAt\"],\n \"timestampFieldsParticipateInArtifactDigest\": false,\n \"artifactDigestScope\": \"web-surface artifactHash excludes deployment manifest timestamps\"\n },\n \"sourceSha\": \"...\",\n \"artifactHash\": \"...\",\n \"deployTarget\": \"libkungfu-dev-preview\",\n \"adapter\": \"aws-s3-cloudfront\",\n \"deployedAt\": \"2026-07-01T00:00:00.000Z\",\n \"retentionClass\": \"preview-sha-immutable\",\n \"expiresAt\": \"2026-09-29T00:00:00.000Z\",\n \"accessControl\": \"none\",\n \"edgeAuth\": \"none\",\n \"noindex\": true,\n \"secretRefs\": [\"AWS_ROLE_ARN\"],\n \"surfaceBindings\": [\n {\n \"surface\": \"hub\",\n \"channel\": \"preview\",\n \"alias\": \"sha-abcdef123456\",\n \"url\": \"https://sha-abcdef123456.preview.libkungfu.dev\",\n \"sourcePath\": \"/\",\n \"artifactPathPrefix\": \"\",\n \"viewerPathPrefix\": \"/\",\n \"directoryIndex\": \"index.html\",\n \"directoryIndexResolution\": true,\n \"canonicalUrl\": \"https://libkungfu.dev\",\n \"bucket\": \"libkungfu-dev-preview\",\n \"distributionId\": \"E-PREVIEW\",\n \"originPath\": \"\",\n \"objectPrefix\": \"sha-abcdef123456\",\n \"manifestKey\": \".buildchain/deployments/sha-abcdef123456/hub.json\",\n \"routing\": {\n \"contract\": \"kungfu-buildchain-web-surface-path-prefix-rewrite\",\n \"viewerPathPrefix\": \"/\",\n \"artifactPathPrefix\": \"\",\n \"objectPrefix\": \"sha-abcdef123456\",\n \"directoryIndex\": \"index.html\",\n \"directoryIndexResolution\": true\n },\n \"smokeUrls\": [\n {\n \"kind\": \"root\",\n \"requestPath\": \"/\",\n \"url\": \"https://sha-abcdef123456.preview.libkungfu.dev/\",\n \"required\": true\n }\n ],\n \"noindex\": true,\n \"accessControl\": \"none\"\n }\n ]\n}\n```\n\nDynamic adapters can also fill `runtimeId`, `configFingerprint`,\n`healthCheck`, `migrationState`, `rollbackPointer`, and\n`rollbackLimitations`. Buildchain records secret reference names only, never\nsecret values.\n\nThe timestamp policy is shared with package site bundles. Public deployment\nmanifests should expose real workflow generation/publication times while\nseparately declaring why the deployed artifact remains reproducible. For\nweb-surface deployment manifests, `artifactHash` is the static site artifact\ndigest and does not include deployment timestamp fields; the manifest itself\nstill records those fields for human and agent audit.\n\n## Deploy Plans\n\nDeploy planning is the default behavior. It plans the adapter steps and writes\nmanifest JSON, but it does not touch AWS, DNS, CloudFront, or deployment\ncredentials.\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode deploy-plan \\\n --cwd fixtures/web-surface-shaped \\\n --source-sha aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \\\n --alias sha-aaaaaaaaaaaa\n```\n\nFor manifest-only output:\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode manifest \\\n --cwd fixtures/web-surface-shaped \\\n --source-sha aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \\\n --alias pr-123 \\\n --output .buildchain/web-surface-manifest.json\n```\n\nThe CLI emits GitHub outputs when `GITHUB_OUTPUT` is present:\n\n- `web-surface-channel`\n- `web-surface-alias`\n- `web-surface-url`\n- `web-surface-urls-json`\n- `web-surface-artifact-hash`\n- `web-surface-manifest-json`\n\n## Explicit Apply\n\n`deploy-apply` and `cleanup-apply` are explicit execution modes for the\n`aws-s3-cloudfront` static-site adapter. They still default to `--dry-run true`;\nlive AWS mutation requires `--dry-run false`.\n\nDeploy apply syncs the artifact, writes the deployment manifest, and invalidates\nCloudFront when a distribution id is configured:\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode deploy-apply \\\n --cwd fixtures/web-surface-shaped \\\n --channel staging \\\n --source-sha aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \\\n --artifact-path dist \\\n --dry-run false \\\n --output .buildchain/web-surface-staging-apply.json\n```\n\nFor multi-surface sites, each surface host is treated as a root-relative view\nof that surface's artifact path prefix. For example, a `buildchain` surface with\n`path = \"/buildchain/\"` and preview URL\n`https://buildchain-pr-29.preview.libkungfu.dev` syncs the artifact subtree\n`dist/buildchain/` to the preview object prefix `pr-29/buildchain`. A viewer\nrequest for `https://buildchain-pr-29.preview.libkungfu.dev/docs/` therefore\nresolves against the artifact's `dist/buildchain/docs/index.html`, not\n`dist/docs/index.html` and not the hub surface root. The deployment manifest\nrecords this as `routing.contract =\n\"kungfu-buildchain-web-surface-path-prefix-rewrite\"` with\n`viewerPathPrefix = \"/\"`, `artifactPathPrefix = \"buildchain\"`, and\n`directoryIndexResolution = true`.\n\nWhen a surface uses an S3 object prefix, directory-index routing must be handled\nat the viewer-request layer. By default, `directory_index_rewrite =\n\"buildchain\"` makes Buildchain install or update one CloudFront Function per\ndistribution before uploading payloads. The function rewrites any request path\nending in `/` to the corresponding `index.html`, so\n`https://buildchain-pr-29.preview.libkungfu.dev/` resolves to\n`pr-29/buildchain/index.html` and\n`https://buildchain-pr-29.preview.libkungfu.dev/docs/` resolves to\n`pr-29/buildchain/docs/index.html`. This keeps multi-host preview roots\ncompatible with S3 REST origins, where copying alias objects such as\n`pr-29/buildchain` or `pr-29/buildchain/` is not a reliable substitute for an\nedge rewrite.\n\nIf the distribution already has a viewer-request function that owns preview\nalias routing and surface-prefix routing, set `directory_index_rewrite =\n\"external\"` on the deploy channel or surface override. In that mode Buildchain\ndoes not create, update, or attach a generic directory-index function. Instead,\nthe deployment manifest records `directoryIndexRewrite = \"external\"` and\n`directoryIndexStrategy = \"external-viewer-request-function\"`, then the normal\nhealth check still verifies every required root and nested surface URL. This is\nthe correct contract for shared preview distributions such as\n`site-libkungfu-dev`, where a generic function cannot replace the existing\nprefix router.\n\nBuildchain still writes directory-index alias objects during apply as\ncompatibility evidence, but root correctness comes from the viewer-request\nrewrite contract, not from extensionless S3 keys. If Buildchain-managed mode\nfinds a distribution with a different viewer-request function, apply fails\nclosed and records that conflict in the apply result instead of silently serving\n403s. The reusable workflow uploads `buildchain-web-surface-*-diagnostics`\nartifacts containing the apply and health JSON so the failing AWS operation or\nHTTP check is visible from the consumer run.\n\nIt can also execute a previously saved deploy plan. In that mode Buildchain\nrecomputes the local artifact hash before running AWS commands and fails closed\nif the artifact no longer matches the saved plan:\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode deploy-apply \\\n --cwd fixtures/web-surface-shaped \\\n --plan .buildchain/web-surface-staging-plan.json \\\n --dry-run false \\\n --output .buildchain/web-surface-staging-apply.json\n```\n\nCleanup apply deletes preview content, deletes the preview manifest, and\ninvalidates CloudFront:\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode cleanup-apply \\\n --cwd fixtures/web-surface-shaped \\\n --event pull-request-closed \\\n --pull-number 123 \\\n --source-sha aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \\\n --dry-run false \\\n --output .buildchain/web-surface-cleanup-apply.json\n```\n\nCleanup apply can also execute a saved cleanup plan:\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode cleanup-apply \\\n --cwd fixtures/web-surface-shaped \\\n --plan .buildchain/web-surface-cleanup-plan.json \\\n --dry-run false \\\n --output .buildchain/web-surface-cleanup-apply.json\n```\n\nApply output records the channel, alias, source SHA, artifact hash, target\nbucket, object prefix, manifest key, all surface URLs, all surface bindings, CDN\ninvalidation paths, actor/run metadata, and every adapter operation with\n`executed`, `exitCode`, `stdout`, and `stderr`. If an operation fails,\nBuildchain records the failed operation, stops subsequent adapter operations,\nand exits non-zero after writing the result JSON. Buildchain records secret\nreference names only; the runner must provide the AWS CLI and credentials\noutside Buildchain, typically through OIDC and the declared `secret_refs`.\n\n## Production Preflight And Health\n\nProduction promotion is not just `deploy-apply --channel production`. Before a\nlive production apply, the reusable workflow runs:\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode production-preflight \\\n --cwd fixtures/web-surface-shaped \\\n --plan .buildchain/web-surface-production-plan.json \\\n --execute true \\\n --output .buildchain/web-surface-production-preflight.json\n```\n\nThe production preflight checks that:\n\n- `channels.production` is canonical and indexable;\n- every surface has concrete production bucket and CloudFront targets;\n- every production surface URL is HTTPS;\n- the production AWS role can inspect the declared bucket and distribution;\n- CloudFront aliases cover every surface host, including product hosts such as\n `kfd.libkungfu.dev`;\n- DNS resolves for every surface host.\n\nAfter preview, staging, and production apply, the workflow runs:\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode health-check \\\n --cwd fixtures/web-surface-shaped \\\n --result .buildchain/web-surface-production-apply.json \\\n --output .buildchain/web-surface-production-health.json\n```\n\nWhen the apply result includes CloudFront invalidations from the\n`aws-s3-cloudfront` adapter, health check waits for those invalidations to reach\n`Completed` before fetching public smoke URLs. This avoids reporting stale\nCloudFront 403 responses as deployment failures immediately after a successful\nS3 sync and invalidation request.\n\nAfter the invalidation wait, HTTP smoke checks also retry transient 403, 404, and\n5xx responses before recording failure. This keeps the public health signal\nstrict while absorbing short CloudFront edge propagation windows that can remain\nvisible for a few seconds after the invalidation waiter returns.\n\nThe health check fetches every surface root URL and any nested smoke URLs\nrecorded in each surface binding. Nested smoke URLs are derived from nested HTML\nartifact files under the surface path prefix, with directory index resolution\nsuch as `dist/buildchain/docs/index.html` becoming `/docs/` on the buildchain\npreview host. If a surface has no nested HTML route, Buildchain records only\nthe root smoke URL; absence of nested HTML is not a deployment failure. When a\nnested route is present, the check fails closed if a deploy reports success but\nthat child page returns 403 or another unexpected status. Surface root checks\nexpect the apply result to have installed the directory-index rewrite, so a\nmulti-host preview root such as `https://buildchain-pr-29.preview.libkungfu.dev/`\nmust resolve to the surface `index.html`, not the bare prefix directory.\nProduction additionally fails if a response is unreachable, returns an\nunexpected status, or still sends `x-robots-tag: noindex`. The health check also\nverifies that each surface binding recorded a deployment manifest pointer. The\nproduction release passport embeds the deploy plan, apply result, production\npreflight, and health check so a reviewer or agent can audit why the production\nsite changed and whether every declared host and every existing nested route was\nactually covered.\n\nChannels declared with `access_control = \"managed-network\"` use a different\nhealth strategy by default. Buildchain does not require a GitHub-hosted runner\nto fetch a URL that is intentionally reachable only from an approved network.\nInstead, after a live apply the health check uses the deploy role to run S3\n`head-object` checks for each surface manifest and the smoke target object, such\nas the surface `index.html` or a nested `docs/index.html`. The check records\n`healthStrategy = \"s3-object\"` and skips the public HTTP fetch. Dry-run and\nplan-only checks fall back to deployment evidence: each surface must have a\nmanifest key, bucket, object prefix, `sync-static-artifact`, and\n`write-deployment-manifest` evidence, recorded as\n`healthStrategy = \"deployment-evidence\"`. If the workflow is running on a runner\nthat is allowed to reach the managed network, set\n`BUILDCHAIN_WEB_SURFACE_HEALTH_ALLOWED_RUNNER=true` or pass\n`--allowed-managed-network-runner true` to keep the normal HTTP smoke checks.\nSet `BUILDCHAIN_WEB_SURFACE_HEALTH_S3_OBJECTS=false` or pass\n`--managed-network-s3-object-verification false` only when an external channel\npolicy owns managed-network object verification.\n\n## Cleanup Plans\n\nPreview cleanup is an auditable cleanup contract. It can run as a dry-run plan,\nan apply-mode plan, or the explicit `cleanup-apply` executor with preview-only\ncredentials:\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode cleanup-plan \\\n --cwd fixtures/web-surface-shaped \\\n --event pull-request-closed \\\n --pull-number 123 \\\n --aliases pr-123,sha-abcdef123456\n```\n\nThe plan and apply result keep mutable PR aliases and immutable SHA aliases\ndistinct so a caller can expire them with different retention windows. Closed-PR\ncleanup can derive `pr-N` from `--pull-number`, records the event, source SHA,\nactor, run id, preview bucket/prefix, manifest key, and adapter steps, and is an\nauditable no-op when no aliases are requested.\n\n## Reusable Workflow Shape\n\nBuildchain ships `.github/workflows/.web-surface.yml` for repositories that want\nthe standard PR review and promotion flow without copying bespoke glue:\n\n```yaml\njobs:\n web-surface:\n uses: kungfu-systems/buildchain/.github/workflows/.web-surface.yml@v2\n with:\n build-command: npm run build\n verify-command: npm run check\n artifact-path: dist\n```\n\nThe reusable workflow maps GitHub events to Buildchain web-surface semantics:\n\n| Event | Buildchain behavior |\n| --- | --- |\n| `pull_request` opened / synchronized / reopened | validate, build, verify, and plan `preview` for `pr-N` |\n| `pull_request` closed | plan apply-mode cleanup for the `pr-N` preview alias and manifest |\n| `push` to `main` | validate, build, verify, plan and apply `staging` from the merged `main` SHA, then optionally open a production release PR |\n| `push` to `main` from a matching release PR merge | validate the associated release PR, plan `production`, and enter the configured GitHub Environment gate |\n| `workflow_dispatch` with `production-approved = true` | plan `production` and enter the configured GitHub Environment gate |\n\nThe optional `buildchain-ref` input is empty by default. Empty keeps the\nweb-surface run on the stable Buildchain runtime selected by the reusable\nworkflow ref, normally `@v2`. A trusted maintainer can expose a\n`workflow_dispatch` input and pass it through for one-off train validation.\nSee [`runtime-train-validation.md`](runtime-train-validation.md) for the shared\ntrain protocol and notification template:\n\n```yaml\non:\n workflow_dispatch:\n inputs:\n buildchain-ref:\n description: \"Temporary Buildchain runtime ref for trusted manual validation\"\n required: false\n default: \"\"\n\njobs:\n web-surface:\n uses: kungfu-systems/buildchain/.github/workflows/.web-surface.yml@v2\n with:\n buildchain-ref: ${{ inputs.buildchain-ref || '' }}\n build-command: pnpm run build\n verify-command: pnpm run check\n artifact-path: dist\n```\n\nOnly trusted `workflow_dispatch` runs by repository actors with write,\nmaintain, or admin permission may use a non-empty runtime override. Train refs\nsuch as `train/v2/v2.3/site-source-of-truth` are temporary validation refs, not\nstable production dependencies or pending merge targets. They may remain for a\nretention window after release as a fast-use and rollback channel, with old\ntrains handled by periodic Buildchain cleanup. The web-surface deployment\nmanifest records the resolved runtime SHA as `runtimeId` and the stable\nrollback ref as `rollbackPointer`.\n\nThe workflow deliberately plans and emits manifests by default. Live mutation is\nopt-in per channel:\n\n```yaml\npermissions:\n contents: read\n id-token: write\n pull-requests: write\n\njobs:\n web-surface:\n uses: kungfu-systems/buildchain/.github/workflows/.web-surface.yml@v2\n with:\n build-command: pnpm run build\n verify-command: pnpm run check\n artifact-path: dist\n preview-apply: true\n preview-cleanup-apply: true\n preview-aws-role-arn: arn:aws:iam::123456789012:role/site-preview-github-actions\n staging-apply: true\n staging-aws-role-arn: arn:aws:iam::123456789012:role/site-staging-github-actions\n production-apply: false\n production-release-on-main: false\n production-aws-role-arn: arn:aws:iam::123456789012:role/site-production-github-actions\n production-environment: production\n release-feedback-actor-privacy: public\n```\n\nWhen enabled, Buildchain owns the full release apply state machine:\n\n- PR preview deploys run `deploy-apply --dry-run false` with the preview role\n and update a single idempotent PR comment.\n- Closed PR cleanup runs `cleanup-apply --dry-run false` with the preview role\n only.\n- Pushes to `main` run staging `deploy-apply --dry-run false` with the staging\n role, then write a staging release feedback passport artifact and comment the\n associated merged PR with the staging URL, source SHA, artifact identity, run\n URL, and failure context when apply did not complete.\n- When `production-release-on-main=true`, successful staging applies open or\n update a Buildchain-owned release PR from\n `release/<channel>-<short-sha>` to `main`, unless the current push already\n came from a matching release PR merge. The release PR contains one empty\n release-intent commit, carries `production-release-label`, and includes the\n staging URLs, source SHA, artifact hash, and staging release-passport artifact\n link in the PR body.\n- Production release PR handoff is permission-aware. Staging apply and staging\n health remain successful even when the repository or organization has\n GitHub Actions workflow permissions set to read-only. In that case Buildchain\n records `release-pr-status=permission-denied`, uploads the release PR handoff\n summary/body plus staging release passport artifacts, and writes an exact\n manual `gh pr create` command to the step summary. Set\n `fail-on-release-pr-error=true` only when PR creation failure should fail the\n whole workflow.\n- Release pull requests that match the configured production gate get a\n Buildchain review comment with the staging URL and production target, so the\n operator can verify staging from the PR page and use merge as the approval\n action. Consumers do not need to hand-write `gh pr create` or production\n release-intent glue.\n- Production runs when `production-apply` is true and either:\n - a trusted `workflow_dispatch` passes `production-approved=true`; or\n - `production-release-on-main=true` and the `main` push commit is associated\n with exactly one same-repository, merged release pull request matching\n `production-release-label` and `production-release-head-prefix`.\n The production job is then gated by the configured GitHub Environment.\n- Production apply writes a production release feedback passport artifact and\n comments the release PR with the production URL, source SHA, artifact\n identity, run URL, rollback pointer, and failure context when apply did not\n complete.\n\nThe feedback passport records the release responsibility chain:\n\n- human decision actor;\n- trigger actor;\n- runner/execution actor;\n- OIDC/deploy identity reference;\n- decision type and time;\n- source event, PR number, merge commit, and required gate label/head-prefix.\n\n`release-feedback-actor-privacy` controls actor values in the passport and\ncomments. `public` records GitHub actor names, `redacted` records only the actor\nrole, and `private-ref` records a stable private reference hash without exposing\nthe actor name.\n\nFor release-PR publishing, callers opt in explicitly:\n\n```yaml\njobs:\n web-surface:\n uses: kungfu-systems/buildchain/.github/workflows/.web-surface.yml@v2\n with:\n build-command: npm run build\n verify-command: npm run check\n artifact-path: dist\n production-apply: ${{ github.event_name == 'push' && github.ref_name == 'main' }}\n production-release-on-main: true\n production-release-label: buildchain-release\n production-release-head-prefix: release/\n production-release-branch-channel: production\n production-release-pr-mode: auto\n production-aws-role-arn: arn:aws:iam::123456789012:role/site-production-github-actions\n production-environment: production\n```\n\n`production-release-pr-mode` controls the post-staging handoff:\n\n| Mode | Behavior |\n| --- | --- |\n| `auto` | Generate release PR facts, create/update the empty release-intent branch and PR, and label it when token permissions allow. This is the default. |\n| `summary-only` | Generate and upload release PR facts, body, passport evidence, and manual command, but do not call the GitHub PR API. |\n| `disabled` | Record a disabled handoff and skip release PR API calls. |\n\nAutomatic release PR creation normally uses the workflow `github.token`.\nConsumers that cannot enable \"GitHub Actions can create and approve pull\nrequests\" globally should prefer the first-class GitHub App path. Pass the App\nclient id as an input and the private key as a reusable workflow secret; Buildchain\ncreates an installation token inside the release PR job and uses it only for the\nrelease-intent branch, PR, and label operations:\n\n```yaml\nwith:\n production-release-app-client-id: ${{ vars.KUNGFU_RELEASE_APP_CLIENT_ID }}\nsecrets:\n production-release-app-private-key: ${{ secrets.KUNGFU_RELEASE_APP_PRIVATE_KEY }}\n```\n\n`production-release-app-id` remains accepted as a deprecated alias for the input\nname, but the value should be the GitHub App client id. GitHub App numeric App\nIDs and client IDs are distinct, and Buildchain passes the value to\n`actions/create-github-app-token` through its non-deprecated `client-id` input so\nnew runs do not emit the deprecated `app-id` warning.\n\nIf a repository already generates its own narrow token or PAT, it can still pass\nthat through `production-release-pr-token`:\n\n```yaml\nwith:\n production-release-pr-token: ${{ secrets.BUILDCHAIN_RELEASE_PR_TOKEN }}\n```\n\nToken priority is: generated GitHub App installation token,\n`production-release-pr-token`, then `github.token`.\n\nThe merge button becomes the production approval only for a PR that carries the\nrelease label and comes from the configured source-branch prefix. Ordinary pull\nrequests merged into `main` deploy staging and open a release-intent PR; merging\nthat release PR triggers production. A release PR merge push does not open\nanother release PR.\n\nApply-only inputs are validated before the caller build or verification command\nruns. If the current event would run preview, staging, or production apply,\nmissing role inputs or a production apply without `production-approved=true`\non manual dispatch fail immediately instead of spending the build and plan jobs\nfirst.\n\nCallers must grant `id-token: write` for OIDC role assumption. Preview comments\nneed `pull-requests: write`. Automatic release PR creation also needs\n`contents: write`, `pull-requests: write`, and `issues: write` so Buildchain can\ncreate the release branch, write the empty release-intent commit, open or update\nthe PR, and apply the release label. If these permissions are unavailable,\nBuildchain degrades the release handoff instead of marking a successful staging\ndeployment as failed, unless `fail-on-release-pr-error=true`. The AWS roles remain caller-owned and\nshould be scoped by channel: preview can mutate only preview resources, staging\ncan mutate only staging resources, and production can mutate only production\nresources.\n\nApply mode fails closed when the deploy config still contains placeholder AWS\ntargets such as `pending-preview-distribution`. Planning can use placeholders\nfor dry-run-only design work, but live apply requires concrete bucket and\nCloudFront distribution identifiers.\n\n## Site Repository Shape\n\nA site repository can start with:\n\n```toml\nschema = 1\n\n[project]\ntype = \"web-surface\"\nname = \"site-kungfu-tech\"\nsite = \"kungfu-tech\"\n\n[lifecycle.build]\ncommand = \"pnpm run build\"\n\n[lifecycle.verify]\ncommand = \"pnpm run check\"\n```\n\nThen add the channel, deploy, retention, and security declarations shown above.\nThe project may use pnpm, npm, yarn, Vite, Astro, Next static export, Sphinx,\nMkDocs, CMake-generated docs, or another lifecycle command source. Buildchain\nonly needs a deterministic artifact path and the manifest facts.\n\n## Boundaries\n\nBuildchain only performs live AWS mutations in explicit apply modes with\n`--dry-run false`. Production deploys must still be gated by a human-controlled\nworkflow, release, or GitHub Environment. DNS changes, staging auth\nimplementation, CloudFront distribution creation, and credential provisioning\nremain explicitly authorized infrastructure operations outside the web-surface\nartifact apply contract."
|
|
1797
1797
|
},
|
|
1798
1798
|
{
|
|
1799
1799
|
"id": "fixture:infra-contract-aws-cdk-shaped",
|
|
@@ -2382,7 +2382,7 @@
|
|
|
2382
2382
|
"path": "docs/web-surface-deployments.md",
|
|
2383
2383
|
"plane": "use",
|
|
2384
2384
|
"exists": true,
|
|
2385
|
-
"digest": "sha256:
|
|
2385
|
+
"digest": "sha256:b24b9e6f23ad13ccd3a1074a2ecefd26abdd7f848e979dea64b96678f57f0d6d"
|
|
2386
2386
|
}
|
|
2387
2387
|
],
|
|
2388
2388
|
"releaseModel": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"contract": "kungfu-buildchain-public-surface-reverse-audit",
|
|
22
22
|
"path": "dist/site/public-surface-audit.json",
|
|
23
23
|
"status": "passed",
|
|
24
|
-
"sha256": "
|
|
24
|
+
"sha256": "b15fc110e1ee5200c949e70b729c3061a981ceb1e886e7c003a7e6826e38dee7",
|
|
25
25
|
"summary": {
|
|
26
26
|
"cliCommandCount": 74,
|
|
27
27
|
"workflowCount": 37,
|
|
@@ -208,7 +208,7 @@
|
|
|
208
208
|
"contract": "kungfu-buildchain-public-surface-reverse-audit",
|
|
209
209
|
"path": "dist/site/public-surface-audit.json",
|
|
210
210
|
"status": "passed",
|
|
211
|
-
"sha256": "
|
|
211
|
+
"sha256": "b15fc110e1ee5200c949e70b729c3061a981ceb1e886e7c003a7e6826e38dee7",
|
|
212
212
|
"summary": {
|
|
213
213
|
"cliCommandCount": 74,
|
|
214
214
|
"workflowCount": 37,
|
|
@@ -355,7 +355,7 @@
|
|
|
355
355
|
"title": "Web surface deployments",
|
|
356
356
|
"path": "docs/web-surface-deployments.md",
|
|
357
357
|
"plane": "use",
|
|
358
|
-
"digest": "sha256:
|
|
358
|
+
"digest": "sha256:b24b9e6f23ad13ccd3a1074a2ecefd26abdd7f848e979dea64b96678f57f0d6d",
|
|
359
359
|
"capabilityGroup": "site-and-propagation",
|
|
360
360
|
"audience": [
|
|
361
361
|
"site",
|
|
@@ -1646,7 +1646,7 @@
|
|
|
1646
1646
|
],
|
|
1647
1647
|
"maturity": "stable",
|
|
1648
1648
|
"sourcePath": "docs/web-surface-deployments.md",
|
|
1649
|
-
"digest": "sha256:
|
|
1649
|
+
"digest": "sha256:b24b9e6f23ad13ccd3a1074a2ecefd26abdd7f848e979dea64b96678f57f0d6d",
|
|
1650
1650
|
"headings": [
|
|
1651
1651
|
{
|
|
1652
1652
|
"level": 1,
|
|
@@ -1729,7 +1729,7 @@
|
|
|
1729
1729
|
"anchor": "boundaries"
|
|
1730
1730
|
}
|
|
1731
1731
|
],
|
|
1732
|
-
"markdown": "# Web-Surface Deployment Contract\n\nBuildchain supports `project.type = \"web-surface\"` for repositories that publish\nsites, docs, product pages, operator consoles, or browser apps. These projects\nneed auditable deployment semantics, but they are not package release lines and\nshould not be forced into `dev/alpha/release` version-state automation.\n\nThe release object for a web surface is:\n\n```text\nsource commit + build artifact + deploy target + channel + deployment manifest\n```\n\nThis keeps the evidence chain clear:\n\n- the source SHA explains what code was built;\n- the artifact hash explains exactly what was deployed;\n- the channel explains who can see it and whether it is promotable;\n- the deploy target and adapter explain where it would be published;\n- the deployment manifest records retention, rollback, security, and secret\n reference metadata.\n\n## Configuration\n\n`.buildchain/buildchain.toml` is the source of truth. Web-surface projects must declare\npreview, staging, and production channels plus a deploy adapter for each.\n\n```toml\nschema = 1\n\n[project]\ntype = \"web-surface\"\nname = \"site-kungfu-tech\"\nsite = \"kungfu-tech\"\n\n[channels.preview]\nurl_pattern = \"https://{alias}.preview.kungfu.tech\"\nvisibility = \"ephemeral\"\nrequires_auth = false\nnoindex = true\n\n[channels.staging]\nurl = \"https://staging.kungfu.tech\"\nvisibility = \"protected\"\naccess_control = \"managed-network\"\nedge_auth = \"none\"\nnoindex = true\npromotable = true\n\n[channels.production]\nurl = \"https://kungfu.tech\"\nvisibility = \"public\"\ncanonical = true\nnoindex = false\n\n[deploy.preview]\nadapter = \"aws-s3-cloudfront\"\nbucket = \"kungfu-tech-preview\"\ncloudfront_distribution = \"E-PREVIEW\"\nartifact_path = \"dist\"\nsecret_refs = [\"AWS_ROLE_ARN\"]\n# Optional. Defaults to \"buildchain\".\n# Use \"external\" when an existing viewer-request CloudFront Function already\n# owns preview alias, surface-prefix, and directory-index routing.\ndirectory_index_rewrite = \"buildchain\"\n```\n\n### Multi-Surface Host Mapping\n\nSome site repositories publish more than one first-class web surface from the\nsame artifact. For example, `site-libkungfu-dev` has a hub plus separate\nhostnames for core, Buildchain, and Kung Fu Decisions. These are not just\nnavigation paths; staging, production preflight, and post-deploy health checks\nmust verify host-level behavior for each surface.\n\nDeclare named surfaces with per-channel URLs:\n\n```toml\n[surfaces.hub]\npath = \"/\"\nproduction_url = \"https://libkungfu.dev\"\nstaging_url = \"https://staging.libkungfu.dev\"\npreview_url_pattern = \"https://{alias}.preview.libkungfu.dev\"\n\n[surfaces.core]\npath = \"/core/\"\nproduction_url = \"https://core.libkungfu.dev\"\nstaging_url = \"https://core.staging.libkungfu.dev\"\npreview_url_pattern = \"https://core-{alias}.preview.libkungfu.dev\"\n\n[surfaces.buildchain]\npath = \"/buildchain/\"\nproduction_url = \"https://buildchain.libkungfu.dev\"\nstaging_url = \"https://buildchain.staging.libkungfu.dev\"\npreview_url_pattern = \"https://buildchain-{alias}.preview.libkungfu.dev\"\n\n[surfaces.kfd]\npath = \"/kfd/\"\nproduction_url = \"https://kfd.libkungfu.dev\"\nstaging_url = \"https://kfd.staging.libkungfu.dev\"\npreview_url_pattern = \"https://kfd-{alias}.preview.libkungfu.dev\"\n```\n\nBuildchain resolves every `(channel, surface)` pair. A preview alias such as\n`pr-12` becomes:\n\n```text\nhub: https://pr-12.preview.libkungfu.dev\ncore: https://core-pr-12.preview.libkungfu.dev\nbuildchain: https://buildchain-pr-12.preview.libkungfu.dev\nkfd: https://kfd-pr-12.preview.libkungfu.dev\n```\n\nWhen `surfaces` is omitted, Buildchain preserves the legacy single-surface\ncontract by creating an implicit `default` surface from the channel URL. When a\nsurface is intentionally path-only, declare it explicitly:\n\n```toml\n[surfaces.docs]\npath = \"/docs/\"\npath_only = true\n```\n\n`path_only = true` is an exception, not the default. Without it, every named\nsurface must declare `preview_url_pattern`, `staging_url`, and\n`production_url`. This makes staging/production mismatches fail during\nvalidation instead of becoming invisible deploy drift.\n\nAdapter strategy remains explicit. The default `aws-s3-cloudfront` plan uses the\nchannel deploy target for every surface, and each binding records its own\nbucket, distribution id, object prefix, manifest key, source path, and URL. A\nchannel can override target details per surface:\n\n```toml\n[deploy.staging.surfaces.core]\nbucket = \"libkungfu-dev-core-staging\"\ncloudfront_distribution = \"E-CORE-STAGING\"\norigin_path = \"/core\"\n```\n\nBuildchain validates these hard constraints:\n\n- `channels.preview.url_pattern` is required and must contain the alias shape\n used by preview deployments.\n- `channels.staging.access_control` must protect staging. Supported modes are\n `managed-network`, `edge-basic-auth`, `oidc`, and `app-auth`.\n- `channels.staging.edge_auth` records whether the edge layer owns auth. Use\n `edge_auth = \"none\"` when staging is protected by managed network controls\n such as WAF/IP allowlists or VPN access.\n- `channels.staging.noindex = true` is required.\n- `channels.production.url` is required.\n- deploy adapters must be declared per channel.\n- named surfaces must declare first-class URLs for every channel unless\n `path_only = true` is explicitly set.\n- secret material must be declared as reference names, such as\n `secret_refs = [\"AWS_ROLE_ARN\"]`; inline secret-like deploy keys are rejected.\n\n### Floating Runtime Contract Lock\n\nWeb-surface repositories can consume the stable Buildchain workflow shell with a\nfloating ref, such as:\n\n```yaml\njobs:\n web:\n uses: kungfu-systems/buildchain/.github/workflows/.web-surface.yml@v2\n with:\n buildchain-contract-lock-path: .buildchain/contract-lock.json\n buildchain-contract-compatibility-policy: major-compatible\n buildchain-contract-drift-issue-mode: compatible-and-breaking\n build-command: pnpm build\n artifact-path: dist\n```\n\nThe caller repository commits `.buildchain/contract-lock.json` after reviewing an\naccepted Buildchain runtime SHA and contract digest. The reusable workflow then\nresolves the floating runtime to an immutable SHA, checks the lock before the\ncaller build command, and applies these rules:\n\n- unchanged lock: continue without feedback;\n- compatible drift: continue, write the drift summary, and open or update a\n caller-repository issue when permissions allow;\n- breaking drift: fail closed before rendering, deployment planning, deploy\n apply, or release publication.\n\nThe caller no longer needs to run `scripts/buildchain-contract-lock.mjs` inside\nits own build command. That check belongs to Buildchain because the actual\ncontract world is stored in the Buildchain runtime ref being used.\n\nSupported adapter names are:\n\n| Adapter | Initial use |\n| --- | --- |\n| `aws-s3-cloudfront` | Static site artifact sync plus CDN invalidation plan |\n| `aws-elastic-beanstalk` | Future dynamic app environment adapter |\n| `aws-ecs-service` | Future dynamic service adapter |\n\nThe channel ontology is independent of the adapter. A future dynamic staging\nenvironment still remains `channel = \"staging\"` with protected/noindex/security\nrequirements.\n\n## Preview Aliases\n\nPreview uses subdomains, not path prefixes:\n\n```text\nhttps://pr-123.preview.kungfu.tech\nhttps://sha-abcdef123456.preview.kungfu.tech\n```\n\nAlias semantics are explicit:\n\n| Alias | Meaning | Mutable | Retention |\n| --- | --- | --- | --- |\n| `pr-123` | Current preview for a pull request | yes | short-lived |\n| `sha-abcdef123456` | Immutable preview for one source SHA | no | longer-lived |\n\nThis allows PR comments to stay stable while preserving immutable evidence for a\nspecific source commit.\n\n## Deployment Manifest\n\nBuildchain emits a manifest with the deployment facts that matter for audit and\nrollback:\n\n```json\n{\n \"schemaVersion\": 1,\n \"contract\": \"kungfu-buildchain-web-surface-deployment\",\n \"site\": \"libkungfu-dev\",\n \"channel\": \"preview\",\n \"alias\": \"sha-abcdef123456\",\n \"url\": \"https://sha-abcdef123456.preview.libkungfu.dev\",\n \"generatedAt\": \"2026-07-01T00:00:00.000Z\",\n \"publishedAt\": \"2026-07-01T00:00:00.000Z\",\n \"reproducible\": true,\n \"timestampPolicy\": \"ci-injected\",\n \"deterministicInputs\": [\n \"web-surface artifact content\",\n \"buildchain.toml web-surface channels/deploy/surfaces\",\n \"sourceSha\",\n \"artifactHash\",\n \"deployment channel\",\n \"deployment alias\"\n ],\n \"sourceRevision\": \"...\",\n \"timestampPolicyDetails\": {\n \"contract\": \"kungfu-buildchain-surface-timestamp-policy\",\n \"timestampFields\": [\"generatedAt\", \"publishedAt\", \"deployedAt\"],\n \"timestampFieldsParticipateInArtifactDigest\": false,\n \"artifactDigestScope\": \"web-surface artifactHash excludes deployment manifest timestamps\"\n },\n \"sourceSha\": \"...\",\n \"artifactHash\": \"...\",\n \"deployTarget\": \"libkungfu-dev-preview\",\n \"adapter\": \"aws-s3-cloudfront\",\n \"deployedAt\": \"2026-07-01T00:00:00.000Z\",\n \"retentionClass\": \"preview-sha-immutable\",\n \"expiresAt\": \"2026-09-29T00:00:00.000Z\",\n \"accessControl\": \"none\",\n \"edgeAuth\": \"none\",\n \"noindex\": true,\n \"secretRefs\": [\"AWS_ROLE_ARN\"],\n \"surfaceBindings\": [\n {\n \"surface\": \"hub\",\n \"channel\": \"preview\",\n \"alias\": \"sha-abcdef123456\",\n \"url\": \"https://sha-abcdef123456.preview.libkungfu.dev\",\n \"sourcePath\": \"/\",\n \"artifactPathPrefix\": \"\",\n \"viewerPathPrefix\": \"/\",\n \"directoryIndex\": \"index.html\",\n \"directoryIndexResolution\": true,\n \"canonicalUrl\": \"https://libkungfu.dev\",\n \"bucket\": \"libkungfu-dev-preview\",\n \"distributionId\": \"E-PREVIEW\",\n \"originPath\": \"\",\n \"objectPrefix\": \"sha-abcdef123456\",\n \"manifestKey\": \".buildchain/deployments/sha-abcdef123456/hub.json\",\n \"routing\": {\n \"contract\": \"kungfu-buildchain-web-surface-path-prefix-rewrite\",\n \"viewerPathPrefix\": \"/\",\n \"artifactPathPrefix\": \"\",\n \"objectPrefix\": \"sha-abcdef123456\",\n \"directoryIndex\": \"index.html\",\n \"directoryIndexResolution\": true\n },\n \"smokeUrls\": [\n {\n \"kind\": \"root\",\n \"requestPath\": \"/\",\n \"url\": \"https://sha-abcdef123456.preview.libkungfu.dev/\",\n \"required\": true\n }\n ],\n \"noindex\": true,\n \"accessControl\": \"none\"\n }\n ]\n}\n```\n\nDynamic adapters can also fill `runtimeId`, `configFingerprint`,\n`healthCheck`, `migrationState`, `rollbackPointer`, and\n`rollbackLimitations`. Buildchain records secret reference names only, never\nsecret values.\n\nThe timestamp policy is shared with package site bundles. Public deployment\nmanifests should expose real workflow generation/publication times while\nseparately declaring why the deployed artifact remains reproducible. For\nweb-surface deployment manifests, `artifactHash` is the static site artifact\ndigest and does not include deployment timestamp fields; the manifest itself\nstill records those fields for human and agent audit.\n\n## Deploy Plans\n\nDeploy planning is the default behavior. It plans the adapter steps and writes\nmanifest JSON, but it does not touch AWS, DNS, CloudFront, or deployment\ncredentials.\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode deploy-plan \\\n --cwd fixtures/web-surface-shaped \\\n --source-sha aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \\\n --alias sha-aaaaaaaaaaaa\n```\n\nFor manifest-only output:\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode manifest \\\n --cwd fixtures/web-surface-shaped \\\n --source-sha aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \\\n --alias pr-123 \\\n --output .buildchain/web-surface-manifest.json\n```\n\nThe CLI emits GitHub outputs when `GITHUB_OUTPUT` is present:\n\n- `web-surface-channel`\n- `web-surface-alias`\n- `web-surface-url`\n- `web-surface-urls-json`\n- `web-surface-artifact-hash`\n- `web-surface-manifest-json`\n\n## Explicit Apply\n\n`deploy-apply` and `cleanup-apply` are explicit execution modes for the\n`aws-s3-cloudfront` static-site adapter. They still default to `--dry-run true`;\nlive AWS mutation requires `--dry-run false`.\n\nDeploy apply syncs the artifact, writes the deployment manifest, and invalidates\nCloudFront when a distribution id is configured:\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode deploy-apply \\\n --cwd fixtures/web-surface-shaped \\\n --channel staging \\\n --source-sha aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \\\n --artifact-path dist \\\n --dry-run false \\\n --output .buildchain/web-surface-staging-apply.json\n```\n\nFor multi-surface sites, each surface host is treated as a root-relative view\nof that surface's artifact path prefix. For example, a `buildchain` surface with\n`path = \"/buildchain/\"` and preview URL\n`https://buildchain-pr-29.preview.libkungfu.dev` syncs the artifact subtree\n`dist/buildchain/` to the preview object prefix `pr-29/buildchain`. A viewer\nrequest for `https://buildchain-pr-29.preview.libkungfu.dev/docs/` therefore\nresolves against the artifact's `dist/buildchain/docs/index.html`, not\n`dist/docs/index.html` and not the hub surface root. The deployment manifest\nrecords this as `routing.contract =\n\"kungfu-buildchain-web-surface-path-prefix-rewrite\"` with\n`viewerPathPrefix = \"/\"`, `artifactPathPrefix = \"buildchain\"`, and\n`directoryIndexResolution = true`.\n\nWhen a surface uses an S3 object prefix, directory-index routing must be handled\nat the viewer-request layer. By default, `directory_index_rewrite =\n\"buildchain\"` makes Buildchain install or update one CloudFront Function per\ndistribution before uploading payloads. The function rewrites any request path\nending in `/` to the corresponding `index.html`, so\n`https://buildchain-pr-29.preview.libkungfu.dev/` resolves to\n`pr-29/buildchain/index.html` and\n`https://buildchain-pr-29.preview.libkungfu.dev/docs/` resolves to\n`pr-29/buildchain/docs/index.html`. This keeps multi-host preview roots\ncompatible with S3 REST origins, where copying alias objects such as\n`pr-29/buildchain` or `pr-29/buildchain/` is not a reliable substitute for an\nedge rewrite.\n\nIf the distribution already has a viewer-request function that owns preview\nalias routing and surface-prefix routing, set `directory_index_rewrite =\n\"external\"` on the deploy channel or surface override. In that mode Buildchain\ndoes not create, update, or attach a generic directory-index function. Instead,\nthe deployment manifest records `directoryIndexRewrite = \"external\"` and\n`directoryIndexStrategy = \"external-viewer-request-function\"`, then the normal\nhealth check still verifies every required root and nested surface URL. This is\nthe correct contract for shared preview distributions such as\n`site-libkungfu-dev`, where a generic function cannot replace the existing\nprefix router.\n\nBuildchain still writes directory-index alias objects during apply as\ncompatibility evidence, but root correctness comes from the viewer-request\nrewrite contract, not from extensionless S3 keys. If Buildchain-managed mode\nfinds a distribution with a different viewer-request function, apply fails\nclosed and records that conflict in the apply result instead of silently serving\n403s. The reusable workflow uploads `buildchain-web-surface-*-diagnostics`\nartifacts containing the apply and health JSON so the failing AWS operation or\nHTTP check is visible from the consumer run.\n\nIt can also execute a previously saved deploy plan. In that mode Buildchain\nrecomputes the local artifact hash before running AWS commands and fails closed\nif the artifact no longer matches the saved plan:\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode deploy-apply \\\n --cwd fixtures/web-surface-shaped \\\n --plan .buildchain/web-surface-staging-plan.json \\\n --dry-run false \\\n --output .buildchain/web-surface-staging-apply.json\n```\n\nCleanup apply deletes preview content, deletes the preview manifest, and\ninvalidates CloudFront:\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode cleanup-apply \\\n --cwd fixtures/web-surface-shaped \\\n --event pull-request-closed \\\n --pull-number 123 \\\n --source-sha aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \\\n --dry-run false \\\n --output .buildchain/web-surface-cleanup-apply.json\n```\n\nCleanup apply can also execute a saved cleanup plan:\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode cleanup-apply \\\n --cwd fixtures/web-surface-shaped \\\n --plan .buildchain/web-surface-cleanup-plan.json \\\n --dry-run false \\\n --output .buildchain/web-surface-cleanup-apply.json\n```\n\nApply output records the channel, alias, source SHA, artifact hash, target\nbucket, object prefix, manifest key, all surface URLs, all surface bindings, CDN\ninvalidation paths, actor/run metadata, and every adapter operation with\n`executed`, `exitCode`, `stdout`, and `stderr`. If an operation fails,\nBuildchain records the failed operation, stops subsequent adapter operations,\nand exits non-zero after writing the result JSON. Buildchain records secret\nreference names only; the runner must provide the AWS CLI and credentials\noutside Buildchain, typically through OIDC and the declared `secret_refs`.\n\n## Production Preflight And Health\n\nProduction promotion is not just `deploy-apply --channel production`. Before a\nlive production apply, the reusable workflow runs:\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode production-preflight \\\n --cwd fixtures/web-surface-shaped \\\n --plan .buildchain/web-surface-production-plan.json \\\n --execute true \\\n --output .buildchain/web-surface-production-preflight.json\n```\n\nThe production preflight checks that:\n\n- `channels.production` is canonical and indexable;\n- every surface has concrete production bucket and CloudFront targets;\n- every production surface URL is HTTPS;\n- the production AWS role can inspect the declared bucket and distribution;\n- CloudFront aliases cover every surface host, including product hosts such as\n `kfd.libkungfu.dev`;\n- DNS resolves for every surface host.\n\nAfter preview, staging, and production apply, the workflow runs:\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode health-check \\\n --cwd fixtures/web-surface-shaped \\\n --result .buildchain/web-surface-production-apply.json \\\n --output .buildchain/web-surface-production-health.json\n```\n\nWhen the apply result includes CloudFront invalidations from the\n`aws-s3-cloudfront` adapter, health check waits for those invalidations to reach\n`Completed` before fetching public smoke URLs. This avoids reporting stale\nCloudFront 403 responses as deployment failures immediately after a successful\nS3 sync and invalidation request.\n\nThe health check fetches every surface root URL and any nested smoke URLs\nrecorded in each surface binding. Nested smoke URLs are derived from nested HTML\nartifact files under the surface path prefix, with directory index resolution\nsuch as `dist/buildchain/docs/index.html` becoming `/docs/` on the buildchain\npreview host. If a surface has no nested HTML route, Buildchain records only\nthe root smoke URL; absence of nested HTML is not a deployment failure. When a\nnested route is present, the check fails closed if a deploy reports success but\nthat child page returns 403 or another unexpected status. Surface root checks\nexpect the apply result to have installed the directory-index rewrite, so a\nmulti-host preview root such as `https://buildchain-pr-29.preview.libkungfu.dev/`\nmust resolve to the surface `index.html`, not the bare prefix directory.\nProduction additionally fails if a response is unreachable, returns an\nunexpected status, or still sends `x-robots-tag: noindex`. The health check also\nverifies that each surface binding recorded a deployment manifest pointer. The\nproduction release passport embeds the deploy plan, apply result, production\npreflight, and health check so a reviewer or agent can audit why the production\nsite changed and whether every declared host and every existing nested route was\nactually covered.\n\nChannels declared with `access_control = \"managed-network\"` use a different\nhealth strategy by default. Buildchain does not require a GitHub-hosted runner\nto fetch a URL that is intentionally reachable only from an approved network.\nInstead, after a live apply the health check uses the deploy role to run S3\n`head-object` checks for each surface manifest and the smoke target object, such\nas the surface `index.html` or a nested `docs/index.html`. The check records\n`healthStrategy = \"s3-object\"` and skips the public HTTP fetch. Dry-run and\nplan-only checks fall back to deployment evidence: each surface must have a\nmanifest key, bucket, object prefix, `sync-static-artifact`, and\n`write-deployment-manifest` evidence, recorded as\n`healthStrategy = \"deployment-evidence\"`. If the workflow is running on a runner\nthat is allowed to reach the managed network, set\n`BUILDCHAIN_WEB_SURFACE_HEALTH_ALLOWED_RUNNER=true` or pass\n`--allowed-managed-network-runner true` to keep the normal HTTP smoke checks.\nSet `BUILDCHAIN_WEB_SURFACE_HEALTH_S3_OBJECTS=false` or pass\n`--managed-network-s3-object-verification false` only when an external channel\npolicy owns managed-network object verification.\n\n## Cleanup Plans\n\nPreview cleanup is an auditable cleanup contract. It can run as a dry-run plan,\nan apply-mode plan, or the explicit `cleanup-apply` executor with preview-only\ncredentials:\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode cleanup-plan \\\n --cwd fixtures/web-surface-shaped \\\n --event pull-request-closed \\\n --pull-number 123 \\\n --aliases pr-123,sha-abcdef123456\n```\n\nThe plan and apply result keep mutable PR aliases and immutable SHA aliases\ndistinct so a caller can expire them with different retention windows. Closed-PR\ncleanup can derive `pr-N` from `--pull-number`, records the event, source SHA,\nactor, run id, preview bucket/prefix, manifest key, and adapter steps, and is an\nauditable no-op when no aliases are requested.\n\n## Reusable Workflow Shape\n\nBuildchain ships `.github/workflows/.web-surface.yml` for repositories that want\nthe standard PR review and promotion flow without copying bespoke glue:\n\n```yaml\njobs:\n web-surface:\n uses: kungfu-systems/buildchain/.github/workflows/.web-surface.yml@v2\n with:\n build-command: npm run build\n verify-command: npm run check\n artifact-path: dist\n```\n\nThe reusable workflow maps GitHub events to Buildchain web-surface semantics:\n\n| Event | Buildchain behavior |\n| --- | --- |\n| `pull_request` opened / synchronized / reopened | validate, build, verify, and plan `preview` for `pr-N` |\n| `pull_request` closed | plan apply-mode cleanup for the `pr-N` preview alias and manifest |\n| `push` to `main` | validate, build, verify, plan and apply `staging` from the merged `main` SHA, then optionally open a production release PR |\n| `push` to `main` from a matching release PR merge | validate the associated release PR, plan `production`, and enter the configured GitHub Environment gate |\n| `workflow_dispatch` with `production-approved = true` | plan `production` and enter the configured GitHub Environment gate |\n\nThe optional `buildchain-ref` input is empty by default. Empty keeps the\nweb-surface run on the stable Buildchain runtime selected by the reusable\nworkflow ref, normally `@v2`. A trusted maintainer can expose a\n`workflow_dispatch` input and pass it through for one-off train validation.\nSee [`runtime-train-validation.md`](runtime-train-validation.md) for the shared\ntrain protocol and notification template:\n\n```yaml\non:\n workflow_dispatch:\n inputs:\n buildchain-ref:\n description: \"Temporary Buildchain runtime ref for trusted manual validation\"\n required: false\n default: \"\"\n\njobs:\n web-surface:\n uses: kungfu-systems/buildchain/.github/workflows/.web-surface.yml@v2\n with:\n buildchain-ref: ${{ inputs.buildchain-ref || '' }}\n build-command: pnpm run build\n verify-command: pnpm run check\n artifact-path: dist\n```\n\nOnly trusted `workflow_dispatch` runs by repository actors with write,\nmaintain, or admin permission may use a non-empty runtime override. Train refs\nsuch as `train/v2/v2.3/site-source-of-truth` are temporary validation refs, not\nstable production dependencies or pending merge targets. They may remain for a\nretention window after release as a fast-use and rollback channel, with old\ntrains handled by periodic Buildchain cleanup. The web-surface deployment\nmanifest records the resolved runtime SHA as `runtimeId` and the stable\nrollback ref as `rollbackPointer`.\n\nThe workflow deliberately plans and emits manifests by default. Live mutation is\nopt-in per channel:\n\n```yaml\npermissions:\n contents: read\n id-token: write\n pull-requests: write\n\njobs:\n web-surface:\n uses: kungfu-systems/buildchain/.github/workflows/.web-surface.yml@v2\n with:\n build-command: pnpm run build\n verify-command: pnpm run check\n artifact-path: dist\n preview-apply: true\n preview-cleanup-apply: true\n preview-aws-role-arn: arn:aws:iam::123456789012:role/site-preview-github-actions\n staging-apply: true\n staging-aws-role-arn: arn:aws:iam::123456789012:role/site-staging-github-actions\n production-apply: false\n production-release-on-main: false\n production-aws-role-arn: arn:aws:iam::123456789012:role/site-production-github-actions\n production-environment: production\n release-feedback-actor-privacy: public\n```\n\nWhen enabled, Buildchain owns the full release apply state machine:\n\n- PR preview deploys run `deploy-apply --dry-run false` with the preview role\n and update a single idempotent PR comment.\n- Closed PR cleanup runs `cleanup-apply --dry-run false` with the preview role\n only.\n- Pushes to `main` run staging `deploy-apply --dry-run false` with the staging\n role, then write a staging release feedback passport artifact and comment the\n associated merged PR with the staging URL, source SHA, artifact identity, run\n URL, and failure context when apply did not complete.\n- When `production-release-on-main=true`, successful staging applies open or\n update a Buildchain-owned release PR from\n `release/<channel>-<short-sha>` to `main`, unless the current push already\n came from a matching release PR merge. The release PR contains one empty\n release-intent commit, carries `production-release-label`, and includes the\n staging URLs, source SHA, artifact hash, and staging release-passport artifact\n link in the PR body.\n- Production release PR handoff is permission-aware. Staging apply and staging\n health remain successful even when the repository or organization has\n GitHub Actions workflow permissions set to read-only. In that case Buildchain\n records `release-pr-status=permission-denied`, uploads the release PR handoff\n summary/body plus staging release passport artifacts, and writes an exact\n manual `gh pr create` command to the step summary. Set\n `fail-on-release-pr-error=true` only when PR creation failure should fail the\n whole workflow.\n- Release pull requests that match the configured production gate get a\n Buildchain review comment with the staging URL and production target, so the\n operator can verify staging from the PR page and use merge as the approval\n action. Consumers do not need to hand-write `gh pr create` or production\n release-intent glue.\n- Production runs when `production-apply` is true and either:\n - a trusted `workflow_dispatch` passes `production-approved=true`; or\n - `production-release-on-main=true` and the `main` push commit is associated\n with exactly one same-repository, merged release pull request matching\n `production-release-label` and `production-release-head-prefix`.\n The production job is then gated by the configured GitHub Environment.\n- Production apply writes a production release feedback passport artifact and\n comments the release PR with the production URL, source SHA, artifact\n identity, run URL, rollback pointer, and failure context when apply did not\n complete.\n\nThe feedback passport records the release responsibility chain:\n\n- human decision actor;\n- trigger actor;\n- runner/execution actor;\n- OIDC/deploy identity reference;\n- decision type and time;\n- source event, PR number, merge commit, and required gate label/head-prefix.\n\n`release-feedback-actor-privacy` controls actor values in the passport and\ncomments. `public` records GitHub actor names, `redacted` records only the actor\nrole, and `private-ref` records a stable private reference hash without exposing\nthe actor name.\n\nFor release-PR publishing, callers opt in explicitly:\n\n```yaml\njobs:\n web-surface:\n uses: kungfu-systems/buildchain/.github/workflows/.web-surface.yml@v2\n with:\n build-command: npm run build\n verify-command: npm run check\n artifact-path: dist\n production-apply: ${{ github.event_name == 'push' && github.ref_name == 'main' }}\n production-release-on-main: true\n production-release-label: buildchain-release\n production-release-head-prefix: release/\n production-release-branch-channel: production\n production-release-pr-mode: auto\n production-aws-role-arn: arn:aws:iam::123456789012:role/site-production-github-actions\n production-environment: production\n```\n\n`production-release-pr-mode` controls the post-staging handoff:\n\n| Mode | Behavior |\n| --- | --- |\n| `auto` | Generate release PR facts, create/update the empty release-intent branch and PR, and label it when token permissions allow. This is the default. |\n| `summary-only` | Generate and upload release PR facts, body, passport evidence, and manual command, but do not call the GitHub PR API. |\n| `disabled` | Record a disabled handoff and skip release PR API calls. |\n\nAutomatic release PR creation normally uses the workflow `github.token`.\nConsumers that cannot enable \"GitHub Actions can create and approve pull\nrequests\" globally should prefer the first-class GitHub App path. Pass the App\nclient id as an input and the private key as a reusable workflow secret; Buildchain\ncreates an installation token inside the release PR job and uses it only for the\nrelease-intent branch, PR, and label operations:\n\n```yaml\nwith:\n production-release-app-client-id: ${{ vars.KUNGFU_RELEASE_APP_CLIENT_ID }}\nsecrets:\n production-release-app-private-key: ${{ secrets.KUNGFU_RELEASE_APP_PRIVATE_KEY }}\n```\n\n`production-release-app-id` remains accepted as a deprecated alias for the input\nname, but the value should be the GitHub App client id. GitHub App numeric App\nIDs and client IDs are distinct, and Buildchain passes the value to\n`actions/create-github-app-token` through its non-deprecated `client-id` input so\nnew runs do not emit the deprecated `app-id` warning.\n\nIf a repository already generates its own narrow token or PAT, it can still pass\nthat through `production-release-pr-token`:\n\n```yaml\nwith:\n production-release-pr-token: ${{ secrets.BUILDCHAIN_RELEASE_PR_TOKEN }}\n```\n\nToken priority is: generated GitHub App installation token,\n`production-release-pr-token`, then `github.token`.\n\nThe merge button becomes the production approval only for a PR that carries the\nrelease label and comes from the configured source-branch prefix. Ordinary pull\nrequests merged into `main` deploy staging and open a release-intent PR; merging\nthat release PR triggers production. A release PR merge push does not open\nanother release PR.\n\nApply-only inputs are validated before the caller build or verification command\nruns. If the current event would run preview, staging, or production apply,\nmissing role inputs or a production apply without `production-approved=true`\non manual dispatch fail immediately instead of spending the build and plan jobs\nfirst.\n\nCallers must grant `id-token: write` for OIDC role assumption. Preview comments\nneed `pull-requests: write`. Automatic release PR creation also needs\n`contents: write`, `pull-requests: write`, and `issues: write` so Buildchain can\ncreate the release branch, write the empty release-intent commit, open or update\nthe PR, and apply the release label. If these permissions are unavailable,\nBuildchain degrades the release handoff instead of marking a successful staging\ndeployment as failed, unless `fail-on-release-pr-error=true`. The AWS roles remain caller-owned and\nshould be scoped by channel: preview can mutate only preview resources, staging\ncan mutate only staging resources, and production can mutate only production\nresources.\n\nApply mode fails closed when the deploy config still contains placeholder AWS\ntargets such as `pending-preview-distribution`. Planning can use placeholders\nfor dry-run-only design work, but live apply requires concrete bucket and\nCloudFront distribution identifiers.\n\n## Site Repository Shape\n\nA site repository can start with:\n\n```toml\nschema = 1\n\n[project]\ntype = \"web-surface\"\nname = \"site-kungfu-tech\"\nsite = \"kungfu-tech\"\n\n[lifecycle.build]\ncommand = \"pnpm run build\"\n\n[lifecycle.verify]\ncommand = \"pnpm run check\"\n```\n\nThen add the channel, deploy, retention, and security declarations shown above.\nThe project may use pnpm, npm, yarn, Vite, Astro, Next static export, Sphinx,\nMkDocs, CMake-generated docs, or another lifecycle command source. Buildchain\nonly needs a deterministic artifact path and the manifest facts.\n\n## Boundaries\n\nBuildchain only performs live AWS mutations in explicit apply modes with\n`--dry-run false`. Production deploys must still be gated by a human-controlled\nworkflow, release, or GitHub Environment. DNS changes, staging auth\nimplementation, CloudFront distribution creation, and credential provisioning\nremain explicitly authorized infrastructure operations outside the web-surface\nartifact apply contract."
|
|
1732
|
+
"markdown": "# Web-Surface Deployment Contract\n\nBuildchain supports `project.type = \"web-surface\"` for repositories that publish\nsites, docs, product pages, operator consoles, or browser apps. These projects\nneed auditable deployment semantics, but they are not package release lines and\nshould not be forced into `dev/alpha/release` version-state automation.\n\nThe release object for a web surface is:\n\n```text\nsource commit + build artifact + deploy target + channel + deployment manifest\n```\n\nThis keeps the evidence chain clear:\n\n- the source SHA explains what code was built;\n- the artifact hash explains exactly what was deployed;\n- the channel explains who can see it and whether it is promotable;\n- the deploy target and adapter explain where it would be published;\n- the deployment manifest records retention, rollback, security, and secret\n reference metadata.\n\n## Configuration\n\n`.buildchain/buildchain.toml` is the source of truth. Web-surface projects must declare\npreview, staging, and production channels plus a deploy adapter for each.\n\n```toml\nschema = 1\n\n[project]\ntype = \"web-surface\"\nname = \"site-kungfu-tech\"\nsite = \"kungfu-tech\"\n\n[channels.preview]\nurl_pattern = \"https://{alias}.preview.kungfu.tech\"\nvisibility = \"ephemeral\"\nrequires_auth = false\nnoindex = true\n\n[channels.staging]\nurl = \"https://staging.kungfu.tech\"\nvisibility = \"protected\"\naccess_control = \"managed-network\"\nedge_auth = \"none\"\nnoindex = true\npromotable = true\n\n[channels.production]\nurl = \"https://kungfu.tech\"\nvisibility = \"public\"\ncanonical = true\nnoindex = false\n\n[deploy.preview]\nadapter = \"aws-s3-cloudfront\"\nbucket = \"kungfu-tech-preview\"\ncloudfront_distribution = \"E-PREVIEW\"\nartifact_path = \"dist\"\nsecret_refs = [\"AWS_ROLE_ARN\"]\n# Optional. Defaults to \"buildchain\".\n# Use \"external\" when an existing viewer-request CloudFront Function already\n# owns preview alias, surface-prefix, and directory-index routing.\ndirectory_index_rewrite = \"buildchain\"\n```\n\n### Multi-Surface Host Mapping\n\nSome site repositories publish more than one first-class web surface from the\nsame artifact. For example, `site-libkungfu-dev` has a hub plus separate\nhostnames for core, Buildchain, and Kung Fu Decisions. These are not just\nnavigation paths; staging, production preflight, and post-deploy health checks\nmust verify host-level behavior for each surface.\n\nDeclare named surfaces with per-channel URLs:\n\n```toml\n[surfaces.hub]\npath = \"/\"\nproduction_url = \"https://libkungfu.dev\"\nstaging_url = \"https://staging.libkungfu.dev\"\npreview_url_pattern = \"https://{alias}.preview.libkungfu.dev\"\n\n[surfaces.core]\npath = \"/core/\"\nproduction_url = \"https://core.libkungfu.dev\"\nstaging_url = \"https://core.staging.libkungfu.dev\"\npreview_url_pattern = \"https://core-{alias}.preview.libkungfu.dev\"\n\n[surfaces.buildchain]\npath = \"/buildchain/\"\nproduction_url = \"https://buildchain.libkungfu.dev\"\nstaging_url = \"https://buildchain.staging.libkungfu.dev\"\npreview_url_pattern = \"https://buildchain-{alias}.preview.libkungfu.dev\"\n\n[surfaces.kfd]\npath = \"/kfd/\"\nproduction_url = \"https://kfd.libkungfu.dev\"\nstaging_url = \"https://kfd.staging.libkungfu.dev\"\npreview_url_pattern = \"https://kfd-{alias}.preview.libkungfu.dev\"\n```\n\nBuildchain resolves every `(channel, surface)` pair. A preview alias such as\n`pr-12` becomes:\n\n```text\nhub: https://pr-12.preview.libkungfu.dev\ncore: https://core-pr-12.preview.libkungfu.dev\nbuildchain: https://buildchain-pr-12.preview.libkungfu.dev\nkfd: https://kfd-pr-12.preview.libkungfu.dev\n```\n\nWhen `surfaces` is omitted, Buildchain preserves the legacy single-surface\ncontract by creating an implicit `default` surface from the channel URL. When a\nsurface is intentionally path-only, declare it explicitly:\n\n```toml\n[surfaces.docs]\npath = \"/docs/\"\npath_only = true\n```\n\n`path_only = true` is an exception, not the default. Without it, every named\nsurface must declare `preview_url_pattern`, `staging_url`, and\n`production_url`. This makes staging/production mismatches fail during\nvalidation instead of becoming invisible deploy drift.\n\nAdapter strategy remains explicit. The default `aws-s3-cloudfront` plan uses the\nchannel deploy target for every surface, and each binding records its own\nbucket, distribution id, object prefix, manifest key, source path, and URL. A\nchannel can override target details per surface:\n\n```toml\n[deploy.staging.surfaces.core]\nbucket = \"libkungfu-dev-core-staging\"\ncloudfront_distribution = \"E-CORE-STAGING\"\norigin_path = \"/core\"\n```\n\nBuildchain validates these hard constraints:\n\n- `channels.preview.url_pattern` is required and must contain the alias shape\n used by preview deployments.\n- `channels.staging.access_control` must protect staging. Supported modes are\n `managed-network`, `edge-basic-auth`, `oidc`, and `app-auth`.\n- `channels.staging.edge_auth` records whether the edge layer owns auth. Use\n `edge_auth = \"none\"` when staging is protected by managed network controls\n such as WAF/IP allowlists or VPN access.\n- `channels.staging.noindex = true` is required.\n- `channels.production.url` is required.\n- deploy adapters must be declared per channel.\n- named surfaces must declare first-class URLs for every channel unless\n `path_only = true` is explicitly set.\n- secret material must be declared as reference names, such as\n `secret_refs = [\"AWS_ROLE_ARN\"]`; inline secret-like deploy keys are rejected.\n\n### Floating Runtime Contract Lock\n\nWeb-surface repositories can consume the stable Buildchain workflow shell with a\nfloating ref, such as:\n\n```yaml\njobs:\n web:\n uses: kungfu-systems/buildchain/.github/workflows/.web-surface.yml@v2\n with:\n buildchain-contract-lock-path: .buildchain/contract-lock.json\n buildchain-contract-compatibility-policy: major-compatible\n buildchain-contract-drift-issue-mode: compatible-and-breaking\n build-command: pnpm build\n artifact-path: dist\n```\n\nThe caller repository commits `.buildchain/contract-lock.json` after reviewing an\naccepted Buildchain runtime SHA and contract digest. The reusable workflow then\nresolves the floating runtime to an immutable SHA, checks the lock before the\ncaller build command, and applies these rules:\n\n- unchanged lock: continue without feedback;\n- compatible drift: continue, write the drift summary, and open or update a\n caller-repository issue when permissions allow;\n- breaking drift: fail closed before rendering, deployment planning, deploy\n apply, or release publication.\n\nThe caller no longer needs to run `scripts/buildchain-contract-lock.mjs` inside\nits own build command. That check belongs to Buildchain because the actual\ncontract world is stored in the Buildchain runtime ref being used.\n\nSupported adapter names are:\n\n| Adapter | Initial use |\n| --- | --- |\n| `aws-s3-cloudfront` | Static site artifact sync plus CDN invalidation plan |\n| `aws-elastic-beanstalk` | Future dynamic app environment adapter |\n| `aws-ecs-service` | Future dynamic service adapter |\n\nThe channel ontology is independent of the adapter. A future dynamic staging\nenvironment still remains `channel = \"staging\"` with protected/noindex/security\nrequirements.\n\n## Preview Aliases\n\nPreview uses subdomains, not path prefixes:\n\n```text\nhttps://pr-123.preview.kungfu.tech\nhttps://sha-abcdef123456.preview.kungfu.tech\n```\n\nAlias semantics are explicit:\n\n| Alias | Meaning | Mutable | Retention |\n| --- | --- | --- | --- |\n| `pr-123` | Current preview for a pull request | yes | short-lived |\n| `sha-abcdef123456` | Immutable preview for one source SHA | no | longer-lived |\n\nThis allows PR comments to stay stable while preserving immutable evidence for a\nspecific source commit.\n\n## Deployment Manifest\n\nBuildchain emits a manifest with the deployment facts that matter for audit and\nrollback:\n\n```json\n{\n \"schemaVersion\": 1,\n \"contract\": \"kungfu-buildchain-web-surface-deployment\",\n \"site\": \"libkungfu-dev\",\n \"channel\": \"preview\",\n \"alias\": \"sha-abcdef123456\",\n \"url\": \"https://sha-abcdef123456.preview.libkungfu.dev\",\n \"generatedAt\": \"2026-07-01T00:00:00.000Z\",\n \"publishedAt\": \"2026-07-01T00:00:00.000Z\",\n \"reproducible\": true,\n \"timestampPolicy\": \"ci-injected\",\n \"deterministicInputs\": [\n \"web-surface artifact content\",\n \"buildchain.toml web-surface channels/deploy/surfaces\",\n \"sourceSha\",\n \"artifactHash\",\n \"deployment channel\",\n \"deployment alias\"\n ],\n \"sourceRevision\": \"...\",\n \"timestampPolicyDetails\": {\n \"contract\": \"kungfu-buildchain-surface-timestamp-policy\",\n \"timestampFields\": [\"generatedAt\", \"publishedAt\", \"deployedAt\"],\n \"timestampFieldsParticipateInArtifactDigest\": false,\n \"artifactDigestScope\": \"web-surface artifactHash excludes deployment manifest timestamps\"\n },\n \"sourceSha\": \"...\",\n \"artifactHash\": \"...\",\n \"deployTarget\": \"libkungfu-dev-preview\",\n \"adapter\": \"aws-s3-cloudfront\",\n \"deployedAt\": \"2026-07-01T00:00:00.000Z\",\n \"retentionClass\": \"preview-sha-immutable\",\n \"expiresAt\": \"2026-09-29T00:00:00.000Z\",\n \"accessControl\": \"none\",\n \"edgeAuth\": \"none\",\n \"noindex\": true,\n \"secretRefs\": [\"AWS_ROLE_ARN\"],\n \"surfaceBindings\": [\n {\n \"surface\": \"hub\",\n \"channel\": \"preview\",\n \"alias\": \"sha-abcdef123456\",\n \"url\": \"https://sha-abcdef123456.preview.libkungfu.dev\",\n \"sourcePath\": \"/\",\n \"artifactPathPrefix\": \"\",\n \"viewerPathPrefix\": \"/\",\n \"directoryIndex\": \"index.html\",\n \"directoryIndexResolution\": true,\n \"canonicalUrl\": \"https://libkungfu.dev\",\n \"bucket\": \"libkungfu-dev-preview\",\n \"distributionId\": \"E-PREVIEW\",\n \"originPath\": \"\",\n \"objectPrefix\": \"sha-abcdef123456\",\n \"manifestKey\": \".buildchain/deployments/sha-abcdef123456/hub.json\",\n \"routing\": {\n \"contract\": \"kungfu-buildchain-web-surface-path-prefix-rewrite\",\n \"viewerPathPrefix\": \"/\",\n \"artifactPathPrefix\": \"\",\n \"objectPrefix\": \"sha-abcdef123456\",\n \"directoryIndex\": \"index.html\",\n \"directoryIndexResolution\": true\n },\n \"smokeUrls\": [\n {\n \"kind\": \"root\",\n \"requestPath\": \"/\",\n \"url\": \"https://sha-abcdef123456.preview.libkungfu.dev/\",\n \"required\": true\n }\n ],\n \"noindex\": true,\n \"accessControl\": \"none\"\n }\n ]\n}\n```\n\nDynamic adapters can also fill `runtimeId`, `configFingerprint`,\n`healthCheck`, `migrationState`, `rollbackPointer`, and\n`rollbackLimitations`. Buildchain records secret reference names only, never\nsecret values.\n\nThe timestamp policy is shared with package site bundles. Public deployment\nmanifests should expose real workflow generation/publication times while\nseparately declaring why the deployed artifact remains reproducible. For\nweb-surface deployment manifests, `artifactHash` is the static site artifact\ndigest and does not include deployment timestamp fields; the manifest itself\nstill records those fields for human and agent audit.\n\n## Deploy Plans\n\nDeploy planning is the default behavior. It plans the adapter steps and writes\nmanifest JSON, but it does not touch AWS, DNS, CloudFront, or deployment\ncredentials.\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode deploy-plan \\\n --cwd fixtures/web-surface-shaped \\\n --source-sha aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \\\n --alias sha-aaaaaaaaaaaa\n```\n\nFor manifest-only output:\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode manifest \\\n --cwd fixtures/web-surface-shaped \\\n --source-sha aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \\\n --alias pr-123 \\\n --output .buildchain/web-surface-manifest.json\n```\n\nThe CLI emits GitHub outputs when `GITHUB_OUTPUT` is present:\n\n- `web-surface-channel`\n- `web-surface-alias`\n- `web-surface-url`\n- `web-surface-urls-json`\n- `web-surface-artifact-hash`\n- `web-surface-manifest-json`\n\n## Explicit Apply\n\n`deploy-apply` and `cleanup-apply` are explicit execution modes for the\n`aws-s3-cloudfront` static-site adapter. They still default to `--dry-run true`;\nlive AWS mutation requires `--dry-run false`.\n\nDeploy apply syncs the artifact, writes the deployment manifest, and invalidates\nCloudFront when a distribution id is configured:\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode deploy-apply \\\n --cwd fixtures/web-surface-shaped \\\n --channel staging \\\n --source-sha aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \\\n --artifact-path dist \\\n --dry-run false \\\n --output .buildchain/web-surface-staging-apply.json\n```\n\nFor multi-surface sites, each surface host is treated as a root-relative view\nof that surface's artifact path prefix. For example, a `buildchain` surface with\n`path = \"/buildchain/\"` and preview URL\n`https://buildchain-pr-29.preview.libkungfu.dev` syncs the artifact subtree\n`dist/buildchain/` to the preview object prefix `pr-29/buildchain`. A viewer\nrequest for `https://buildchain-pr-29.preview.libkungfu.dev/docs/` therefore\nresolves against the artifact's `dist/buildchain/docs/index.html`, not\n`dist/docs/index.html` and not the hub surface root. The deployment manifest\nrecords this as `routing.contract =\n\"kungfu-buildchain-web-surface-path-prefix-rewrite\"` with\n`viewerPathPrefix = \"/\"`, `artifactPathPrefix = \"buildchain\"`, and\n`directoryIndexResolution = true`.\n\nWhen a surface uses an S3 object prefix, directory-index routing must be handled\nat the viewer-request layer. By default, `directory_index_rewrite =\n\"buildchain\"` makes Buildchain install or update one CloudFront Function per\ndistribution before uploading payloads. The function rewrites any request path\nending in `/` to the corresponding `index.html`, so\n`https://buildchain-pr-29.preview.libkungfu.dev/` resolves to\n`pr-29/buildchain/index.html` and\n`https://buildchain-pr-29.preview.libkungfu.dev/docs/` resolves to\n`pr-29/buildchain/docs/index.html`. This keeps multi-host preview roots\ncompatible with S3 REST origins, where copying alias objects such as\n`pr-29/buildchain` or `pr-29/buildchain/` is not a reliable substitute for an\nedge rewrite.\n\nIf the distribution already has a viewer-request function that owns preview\nalias routing and surface-prefix routing, set `directory_index_rewrite =\n\"external\"` on the deploy channel or surface override. In that mode Buildchain\ndoes not create, update, or attach a generic directory-index function. Instead,\nthe deployment manifest records `directoryIndexRewrite = \"external\"` and\n`directoryIndexStrategy = \"external-viewer-request-function\"`, then the normal\nhealth check still verifies every required root and nested surface URL. This is\nthe correct contract for shared preview distributions such as\n`site-libkungfu-dev`, where a generic function cannot replace the existing\nprefix router.\n\nBuildchain still writes directory-index alias objects during apply as\ncompatibility evidence, but root correctness comes from the viewer-request\nrewrite contract, not from extensionless S3 keys. If Buildchain-managed mode\nfinds a distribution with a different viewer-request function, apply fails\nclosed and records that conflict in the apply result instead of silently serving\n403s. The reusable workflow uploads `buildchain-web-surface-*-diagnostics`\nartifacts containing the apply and health JSON so the failing AWS operation or\nHTTP check is visible from the consumer run.\n\nIt can also execute a previously saved deploy plan. In that mode Buildchain\nrecomputes the local artifact hash before running AWS commands and fails closed\nif the artifact no longer matches the saved plan:\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode deploy-apply \\\n --cwd fixtures/web-surface-shaped \\\n --plan .buildchain/web-surface-staging-plan.json \\\n --dry-run false \\\n --output .buildchain/web-surface-staging-apply.json\n```\n\nCleanup apply deletes preview content, deletes the preview manifest, and\ninvalidates CloudFront:\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode cleanup-apply \\\n --cwd fixtures/web-surface-shaped \\\n --event pull-request-closed \\\n --pull-number 123 \\\n --source-sha aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \\\n --dry-run false \\\n --output .buildchain/web-surface-cleanup-apply.json\n```\n\nCleanup apply can also execute a saved cleanup plan:\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode cleanup-apply \\\n --cwd fixtures/web-surface-shaped \\\n --plan .buildchain/web-surface-cleanup-plan.json \\\n --dry-run false \\\n --output .buildchain/web-surface-cleanup-apply.json\n```\n\nApply output records the channel, alias, source SHA, artifact hash, target\nbucket, object prefix, manifest key, all surface URLs, all surface bindings, CDN\ninvalidation paths, actor/run metadata, and every adapter operation with\n`executed`, `exitCode`, `stdout`, and `stderr`. If an operation fails,\nBuildchain records the failed operation, stops subsequent adapter operations,\nand exits non-zero after writing the result JSON. Buildchain records secret\nreference names only; the runner must provide the AWS CLI and credentials\noutside Buildchain, typically through OIDC and the declared `secret_refs`.\n\n## Production Preflight And Health\n\nProduction promotion is not just `deploy-apply --channel production`. Before a\nlive production apply, the reusable workflow runs:\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode production-preflight \\\n --cwd fixtures/web-surface-shaped \\\n --plan .buildchain/web-surface-production-plan.json \\\n --execute true \\\n --output .buildchain/web-surface-production-preflight.json\n```\n\nThe production preflight checks that:\n\n- `channels.production` is canonical and indexable;\n- every surface has concrete production bucket and CloudFront targets;\n- every production surface URL is HTTPS;\n- the production AWS role can inspect the declared bucket and distribution;\n- CloudFront aliases cover every surface host, including product hosts such as\n `kfd.libkungfu.dev`;\n- DNS resolves for every surface host.\n\nAfter preview, staging, and production apply, the workflow runs:\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode health-check \\\n --cwd fixtures/web-surface-shaped \\\n --result .buildchain/web-surface-production-apply.json \\\n --output .buildchain/web-surface-production-health.json\n```\n\nWhen the apply result includes CloudFront invalidations from the\n`aws-s3-cloudfront` adapter, health check waits for those invalidations to reach\n`Completed` before fetching public smoke URLs. This avoids reporting stale\nCloudFront 403 responses as deployment failures immediately after a successful\nS3 sync and invalidation request.\n\nAfter the invalidation wait, HTTP smoke checks also retry transient 403, 404, and\n5xx responses before recording failure. This keeps the public health signal\nstrict while absorbing short CloudFront edge propagation windows that can remain\nvisible for a few seconds after the invalidation waiter returns.\n\nThe health check fetches every surface root URL and any nested smoke URLs\nrecorded in each surface binding. Nested smoke URLs are derived from nested HTML\nartifact files under the surface path prefix, with directory index resolution\nsuch as `dist/buildchain/docs/index.html` becoming `/docs/` on the buildchain\npreview host. If a surface has no nested HTML route, Buildchain records only\nthe root smoke URL; absence of nested HTML is not a deployment failure. When a\nnested route is present, the check fails closed if a deploy reports success but\nthat child page returns 403 or another unexpected status. Surface root checks\nexpect the apply result to have installed the directory-index rewrite, so a\nmulti-host preview root such as `https://buildchain-pr-29.preview.libkungfu.dev/`\nmust resolve to the surface `index.html`, not the bare prefix directory.\nProduction additionally fails if a response is unreachable, returns an\nunexpected status, or still sends `x-robots-tag: noindex`. The health check also\nverifies that each surface binding recorded a deployment manifest pointer. The\nproduction release passport embeds the deploy plan, apply result, production\npreflight, and health check so a reviewer or agent can audit why the production\nsite changed and whether every declared host and every existing nested route was\nactually covered.\n\nChannels declared with `access_control = \"managed-network\"` use a different\nhealth strategy by default. Buildchain does not require a GitHub-hosted runner\nto fetch a URL that is intentionally reachable only from an approved network.\nInstead, after a live apply the health check uses the deploy role to run S3\n`head-object` checks for each surface manifest and the smoke target object, such\nas the surface `index.html` or a nested `docs/index.html`. The check records\n`healthStrategy = \"s3-object\"` and skips the public HTTP fetch. Dry-run and\nplan-only checks fall back to deployment evidence: each surface must have a\nmanifest key, bucket, object prefix, `sync-static-artifact`, and\n`write-deployment-manifest` evidence, recorded as\n`healthStrategy = \"deployment-evidence\"`. If the workflow is running on a runner\nthat is allowed to reach the managed network, set\n`BUILDCHAIN_WEB_SURFACE_HEALTH_ALLOWED_RUNNER=true` or pass\n`--allowed-managed-network-runner true` to keep the normal HTTP smoke checks.\nSet `BUILDCHAIN_WEB_SURFACE_HEALTH_S3_OBJECTS=false` or pass\n`--managed-network-s3-object-verification false` only when an external channel\npolicy owns managed-network object verification.\n\n## Cleanup Plans\n\nPreview cleanup is an auditable cleanup contract. It can run as a dry-run plan,\nan apply-mode plan, or the explicit `cleanup-apply` executor with preview-only\ncredentials:\n\n```bash\nnode scripts/web-surface.mjs \\\n --mode cleanup-plan \\\n --cwd fixtures/web-surface-shaped \\\n --event pull-request-closed \\\n --pull-number 123 \\\n --aliases pr-123,sha-abcdef123456\n```\n\nThe plan and apply result keep mutable PR aliases and immutable SHA aliases\ndistinct so a caller can expire them with different retention windows. Closed-PR\ncleanup can derive `pr-N` from `--pull-number`, records the event, source SHA,\nactor, run id, preview bucket/prefix, manifest key, and adapter steps, and is an\nauditable no-op when no aliases are requested.\n\n## Reusable Workflow Shape\n\nBuildchain ships `.github/workflows/.web-surface.yml` for repositories that want\nthe standard PR review and promotion flow without copying bespoke glue:\n\n```yaml\njobs:\n web-surface:\n uses: kungfu-systems/buildchain/.github/workflows/.web-surface.yml@v2\n with:\n build-command: npm run build\n verify-command: npm run check\n artifact-path: dist\n```\n\nThe reusable workflow maps GitHub events to Buildchain web-surface semantics:\n\n| Event | Buildchain behavior |\n| --- | --- |\n| `pull_request` opened / synchronized / reopened | validate, build, verify, and plan `preview` for `pr-N` |\n| `pull_request` closed | plan apply-mode cleanup for the `pr-N` preview alias and manifest |\n| `push` to `main` | validate, build, verify, plan and apply `staging` from the merged `main` SHA, then optionally open a production release PR |\n| `push` to `main` from a matching release PR merge | validate the associated release PR, plan `production`, and enter the configured GitHub Environment gate |\n| `workflow_dispatch` with `production-approved = true` | plan `production` and enter the configured GitHub Environment gate |\n\nThe optional `buildchain-ref` input is empty by default. Empty keeps the\nweb-surface run on the stable Buildchain runtime selected by the reusable\nworkflow ref, normally `@v2`. A trusted maintainer can expose a\n`workflow_dispatch` input and pass it through for one-off train validation.\nSee [`runtime-train-validation.md`](runtime-train-validation.md) for the shared\ntrain protocol and notification template:\n\n```yaml\non:\n workflow_dispatch:\n inputs:\n buildchain-ref:\n description: \"Temporary Buildchain runtime ref for trusted manual validation\"\n required: false\n default: \"\"\n\njobs:\n web-surface:\n uses: kungfu-systems/buildchain/.github/workflows/.web-surface.yml@v2\n with:\n buildchain-ref: ${{ inputs.buildchain-ref || '' }}\n build-command: pnpm run build\n verify-command: pnpm run check\n artifact-path: dist\n```\n\nOnly trusted `workflow_dispatch` runs by repository actors with write,\nmaintain, or admin permission may use a non-empty runtime override. Train refs\nsuch as `train/v2/v2.3/site-source-of-truth` are temporary validation refs, not\nstable production dependencies or pending merge targets. They may remain for a\nretention window after release as a fast-use and rollback channel, with old\ntrains handled by periodic Buildchain cleanup. The web-surface deployment\nmanifest records the resolved runtime SHA as `runtimeId` and the stable\nrollback ref as `rollbackPointer`.\n\nThe workflow deliberately plans and emits manifests by default. Live mutation is\nopt-in per channel:\n\n```yaml\npermissions:\n contents: read\n id-token: write\n pull-requests: write\n\njobs:\n web-surface:\n uses: kungfu-systems/buildchain/.github/workflows/.web-surface.yml@v2\n with:\n build-command: pnpm run build\n verify-command: pnpm run check\n artifact-path: dist\n preview-apply: true\n preview-cleanup-apply: true\n preview-aws-role-arn: arn:aws:iam::123456789012:role/site-preview-github-actions\n staging-apply: true\n staging-aws-role-arn: arn:aws:iam::123456789012:role/site-staging-github-actions\n production-apply: false\n production-release-on-main: false\n production-aws-role-arn: arn:aws:iam::123456789012:role/site-production-github-actions\n production-environment: production\n release-feedback-actor-privacy: public\n```\n\nWhen enabled, Buildchain owns the full release apply state machine:\n\n- PR preview deploys run `deploy-apply --dry-run false` with the preview role\n and update a single idempotent PR comment.\n- Closed PR cleanup runs `cleanup-apply --dry-run false` with the preview role\n only.\n- Pushes to `main` run staging `deploy-apply --dry-run false` with the staging\n role, then write a staging release feedback passport artifact and comment the\n associated merged PR with the staging URL, source SHA, artifact identity, run\n URL, and failure context when apply did not complete.\n- When `production-release-on-main=true`, successful staging applies open or\n update a Buildchain-owned release PR from\n `release/<channel>-<short-sha>` to `main`, unless the current push already\n came from a matching release PR merge. The release PR contains one empty\n release-intent commit, carries `production-release-label`, and includes the\n staging URLs, source SHA, artifact hash, and staging release-passport artifact\n link in the PR body.\n- Production release PR handoff is permission-aware. Staging apply and staging\n health remain successful even when the repository or organization has\n GitHub Actions workflow permissions set to read-only. In that case Buildchain\n records `release-pr-status=permission-denied`, uploads the release PR handoff\n summary/body plus staging release passport artifacts, and writes an exact\n manual `gh pr create` command to the step summary. Set\n `fail-on-release-pr-error=true` only when PR creation failure should fail the\n whole workflow.\n- Release pull requests that match the configured production gate get a\n Buildchain review comment with the staging URL and production target, so the\n operator can verify staging from the PR page and use merge as the approval\n action. Consumers do not need to hand-write `gh pr create` or production\n release-intent glue.\n- Production runs when `production-apply` is true and either:\n - a trusted `workflow_dispatch` passes `production-approved=true`; or\n - `production-release-on-main=true` and the `main` push commit is associated\n with exactly one same-repository, merged release pull request matching\n `production-release-label` and `production-release-head-prefix`.\n The production job is then gated by the configured GitHub Environment.\n- Production apply writes a production release feedback passport artifact and\n comments the release PR with the production URL, source SHA, artifact\n identity, run URL, rollback pointer, and failure context when apply did not\n complete.\n\nThe feedback passport records the release responsibility chain:\n\n- human decision actor;\n- trigger actor;\n- runner/execution actor;\n- OIDC/deploy identity reference;\n- decision type and time;\n- source event, PR number, merge commit, and required gate label/head-prefix.\n\n`release-feedback-actor-privacy` controls actor values in the passport and\ncomments. `public` records GitHub actor names, `redacted` records only the actor\nrole, and `private-ref` records a stable private reference hash without exposing\nthe actor name.\n\nFor release-PR publishing, callers opt in explicitly:\n\n```yaml\njobs:\n web-surface:\n uses: kungfu-systems/buildchain/.github/workflows/.web-surface.yml@v2\n with:\n build-command: npm run build\n verify-command: npm run check\n artifact-path: dist\n production-apply: ${{ github.event_name == 'push' && github.ref_name == 'main' }}\n production-release-on-main: true\n production-release-label: buildchain-release\n production-release-head-prefix: release/\n production-release-branch-channel: production\n production-release-pr-mode: auto\n production-aws-role-arn: arn:aws:iam::123456789012:role/site-production-github-actions\n production-environment: production\n```\n\n`production-release-pr-mode` controls the post-staging handoff:\n\n| Mode | Behavior |\n| --- | --- |\n| `auto` | Generate release PR facts, create/update the empty release-intent branch and PR, and label it when token permissions allow. This is the default. |\n| `summary-only` | Generate and upload release PR facts, body, passport evidence, and manual command, but do not call the GitHub PR API. |\n| `disabled` | Record a disabled handoff and skip release PR API calls. |\n\nAutomatic release PR creation normally uses the workflow `github.token`.\nConsumers that cannot enable \"GitHub Actions can create and approve pull\nrequests\" globally should prefer the first-class GitHub App path. Pass the App\nclient id as an input and the private key as a reusable workflow secret; Buildchain\ncreates an installation token inside the release PR job and uses it only for the\nrelease-intent branch, PR, and label operations:\n\n```yaml\nwith:\n production-release-app-client-id: ${{ vars.KUNGFU_RELEASE_APP_CLIENT_ID }}\nsecrets:\n production-release-app-private-key: ${{ secrets.KUNGFU_RELEASE_APP_PRIVATE_KEY }}\n```\n\n`production-release-app-id` remains accepted as a deprecated alias for the input\nname, but the value should be the GitHub App client id. GitHub App numeric App\nIDs and client IDs are distinct, and Buildchain passes the value to\n`actions/create-github-app-token` through its non-deprecated `client-id` input so\nnew runs do not emit the deprecated `app-id` warning.\n\nIf a repository already generates its own narrow token or PAT, it can still pass\nthat through `production-release-pr-token`:\n\n```yaml\nwith:\n production-release-pr-token: ${{ secrets.BUILDCHAIN_RELEASE_PR_TOKEN }}\n```\n\nToken priority is: generated GitHub App installation token,\n`production-release-pr-token`, then `github.token`.\n\nThe merge button becomes the production approval only for a PR that carries the\nrelease label and comes from the configured source-branch prefix. Ordinary pull\nrequests merged into `main` deploy staging and open a release-intent PR; merging\nthat release PR triggers production. A release PR merge push does not open\nanother release PR.\n\nApply-only inputs are validated before the caller build or verification command\nruns. If the current event would run preview, staging, or production apply,\nmissing role inputs or a production apply without `production-approved=true`\non manual dispatch fail immediately instead of spending the build and plan jobs\nfirst.\n\nCallers must grant `id-token: write` for OIDC role assumption. Preview comments\nneed `pull-requests: write`. Automatic release PR creation also needs\n`contents: write`, `pull-requests: write`, and `issues: write` so Buildchain can\ncreate the release branch, write the empty release-intent commit, open or update\nthe PR, and apply the release label. If these permissions are unavailable,\nBuildchain degrades the release handoff instead of marking a successful staging\ndeployment as failed, unless `fail-on-release-pr-error=true`. The AWS roles remain caller-owned and\nshould be scoped by channel: preview can mutate only preview resources, staging\ncan mutate only staging resources, and production can mutate only production\nresources.\n\nApply mode fails closed when the deploy config still contains placeholder AWS\ntargets such as `pending-preview-distribution`. Planning can use placeholders\nfor dry-run-only design work, but live apply requires concrete bucket and\nCloudFront distribution identifiers.\n\n## Site Repository Shape\n\nA site repository can start with:\n\n```toml\nschema = 1\n\n[project]\ntype = \"web-surface\"\nname = \"site-kungfu-tech\"\nsite = \"kungfu-tech\"\n\n[lifecycle.build]\ncommand = \"pnpm run build\"\n\n[lifecycle.verify]\ncommand = \"pnpm run check\"\n```\n\nThen add the channel, deploy, retention, and security declarations shown above.\nThe project may use pnpm, npm, yarn, Vite, Astro, Next static export, Sphinx,\nMkDocs, CMake-generated docs, or another lifecycle command source. Buildchain\nonly needs a deterministic artifact path and the manifest facts.\n\n## Boundaries\n\nBuildchain only performs live AWS mutations in explicit apply modes with\n`--dry-run false`. Production deploys must still be gated by a human-controlled\nworkflow, release, or GitHub Environment. DNS changes, staging auth\nimplementation, CloudFront distribution creation, and credential provisioning\nremain explicitly authorized infrastructure operations outside the web-surface\nartifact apply contract."
|
|
1733
1733
|
},
|
|
1734
1734
|
{
|
|
1735
1735
|
"id": "fixture:infra-contract-aws-cdk-shaped",
|
|
@@ -2504,7 +2504,7 @@
|
|
|
2504
2504
|
"pageRegistryPath": "dist/site/page-registry.json",
|
|
2505
2505
|
"cliRegistryDigest": "fb33fe38423f56ebf95919e2b58e13dc2f544aee10169bc87d7afe980bc1b142",
|
|
2506
2506
|
"workflowRegistryDigest": "4041321df08640a802a87ff0fbe8584d95dc416945b43778727adb820cab9097",
|
|
2507
|
-
"pageRegistryDigest": "
|
|
2507
|
+
"pageRegistryDigest": "a80f74c3b93d9c1b8baa438a81c2d875b9995fb005dae923d1d32189b6a5573d"
|
|
2508
2508
|
},
|
|
2509
2509
|
"comparison": {
|
|
2510
2510
|
"missingCliRegistry": [],
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"contract": "kungfu-buildchain-publication-release-registry",
|
|
4
|
-
"generatedAt": "2026-07-
|
|
5
|
-
"publishedAt": "2026-07-
|
|
4
|
+
"generatedAt": "2026-07-09T21:23:58.798Z",
|
|
5
|
+
"publishedAt": "2026-07-09T21:23:58.798Z",
|
|
6
6
|
"reproducible": true,
|
|
7
7
|
"timestampPolicy": "ci-injected",
|
|
8
8
|
"deterministicInputs": [
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"declared Buildchain surface manifest contract"
|
|
20
20
|
],
|
|
21
21
|
"sourceDateEpoch": "0",
|
|
22
|
-
"sourceRevision": "
|
|
22
|
+
"sourceRevision": "df6967b5d0a5c5c29cbe953c78f0d0a5df673ccf",
|
|
23
23
|
"timestampPolicyDetails": {
|
|
24
24
|
"contract": "kungfu-buildchain-surface-timestamp-policy",
|
|
25
25
|
"timestampFields": [
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"package": {
|
|
34
34
|
"name": "@kungfu-tech/buildchain",
|
|
35
|
-
"version": "2.11.
|
|
35
|
+
"version": "2.11.4",
|
|
36
36
|
"versionSource": "package.json#version"
|
|
37
37
|
},
|
|
38
38
|
"sourceKind": "package-site-bundle",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"contract": "kungfu-buildchain-site-manifest",
|
|
4
|
-
"generatedAt": "2026-07-
|
|
5
|
-
"publishedAt": "2026-07-
|
|
4
|
+
"generatedAt": "2026-07-09T21:23:58.798Z",
|
|
5
|
+
"publishedAt": "2026-07-09T21:23:58.798Z",
|
|
6
6
|
"reproducible": true,
|
|
7
7
|
"timestampPolicy": "ci-injected",
|
|
8
8
|
"deterministicInputs": [
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"declared Buildchain surface manifest contract"
|
|
20
20
|
],
|
|
21
21
|
"sourceDateEpoch": "0",
|
|
22
|
-
"sourceRevision": "
|
|
22
|
+
"sourceRevision": "df6967b5d0a5c5c29cbe953c78f0d0a5df673ccf",
|
|
23
23
|
"timestampPolicyDetails": {
|
|
24
24
|
"contract": "kungfu-buildchain-surface-timestamp-policy",
|
|
25
25
|
"timestampFields": [
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"package": {
|
|
39
39
|
"name": "@kungfu-tech/buildchain",
|
|
40
|
-
"version": "2.11.
|
|
40
|
+
"version": "2.11.4",
|
|
41
41
|
"versionSource": "package.json#version"
|
|
42
42
|
},
|
|
43
43
|
"entrypoint": "buildchain-site.json",
|
|
@@ -253,7 +253,7 @@
|
|
|
253
253
|
"path": "docs/web-surface-deployments.md",
|
|
254
254
|
"plane": "use",
|
|
255
255
|
"exists": true,
|
|
256
|
-
"digest": "sha256:
|
|
256
|
+
"digest": "sha256:b24b9e6f23ad13ccd3a1074a2ecefd26abdd7f848e979dea64b96678f57f0d6d"
|
|
257
257
|
}
|
|
258
258
|
],
|
|
259
259
|
"facts": [
|
|
@@ -500,6 +500,11 @@ When the apply result includes CloudFront invalidations from the
|
|
|
500
500
|
CloudFront 403 responses as deployment failures immediately after a successful
|
|
501
501
|
S3 sync and invalidation request.
|
|
502
502
|
|
|
503
|
+
After the invalidation wait, HTTP smoke checks also retry transient 403, 404, and
|
|
504
|
+
5xx responses before recording failure. This keeps the public health signal
|
|
505
|
+
strict while absorbing short CloudFront edge propagation windows that can remain
|
|
506
|
+
visible for a few seconds after the invalidation waiter returns.
|
|
507
|
+
|
|
503
508
|
The health check fetches every surface root URL and any nested smoke URLs
|
|
504
509
|
recorded in each surface binding. Nested smoke URLs are derived from nested HTML
|
|
505
510
|
artifact files under the surface path prefix, with directory index resolution
|
package/package.json
CHANGED
|
@@ -4,6 +4,7 @@ import dns from "node:dns/promises";
|
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
7
8
|
import { fileURLToPath } from "node:url";
|
|
8
9
|
import { loadBuildchainConfig, validateBuildchainConfig } from "../packages/core/buildchain-config.js";
|
|
9
10
|
import { createSurfaceTimestampPolicy } from "../packages/core/surface-manifest.js";
|
|
@@ -1436,6 +1437,37 @@ function managedNetworkHealthCheck({ target, result, plan, commandRunner, verify
|
|
|
1436
1437
|
};
|
|
1437
1438
|
}
|
|
1438
1439
|
|
|
1440
|
+
async function fetchWithRetry(url, {
|
|
1441
|
+
fetchImpl,
|
|
1442
|
+
attempts = 3,
|
|
1443
|
+
intervalMs = 5000,
|
|
1444
|
+
shouldRetry = () => false,
|
|
1445
|
+
} = {}) {
|
|
1446
|
+
const maxAttempts = Math.max(1, Number(attempts) || 1);
|
|
1447
|
+
let lastError = null;
|
|
1448
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
1449
|
+
try {
|
|
1450
|
+
const response = await fetchImpl(url, { redirect: "follow" });
|
|
1451
|
+
if (attempt >= maxAttempts || !shouldRetry(response)) {
|
|
1452
|
+
return { response, attempts: attempt };
|
|
1453
|
+
}
|
|
1454
|
+
} catch (error) {
|
|
1455
|
+
lastError = error;
|
|
1456
|
+
if (attempt >= maxAttempts) {
|
|
1457
|
+
throw Object.assign(error, { attempts: attempt });
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
if (intervalMs > 0) {
|
|
1461
|
+
await sleep(intervalMs);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
throw Object.assign(lastError || new Error("web-surface fetch retry exhausted"), { attempts: maxAttempts });
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
function retryableHealthResponse(response) {
|
|
1468
|
+
return response.status === 403 || response.status === 404 || response.status >= 500;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1439
1471
|
export async function checkWebSurfaceHealth({
|
|
1440
1472
|
result = null,
|
|
1441
1473
|
plan = null,
|
|
@@ -1446,6 +1478,8 @@ export async function checkWebSurfaceHealth({
|
|
|
1446
1478
|
allowedManagedNetworkRunner = false,
|
|
1447
1479
|
managedNetworkS3ObjectVerification = true,
|
|
1448
1480
|
commandRunner = defaultCommandRunner,
|
|
1481
|
+
httpRetryAttempts = Number(process.env.BUILDCHAIN_WEB_SURFACE_HEALTH_HTTP_RETRY_ATTEMPTS || 3),
|
|
1482
|
+
httpRetryIntervalMs = Number(process.env.BUILDCHAIN_WEB_SURFACE_HEALTH_HTTP_RETRY_INTERVAL_MS || 5000),
|
|
1449
1483
|
} = {}) {
|
|
1450
1484
|
const loadedConfig = loadBuildchainConfig(cwd);
|
|
1451
1485
|
const config = assertWebSurfaceConfig(loadedConfig);
|
|
@@ -1523,7 +1557,12 @@ export async function checkWebSurfaceHealth({
|
|
|
1523
1557
|
continue;
|
|
1524
1558
|
}
|
|
1525
1559
|
try {
|
|
1526
|
-
const response = await
|
|
1560
|
+
const { response, attempts } = await fetchWithRetry(url, {
|
|
1561
|
+
fetchImpl,
|
|
1562
|
+
attempts: httpRetryAttempts,
|
|
1563
|
+
intervalMs: httpRetryIntervalMs,
|
|
1564
|
+
shouldRetry: retryableHealthResponse,
|
|
1565
|
+
});
|
|
1527
1566
|
const expectedNoindex = Boolean(config.channels?.[channel]?.noindex);
|
|
1528
1567
|
const headerNoindex = noindexHeader(response.headers);
|
|
1529
1568
|
const body = htmlLikeResponse(response, url) && typeof response.text === "function"
|
|
@@ -1542,6 +1581,7 @@ export async function checkWebSurfaceHealth({
|
|
|
1542
1581
|
healthStrategy: "http",
|
|
1543
1582
|
status: statusOk && noindexOk ? "pass" : "fail",
|
|
1544
1583
|
httpStatus: response.status,
|
|
1584
|
+
attempts,
|
|
1545
1585
|
finalUrl: response.url || url,
|
|
1546
1586
|
noindexHeader: noindex,
|
|
1547
1587
|
noindexHeaderValue: headerNoindex,
|
|
@@ -1558,6 +1598,7 @@ export async function checkWebSurfaceHealth({
|
|
|
1558
1598
|
url,
|
|
1559
1599
|
status: "fail",
|
|
1560
1600
|
httpStatus: 0,
|
|
1601
|
+
attempts: error.attempts || 1,
|
|
1561
1602
|
finalUrl: "",
|
|
1562
1603
|
noindexHeader: false,
|
|
1563
1604
|
message: String(error.message || error),
|