@kungfu-tech/buildchain 2.2.6-alpha.0 → 2.3.0-alpha.0
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/docs/lifecycle-protocol.md +21 -8
- package/docs/versioning.md +1 -0
- package/docs/web-surface-deployments.md +97 -11
- package/fixtures/web-surface-shaped/README.md +16 -2
- package/package.json +1 -1
- package/packages/core/buildchain-config.js +132 -2
- package/scripts/web-surface-core.mjs +320 -94
- package/scripts/web-surface.mjs +3 -0
|
@@ -307,16 +307,16 @@ schema = 1
|
|
|
307
307
|
|
|
308
308
|
[project]
|
|
309
309
|
type = "web-surface"
|
|
310
|
-
name = "site-
|
|
311
|
-
site = "
|
|
310
|
+
name = "site-libkungfu-dev"
|
|
311
|
+
site = "libkungfu-dev"
|
|
312
312
|
|
|
313
313
|
[channels.preview]
|
|
314
|
-
url_pattern = "https://{alias}.preview.
|
|
314
|
+
url_pattern = "https://{alias}.preview.libkungfu.dev"
|
|
315
315
|
visibility = "ephemeral"
|
|
316
316
|
noindex = true
|
|
317
317
|
|
|
318
318
|
[channels.staging]
|
|
319
|
-
url = "https://staging.
|
|
319
|
+
url = "https://staging.libkungfu.dev"
|
|
320
320
|
visibility = "protected"
|
|
321
321
|
access_control = "managed-network"
|
|
322
322
|
edge_auth = "none"
|
|
@@ -324,20 +324,33 @@ noindex = true
|
|
|
324
324
|
promotable = true
|
|
325
325
|
|
|
326
326
|
[channels.production]
|
|
327
|
-
url = "https://
|
|
327
|
+
url = "https://libkungfu.dev"
|
|
328
328
|
visibility = "public"
|
|
329
329
|
canonical = true
|
|
330
330
|
noindex = false
|
|
331
331
|
|
|
332
|
+
[surfaces.hub]
|
|
333
|
+
path = "/"
|
|
334
|
+
production_url = "https://libkungfu.dev"
|
|
335
|
+
staging_url = "https://staging.libkungfu.dev"
|
|
336
|
+
preview_url_pattern = "https://{alias}.preview.libkungfu.dev"
|
|
337
|
+
|
|
338
|
+
[surfaces.core]
|
|
339
|
+
path = "/core/"
|
|
340
|
+
production_url = "https://core.libkungfu.dev"
|
|
341
|
+
staging_url = "https://core.staging.libkungfu.dev"
|
|
342
|
+
preview_url_pattern = "https://core-{alias}.preview.libkungfu.dev"
|
|
343
|
+
|
|
332
344
|
[deploy.production]
|
|
333
345
|
adapter = "aws-s3-cloudfront"
|
|
334
|
-
bucket = "
|
|
346
|
+
bucket = "libkungfu-dev-production"
|
|
335
347
|
artifact_path = "dist"
|
|
336
348
|
secret_refs = ["AWS_ROLE_ARN"]
|
|
337
349
|
```
|
|
338
350
|
|
|
339
|
-
See [Web-surface deployments](web-surface-deployments.md) for
|
|
340
|
-
preview alias, retention, cleanup, and dry-run deploy
|
|
351
|
+
See [Web-surface deployments](web-surface-deployments.md) for named surface host
|
|
352
|
+
mappings, the manifest, preview alias, retention, cleanup, and dry-run deploy
|
|
353
|
+
contract.
|
|
341
354
|
|
|
342
355
|
## Examples
|
|
343
356
|
|
package/docs/versioning.md
CHANGED
|
@@ -42,6 +42,7 @@ expected ref flow requires a major bump.
|
|
|
42
42
|
| --- | --- | --- | --- |
|
|
43
43
|
| 2026-07-02 | Buildchain toolkit observability is a minor surface. | `v2.1` | It adds the public logging SDK, CLI observability commands, and package subpaths that consumers can import. |
|
|
44
44
|
| 2026-07-02 | Release passport and binary distribution are a minor surface. | `v2.2` | They add agent-readable release passport files, artifact evidence, impact ledger, agent index, GitHub Release collection and verification commands, and standalone binary assets. |
|
|
45
|
+
| 2026-07-02 | Web surface host mapping is a minor surface. | `v2.3` | It adds first-class multi-host surface bindings, reusable workflow URL outputs, per-surface deployment overrides, and an agent-readable fixture contract. |
|
|
45
46
|
|
|
46
47
|
## Runner Policy
|
|
47
48
|
|
|
@@ -61,6 +61,72 @@ artifact_path = "dist"
|
|
|
61
61
|
secret_refs = ["AWS_ROLE_ARN"]
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
### Multi-Surface Host Mapping
|
|
65
|
+
|
|
66
|
+
Some site repositories publish more than one first-class web surface from the
|
|
67
|
+
same artifact. For example, a developer substrate site may have a hub plus
|
|
68
|
+
separate hostnames for core and Buildchain documentation. These are not just
|
|
69
|
+
navigation paths; staging and preview should be able to verify host-level
|
|
70
|
+
behavior for each surface.
|
|
71
|
+
|
|
72
|
+
Declare named surfaces with per-channel URLs:
|
|
73
|
+
|
|
74
|
+
```toml
|
|
75
|
+
[surfaces.hub]
|
|
76
|
+
path = "/"
|
|
77
|
+
production_url = "https://libkungfu.dev"
|
|
78
|
+
staging_url = "https://staging.libkungfu.dev"
|
|
79
|
+
preview_url_pattern = "https://{alias}.preview.libkungfu.dev"
|
|
80
|
+
|
|
81
|
+
[surfaces.core]
|
|
82
|
+
path = "/core/"
|
|
83
|
+
production_url = "https://core.libkungfu.dev"
|
|
84
|
+
staging_url = "https://core.staging.libkungfu.dev"
|
|
85
|
+
preview_url_pattern = "https://core-{alias}.preview.libkungfu.dev"
|
|
86
|
+
|
|
87
|
+
[surfaces.buildchain]
|
|
88
|
+
path = "/buildchain/"
|
|
89
|
+
production_url = "https://buildchain.libkungfu.dev"
|
|
90
|
+
staging_url = "https://buildchain.staging.libkungfu.dev"
|
|
91
|
+
preview_url_pattern = "https://buildchain-{alias}.preview.libkungfu.dev"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Buildchain resolves every `(channel, surface)` pair. A preview alias such as
|
|
95
|
+
`pr-12` becomes:
|
|
96
|
+
|
|
97
|
+
```text
|
|
98
|
+
hub: https://pr-12.preview.libkungfu.dev
|
|
99
|
+
core: https://core-pr-12.preview.libkungfu.dev
|
|
100
|
+
buildchain: https://buildchain-pr-12.preview.libkungfu.dev
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
When `surfaces` is omitted, Buildchain preserves the legacy single-surface
|
|
104
|
+
contract by creating an implicit `default` surface from the channel URL. When a
|
|
105
|
+
surface is intentionally path-only, declare it explicitly:
|
|
106
|
+
|
|
107
|
+
```toml
|
|
108
|
+
[surfaces.docs]
|
|
109
|
+
path = "/docs/"
|
|
110
|
+
path_only = true
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`path_only = true` is an exception, not the default. Without it, every named
|
|
114
|
+
surface must declare `preview_url_pattern`, `staging_url`, and
|
|
115
|
+
`production_url`. This makes staging/production mismatches fail during
|
|
116
|
+
validation instead of becoming invisible deploy drift.
|
|
117
|
+
|
|
118
|
+
Adapter strategy remains explicit. The default `aws-s3-cloudfront` plan uses the
|
|
119
|
+
channel deploy target for every surface, and each binding records its own
|
|
120
|
+
bucket, distribution id, object prefix, manifest key, source path, and URL. A
|
|
121
|
+
channel can override target details per surface:
|
|
122
|
+
|
|
123
|
+
```toml
|
|
124
|
+
[deploy.staging.surfaces.core]
|
|
125
|
+
bucket = "libkungfu-dev-core-staging"
|
|
126
|
+
cloudfront_distribution = "E-CORE-STAGING"
|
|
127
|
+
origin_path = "/core"
|
|
128
|
+
```
|
|
129
|
+
|
|
64
130
|
Buildchain validates these hard constraints:
|
|
65
131
|
|
|
66
132
|
- `channels.preview.url_pattern` is required and must contain the alias shape
|
|
@@ -73,6 +139,8 @@ Buildchain validates these hard constraints:
|
|
|
73
139
|
- `channels.staging.noindex = true` is required.
|
|
74
140
|
- `channels.production.url` is required.
|
|
75
141
|
- deploy adapters must be declared per channel.
|
|
142
|
+
- named surfaces must declare first-class URLs for every channel unless
|
|
143
|
+
`path_only = true` is explicitly set.
|
|
76
144
|
- secret material must be declared as reference names, such as
|
|
77
145
|
`secret_refs = ["AWS_ROLE_ARN"]`; inline secret-like deploy keys are rejected.
|
|
78
146
|
|
|
@@ -116,13 +184,13 @@ rollback:
|
|
|
116
184
|
{
|
|
117
185
|
"schemaVersion": 1,
|
|
118
186
|
"contract": "kungfu-buildchain-web-surface-deployment",
|
|
119
|
-
"site": "
|
|
187
|
+
"site": "libkungfu-dev",
|
|
120
188
|
"channel": "preview",
|
|
121
189
|
"alias": "sha-abcdef123456",
|
|
122
|
-
"url": "https://sha-abcdef123456.preview.
|
|
190
|
+
"url": "https://sha-abcdef123456.preview.libkungfu.dev",
|
|
123
191
|
"sourceSha": "...",
|
|
124
192
|
"artifactHash": "...",
|
|
125
|
-
"deployTarget": "
|
|
193
|
+
"deployTarget": "libkungfu-dev-preview",
|
|
126
194
|
"adapter": "aws-s3-cloudfront",
|
|
127
195
|
"deployedAt": "2026-07-01T00:00:00.000Z",
|
|
128
196
|
"retentionClass": "preview-sha-immutable",
|
|
@@ -130,7 +198,24 @@ rollback:
|
|
|
130
198
|
"accessControl": "none",
|
|
131
199
|
"edgeAuth": "none",
|
|
132
200
|
"noindex": true,
|
|
133
|
-
"secretRefs": ["AWS_ROLE_ARN"]
|
|
201
|
+
"secretRefs": ["AWS_ROLE_ARN"],
|
|
202
|
+
"surfaceBindings": [
|
|
203
|
+
{
|
|
204
|
+
"surface": "hub",
|
|
205
|
+
"channel": "preview",
|
|
206
|
+
"alias": "sha-abcdef123456",
|
|
207
|
+
"url": "https://sha-abcdef123456.preview.libkungfu.dev",
|
|
208
|
+
"sourcePath": "/",
|
|
209
|
+
"canonicalUrl": "https://libkungfu.dev",
|
|
210
|
+
"bucket": "libkungfu-dev-preview",
|
|
211
|
+
"distributionId": "E-PREVIEW",
|
|
212
|
+
"originPath": "",
|
|
213
|
+
"objectPrefix": "sha-abcdef123456",
|
|
214
|
+
"manifestKey": ".buildchain/deployments/sha-abcdef123456/hub.json",
|
|
215
|
+
"noindex": true,
|
|
216
|
+
"accessControl": "none"
|
|
217
|
+
}
|
|
218
|
+
]
|
|
134
219
|
}
|
|
135
220
|
```
|
|
136
221
|
|
|
@@ -169,6 +254,7 @@ The CLI emits GitHub outputs when `GITHUB_OUTPUT` is present:
|
|
|
169
254
|
- `web-surface-channel`
|
|
170
255
|
- `web-surface-alias`
|
|
171
256
|
- `web-surface-url`
|
|
257
|
+
- `web-surface-urls-json`
|
|
172
258
|
- `web-surface-artifact-hash`
|
|
173
259
|
- `web-surface-manifest-json`
|
|
174
260
|
|
|
@@ -231,13 +317,13 @@ node scripts/web-surface.mjs \
|
|
|
231
317
|
```
|
|
232
318
|
|
|
233
319
|
Apply output records the channel, alias, source SHA, artifact hash, target
|
|
234
|
-
bucket, object prefix, manifest key,
|
|
235
|
-
and every adapter operation with
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
`secret_refs`.
|
|
320
|
+
bucket, object prefix, manifest key, all surface URLs, all surface bindings, CDN
|
|
321
|
+
invalidation paths, actor/run metadata, and every adapter operation with
|
|
322
|
+
`executed`, `exitCode`, `stdout`, and `stderr`. If an operation fails,
|
|
323
|
+
Buildchain records the failed operation, stops subsequent adapter operations,
|
|
324
|
+
and exits non-zero after writing the result JSON. Buildchain records secret
|
|
325
|
+
reference names only; the runner must provide the AWS CLI and credentials
|
|
326
|
+
outside Buildchain, typically through OIDC and the declared `secret_refs`.
|
|
241
327
|
|
|
242
328
|
## Cleanup Plans
|
|
243
329
|
|
|
@@ -4,6 +4,20 @@ This fixture models a static site repository that wants Buildchain deployment
|
|
|
4
4
|
semantics without opting into package release lines.
|
|
5
5
|
|
|
6
6
|
It proves that `project.type = "web-surface"` can declare preview, staging, and
|
|
7
|
-
production channels,
|
|
8
|
-
dry-run deploy and cleanup plans without touching AWS.
|
|
7
|
+
production channels, named surface host mappings, deterministic deployment
|
|
8
|
+
manifests, and dry-run deploy and cleanup plans without touching AWS.
|
|
9
9
|
|
|
10
|
+
The fixture is shaped like a `site-libkungfu-dev` repository with three
|
|
11
|
+
first-class surfaces:
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
hub https://libkungfu.dev
|
|
15
|
+
core https://core.libkungfu.dev
|
|
16
|
+
buildchain https://buildchain.libkungfu.dev
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
For preview and staging, Buildchain resolves the same surface names to the
|
|
20
|
+
channel-specific hosts and records them in `surfaceBindings`. Path-only pages
|
|
21
|
+
can still be declared with `path_only = true`, but this fixture intentionally
|
|
22
|
+
uses first-class hosts for every surface so validation can catch missing
|
|
23
|
+
channel mappings.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kungfu-tech/buildchain",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0-alpha.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Buildchain Release Passport, release governance, CLI toolkit, and site facts.",
|
|
6
6
|
"repository": "https://github.com/kungfu-systems/buildchain",
|
|
@@ -116,6 +116,9 @@ export function normalizeBuildchainConfig(config) {
|
|
|
116
116
|
if (normalized.deploy !== undefined) {
|
|
117
117
|
normalized.deploy = normalizeDeploySection(normalized.deploy, normalized.project);
|
|
118
118
|
}
|
|
119
|
+
if (normalized.surfaces !== undefined) {
|
|
120
|
+
normalized.surfaces = normalizeSurfacesSection(normalized.surfaces);
|
|
121
|
+
}
|
|
119
122
|
if (normalized.retention !== undefined) {
|
|
120
123
|
normalized.retention = normalizeRetentionSection(normalized.retention);
|
|
121
124
|
}
|
|
@@ -291,20 +294,113 @@ function normalizeDeployConfig(name, config) {
|
|
|
291
294
|
? undefined
|
|
292
295
|
: posixPath(assertString(config.artifact_path, `deploy.${name}.artifact_path`)),
|
|
293
296
|
secretRefs: normalizeStringArray(config.secret_refs, `deploy.${name}.secret_refs`),
|
|
297
|
+
surfaces: config.surfaces === undefined
|
|
298
|
+
? undefined
|
|
299
|
+
: normalizeDeploySurfaceOverrides(name, config.surfaces),
|
|
300
|
+
};
|
|
301
|
+
delete normalized.artifact_path;
|
|
302
|
+
delete normalized.secret_refs;
|
|
303
|
+
if (normalized.surfaces === undefined) {
|
|
304
|
+
delete normalized.surfaces;
|
|
305
|
+
}
|
|
306
|
+
assertNoInlineSecretValues(config, `deploy.${name}`, new Set(["secret_refs", "surfaces"]));
|
|
307
|
+
return normalized;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function normalizeDeploySurfaceOverrides(channelName, surfaces) {
|
|
311
|
+
assertPlainObject(surfaces, `deploy.${channelName}.surfaces`);
|
|
312
|
+
return Object.fromEntries(
|
|
313
|
+
Object.entries(surfaces).map(([surfaceName, override]) => [
|
|
314
|
+
surfaceName,
|
|
315
|
+
normalizeDeploySurfaceOverride(channelName, surfaceName, override),
|
|
316
|
+
]),
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function normalizeDeploySurfaceOverride(channelName, surfaceName, override) {
|
|
321
|
+
assertPlainObject(override, `deploy.${channelName}.surfaces.${surfaceName}`);
|
|
322
|
+
const label = `deploy.${channelName}.surfaces.${surfaceName}`;
|
|
323
|
+
const normalized = {
|
|
324
|
+
...override,
|
|
325
|
+
artifactPath: override.artifact_path === undefined
|
|
326
|
+
? undefined
|
|
327
|
+
: posixPath(assertString(override.artifact_path, `${label}.artifact_path`)),
|
|
328
|
+
originPath: override.origin_path === undefined
|
|
329
|
+
? undefined
|
|
330
|
+
: assertString(override.origin_path, `${label}.origin_path`),
|
|
331
|
+
secretRefs: normalizeStringArray(override.secret_refs, `${label}.secret_refs`),
|
|
294
332
|
};
|
|
295
333
|
delete normalized.artifact_path;
|
|
334
|
+
delete normalized.origin_path;
|
|
296
335
|
delete normalized.secret_refs;
|
|
336
|
+
if (normalized.artifactPath === undefined) {
|
|
337
|
+
delete normalized.artifactPath;
|
|
338
|
+
}
|
|
339
|
+
if (normalized.originPath === undefined) {
|
|
340
|
+
delete normalized.originPath;
|
|
341
|
+
}
|
|
342
|
+
if (normalized.secretRefs.length === 0) {
|
|
343
|
+
delete normalized.secretRefs;
|
|
344
|
+
}
|
|
345
|
+
assertNoInlineSecretValues(override, label, new Set(["secret_refs"]));
|
|
346
|
+
return normalized;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function assertNoInlineSecretValues(config, label, ignoredKeys = new Set()) {
|
|
297
350
|
for (const [key, value] of Object.entries(config)) {
|
|
298
|
-
if (key
|
|
351
|
+
if (ignoredKeys.has(key) || key.endsWith("_ref") || key.endsWith("_refs")) {
|
|
299
352
|
continue;
|
|
300
353
|
}
|
|
301
354
|
if (/(secret|token|password)/i.test(key) && value !== undefined) {
|
|
302
|
-
throw new Error(
|
|
355
|
+
throw new Error(`${label}.${key} must be declared as a secret reference, not a secret value`);
|
|
303
356
|
}
|
|
304
357
|
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function normalizeSurfacesSection(surfaces) {
|
|
361
|
+
assertPlainObject(surfaces, "surfaces");
|
|
362
|
+
const entries = Object.entries(surfaces);
|
|
363
|
+
if (entries.length === 0) {
|
|
364
|
+
throw new Error("surfaces must declare at least one surface");
|
|
365
|
+
}
|
|
366
|
+
return Object.fromEntries(
|
|
367
|
+
entries.map(([name, surface]) => [
|
|
368
|
+
name,
|
|
369
|
+
normalizeSurfaceConfig(name, surface),
|
|
370
|
+
]),
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function normalizeSurfaceConfig(name, surface) {
|
|
375
|
+
assertPlainObject(surface, `surfaces.${name}`);
|
|
376
|
+
const normalized = {
|
|
377
|
+
name,
|
|
378
|
+
path: surface.path === undefined
|
|
379
|
+
? "/"
|
|
380
|
+
: normalizeSurfacePath(assertString(surface.path, `surfaces.${name}.path`)),
|
|
381
|
+
pathOnly: optionalBoolean(surface.path_only, false),
|
|
382
|
+
canonical: optionalBoolean(surface.canonical, name === "default" || name === "hub"),
|
|
383
|
+
};
|
|
384
|
+
if (surface.preview_url_pattern !== undefined) {
|
|
385
|
+
normalized.previewUrlPattern = assertString(surface.preview_url_pattern, `surfaces.${name}.preview_url_pattern`);
|
|
386
|
+
}
|
|
387
|
+
if (surface.staging_url !== undefined) {
|
|
388
|
+
normalized.stagingUrl = assertString(surface.staging_url, `surfaces.${name}.staging_url`);
|
|
389
|
+
}
|
|
390
|
+
if (surface.production_url !== undefined) {
|
|
391
|
+
normalized.productionUrl = assertString(surface.production_url, `surfaces.${name}.production_url`);
|
|
392
|
+
}
|
|
305
393
|
return normalized;
|
|
306
394
|
}
|
|
307
395
|
|
|
396
|
+
function normalizeSurfacePath(value) {
|
|
397
|
+
const normalized = `/${String(value).replace(/^\/+/, "")}`.replace(/\/{2,}/g, "/");
|
|
398
|
+
if (normalized === "/") {
|
|
399
|
+
return normalized;
|
|
400
|
+
}
|
|
401
|
+
return normalized.endsWith("/") ? normalized : `${normalized}/`;
|
|
402
|
+
}
|
|
403
|
+
|
|
308
404
|
function normalizeRetentionSection(retention) {
|
|
309
405
|
assertPlainObject(retention, "retention");
|
|
310
406
|
return Object.fromEntries(
|
|
@@ -390,6 +486,25 @@ function validateWebSurfaceConfig(config) {
|
|
|
390
486
|
throw new Error("security.staging.isolated_providers must not be false for web-surface");
|
|
391
487
|
}
|
|
392
488
|
}
|
|
489
|
+
validateWebSurfaceSurfaces(config);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function validateWebSurfaceSurfaces(config) {
|
|
493
|
+
const surfaces = config.surfaces || {};
|
|
494
|
+
for (const [name, surface] of Object.entries(surfaces)) {
|
|
495
|
+
if (!surface.pathOnly && !surface.previewUrlPattern) {
|
|
496
|
+
throw new Error(`surfaces.${name}.preview_url_pattern is required unless path_only = true`);
|
|
497
|
+
}
|
|
498
|
+
if (!surface.pathOnly && !surface.stagingUrl) {
|
|
499
|
+
throw new Error(`surfaces.${name}.staging_url is required unless path_only = true`);
|
|
500
|
+
}
|
|
501
|
+
if (!surface.pathOnly && !surface.productionUrl) {
|
|
502
|
+
throw new Error(`surfaces.${name}.production_url is required unless path_only = true`);
|
|
503
|
+
}
|
|
504
|
+
if (surface.productionUrl && !surface.pathOnly && !surface.stagingUrl) {
|
|
505
|
+
throw new Error(`surfaces.${name}.staging_url is required when production_url declares a first-class host`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
393
508
|
}
|
|
394
509
|
|
|
395
510
|
function normalizeVersionSection(version) {
|
|
@@ -734,6 +849,21 @@ export function validateBuildchainConfig(
|
|
|
734
849
|
]),
|
|
735
850
|
)
|
|
736
851
|
: undefined,
|
|
852
|
+
surfaces: loadedConfig.config.surfaces
|
|
853
|
+
? Object.fromEntries(
|
|
854
|
+
Object.entries(loadedConfig.config.surfaces).map(([name, surface]) => [
|
|
855
|
+
name,
|
|
856
|
+
{
|
|
857
|
+
path: surface.path,
|
|
858
|
+
pathOnly: surface.pathOnly,
|
|
859
|
+
canonical: surface.canonical,
|
|
860
|
+
previewUrlPattern: surface.previewUrlPattern,
|
|
861
|
+
stagingUrl: surface.stagingUrl,
|
|
862
|
+
productionUrl: surface.productionUrl,
|
|
863
|
+
},
|
|
864
|
+
]),
|
|
865
|
+
)
|
|
866
|
+
: undefined,
|
|
737
867
|
retention: loadedConfig.config.retention,
|
|
738
868
|
security: loadedConfig.config.security,
|
|
739
869
|
version: loadedConfig.config.version
|
|
@@ -117,6 +117,38 @@ function resolveChannelUrl(channel, alias) {
|
|
|
117
117
|
return channel.url;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
function resolvePathOnlyUrl(channel, sourcePath, alias) {
|
|
121
|
+
const base = resolveChannelUrl(channel, alias);
|
|
122
|
+
const url = new URL(base);
|
|
123
|
+
const normalizedPath = normalizeSurfacePath(sourcePath);
|
|
124
|
+
if (normalizedPath === "/") {
|
|
125
|
+
return `${url.origin}/`;
|
|
126
|
+
}
|
|
127
|
+
return new URL(normalizedPath.replace(/^\//, ""), `${url.origin}/`).toString();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function resolveSurfaceUrl({ surface, channel, channelName, alias }) {
|
|
131
|
+
if (channelName === "preview" && surface.previewUrlPattern) {
|
|
132
|
+
if (!alias) {
|
|
133
|
+
throw new Error(`surface ${surface.name} requires alias for preview_url_pattern`);
|
|
134
|
+
}
|
|
135
|
+
return surface.previewUrlPattern.replaceAll("{alias}", alias);
|
|
136
|
+
}
|
|
137
|
+
if (channelName === "staging" && surface.stagingUrl) {
|
|
138
|
+
return surface.stagingUrl;
|
|
139
|
+
}
|
|
140
|
+
if (channelName === "production" && surface.productionUrl) {
|
|
141
|
+
return surface.productionUrl;
|
|
142
|
+
}
|
|
143
|
+
if (surface.pathOnly) {
|
|
144
|
+
return resolvePathOnlyUrl(channel, surface.path, alias);
|
|
145
|
+
}
|
|
146
|
+
const key = channelName === "preview"
|
|
147
|
+
? "preview_url_pattern"
|
|
148
|
+
: `${channelName}_url`;
|
|
149
|
+
throw new Error(`surfaces.${surface.name}.${key} is required for channel ${channelName}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
120
152
|
function manifestPrefixFor(deployConfig) {
|
|
121
153
|
return deployConfig.manifest_prefix || ".buildchain/deployments";
|
|
122
154
|
}
|
|
@@ -128,10 +160,51 @@ function objectPrefixFor(deployConfig, alias) {
|
|
|
128
160
|
return alias || "preview";
|
|
129
161
|
}
|
|
130
162
|
|
|
163
|
+
function normalizeSurfacePath(value) {
|
|
164
|
+
const normalized = `/${String(value || "/").replace(/^\/+/, "")}`.replace(/\/{2,}/g, "/");
|
|
165
|
+
if (normalized === "/") {
|
|
166
|
+
return normalized;
|
|
167
|
+
}
|
|
168
|
+
return normalized.endsWith("/") ? normalized : `${normalized}/`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function surfaceDeployConfig(deployConfig, surfaceName) {
|
|
172
|
+
const overrides = deployConfig.surfaces?.[surfaceName];
|
|
173
|
+
if (!overrides || typeof overrides !== "object" || Array.isArray(overrides)) {
|
|
174
|
+
return deployConfig;
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
...deployConfig,
|
|
178
|
+
...overrides,
|
|
179
|
+
surfaces: deployConfig.surfaces,
|
|
180
|
+
secretRefs: [
|
|
181
|
+
...new Set([
|
|
182
|
+
...(deployConfig.secretRefs || []),
|
|
183
|
+
...(Array.isArray(overrides.secretRefs) ? overrides.secretRefs : []),
|
|
184
|
+
]),
|
|
185
|
+
],
|
|
186
|
+
artifactPath: overrides.artifactPath || deployConfig.artifactPath,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function surfaceObjectPrefixFor({ deployConfig, surface, alias }) {
|
|
191
|
+
const effective = surfaceDeployConfig(deployConfig, surface.name);
|
|
192
|
+
if (Object.hasOwn(effective, "prefix")) {
|
|
193
|
+
return normalizeS3Key(effective.prefix);
|
|
194
|
+
}
|
|
195
|
+
const root = objectPrefixFor(deployConfig, alias);
|
|
196
|
+
const surfacePath = normalizeS3Key(surface.path);
|
|
197
|
+
if (!surfacePath) {
|
|
198
|
+
return root;
|
|
199
|
+
}
|
|
200
|
+
return joinS3Key(root, surfacePath);
|
|
201
|
+
}
|
|
202
|
+
|
|
131
203
|
function normalizeS3Key(value) {
|
|
132
204
|
return String(value || "")
|
|
133
205
|
.replace(/^\/+/, "")
|
|
134
|
-
.replace(/\/{2,}/g, "/")
|
|
206
|
+
.replace(/\/{2,}/g, "/")
|
|
207
|
+
.replace(/\/+$/, "");
|
|
135
208
|
}
|
|
136
209
|
|
|
137
210
|
function joinS3Key(...parts) {
|
|
@@ -268,6 +341,13 @@ function deployManifestKey(deployConfig, manifest) {
|
|
|
268
341
|
return joinS3Key(manifestPrefixFor(deployConfig), `${manifest.alias || manifest.channel}.json`);
|
|
269
342
|
}
|
|
270
343
|
|
|
344
|
+
function deploySurfaceManifestKey(deployConfig, binding) {
|
|
345
|
+
const suffix = binding.surface === "default"
|
|
346
|
+
? `${binding.alias || binding.channel}.json`
|
|
347
|
+
: `${binding.alias || binding.channel}/${binding.surface}.json`;
|
|
348
|
+
return joinS3Key(manifestPrefixFor(deployConfig), suffix);
|
|
349
|
+
}
|
|
350
|
+
|
|
271
351
|
function assertDeployPlan(plan) {
|
|
272
352
|
if (plan?.contract !== "kungfu-buildchain-web-surface-deploy-plan") {
|
|
273
353
|
throw new Error("web-surface deploy apply requires a deploy plan");
|
|
@@ -345,6 +425,55 @@ function retentionFor({ config, channelName, alias, deployedAt }) {
|
|
|
345
425
|
};
|
|
346
426
|
}
|
|
347
427
|
|
|
428
|
+
function configuredSurfaces(config) {
|
|
429
|
+
if (config.surfaces && Object.keys(config.surfaces).length > 0) {
|
|
430
|
+
return Object.values(config.surfaces);
|
|
431
|
+
}
|
|
432
|
+
return [{
|
|
433
|
+
name: "default",
|
|
434
|
+
path: "/",
|
|
435
|
+
pathOnly: false,
|
|
436
|
+
canonical: true,
|
|
437
|
+
}];
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function resolveSurfaceBindings({ config, channelName, alias, deployConfig }) {
|
|
441
|
+
const channel = config.channels?.[channelName];
|
|
442
|
+
if (!channel) {
|
|
443
|
+
throw new Error(`unknown web-surface channel: ${channelName}`);
|
|
444
|
+
}
|
|
445
|
+
return configuredSurfaces(config).map((surface) => {
|
|
446
|
+
const effectiveDeploy = surfaceDeployConfig(deployConfig, surface.name);
|
|
447
|
+
const url = surface.name === "default" && !config.surfaces
|
|
448
|
+
? resolveChannelUrl(channel, alias)
|
|
449
|
+
: resolveSurfaceUrl({ surface, channel, channelName, alias });
|
|
450
|
+
const objectPrefix = surface.name === "default" && !config.surfaces
|
|
451
|
+
? objectPrefixFor(deployConfig, alias || channelName)
|
|
452
|
+
: surfaceObjectPrefixFor({ deployConfig, surface, alias: alias || channelName });
|
|
453
|
+
const bucket = effectiveDeploy.bucket || effectiveDeploy.target || "";
|
|
454
|
+
const distributionId = effectiveDeploy.cloudfront_distribution || effectiveDeploy.distribution || "";
|
|
455
|
+
return {
|
|
456
|
+
surface: surface.name,
|
|
457
|
+
channel: channelName,
|
|
458
|
+
alias,
|
|
459
|
+
url,
|
|
460
|
+
sourcePath: normalizeSurfacePath(surface.path),
|
|
461
|
+
canonicalUrl: surface.productionUrl || (channelName === "production" ? url : ""),
|
|
462
|
+
pathOnly: Boolean(surface.pathOnly),
|
|
463
|
+
bucket,
|
|
464
|
+
distributionId,
|
|
465
|
+
originPath: effectiveDeploy.origin_path || effectiveDeploy.originPath || "",
|
|
466
|
+
objectPrefix,
|
|
467
|
+
manifestKey: "",
|
|
468
|
+
noindex: channel.noindex,
|
|
469
|
+
accessControl: channel.accessControl,
|
|
470
|
+
};
|
|
471
|
+
}).map((binding) => ({
|
|
472
|
+
...binding,
|
|
473
|
+
manifestKey: deploySurfaceManifestKey(deployConfig, binding),
|
|
474
|
+
}));
|
|
475
|
+
}
|
|
476
|
+
|
|
348
477
|
export function validateWebSurfaceProject(cwd = process.cwd()) {
|
|
349
478
|
const summary = validateBuildchainConfig(cwd, {
|
|
350
479
|
requireConfig: true,
|
|
@@ -404,13 +533,20 @@ export function createWebSurfaceDeploymentManifest({
|
|
|
404
533
|
throw new Error(`missing deploy.${channel}`);
|
|
405
534
|
}
|
|
406
535
|
const retention = retentionFor({ config, channelName: channel, alias, deployedAt });
|
|
536
|
+
const surfaceBindings = resolveSurfaceBindings({
|
|
537
|
+
config,
|
|
538
|
+
channelName: channel,
|
|
539
|
+
alias,
|
|
540
|
+
deployConfig,
|
|
541
|
+
});
|
|
542
|
+
const primaryBinding = surfaceBindings[0];
|
|
407
543
|
return {
|
|
408
544
|
schemaVersion: 1,
|
|
409
545
|
contract: "kungfu-buildchain-web-surface-deployment",
|
|
410
546
|
site: config.project.site || config.project.name || "",
|
|
411
547
|
channel,
|
|
412
548
|
alias,
|
|
413
|
-
url:
|
|
549
|
+
url: primaryBinding.url,
|
|
414
550
|
sourceSha: assertSha(sourceSha, "sourceSha"),
|
|
415
551
|
artifactHash: assertHash(artifactHash, "artifactHash"),
|
|
416
552
|
deployTarget: deployConfig.target || deployConfig.bucket || deployConfig.environment || deployConfig.service || "",
|
|
@@ -437,6 +573,7 @@ export function createWebSurfaceDeploymentManifest({
|
|
|
437
573
|
migrationState,
|
|
438
574
|
rollbackPointer,
|
|
439
575
|
rollbackLimitations,
|
|
576
|
+
surfaceBindings,
|
|
440
577
|
};
|
|
441
578
|
}
|
|
442
579
|
|
|
@@ -481,12 +618,14 @@ export function planWebSurfaceDeploy({
|
|
|
481
618
|
channel,
|
|
482
619
|
alias,
|
|
483
620
|
url: manifest.url,
|
|
621
|
+
urls: Object.fromEntries(manifest.surfaceBindings.map((binding) => [binding.surface, binding.url])),
|
|
484
622
|
artifact: {
|
|
485
623
|
path: artifactPath || deployConfig.artifactPath || ".",
|
|
486
624
|
hash: resolvedArtifact.artifactHash,
|
|
487
625
|
files: resolvedArtifact.files,
|
|
488
626
|
},
|
|
489
627
|
manifest,
|
|
628
|
+
surfaceBindings: manifest.surfaceBindings,
|
|
490
629
|
steps: planAdapterSteps(deployConfig.adapter, deployConfig, manifest),
|
|
491
630
|
};
|
|
492
631
|
}
|
|
@@ -526,39 +665,28 @@ export function applyWebSurfaceDeploy({
|
|
|
526
665
|
if (resolvedPlan.adapter !== "aws-s3-cloudfront") {
|
|
527
666
|
throw new Error(`web-surface deploy apply does not support adapter: ${resolvedPlan.adapter}`);
|
|
528
667
|
}
|
|
529
|
-
if (!dryRun) {
|
|
530
|
-
assertConcreteAwsDeployConfig({
|
|
531
|
-
deployConfig,
|
|
532
|
-
channel: resolvedPlan.channel,
|
|
533
|
-
operation: "deploy",
|
|
534
|
-
});
|
|
535
|
-
}
|
|
536
668
|
const bucket = deployConfig.bucket || deployConfig.target || "";
|
|
537
669
|
const artifactRoot = path.resolve(cwd, resolvedPlan.artifact.path);
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
}
|
|
547
|
-
{
|
|
548
|
-
action: "write-deployment-manifest",
|
|
549
|
-
command: "aws",
|
|
550
|
-
args: ["s3", "cp", "-", s3Uri(bucket, manifestKey), "--content-type", "application/json"],
|
|
551
|
-
stdin: `${JSON.stringify(resolvedPlan.manifest, null, 2)}\n`,
|
|
552
|
-
},
|
|
553
|
-
];
|
|
554
|
-
const distribution = deployConfig.cloudfront_distribution || deployConfig.distribution || "";
|
|
555
|
-
if (distribution) {
|
|
556
|
-
operations.push({
|
|
557
|
-
action: "invalidate-cdn",
|
|
558
|
-
command: "aws",
|
|
559
|
-
args: ["cloudfront", "create-invalidation", "--distribution-id", distribution, "--paths", ...invalidationPaths],
|
|
560
|
-
});
|
|
670
|
+
const bindings = resolvedPlan.manifest.surfaceBindings || [];
|
|
671
|
+
if (!dryRun) {
|
|
672
|
+
for (const binding of bindings) {
|
|
673
|
+
assertConcreteAwsDeployConfig({
|
|
674
|
+
deployConfig: surfaceDeployConfig(deployConfig, binding.surface),
|
|
675
|
+
channel: `${resolvedPlan.channel}.surfaces.${binding.surface}`,
|
|
676
|
+
operation: "deploy",
|
|
677
|
+
});
|
|
678
|
+
}
|
|
561
679
|
}
|
|
680
|
+
const operations = bindings.flatMap((binding) => deployBindingOperations({
|
|
681
|
+
artifactRoot,
|
|
682
|
+
deployConfig,
|
|
683
|
+
manifest: resolvedPlan.manifest,
|
|
684
|
+
binding,
|
|
685
|
+
}));
|
|
686
|
+
const primaryBinding = bindings[0] || {};
|
|
687
|
+
const objectPrefix = primaryBinding.objectPrefix || objectPrefixFor(deployConfig, resolvedPlan.manifest.alias || resolvedPlan.manifest.channel);
|
|
688
|
+
const manifestKey = primaryBinding.manifestKey || deployManifestKey(deployConfig, resolvedPlan.manifest);
|
|
689
|
+
const invalidationPaths = bindings.flatMap((binding) => [cdnWildcardPath(binding.objectPrefix), cdnPath(binding.manifestKey)]);
|
|
562
690
|
const operationResults = runAdapterOperations({ operations, dryRun, commandRunner });
|
|
563
691
|
return {
|
|
564
692
|
schemaVersion: 1,
|
|
@@ -572,6 +700,7 @@ export function applyWebSurfaceDeploy({
|
|
|
572
700
|
channel: resolvedPlan.channel,
|
|
573
701
|
alias: resolvedPlan.alias,
|
|
574
702
|
url: resolvedPlan.url,
|
|
703
|
+
urls: resolvedPlan.urls || Object.fromEntries(bindings.map((binding) => [binding.surface, binding.url])),
|
|
575
704
|
sourceSha: resolvedPlan.manifest.sourceSha,
|
|
576
705
|
artifactHash: resolvedPlan.artifact.hash,
|
|
577
706
|
adapter: resolvedPlan.adapter,
|
|
@@ -580,26 +709,76 @@ export function applyWebSurfaceDeploy({
|
|
|
580
709
|
manifestKey,
|
|
581
710
|
invalidationPaths,
|
|
582
711
|
manifest: resolvedPlan.manifest,
|
|
712
|
+
surfaceBindings: bindings,
|
|
583
713
|
operations: operationResults,
|
|
584
714
|
};
|
|
585
715
|
}
|
|
586
716
|
|
|
717
|
+
function deployBindingOperations({ artifactRoot, deployConfig, manifest, binding }) {
|
|
718
|
+
const effectiveDeploy = surfaceDeployConfig(deployConfig, binding.surface);
|
|
719
|
+
const bucket = binding.bucket || effectiveDeploy.bucket || effectiveDeploy.target || "";
|
|
720
|
+
const distribution = binding.distributionId || effectiveDeploy.cloudfront_distribution || effectiveDeploy.distribution || "";
|
|
721
|
+
const operations = [
|
|
722
|
+
{
|
|
723
|
+
action: "sync-static-artifact",
|
|
724
|
+
surface: binding.surface,
|
|
725
|
+
command: "aws",
|
|
726
|
+
args: syncStaticArtifactArgs({ artifactRoot, bucket, objectPrefix: binding.objectPrefix }),
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
action: "write-deployment-manifest",
|
|
730
|
+
surface: binding.surface,
|
|
731
|
+
command: "aws",
|
|
732
|
+
args: ["s3", "cp", "-", s3Uri(bucket, binding.manifestKey), "--content-type", "application/json"],
|
|
733
|
+
stdin: `${JSON.stringify({
|
|
734
|
+
...manifest,
|
|
735
|
+
surface: binding.surface,
|
|
736
|
+
url: binding.url,
|
|
737
|
+
surfaceBinding: binding,
|
|
738
|
+
}, null, 2)}\n`,
|
|
739
|
+
},
|
|
740
|
+
];
|
|
741
|
+
if (distribution) {
|
|
742
|
+
operations.push({
|
|
743
|
+
action: "invalidate-cdn",
|
|
744
|
+
surface: binding.surface,
|
|
745
|
+
command: "aws",
|
|
746
|
+
args: [
|
|
747
|
+
"cloudfront",
|
|
748
|
+
"create-invalidation",
|
|
749
|
+
"--distribution-id",
|
|
750
|
+
distribution,
|
|
751
|
+
"--paths",
|
|
752
|
+
cdnWildcardPath(binding.objectPrefix),
|
|
753
|
+
cdnPath(binding.manifestKey),
|
|
754
|
+
],
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
return operations;
|
|
758
|
+
}
|
|
759
|
+
|
|
587
760
|
function planAdapterSteps(adapter, deployConfig, manifest) {
|
|
588
761
|
if (adapter === "aws-s3-cloudfront") {
|
|
589
762
|
return [
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
763
|
+
...manifest.surfaceBindings.flatMap((binding) => [
|
|
764
|
+
{
|
|
765
|
+
action: "sync-static-artifact",
|
|
766
|
+
surface: binding.surface,
|
|
767
|
+
target: binding.bucket,
|
|
768
|
+
prefix: binding.objectPrefix,
|
|
769
|
+
},
|
|
770
|
+
{
|
|
771
|
+
action: "write-deployment-manifest",
|
|
772
|
+
surface: binding.surface,
|
|
773
|
+
target: manifestPrefixFor(deployConfig),
|
|
774
|
+
key: binding.manifestKey,
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
action: "invalidate-cdn",
|
|
778
|
+
surface: binding.surface,
|
|
779
|
+
distribution: binding.distributionId,
|
|
780
|
+
},
|
|
781
|
+
]),
|
|
603
782
|
];
|
|
604
783
|
}
|
|
605
784
|
return [
|
|
@@ -640,6 +819,10 @@ export function planWebSurfaceCleanup({
|
|
|
640
819
|
requestedAliases.push(`pr-${pullNumber}`);
|
|
641
820
|
}
|
|
642
821
|
const manifestPrefix = manifestPrefixFor(deployConfig);
|
|
822
|
+
const bindingsByAlias = new Map(requestedAliases.map((alias) => [
|
|
823
|
+
alias,
|
|
824
|
+
resolveSurfaceBindings({ config, channelName: channel, alias, deployConfig }),
|
|
825
|
+
]));
|
|
643
826
|
return {
|
|
644
827
|
schemaVersion: 1,
|
|
645
828
|
contract: "kungfu-buildchain-web-surface-cleanup-plan",
|
|
@@ -660,7 +843,9 @@ export function planWebSurfaceCleanup({
|
|
|
660
843
|
entries: requestedAliases.map((alias) => {
|
|
661
844
|
const classified = classifyPreviewAlias(alias);
|
|
662
845
|
const retention = retentionConfig(config, "preview");
|
|
663
|
-
const
|
|
846
|
+
const surfaceBindings = bindingsByAlias.get(alias) || [];
|
|
847
|
+
const primaryBinding = surfaceBindings[0] || {};
|
|
848
|
+
const objectPrefix = primaryBinding.objectPrefix || objectPrefixFor(deployConfig, alias);
|
|
664
849
|
return {
|
|
665
850
|
alias,
|
|
666
851
|
aliasKind: classified.kind,
|
|
@@ -669,11 +854,13 @@ export function planWebSurfaceCleanup({
|
|
|
669
854
|
retentionDays: retention[classified.retentionKey],
|
|
670
855
|
action: "delete-preview-alias",
|
|
671
856
|
objectPrefix,
|
|
672
|
-
manifestKey: `${manifestPrefix.replace(/\/$/, "")}/${alias}.json`,
|
|
857
|
+
manifestKey: primaryBinding.manifestKey || `${manifestPrefix.replace(/\/$/, "")}/${alias}.json`,
|
|
858
|
+
surfaceBindings,
|
|
673
859
|
steps: cleanupAdapterSteps(deployConfig.adapter, deployConfig, {
|
|
674
860
|
alias,
|
|
675
861
|
objectPrefix,
|
|
676
862
|
manifestPrefix,
|
|
863
|
+
surfaceBindings,
|
|
677
864
|
}),
|
|
678
865
|
};
|
|
679
866
|
}),
|
|
@@ -718,33 +905,80 @@ export function applyWebSurfaceCleanup({
|
|
|
718
905
|
throw new Error(`web-surface cleanup apply does not support adapter: ${cleanup.adapter}`);
|
|
719
906
|
}
|
|
720
907
|
if (!dryRun) {
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
908
|
+
for (const entry of cleanup.entries) {
|
|
909
|
+
const bindings = entry.surfaceBindings?.length
|
|
910
|
+
? entry.surfaceBindings
|
|
911
|
+
: [{
|
|
912
|
+
surface: "default",
|
|
913
|
+
}];
|
|
914
|
+
for (const binding of bindings) {
|
|
915
|
+
assertConcreteAwsDeployConfig({
|
|
916
|
+
deployConfig: surfaceDeployConfig(deployConfig, binding.surface),
|
|
917
|
+
channel: `${cleanup.channel}.surfaces.${binding.surface}`,
|
|
918
|
+
operation: "cleanup",
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
}
|
|
726
922
|
}
|
|
727
923
|
const bucket = deployConfig.bucket || deployConfig.target || "";
|
|
728
|
-
const operations = cleanup.entries.flatMap((entry) => {
|
|
924
|
+
const operations = cleanup.entries.flatMap((entry) => cleanupEntryOperations({ deployConfig, entry, bucket }));
|
|
925
|
+
const operationResults = runAdapterOperations({ operations, dryRun, commandRunner });
|
|
926
|
+
return {
|
|
927
|
+
schemaVersion: 1,
|
|
928
|
+
contract: "kungfu-buildchain-web-surface-cleanup-apply",
|
|
929
|
+
dryRun,
|
|
930
|
+
applyMode: dryRun ? "dry-run" : "apply",
|
|
931
|
+
status: appliedStatus({ dryRun, operations: operationResults, noOp: cleanup.status === "no-op" }),
|
|
932
|
+
event,
|
|
933
|
+
channel,
|
|
934
|
+
now,
|
|
935
|
+
sourceSha: cleanup.sourceSha,
|
|
936
|
+
pullNumber: cleanup.pullNumber,
|
|
937
|
+
actor,
|
|
938
|
+
runId,
|
|
939
|
+
adapter: cleanup.adapter,
|
|
940
|
+
target: bucket,
|
|
941
|
+
manifestPrefix: cleanup.manifestPrefix,
|
|
942
|
+
entries: cleanup.entries,
|
|
943
|
+
operations: operationResults,
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function cleanupEntryOperations({ deployConfig, entry, bucket }) {
|
|
948
|
+
const bindings = entry.surfaceBindings?.length
|
|
949
|
+
? entry.surfaceBindings
|
|
950
|
+
: [{
|
|
951
|
+
surface: "default",
|
|
952
|
+
objectPrefix: entry.objectPrefix,
|
|
953
|
+
manifestKey: entry.manifestKey,
|
|
954
|
+
bucket,
|
|
955
|
+
distributionId: deployConfig.cloudfront_distribution || deployConfig.distribution || "",
|
|
956
|
+
}];
|
|
957
|
+
return bindings.flatMap((binding) => {
|
|
958
|
+
const effectiveDeploy = surfaceDeployConfig(deployConfig, binding.surface);
|
|
959
|
+
const bindingBucket = binding.bucket || effectiveDeploy.bucket || effectiveDeploy.target || bucket;
|
|
960
|
+
const distribution = binding.distributionId || effectiveDeploy.cloudfront_distribution || effectiveDeploy.distribution || "";
|
|
729
961
|
const entryOperations = [
|
|
730
962
|
{
|
|
731
963
|
action: "delete-static-prefix",
|
|
732
964
|
alias: entry.alias,
|
|
965
|
+
surface: binding.surface,
|
|
733
966
|
command: "aws",
|
|
734
|
-
args: ["s3", "rm", s3Uri(
|
|
967
|
+
args: ["s3", "rm", s3Uri(bindingBucket, binding.objectPrefix), "--recursive"],
|
|
735
968
|
},
|
|
736
969
|
{
|
|
737
970
|
action: "delete-deployment-manifest",
|
|
738
971
|
alias: entry.alias,
|
|
972
|
+
surface: binding.surface,
|
|
739
973
|
command: "aws",
|
|
740
|
-
args: ["s3", "rm", s3Uri(
|
|
974
|
+
args: ["s3", "rm", s3Uri(bindingBucket, binding.manifestKey)],
|
|
741
975
|
},
|
|
742
976
|
];
|
|
743
|
-
const distribution = deployConfig.cloudfront_distribution || deployConfig.distribution || "";
|
|
744
977
|
if (distribution) {
|
|
745
978
|
entryOperations.push({
|
|
746
979
|
action: "invalidate-cdn",
|
|
747
980
|
alias: entry.alias,
|
|
981
|
+
surface: binding.surface,
|
|
748
982
|
command: "aws",
|
|
749
983
|
args: [
|
|
750
984
|
"cloudfront",
|
|
@@ -752,53 +986,45 @@ export function applyWebSurfaceCleanup({
|
|
|
752
986
|
"--distribution-id",
|
|
753
987
|
distribution,
|
|
754
988
|
"--paths",
|
|
755
|
-
`${cdnPath(
|
|
756
|
-
cdnPath(
|
|
989
|
+
`${cdnPath(binding.objectPrefix)}/*`,
|
|
990
|
+
cdnPath(binding.manifestKey),
|
|
757
991
|
],
|
|
758
992
|
});
|
|
759
993
|
}
|
|
760
994
|
return entryOperations;
|
|
761
995
|
});
|
|
762
|
-
const operationResults = runAdapterOperations({ operations, dryRun, commandRunner });
|
|
763
|
-
return {
|
|
764
|
-
schemaVersion: 1,
|
|
765
|
-
contract: "kungfu-buildchain-web-surface-cleanup-apply",
|
|
766
|
-
dryRun,
|
|
767
|
-
applyMode: dryRun ? "dry-run" : "apply",
|
|
768
|
-
status: appliedStatus({ dryRun, operations: operationResults, noOp: cleanup.status === "no-op" }),
|
|
769
|
-
event,
|
|
770
|
-
channel,
|
|
771
|
-
now,
|
|
772
|
-
sourceSha: cleanup.sourceSha,
|
|
773
|
-
pullNumber: cleanup.pullNumber,
|
|
774
|
-
actor,
|
|
775
|
-
runId,
|
|
776
|
-
adapter: cleanup.adapter,
|
|
777
|
-
target: bucket,
|
|
778
|
-
manifestPrefix: cleanup.manifestPrefix,
|
|
779
|
-
entries: cleanup.entries,
|
|
780
|
-
operations: operationResults,
|
|
781
|
-
};
|
|
782
996
|
}
|
|
783
997
|
|
|
784
998
|
function cleanupAdapterSteps(adapter, deployConfig, entry) {
|
|
785
999
|
if (adapter === "aws-s3-cloudfront") {
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
1000
|
+
const bindings = entry.surfaceBindings?.length
|
|
1001
|
+
? entry.surfaceBindings
|
|
1002
|
+
: [{
|
|
1003
|
+
surface: "default",
|
|
1004
|
+
objectPrefix: entry.objectPrefix,
|
|
1005
|
+
manifestKey: `${entry.manifestPrefix.replace(/\/$/, "")}/${entry.alias}.json`,
|
|
1006
|
+
bucket: deployConfig.bucket || deployConfig.target || "",
|
|
1007
|
+
distributionId: deployConfig.cloudfront_distribution || deployConfig.distribution || "",
|
|
1008
|
+
}];
|
|
1009
|
+
return bindings.flatMap((binding) => [
|
|
1010
|
+
{
|
|
1011
|
+
action: "delete-static-prefix",
|
|
1012
|
+
surface: binding.surface,
|
|
1013
|
+
target: binding.bucket,
|
|
1014
|
+
prefix: binding.objectPrefix,
|
|
1015
|
+
},
|
|
1016
|
+
{
|
|
1017
|
+
action: "delete-deployment-manifest",
|
|
1018
|
+
surface: binding.surface,
|
|
1019
|
+
target: entry.manifestPrefix,
|
|
1020
|
+
key: binding.manifestKey,
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
action: "invalidate-cdn",
|
|
1024
|
+
surface: binding.surface,
|
|
1025
|
+
distribution: binding.distributionId,
|
|
1026
|
+
},
|
|
1027
|
+
]);
|
|
802
1028
|
}
|
|
803
1029
|
return [
|
|
804
1030
|
{
|
package/scripts/web-surface.mjs
CHANGED
|
@@ -90,6 +90,7 @@ export function webSurfaceCli() {
|
|
|
90
90
|
"web-surface-channel": result.channel,
|
|
91
91
|
"web-surface-alias": result.alias,
|
|
92
92
|
"web-surface-url": result.url,
|
|
93
|
+
"web-surface-urls-json": JSON.stringify(result.urls || {}),
|
|
93
94
|
"web-surface-artifact-hash": result.artifact.hash,
|
|
94
95
|
"web-surface-manifest-json": JSON.stringify(result.manifest),
|
|
95
96
|
});
|
|
@@ -121,10 +122,12 @@ export function webSurfaceCli() {
|
|
|
121
122
|
"web-surface-channel": result.channel,
|
|
122
123
|
"web-surface-alias": result.alias,
|
|
123
124
|
"web-surface-url": result.url,
|
|
125
|
+
"web-surface-urls-json": JSON.stringify(result.urls || {}),
|
|
124
126
|
"web-surface-artifact-hash": result.artifactHash,
|
|
125
127
|
"web-surface-apply-mode": result.applyMode,
|
|
126
128
|
"web-surface-apply-status": result.status,
|
|
127
129
|
"web-surface-manifest-json": JSON.stringify(result.manifest),
|
|
130
|
+
"web-surface-bindings-json": JSON.stringify(result.surfaceBindings || []),
|
|
128
131
|
"web-surface-apply-result-json": JSON.stringify(result),
|
|
129
132
|
});
|
|
130
133
|
assertApplySucceeded(result);
|