@kungfu-tech/buildchain 2.0.14-alpha.2 → 2.0.14-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/docs/lifecycle-protocol.md +2 -1
- package/docs/web-surface-deployments.md +49 -5
- package/package.json +1 -1
- package/packages/core/buildchain-config.js +49 -6
- package/scripts/init-repo.mjs +2 -2
- package/scripts/web-surface-core.mjs +86 -10
- package/scripts/web-surface.mjs +7 -0
package/README.md
CHANGED
|
@@ -104,6 +104,9 @@ Buildchain v2 ships these active surfaces:
|
|
|
104
104
|
commands, trusted event gating, artifact name templates, expected artifact
|
|
105
105
|
checks, deterministic artifact manifests, publish-gate source locks, resolved
|
|
106
106
|
release manifests, and aggregate build summaries;
|
|
107
|
+
- `.github/workflows/.web-surface.yml` as the reusable site/app surface for PR
|
|
108
|
+
preview plans, closed-PR preview cleanup plans, push-main staging promotion
|
|
109
|
+
plans, and explicit production environment gates;
|
|
107
110
|
- `actions/run-lifecycle` for callers that need the same lifecycle/manifest
|
|
108
111
|
contract inside their own workflows;
|
|
109
112
|
- governance-closed self-promotion through `Buildchain Ref Promotion`.
|
|
@@ -42,7 +42,8 @@ noindex = true
|
|
|
42
42
|
[channels.staging]
|
|
43
43
|
url = "https://staging.kungfu.tech"
|
|
44
44
|
visibility = "protected"
|
|
45
|
-
|
|
45
|
+
access_control = "managed-network"
|
|
46
|
+
edge_auth = "none"
|
|
46
47
|
noindex = true
|
|
47
48
|
promotable = true
|
|
48
49
|
|
|
@@ -64,7 +65,11 @@ Buildchain validates these hard constraints:
|
|
|
64
65
|
|
|
65
66
|
- `channels.preview.url_pattern` is required and must contain the alias shape
|
|
66
67
|
used by preview deployments.
|
|
67
|
-
- `channels.staging.
|
|
68
|
+
- `channels.staging.access_control` must protect staging. Supported modes are
|
|
69
|
+
`managed-network`, `edge-basic-auth`, `oidc`, and `app-auth`.
|
|
70
|
+
- `channels.staging.edge_auth` records whether the edge layer owns auth. Use
|
|
71
|
+
`edge_auth = "none"` when staging is protected by managed network controls
|
|
72
|
+
such as WAF/IP allowlists or VPN access.
|
|
68
73
|
- `channels.staging.noindex = true` is required.
|
|
69
74
|
- `channels.production.url` is required.
|
|
70
75
|
- deploy adapters must be declared per channel.
|
|
@@ -122,6 +127,9 @@ rollback:
|
|
|
122
127
|
"deployedAt": "2026-07-01T00:00:00.000Z",
|
|
123
128
|
"retentionClass": "preview-sha-immutable",
|
|
124
129
|
"expiresAt": "2026-09-29T00:00:00.000Z",
|
|
130
|
+
"accessControl": "none",
|
|
131
|
+
"edgeAuth": "none",
|
|
132
|
+
"noindex": true,
|
|
125
133
|
"secretRefs": ["AWS_ROLE_ARN"]
|
|
126
134
|
}
|
|
127
135
|
```
|
|
@@ -166,17 +174,54 @@ The CLI emits GitHub outputs when `GITHUB_OUTPUT` is present:
|
|
|
166
174
|
|
|
167
175
|
## Cleanup Plans
|
|
168
176
|
|
|
169
|
-
Preview cleanup is
|
|
177
|
+
Preview cleanup is an auditable cleanup contract. It can run as a dry-run plan
|
|
178
|
+
or as an apply-mode plan for the caller workflow to execute with preview-only
|
|
179
|
+
credentials:
|
|
170
180
|
|
|
171
181
|
```bash
|
|
172
182
|
node scripts/web-surface.mjs \
|
|
173
183
|
--mode cleanup-plan \
|
|
174
184
|
--cwd fixtures/web-surface-shaped \
|
|
185
|
+
--event pull-request-closed \
|
|
186
|
+
--pull-number 123 \
|
|
175
187
|
--aliases pr-123,sha-abcdef123456
|
|
176
188
|
```
|
|
177
189
|
|
|
178
190
|
The plan keeps mutable PR aliases and immutable SHA aliases distinct so a caller
|
|
179
|
-
can expire them with different retention windows.
|
|
191
|
+
can expire them with different retention windows. Closed-PR cleanup can derive
|
|
192
|
+
`pr-N` from `--pull-number`, records the event, source SHA, actor, run id,
|
|
193
|
+
preview bucket/prefix, manifest key, and adapter steps, and is an auditable
|
|
194
|
+
no-op when no aliases are requested.
|
|
195
|
+
|
|
196
|
+
## Reusable Workflow Shape
|
|
197
|
+
|
|
198
|
+
Buildchain ships `.github/workflows/.web-surface.yml` for repositories that want
|
|
199
|
+
the standard PR review and promotion flow without copying bespoke glue:
|
|
200
|
+
|
|
201
|
+
```yaml
|
|
202
|
+
jobs:
|
|
203
|
+
web-surface:
|
|
204
|
+
uses: kungfu-systems/buildchain/.github/workflows/.web-surface.yml@v2
|
|
205
|
+
with:
|
|
206
|
+
buildchain-ref: v2
|
|
207
|
+
build-command: npm run build
|
|
208
|
+
verify-command: npm run check
|
|
209
|
+
artifact-path: dist
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
The reusable workflow maps GitHub events to Buildchain web-surface semantics:
|
|
213
|
+
|
|
214
|
+
| Event | Buildchain behavior |
|
|
215
|
+
| --- | --- |
|
|
216
|
+
| `pull_request` opened / synchronized / reopened | validate, build, verify, and plan `preview` for `pr-N` |
|
|
217
|
+
| `pull_request` closed | plan apply-mode cleanup for the `pr-N` preview alias and manifest |
|
|
218
|
+
| `push` to `main` | validate, build, verify, and plan `staging` from the merged `main` SHA |
|
|
219
|
+
| `workflow_dispatch` with `production-approved = true` | plan `production` and enter the configured GitHub Environment gate |
|
|
220
|
+
|
|
221
|
+
The workflow deliberately plans and emits manifests; live AWS mutation still
|
|
222
|
+
belongs to the caller's controlled deploy step or a later adapter apply
|
|
223
|
+
implementation. This keeps production from being an implicit side effect of
|
|
224
|
+
merging to `main`.
|
|
180
225
|
|
|
181
226
|
## Site Repository Shape
|
|
182
227
|
|
|
@@ -208,4 +253,3 @@ Buildchain does not currently perform live AWS mutations for web surfaces.
|
|
|
208
253
|
Production deploy, DNS changes, staging auth implementation, CloudFront
|
|
209
254
|
distribution creation, and credential provisioning remain explicitly authorized
|
|
210
255
|
operations outside the dry-run contract.
|
|
211
|
-
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kungfu-tech/buildchain",
|
|
3
|
-
"version": "2.0.14-alpha.
|
|
3
|
+
"version": "2.0.14-alpha.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Kungfu Buildchain reusable workflows, release governance, and CLI tooling.",
|
|
6
6
|
"repository": "https://github.com/kungfu-systems/buildchain",
|
|
@@ -12,6 +12,8 @@ const SUPPORTED_VERSION_NEXT = new Set(["auto", "manual"]);
|
|
|
12
12
|
const SUPPORTED_PROJECT_TYPES = new Set(["package", "web-surface"]);
|
|
13
13
|
const WEB_SURFACE_CHANNELS = ["preview", "staging", "production"];
|
|
14
14
|
const SUPPORTED_CHANNEL_VISIBILITY = new Set(["ephemeral", "protected", "public", "internal"]);
|
|
15
|
+
const SUPPORTED_ACCESS_CONTROL = new Set(["none", "managed-network", "edge-basic-auth", "oidc", "app-auth"]);
|
|
16
|
+
const SUPPORTED_EDGE_AUTH = new Set(["none", "cloudfront-basic-auth", "oidc", "app-auth"]);
|
|
15
17
|
const SUPPORTED_DEPLOY_ADAPTERS = new Set([
|
|
16
18
|
"aws-s3-cloudfront",
|
|
17
19
|
"aws-elastic-beanstalk",
|
|
@@ -159,13 +161,18 @@ function normalizeChannelConfig(name, channel) {
|
|
|
159
161
|
if (!SUPPORTED_CHANNEL_VISIBILITY.has(visibility)) {
|
|
160
162
|
throw new Error(`channels.${name}.visibility must be one of ephemeral, protected, public, or internal`);
|
|
161
163
|
}
|
|
164
|
+
const accessControl = normalizeAccessControl(channel, name);
|
|
165
|
+
const edgeAuth = normalizeEdgeAuth(channel, name, accessControl);
|
|
162
166
|
const normalized = {
|
|
163
167
|
name,
|
|
164
168
|
visibility,
|
|
165
|
-
requiresAuth: optionalBoolean(channel.requires_auth,
|
|
169
|
+
requiresAuth: optionalBoolean(channel.requires_auth, accessControl !== "none"),
|
|
170
|
+
requiresControlledAccess: accessControl !== "none",
|
|
166
171
|
noindex: optionalBoolean(channel.noindex, name !== "production"),
|
|
167
172
|
promotable: optionalBoolean(channel.promotable, name === "staging"),
|
|
168
173
|
canonical: optionalBoolean(channel.canonical, name === "production"),
|
|
174
|
+
accessControl,
|
|
175
|
+
edgeAuth,
|
|
169
176
|
};
|
|
170
177
|
if (hasUrl) {
|
|
171
178
|
normalized.url = assertString(channel.url, `channels.${name}.url`);
|
|
@@ -176,6 +183,36 @@ function normalizeChannelConfig(name, channel) {
|
|
|
176
183
|
return normalized;
|
|
177
184
|
}
|
|
178
185
|
|
|
186
|
+
function normalizeAccessControl(channel, name) {
|
|
187
|
+
if (channel.access_control !== undefined) {
|
|
188
|
+
const accessControl = assertString(channel.access_control, `channels.${name}.access_control`);
|
|
189
|
+
if (!SUPPORTED_ACCESS_CONTROL.has(accessControl)) {
|
|
190
|
+
throw new Error(`channels.${name}.access_control must be one of none, managed-network, edge-basic-auth, oidc, or app-auth`);
|
|
191
|
+
}
|
|
192
|
+
return accessControl;
|
|
193
|
+
}
|
|
194
|
+
if (name === "staging") {
|
|
195
|
+
return "edge-basic-auth";
|
|
196
|
+
}
|
|
197
|
+
if (name === "production") {
|
|
198
|
+
return "none";
|
|
199
|
+
}
|
|
200
|
+
return "none";
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function normalizeEdgeAuth(channel, name, accessControl) {
|
|
204
|
+
if (channel.edge_auth !== undefined) {
|
|
205
|
+
const edgeAuth = assertString(channel.edge_auth, `channels.${name}.edge_auth`);
|
|
206
|
+
if (!SUPPORTED_EDGE_AUTH.has(edgeAuth)) {
|
|
207
|
+
throw new Error(`channels.${name}.edge_auth must be one of none, cloudfront-basic-auth, oidc, or app-auth`);
|
|
208
|
+
}
|
|
209
|
+
return edgeAuth;
|
|
210
|
+
}
|
|
211
|
+
return accessControl === "edge-basic-auth"
|
|
212
|
+
? "cloudfront-basic-auth"
|
|
213
|
+
: "none";
|
|
214
|
+
}
|
|
215
|
+
|
|
179
216
|
function defaultChannelVisibility(name) {
|
|
180
217
|
if (name === "preview") {
|
|
181
218
|
return "ephemeral";
|
|
@@ -290,17 +327,20 @@ function validateWebSurfaceConfig(config) {
|
|
|
290
327
|
throw new Error(`channels.${name}.url is required for web-surface`);
|
|
291
328
|
}
|
|
292
329
|
}
|
|
293
|
-
if (
|
|
294
|
-
throw new Error("channels.staging.
|
|
330
|
+
if (config.channels.staging.visibility === "public") {
|
|
331
|
+
throw new Error("channels.staging.visibility must not be public for web-surface");
|
|
332
|
+
}
|
|
333
|
+
if (config.channels.staging.accessControl === "none") {
|
|
334
|
+
throw new Error("channels.staging.access_control must protect staging for web-surface");
|
|
335
|
+
}
|
|
336
|
+
if (config.channels.staging.accessControl === "edge-basic-auth" && config.channels.staging.edgeAuth === "none") {
|
|
337
|
+
throw new Error("channels.staging.edge_auth must not be none when access_control = edge-basic-auth");
|
|
295
338
|
}
|
|
296
339
|
if (!config.channels.staging.noindex) {
|
|
297
340
|
throw new Error("channels.staging.noindex must be true for web-surface");
|
|
298
341
|
}
|
|
299
342
|
const stagingSecurity = config.security?.staging;
|
|
300
343
|
if (stagingSecurity) {
|
|
301
|
-
if (stagingSecurity.requiresAuth === false) {
|
|
302
|
-
throw new Error("security.staging.requires_auth must not disable staging auth");
|
|
303
|
-
}
|
|
304
344
|
if (stagingSecurity.noindex === false) {
|
|
305
345
|
throw new Error("security.staging.noindex must not disable staging noindex");
|
|
306
346
|
}
|
|
@@ -624,6 +664,9 @@ export function validateBuildchainConfig(
|
|
|
624
664
|
{
|
|
625
665
|
visibility: channel.visibility,
|
|
626
666
|
requiresAuth: channel.requiresAuth,
|
|
667
|
+
requiresControlledAccess: channel.requiresControlledAccess,
|
|
668
|
+
accessControl: channel.accessControl,
|
|
669
|
+
edgeAuth: channel.edgeAuth,
|
|
627
670
|
noindex: channel.noindex,
|
|
628
671
|
promotable: channel.promotable,
|
|
629
672
|
canonical: channel.canonical,
|
package/scripts/init-repo.mjs
CHANGED
|
@@ -163,7 +163,8 @@ noindex = true
|
|
|
163
163
|
[channels.staging]
|
|
164
164
|
url = "https://staging.example.com"
|
|
165
165
|
visibility = "protected"
|
|
166
|
-
|
|
166
|
+
access_control = "managed-network"
|
|
167
|
+
edge_auth = "none"
|
|
167
168
|
noindex = true
|
|
168
169
|
promotable = true
|
|
169
170
|
|
|
@@ -193,7 +194,6 @@ bucket_ref = "AWS_PRODUCTION_BUCKET"
|
|
|
193
194
|
distribution_ref = "AWS_PRODUCTION_DISTRIBUTION"
|
|
194
195
|
|
|
195
196
|
[security.staging]
|
|
196
|
-
requires_auth = true
|
|
197
197
|
noindex = true
|
|
198
198
|
isolated_providers = true
|
|
199
199
|
|
|
@@ -116,6 +116,14 @@ function resolveChannelUrl(channel, alias) {
|
|
|
116
116
|
return channel.url;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
function manifestPrefixFor(deployConfig) {
|
|
120
|
+
return deployConfig.manifest_prefix || ".buildchain/deployments";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function objectPrefixFor(deployConfig, alias) {
|
|
124
|
+
return deployConfig.prefix || alias || "preview";
|
|
125
|
+
}
|
|
126
|
+
|
|
119
127
|
function retentionFor({ config, channelName, alias, deployedAt }) {
|
|
120
128
|
if (channelName === "preview") {
|
|
121
129
|
const classified = classifyPreviewAlias(alias);
|
|
@@ -224,6 +232,11 @@ export function createWebSurfaceDeploymentManifest({
|
|
|
224
232
|
retentionClass: retention.retentionClass,
|
|
225
233
|
expiresAt: retention.expiresAt,
|
|
226
234
|
mutableAlias: retention.mutableAlias,
|
|
235
|
+
accessControl: channelConfig.accessControl,
|
|
236
|
+
edgeAuth: channelConfig.edgeAuth,
|
|
237
|
+
noindex: channelConfig.noindex,
|
|
238
|
+
promotable: channelConfig.promotable,
|
|
239
|
+
canonical: channelConfig.canonical,
|
|
227
240
|
runtimeId,
|
|
228
241
|
configFingerprint,
|
|
229
242
|
secretRefs: [
|
|
@@ -296,11 +309,11 @@ function planAdapterSteps(adapter, deployConfig, manifest) {
|
|
|
296
309
|
{
|
|
297
310
|
action: "sync-static-artifact",
|
|
298
311
|
target: deployConfig.bucket || deployConfig.target || "",
|
|
299
|
-
prefix: deployConfig
|
|
312
|
+
prefix: objectPrefixFor(deployConfig, manifest.alias || manifest.channel),
|
|
300
313
|
},
|
|
301
314
|
{
|
|
302
315
|
action: "write-deployment-manifest",
|
|
303
|
-
target: deployConfig
|
|
316
|
+
target: manifestPrefixFor(deployConfig),
|
|
304
317
|
},
|
|
305
318
|
{
|
|
306
319
|
action: "invalidate-cdn",
|
|
@@ -315,7 +328,7 @@ function planAdapterSteps(adapter, deployConfig, manifest) {
|
|
|
315
328
|
},
|
|
316
329
|
{
|
|
317
330
|
action: "write-deployment-manifest",
|
|
318
|
-
target: deployConfig
|
|
331
|
+
target: manifestPrefixFor(deployConfig),
|
|
319
332
|
},
|
|
320
333
|
];
|
|
321
334
|
}
|
|
@@ -325,37 +338,100 @@ export function planWebSurfaceCleanup({
|
|
|
325
338
|
aliases = [],
|
|
326
339
|
channel = "preview",
|
|
327
340
|
now = new Date().toISOString(),
|
|
341
|
+
event = "manual",
|
|
342
|
+
sourceSha = "",
|
|
343
|
+
pullNumber = "",
|
|
344
|
+
actor = "",
|
|
345
|
+
runId = "",
|
|
328
346
|
dryRun = true,
|
|
329
347
|
} = {}) {
|
|
330
|
-
if (!dryRun) {
|
|
331
|
-
throw new Error("web-surface cleanup currently supports dry-run planning only");
|
|
332
|
-
}
|
|
333
348
|
const loadedConfig = loadBuildchainConfig(cwd);
|
|
334
349
|
const config = assertWebSurfaceConfig(loadedConfig);
|
|
335
350
|
if (channel !== "preview") {
|
|
336
|
-
throw new Error("web-surface cleanup
|
|
351
|
+
throw new Error("web-surface cleanup currently supports preview aliases only");
|
|
352
|
+
}
|
|
353
|
+
const deployConfig = config.deploy?.[channel];
|
|
354
|
+
if (!deployConfig) {
|
|
355
|
+
throw new Error(`missing deploy.${channel}`);
|
|
356
|
+
}
|
|
357
|
+
const requestedAliases = [...aliases];
|
|
358
|
+
if (requestedAliases.length === 0 && pullNumber) {
|
|
359
|
+
requestedAliases.push(`pr-${pullNumber}`);
|
|
337
360
|
}
|
|
361
|
+
const manifestPrefix = manifestPrefixFor(deployConfig);
|
|
338
362
|
return {
|
|
339
363
|
schemaVersion: 1,
|
|
340
364
|
contract: "kungfu-buildchain-web-surface-cleanup-plan",
|
|
341
|
-
dryRun
|
|
365
|
+
dryRun,
|
|
366
|
+
applyMode: dryRun ? "dry-run" : "apply",
|
|
367
|
+
event,
|
|
342
368
|
channel,
|
|
343
369
|
now,
|
|
344
|
-
|
|
370
|
+
sourceSha: sourceSha ? assertSha(sourceSha, "sourceSha") : "",
|
|
371
|
+
pullNumber: pullNumber ? String(pullNumber) : "",
|
|
372
|
+
actor,
|
|
373
|
+
runId,
|
|
374
|
+
status: requestedAliases.length === 0 ? "no-op" : "planned",
|
|
375
|
+
adapter: deployConfig.adapter,
|
|
376
|
+
target: deployConfig.bucket || deployConfig.target || "",
|
|
377
|
+
manifestPrefix,
|
|
378
|
+
secretRefs: [...new Set([...(deployConfig.secretRefs || [])])],
|
|
379
|
+
entries: requestedAliases.map((alias) => {
|
|
345
380
|
const classified = classifyPreviewAlias(alias);
|
|
346
381
|
const retention = retentionConfig(config, "preview");
|
|
382
|
+
const objectPrefix = objectPrefixFor(deployConfig, alias);
|
|
347
383
|
return {
|
|
348
384
|
alias,
|
|
349
385
|
aliasKind: classified.kind,
|
|
350
386
|
mutableAlias: classified.mutable,
|
|
351
387
|
retentionClass: classified.retentionClass,
|
|
352
388
|
retentionDays: retention[classified.retentionKey],
|
|
353
|
-
action: "
|
|
389
|
+
action: "delete-preview-alias",
|
|
390
|
+
objectPrefix,
|
|
391
|
+
manifestKey: `${manifestPrefix.replace(/\/$/, "")}/${alias}.json`,
|
|
392
|
+
steps: cleanupAdapterSteps(deployConfig.adapter, deployConfig, {
|
|
393
|
+
alias,
|
|
394
|
+
objectPrefix,
|
|
395
|
+
manifestPrefix,
|
|
396
|
+
}),
|
|
354
397
|
};
|
|
355
398
|
}),
|
|
356
399
|
};
|
|
357
400
|
}
|
|
358
401
|
|
|
402
|
+
function cleanupAdapterSteps(adapter, deployConfig, entry) {
|
|
403
|
+
if (adapter === "aws-s3-cloudfront") {
|
|
404
|
+
return [
|
|
405
|
+
{
|
|
406
|
+
action: "delete-static-prefix",
|
|
407
|
+
target: deployConfig.bucket || deployConfig.target || "",
|
|
408
|
+
prefix: entry.objectPrefix,
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
action: "delete-deployment-manifest",
|
|
412
|
+
target: entry.manifestPrefix,
|
|
413
|
+
key: `${entry.manifestPrefix.replace(/\/$/, "")}/${entry.alias}.json`,
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
action: "invalidate-cdn",
|
|
417
|
+
distribution: deployConfig.cloudfront_distribution || deployConfig.distribution || "",
|
|
418
|
+
},
|
|
419
|
+
];
|
|
420
|
+
}
|
|
421
|
+
return [
|
|
422
|
+
{
|
|
423
|
+
action: "delete-preview-environment",
|
|
424
|
+
target: deployConfig.environment || deployConfig.service || deployConfig.target || "",
|
|
425
|
+
alias: entry.alias,
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
action: "delete-deployment-manifest",
|
|
429
|
+
target: entry.manifestPrefix,
|
|
430
|
+
key: `${entry.manifestPrefix.replace(/\/$/, "")}/${entry.alias}.json`,
|
|
431
|
+
},
|
|
432
|
+
];
|
|
433
|
+
}
|
|
434
|
+
|
|
359
435
|
export function defaultWebSurfaceAlias({ channel = "preview", sourceSha = "", pullNumber = "" } = {}) {
|
|
360
436
|
if (channel !== "preview") {
|
|
361
437
|
return "";
|
package/scripts/web-surface.mjs
CHANGED
|
@@ -89,11 +89,18 @@ export function webSurfaceCli() {
|
|
|
89
89
|
cwd,
|
|
90
90
|
channel,
|
|
91
91
|
aliases,
|
|
92
|
+
sourceSha,
|
|
93
|
+
pullNumber,
|
|
94
|
+
event: readArg("event", process.env.BUILDCHAIN_WEB_SURFACE_EVENT || "manual"),
|
|
95
|
+
actor: readArg("actor", process.env.GITHUB_ACTOR || ""),
|
|
96
|
+
runId: readArg("run-id", process.env.GITHUB_RUN_ID || ""),
|
|
92
97
|
dryRun: readBooleanArg("dry-run", true),
|
|
93
98
|
});
|
|
94
99
|
writeJson(result, output);
|
|
95
100
|
writeGitHubOutputs({
|
|
96
101
|
"web-surface-cleanup-count": String(result.entries.length),
|
|
102
|
+
"web-surface-cleanup-mode": result.applyMode,
|
|
103
|
+
"web-surface-cleanup-status": result.status,
|
|
97
104
|
"web-surface-cleanup-plan-json": JSON.stringify(result),
|
|
98
105
|
});
|
|
99
106
|
return result;
|