blogwright 0.3.2 → 0.4.0-beta.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/README.md +11 -11
- package/agent/agent-manifest.json +1 -1
- package/agent/server.js +37 -19
- package/dist/adapters/fetch-ping.d.ts +1 -1
- package/dist/adapters/fetch-ping.js +3 -4
- package/dist/adapters/node-module-loader.d.ts +11 -0
- package/dist/adapters/node-module-loader.js +146 -0
- package/dist/adapters/process-package-manager.d.ts +41 -0
- package/dist/adapters/process-package-manager.js +116 -0
- package/dist/adapters/process-vcs.d.ts +5 -4
- package/dist/adapters/process-vcs.js +6 -6
- package/dist/agent-package.d.ts +1 -1
- package/dist/agent-package.js +4 -4
- package/dist/bin.js +13 -3
- package/dist/cli.d.ts +68 -1
- package/dist/cli.js +270 -89
- package/dist/commands.d.ts +70 -3
- package/dist/commands.js +180 -32
- package/dist/config-block.d.ts +34 -0
- package/dist/config-block.js +262 -0
- package/dist/context.d.ts +76 -6
- package/dist/context.js +98 -19
- package/dist/deploy.d.ts +2 -2
- package/dist/deploy.js +14 -14
- package/dist/graph.d.ts +27 -16
- package/dist/graph.js +1 -2
- package/dist/init.d.ts +37 -3
- package/dist/init.js +146 -23
- package/dist/known-commands.d.ts +63 -0
- package/dist/known-commands.js +78 -0
- package/dist/logger.js +0 -1
- package/dist/microvms.d.ts +2 -2
- package/dist/microvms.js +3 -4
- package/dist/nodes.d.ts +5 -3
- package/dist/nodes.js +97 -31
- package/dist/plugin-commands.d.ts +298 -0
- package/dist/plugin-commands.js +990 -0
- package/dist/plugins.d.ts +194 -0
- package/dist/plugins.js +523 -0
- package/dist/ports.d.ts +89 -1
- package/dist/ports.js +0 -1
- package/dist/render.d.ts +55 -0
- package/dist/render.js +89 -2
- package/dist/repo.d.ts +3 -3
- package/dist/repo.js +8 -9
- package/dist/rkey.js +0 -1
- package/dist/seo.d.ts +1 -1
- package/dist/seo.js +1 -2
- package/package.json +6 -6
- package/dist/adapters/fetch-ping.js.map +0 -1
- package/dist/adapters/process-vcs.js.map +0 -1
- package/dist/agent-package.js.map +0 -1
- package/dist/bin.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/commands.js.map +0 -1
- package/dist/context.js.map +0 -1
- package/dist/deploy.js.map +0 -1
- package/dist/graph.js.map +0 -1
- package/dist/init.js.map +0 -1
- package/dist/logger.js.map +0 -1
- package/dist/microvms.js.map +0 -1
- package/dist/nodes.js.map +0 -1
- package/dist/ports.js.map +0 -1
- package/dist/render.js.map +0 -1
- package/dist/repo.js.map +0 -1
- package/dist/rkey.js.map +0 -1
- package/dist/seo.js.map +0 -1
- package/dist/test-support.d.ts +0 -45
- package/dist/test-support.js +0 -126
- package/dist/test-support.js.map +0 -1
package/dist/commands.d.ts
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
|
+
import { type ResourceNode } from 'blogwright-core';
|
|
1
2
|
import type { OpsContext } from './context.js';
|
|
3
|
+
import { type GraphContext } from './graph.js';
|
|
4
|
+
import { type StatusEntry } from './render.js';
|
|
2
5
|
/** Create the full infrastructure graph. */
|
|
3
6
|
export declare function bootstrap(ctx: OpsContext): Promise<void>;
|
|
7
|
+
/**
|
|
8
|
+
* Refuse to destroy the site while any plugin's own state object still
|
|
9
|
+
* exists in the bucket - §State → Scoped state stores' "`blogwright
|
|
10
|
+
* destroy` therefore refuses while any `state/<env>.<plugin>.json` exists".
|
|
11
|
+
*
|
|
12
|
+
* A scope changes the state object's KEY, not the bucket it lives in
|
|
13
|
+
* (`StateStore`, `packages/core/src/state.ts`) - a scoped and an unscoped
|
|
14
|
+
* store for the same environment are constructed over the very same
|
|
15
|
+
* `names.bucket` - and the site's own bucket node empties every prefix
|
|
16
|
+
* before deleting the bucket (`deletePrefix(ctx.names.bucket, '')`,
|
|
17
|
+
* `nodes.ts`'s `bucketNode().delete()`). Without this guard, a site
|
|
18
|
+
* teardown deletes `state/<env>.<scope>.json` while every resource it
|
|
19
|
+
* records lives on: the plugin's next `destroy` then loads empty state,
|
|
20
|
+
* every node's `read()` returns false, and nothing is removed - the
|
|
21
|
+
* plugin's resources are silently orphaned.
|
|
22
|
+
*
|
|
23
|
+
* Reads the bucket, not the plugin registry, so the refusal holds even for
|
|
24
|
+
* a plugin that has since been uninstalled. Runs inside the teardown verbs
|
|
25
|
+
* themselves, not `createContext` - so no other command pays for the extra
|
|
26
|
+
* `listObjects` call and plugin discovery stays lazy - and ahead of
|
|
27
|
+
* `clearRunningMicrovms`/`destroyGraph` in each, so a refusal has zero side
|
|
28
|
+
* effects: nothing is terminated, nothing is deleted.
|
|
29
|
+
*
|
|
30
|
+
* BOTH teardown verbs call it: `destroy` (below) and `previewTeardown`,
|
|
31
|
+
* which runs the very same `destroyGraph(buildNodes(ctx), ctx)` over the
|
|
32
|
+
* very same `bucketNode().delete()` and so empties the preview stack's
|
|
33
|
+
* bucket - including a `state/<env>.<scope>.json` a plugin bootstrapped
|
|
34
|
+
* against that environment - in exactly the same way. Nothing about the
|
|
35
|
+
* orphaning above is specific to the site's own environment, so nothing
|
|
36
|
+
* about the guard is either.
|
|
37
|
+
*
|
|
38
|
+
* A MISSING BUCKET IS TREATED AS CLEAR, not as an error. `listObjects` does
|
|
39
|
+
* not swallow a 404 the way `getObjectText` does, so an environment whose
|
|
40
|
+
* bucket a previous (interrupted) teardown already deleted would otherwise
|
|
41
|
+
* fail here with a raw `NoSuchBucket` before a single non-bucket resource -
|
|
42
|
+
* roles, log groups, the CloudFront function, the delivery trio - had been
|
|
43
|
+
* cleaned up, on precisely the recovery path an operator reaches for after
|
|
44
|
+
* an interrupted teardown. A bucket that is gone holds no state object at
|
|
45
|
+
* all, scoped or otherwise, so there is nothing to protect and every reason
|
|
46
|
+
* to let the teardown finish: refusing here would CAUSE the orphaning this
|
|
47
|
+
* guard exists to prevent. Only `NoSuchBucket` is treated this way - every
|
|
48
|
+
* other failure (denied, throttled, a network fault) still propagates,
|
|
49
|
+
* because those say nothing about whether scoped state exists.
|
|
50
|
+
*
|
|
51
|
+
* Exported - like `readNodeStatus` above - so a test can pin its outcomes
|
|
52
|
+
* directly against a recording S3 client, rather than only through a full
|
|
53
|
+
* `destroy()` run over the entire production graph.
|
|
54
|
+
*/
|
|
55
|
+
export declare function assertNoScopedState(ctx: OpsContext): Promise<void>;
|
|
4
56
|
/** Destroy the full infrastructure graph. */
|
|
5
57
|
export declare function destroy(ctx: OpsContext, opts: {
|
|
6
58
|
yes: boolean;
|
|
@@ -19,7 +71,7 @@ export declare function previewBootstrap(ctx: OpsContext): Promise<void>;
|
|
|
19
71
|
export declare function previewDeploy(ctx: OpsContext, id: string, opts?: {
|
|
20
72
|
refresh?: boolean;
|
|
21
73
|
}): Promise<string>;
|
|
22
|
-
/** Remove one PR's preview (delete its prefix). No invalidation
|
|
74
|
+
/** Remove one PR's preview (delete its prefix). No invalidation - previews aren't cached. */
|
|
23
75
|
export declare function previewDestroy(ctx: OpsContext, id: string): Promise<void>;
|
|
24
76
|
/** List active previews (by prefix). */
|
|
25
77
|
export declare function previewList(ctx: OpsContext): Promise<void>;
|
|
@@ -33,5 +85,20 @@ export declare function deleteSite(ctx: OpsContext): Promise<void>;
|
|
|
33
85
|
export declare function history(ctx: OpsContext): Promise<void>;
|
|
34
86
|
/** Show CloudWatch build logs for a given hash. */
|
|
35
87
|
export declare function logs(ctx: OpsContext, hash: string): Promise<void>;
|
|
36
|
-
/**
|
|
37
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Read each node's live status against `ctx.state`: present/missing from
|
|
90
|
+
* `node.read(ctx)`, or an `error` entry carrying the message if it throws.
|
|
91
|
+
* A query, not a command - it never writes to the logger, so a caller (the
|
|
92
|
+
* CLI's own `status` below, and a plugin's `status` verb) decides how to
|
|
93
|
+
* report each entry. Iterates `nodes` in the order given (no `topoSort` -
|
|
94
|
+
* status is a read, not a reconcile, so dependency order doesn't matter).
|
|
95
|
+
*/
|
|
96
|
+
export declare function readNodeStatus<Ctx extends GraphContext>(nodes: ResourceNode<Ctx>[], ctx: Ctx): Promise<StatusEntry[]>;
|
|
97
|
+
/**
|
|
98
|
+
* Show the planned graph against live state (drift view). `nodes` defaults to
|
|
99
|
+
* the production graph (`buildNodes(ctx)`) - every real call site is
|
|
100
|
+
* unchanged - but is a parameter, not reached-for, so a test (or a future
|
|
101
|
+
* caller) can hand this the same loop over a different node set without
|
|
102
|
+
* patching a module.
|
|
103
|
+
*/
|
|
104
|
+
export declare function status(ctx: OpsContext, nodes?: ResourceNode<OpsContext>[]): Promise<void>;
|
package/dist/commands.js
CHANGED
|
@@ -1,18 +1,42 @@
|
|
|
1
|
-
import { colors, findRepoRoot } from 'blogwright-core';
|
|
1
|
+
import { AwsError, colors, findRepoRoot } from 'blogwright-core';
|
|
2
|
+
/*
|
|
3
|
+
* A STATIC, NON-OPTIONAL IMPORT, and deliberately the last one left in the
|
|
4
|
+
* CLI. Task 29 deleted every other reference to `blogwright-pds` from the
|
|
5
|
+
* dispatcher (`cli.ts` now knows no namespace by name; the package is
|
|
6
|
+
* discovered and dispatched as an ordinary plugin), and this one still
|
|
7
|
+
* stays, for two reasons:
|
|
8
|
+
*
|
|
9
|
+
* - THE SPI HAS NO LIFECYCLE HOOKS. `Plugin` (`blogwright-core`'s
|
|
10
|
+
* `plugin.ts`) declares `commands`, `nodes`, `configKey`,
|
|
11
|
+
* `validateConfig` and `init` - nothing a plugin could register to be
|
|
12
|
+
* called after a successful deploy. `deploy` below therefore reaches
|
|
13
|
+
* this function by name, exactly as it always has; routing it through
|
|
14
|
+
* plugin dispatch would mean inventing a hook the change spec does not
|
|
15
|
+
* describe.
|
|
16
|
+
* - THE PACKAGE SHIPS BY DEFAULT. `blogwright-pds` is a non-optional
|
|
17
|
+
* `dependencies` entry of `packages/cli/package.json` - that is how the
|
|
18
|
+
* bundled plugin reaches a consuming repo that depends on `blogwright`
|
|
19
|
+
* alone - so the import can never fail to resolve, and no optional-
|
|
20
|
+
* dependency guard is needed around it.
|
|
21
|
+
*
|
|
22
|
+
* `syncAfterDeploy` no-ops for any environment but `production` and for a
|
|
23
|
+
* site with no `pds` config block, so an unconfigured repo pays nothing for
|
|
24
|
+
* it. Both halves are asserted in `commands.test.ts`.
|
|
25
|
+
*/
|
|
2
26
|
import { syncAfterDeploy } from 'blogwright-pds';
|
|
3
27
|
import { invalidateChanged, invalidateCloudFront, manifestKey, microvmLogGroup, runBuild, } from './deploy.js';
|
|
4
28
|
import { applyGraph, destroyGraph } from './graph.js';
|
|
5
29
|
import { clearRunningMicrovms } from './microvms.js';
|
|
6
30
|
import { buildNodes, reconcileBuilderImage } from './nodes.js';
|
|
7
|
-
import { formatDuration,
|
|
31
|
+
import { formatDuration, logStatusEntries, renderHistoryTable, renderSummary, } from './render.js';
|
|
8
32
|
import { buildRepoZip, COMMIT_FILE, listRepoFiles } from './repo.js';
|
|
9
33
|
/** One line per invalidation outcome, shared by the summary card and logs. */
|
|
10
34
|
function describeInvalidation(inv) {
|
|
11
35
|
if (inv.mode === 'none')
|
|
12
|
-
return 'nothing changed
|
|
36
|
+
return 'nothing changed - skipped';
|
|
13
37
|
if (inv.mode === 'paths')
|
|
14
38
|
return `${inv.count} changed path${inv.count === 1 ? '' : 's'}`;
|
|
15
|
-
return inv.count > 0 ? `everything (/*)
|
|
39
|
+
return inv.count > 0 ? `everything (/*) - ${inv.count} paths over cap` : 'everything (/*)';
|
|
16
40
|
}
|
|
17
41
|
/**
|
|
18
42
|
* Canonical origin the live site is served from: the custom domain if configured,
|
|
@@ -34,11 +58,125 @@ export async function bootstrap(ctx) {
|
|
|
34
58
|
if (typeof domain === 'string')
|
|
35
59
|
ctx.logger.info(`Site will be served at https://${domain}`);
|
|
36
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* The prefix every state object - scoped or not - is filed under
|
|
63
|
+
* (`StateStore`, `packages/core/src/state.ts`). Not exported from core, so
|
|
64
|
+
* mirrored here rather than reached for; the guard below only ever reads
|
|
65
|
+
* this prefix, never constructs a key to write.
|
|
66
|
+
*/
|
|
67
|
+
const STATE_PREFIX = 'state/';
|
|
68
|
+
/** The site's own unscoped state key - the one object under `STATE_PREFIX` the guard below must never treat as a plugin's. */
|
|
69
|
+
function siteStateKey(env) {
|
|
70
|
+
return `${STATE_PREFIX}${env}.json`;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Every plugin scope with a `state/<env>.<scope>.json` object present under
|
|
74
|
+
* `STATE_PREFIX`, derived from a listing rather than the plugin registry -
|
|
75
|
+
* so {@link assertNoScopedState} holds even for a plugin that has since
|
|
76
|
+
* been uninstalled. Sorted, so the guard's message is deterministic
|
|
77
|
+
* regardless of the order S3 lists objects in.
|
|
78
|
+
*/
|
|
79
|
+
function scopedStateScopes(env, objects) {
|
|
80
|
+
const prefix = `${STATE_PREFIX}${env}.`;
|
|
81
|
+
const site = siteStateKey(env);
|
|
82
|
+
const scopes = objects
|
|
83
|
+
.map((o) => o.key)
|
|
84
|
+
.filter((key) => key !== site && key.startsWith(prefix) && key.endsWith('.json'))
|
|
85
|
+
.map((key) => key.slice(prefix.length, -'.json'.length));
|
|
86
|
+
return [...new Set(scopes)].sort();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Refuse to destroy the site while any plugin's own state object still
|
|
90
|
+
* exists in the bucket - §State → Scoped state stores' "`blogwright
|
|
91
|
+
* destroy` therefore refuses while any `state/<env>.<plugin>.json` exists".
|
|
92
|
+
*
|
|
93
|
+
* A scope changes the state object's KEY, not the bucket it lives in
|
|
94
|
+
* (`StateStore`, `packages/core/src/state.ts`) - a scoped and an unscoped
|
|
95
|
+
* store for the same environment are constructed over the very same
|
|
96
|
+
* `names.bucket` - and the site's own bucket node empties every prefix
|
|
97
|
+
* before deleting the bucket (`deletePrefix(ctx.names.bucket, '')`,
|
|
98
|
+
* `nodes.ts`'s `bucketNode().delete()`). Without this guard, a site
|
|
99
|
+
* teardown deletes `state/<env>.<scope>.json` while every resource it
|
|
100
|
+
* records lives on: the plugin's next `destroy` then loads empty state,
|
|
101
|
+
* every node's `read()` returns false, and nothing is removed - the
|
|
102
|
+
* plugin's resources are silently orphaned.
|
|
103
|
+
*
|
|
104
|
+
* Reads the bucket, not the plugin registry, so the refusal holds even for
|
|
105
|
+
* a plugin that has since been uninstalled. Runs inside the teardown verbs
|
|
106
|
+
* themselves, not `createContext` - so no other command pays for the extra
|
|
107
|
+
* `listObjects` call and plugin discovery stays lazy - and ahead of
|
|
108
|
+
* `clearRunningMicrovms`/`destroyGraph` in each, so a refusal has zero side
|
|
109
|
+
* effects: nothing is terminated, nothing is deleted.
|
|
110
|
+
*
|
|
111
|
+
* BOTH teardown verbs call it: `destroy` (below) and `previewTeardown`,
|
|
112
|
+
* which runs the very same `destroyGraph(buildNodes(ctx), ctx)` over the
|
|
113
|
+
* very same `bucketNode().delete()` and so empties the preview stack's
|
|
114
|
+
* bucket - including a `state/<env>.<scope>.json` a plugin bootstrapped
|
|
115
|
+
* against that environment - in exactly the same way. Nothing about the
|
|
116
|
+
* orphaning above is specific to the site's own environment, so nothing
|
|
117
|
+
* about the guard is either.
|
|
118
|
+
*
|
|
119
|
+
* A MISSING BUCKET IS TREATED AS CLEAR, not as an error. `listObjects` does
|
|
120
|
+
* not swallow a 404 the way `getObjectText` does, so an environment whose
|
|
121
|
+
* bucket a previous (interrupted) teardown already deleted would otherwise
|
|
122
|
+
* fail here with a raw `NoSuchBucket` before a single non-bucket resource -
|
|
123
|
+
* roles, log groups, the CloudFront function, the delivery trio - had been
|
|
124
|
+
* cleaned up, on precisely the recovery path an operator reaches for after
|
|
125
|
+
* an interrupted teardown. A bucket that is gone holds no state object at
|
|
126
|
+
* all, scoped or otherwise, so there is nothing to protect and every reason
|
|
127
|
+
* to let the teardown finish: refusing here would CAUSE the orphaning this
|
|
128
|
+
* guard exists to prevent. Only `NoSuchBucket` is treated this way - every
|
|
129
|
+
* other failure (denied, throttled, a network fault) still propagates,
|
|
130
|
+
* because those say nothing about whether scoped state exists.
|
|
131
|
+
*
|
|
132
|
+
* Exported - like `readNodeStatus` above - so a test can pin its outcomes
|
|
133
|
+
* directly against a recording S3 client, rather than only through a full
|
|
134
|
+
* `destroy()` run over the entire production graph.
|
|
135
|
+
*/
|
|
136
|
+
export async function assertNoScopedState(ctx) {
|
|
137
|
+
let objects;
|
|
138
|
+
try {
|
|
139
|
+
objects = await ctx.clients.s3.listObjects(ctx.names.bucket, STATE_PREFIX);
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
// No bucket, no state objects - see this function's doc comment. Matched
|
|
143
|
+
// on the bucket's own error code rather than the broad `isNotFound`,
|
|
144
|
+
// which is equally true of `NoSuchKey` and of ANY 404 (`AwsError`,
|
|
145
|
+
// `packages/core/src/aws/errors.ts`): a spurious 404 from a non-AWS,
|
|
146
|
+
// S3-compatible endpoint (`--endpoint`) would otherwise read as "no
|
|
147
|
+
// scoped state" and let the teardown empty the bucket over a plugin's
|
|
148
|
+
// live state object - the exact orphaning this guard exists to prevent.
|
|
149
|
+
// Every other failure propagates, which ends the teardown safely rather
|
|
150
|
+
// than deleting past the guard.
|
|
151
|
+
if (err instanceof AwsError && err.code === 'NoSuchBucket')
|
|
152
|
+
return;
|
|
153
|
+
throw err;
|
|
154
|
+
}
|
|
155
|
+
const scopes = scopedStateScopes(ctx.env, objects);
|
|
156
|
+
if (scopes.length === 0)
|
|
157
|
+
return;
|
|
158
|
+
// The remedy MUST name the environment. `runPlugin` resolves a plugin
|
|
159
|
+
// command's environment as `values.env ?? envPositional ?? DEFAULT_ENV`
|
|
160
|
+
// with `DEFAULT_ENV = 'production'` (`plugin-commands.ts`), so an env-less
|
|
161
|
+
// `blogwright <scope> destroy --yes` silently targets production - always
|
|
162
|
+
// the wrong environment when this guard fires from `previewTeardown`,
|
|
163
|
+
// which builds `env: 'preview'` unconditionally (`cli.ts`'s `runPreview`).
|
|
164
|
+
// Printed against a preview refusal, the env-less form at best loads empty
|
|
165
|
+
// state and removes nothing, leaving the operator stuck in this same
|
|
166
|
+
// refusal, and at worst tears down the live production stack. `deriveNames`
|
|
167
|
+
// keys off `env` alone, so the positional below is the whole fix - and it
|
|
168
|
+
// covers both callers, because each passes the very environment it is
|
|
169
|
+
// tearing down.
|
|
170
|
+
const lines = scopes.map((scope) => ` - ${scope}: run \`blogwright ${scope} destroy ${ctx.env} --yes\` first`);
|
|
171
|
+
throw new Error(`refusing to destroy "${ctx.env}": ${scopes.length} plugin state object(s) still exist in ` +
|
|
172
|
+
`s3://${ctx.names.bucket}/${STATE_PREFIX}\n${lines.join('\n')}`);
|
|
173
|
+
}
|
|
37
174
|
/** Destroy the full infrastructure graph. */
|
|
38
175
|
export async function destroy(ctx, opts) {
|
|
39
176
|
if (!opts.yes) {
|
|
40
177
|
throw new Error(`refusing to destroy "${ctx.env}" without --yes`);
|
|
41
178
|
}
|
|
179
|
+
await assertNoScopedState(ctx);
|
|
42
180
|
ctx.logger.info(colors.bold(`Destroying "${ctx.env}"`));
|
|
43
181
|
// Running builder MicroVMs pin the image and make its deletion fail; clear them first
|
|
44
182
|
// (or let the operator cancel and wait for in-flight builds to finish).
|
|
@@ -70,7 +208,7 @@ export async function deploy(ctx, opts = {}) {
|
|
|
70
208
|
...(opts.refresh ? { refresh: true } : {}),
|
|
71
209
|
});
|
|
72
210
|
const invalidation = await invalidateChanged(ctx, hash);
|
|
73
|
-
// Production content changed
|
|
211
|
+
// Production content changed - mirror it to the PDS (non-fatal; see syncAfterDeploy).
|
|
74
212
|
await syncAfterDeploy(ctx);
|
|
75
213
|
const url = siteBaseUrl(ctx);
|
|
76
214
|
const rows = [
|
|
@@ -102,7 +240,7 @@ export async function rollback(ctx, hash, opts = {}) {
|
|
|
102
240
|
});
|
|
103
241
|
await invalidateChanged(ctx, hash);
|
|
104
242
|
// A rollback changes production content too, but the PDS mirrors the *working tree*
|
|
105
|
-
// content, which a rollback does not restore
|
|
243
|
+
// content, which a rollback does not restore - so only warn about the divergence.
|
|
106
244
|
if (ctx.env === 'production' && ctx.config.pds) {
|
|
107
245
|
ctx.logger.warn('rollback does not sync the PDS (records mirror the current repo content); ' +
|
|
108
246
|
'check out the rolled-back revision and run `blogwright pds sync` if needed');
|
|
@@ -151,7 +289,7 @@ export async function previewDeploy(ctx, id, opts = {}) {
|
|
|
151
289
|
ctx.logger.ok(`preview ready in ${formatDuration(Date.now() - startedAt)}: ${url}`);
|
|
152
290
|
return url;
|
|
153
291
|
}
|
|
154
|
-
/** Remove one PR's preview (delete its prefix). No invalidation
|
|
292
|
+
/** Remove one PR's preview (delete its prefix). No invalidation - previews aren't cached. */
|
|
155
293
|
export async function previewDestroy(ctx, id) {
|
|
156
294
|
assertPreviewId(id);
|
|
157
295
|
const count = await ctx.clients.s3.deletePrefix(ctx.names.bucket, `previews/${id}/`);
|
|
@@ -172,6 +310,12 @@ export async function previewList(ctx) {
|
|
|
172
310
|
export async function previewTeardown(ctx, opts) {
|
|
173
311
|
if (!opts.yes)
|
|
174
312
|
throw new Error('refusing to tear down the preview stack without --yes');
|
|
313
|
+
// The preview stack's teardown is the SITE teardown's graph over the
|
|
314
|
+
// preview environment's own bucket - same `destroyGraph(buildNodes(ctx))`,
|
|
315
|
+
// same `bucketNode().delete()` emptying every prefix - so a plugin
|
|
316
|
+
// bootstrapped against this environment is orphaned here in exactly the
|
|
317
|
+
// way `destroy` above is guarded against. See `assertNoScopedState`.
|
|
318
|
+
await assertNoScopedState(ctx);
|
|
175
319
|
ctx.logger.info(colors.bold('Tearing down preview stack'));
|
|
176
320
|
if (!(await clearRunningMicrovms(ctx)))
|
|
177
321
|
return;
|
|
@@ -205,7 +349,9 @@ export async function history(ctx) {
|
|
|
205
349
|
ctx.logger.warn(`skipping unreadable manifest ${obj.key}`);
|
|
206
350
|
}
|
|
207
351
|
}
|
|
208
|
-
|
|
352
|
+
// Newest first. Codepoint sort, not localeCompare: collation must not depend on host locale/ICU
|
|
353
|
+
// (finishedAt is ISO-8601, which orders correctly by codepoint).
|
|
354
|
+
manifests.sort((a, b) => a.finishedAt > b.finishedAt ? -1 : a.finishedAt < b.finishedAt ? 1 : 0);
|
|
209
355
|
if (ctx.ports.terminal.isInteractive) {
|
|
210
356
|
for (const line of renderHistoryTable(manifests, Date.now()))
|
|
211
357
|
ctx.logger.info(line);
|
|
@@ -228,7 +374,7 @@ export async function logs(ctx, hash) {
|
|
|
228
374
|
manifest = text ? JSON.parse(text) : undefined;
|
|
229
375
|
}
|
|
230
376
|
catch {
|
|
231
|
-
ctx.logger.warn(`manifest for ${hash} is unreadable
|
|
377
|
+
ctx.logger.warn(`manifest for ${hash} is unreadable - showing the unfiltered log window`);
|
|
232
378
|
}
|
|
233
379
|
// Filter to the build's time window (± a minute) from the manifest.
|
|
234
380
|
const startTime = manifest ? Date.parse(manifest.startedAt) - 60_000 : undefined;
|
|
@@ -245,38 +391,40 @@ export async function logs(ctx, hash) {
|
|
|
245
391
|
ctx.logger.info(`${colors.dim(new Date(e.timestamp).toISOString())} ${e.message.trimEnd()}`);
|
|
246
392
|
}
|
|
247
393
|
}
|
|
248
|
-
/**
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
394
|
+
/**
|
|
395
|
+
* Read each node's live status against `ctx.state`: present/missing from
|
|
396
|
+
* `node.read(ctx)`, or an `error` entry carrying the message if it throws.
|
|
397
|
+
* A query, not a command - it never writes to the logger, so a caller (the
|
|
398
|
+
* CLI's own `status` below, and a plugin's `status` verb) decides how to
|
|
399
|
+
* report each entry. Iterates `nodes` in the order given (no `topoSort` -
|
|
400
|
+
* status is a read, not a reconcile, so dependency order doesn't matter).
|
|
401
|
+
*/
|
|
402
|
+
export async function readNodeStatus(nodes, ctx) {
|
|
252
403
|
const entries = [];
|
|
253
|
-
for (const node of
|
|
404
|
+
for (const node of nodes) {
|
|
254
405
|
let exists = false;
|
|
255
406
|
try {
|
|
256
407
|
exists = await node.read(ctx);
|
|
257
408
|
}
|
|
258
409
|
catch (err) {
|
|
259
|
-
|
|
260
|
-
entries.push({ title: node.title, state: 'error', detail: err.message });
|
|
261
|
-
}
|
|
262
|
-
else {
|
|
263
|
-
ctx.logger.warn(`${node.title}: read failed (${err.message})`);
|
|
264
|
-
}
|
|
410
|
+
entries.push({ title: node.title, state: 'error', detail: err.message });
|
|
265
411
|
continue;
|
|
266
412
|
}
|
|
267
413
|
const outputs = ctx.state.resources[node.id];
|
|
268
414
|
const detail = outputs ? JSON.stringify(outputs) : undefined;
|
|
269
|
-
|
|
270
|
-
entries.push({ title: node.title, state: exists ? 'present' : 'missing', detail });
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
273
|
-
// The plain form is the stable contract for CI logs and agents.
|
|
274
|
-
const mark = exists ? colors.green('present') : colors.yellow('missing');
|
|
275
|
-
ctx.logger.info(` ${mark} ${node.title} ${detail ? colors.dim(detail) : ''}`);
|
|
276
|
-
}
|
|
277
|
-
if (pretty) {
|
|
278
|
-
for (const line of renderStatusTree(entries))
|
|
279
|
-
ctx.logger.info(line);
|
|
415
|
+
entries.push({ title: node.title, state: exists ? 'present' : 'missing', detail });
|
|
280
416
|
}
|
|
417
|
+
return entries;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Show the planned graph against live state (drift view). `nodes` defaults to
|
|
421
|
+
* the production graph (`buildNodes(ctx)`) - every real call site is
|
|
422
|
+
* unchanged - but is a parameter, not reached-for, so a test (or a future
|
|
423
|
+
* caller) can hand this the same loop over a different node set without
|
|
424
|
+
* patching a module.
|
|
425
|
+
*/
|
|
426
|
+
export async function status(ctx, nodes = buildNodes(ctx)) {
|
|
427
|
+
ctx.logger.info(colors.bold(`Status for "${ctx.env}" (bucket ${ctx.names.bucket})`));
|
|
428
|
+
const entries = await readNodeStatus(nodes, ctx);
|
|
429
|
+
logStatusEntries(entries, ctx.ports.terminal.isInteractive, ctx.logger);
|
|
281
430
|
}
|
|
282
|
-
//# sourceMappingURL=commands.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** The file a block is spliced into. */
|
|
2
|
+
export interface ConfigSource {
|
|
3
|
+
readonly path: string;
|
|
4
|
+
readonly text: string;
|
|
5
|
+
}
|
|
6
|
+
/** A rendered block ready to splice in, and the key it is filed under. */
|
|
7
|
+
export interface ConfigBlock {
|
|
8
|
+
readonly key: string;
|
|
9
|
+
readonly rendered: string;
|
|
10
|
+
}
|
|
11
|
+
/** One entry of a rendered block, matching `renderConfig`'s own entry shape. */
|
|
12
|
+
export interface ConfigBlockEntry {
|
|
13
|
+
readonly prop: string;
|
|
14
|
+
readonly comment?: string | undefined;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Render `entries` as a `"key": { ... }` block in `renderConfig`'s style
|
|
18
|
+
* (`init.ts:42-69`): two-space indent per nesting level, an optional
|
|
19
|
+
* `// comment` suffix per entry, and a comma between entries but never after
|
|
20
|
+
* the last one. The result is meant to be handed to `spliceConfigBlock` as
|
|
21
|
+
* `block.rendered`, already indented as it will appear once inserted as a
|
|
22
|
+
* property of the top-level object.
|
|
23
|
+
*/
|
|
24
|
+
export declare function renderConfigBlock(key: string, entries: readonly ConfigBlockEntry[]): string;
|
|
25
|
+
/**
|
|
26
|
+
* Splice `block.rendered` into `source.text` as a new property of the
|
|
27
|
+
* document's single top-level object, immediately before its closing brace.
|
|
28
|
+
* The document is scanned, never reparsed: every byte outside the inserted
|
|
29
|
+
* region - comments, indentation, trailing commas - comes back unchanged.
|
|
30
|
+
*
|
|
31
|
+
* Refuses rather than guessing when `block.key` is already present on the
|
|
32
|
+
* object, or when the document is not shaped as a single top-level object.
|
|
33
|
+
*/
|
|
34
|
+
export declare function spliceConfigBlock(source: ConfigSource, block: ConfigBlock): string;
|