blogwright 0.3.3 → 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 -3
- 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 -5
- package/dist/agent-package.d.ts +1 -1
- package/dist/agent-package.js +4 -3
- package/dist/bin.js +13 -2
- package/dist/cli.d.ts +68 -1
- package/dist/cli.js +270 -88
- package/dist/commands.d.ts +70 -3
- package/dist/commands.js +177 -30
- 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 -18
- package/dist/deploy.d.ts +2 -2
- package/dist/deploy.js +12 -12
- package/dist/graph.d.ts +27 -16
- package/dist/graph.js +1 -1
- package/dist/init.d.ts +37 -3
- package/dist/init.js +146 -22
- package/dist/known-commands.d.ts +63 -0
- package/dist/known-commands.js +78 -0
- package/dist/microvms.d.ts +2 -2
- package/dist/microvms.js +3 -3
- package/dist/nodes.d.ts +5 -3
- package/dist/nodes.js +97 -30
- 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/render.d.ts +55 -0
- package/dist/render.js +89 -1
- package/dist/repo.d.ts +3 -3
- package/dist/repo.js +8 -8
- package/dist/seo.d.ts +1 -1
- package/dist/seo.js +1 -1
- package/package.json +6 -6
package/dist/context.d.ts
CHANGED
|
@@ -7,13 +7,26 @@ export interface OpsContext {
|
|
|
7
7
|
/** True for the shared preview stack (host-routed, per-PR prefixes). */
|
|
8
8
|
preview: boolean;
|
|
9
9
|
config: OpsConfig;
|
|
10
|
+
/**
|
|
11
|
+
* The environment's config file exactly as parsed, before `OpsConfig`'s
|
|
12
|
+
* merge and validation - every top-level key the document carries, a
|
|
13
|
+
* plugin's own block included. `OpsConfig` has no index signature, so this
|
|
14
|
+
* is the only typed route to `config[plugin.configKey]` (see
|
|
15
|
+
* `blogwright-core`'s `parseConfigDocument`).
|
|
16
|
+
*
|
|
17
|
+
* CLI-side ONLY, and deliberately absent from `PluginContext`: dispatch
|
|
18
|
+
* reads the DISPATCHED plugin's block off this and hands that one block to
|
|
19
|
+
* that one plugin (`resolvePluginConfig`, `plugins.ts`), so no plugin ever
|
|
20
|
+
* sees another plugin's config.
|
|
21
|
+
*/
|
|
22
|
+
configDocument: Readonly<Record<string, unknown>>;
|
|
10
23
|
names: Names;
|
|
11
24
|
accountId: string;
|
|
12
25
|
clients: AwsClients;
|
|
13
26
|
ports: Ports;
|
|
14
27
|
/**
|
|
15
|
-
* Directory holding the build-agent artifacts
|
|
16
|
-
* and agent-manifest.json
|
|
28
|
+
* Directory holding the build-agent artifacts - Dockerfile, bundled server.js,
|
|
29
|
+
* and agent-manifest.json - copied into this package by its build
|
|
17
30
|
* (scripts/copy-agent.mjs). Resolved at the composition root; tests inject one.
|
|
18
31
|
*/
|
|
19
32
|
agentDir: string;
|
|
@@ -30,7 +43,7 @@ export interface OpsContext {
|
|
|
30
43
|
}
|
|
31
44
|
/**
|
|
32
45
|
* The `app` tag value, by precedence: the explicit `config.app`, else the
|
|
33
|
-
* site's domain, else the repo directory name
|
|
46
|
+
* site's domain, else the repo directory name - always something a human can
|
|
34
47
|
* trace back to the project from a billing or resource listing.
|
|
35
48
|
*/
|
|
36
49
|
export declare function deriveAppTag(config: Pick<OpsConfig, 'app'>, domain: string | undefined, repoRoot: string): string;
|
|
@@ -43,6 +56,42 @@ export interface ContextOptions {
|
|
|
43
56
|
/** Adapter overrides; anything omitted defaults to the real (node) adapter. */
|
|
44
57
|
ports?: Partial<Ports> | undefined;
|
|
45
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* The directory holding the CLI's own `package.json` - `blogwright`'s package
|
|
61
|
+
* root. Located from `import.meta.url` the same way {@link OpsContext.agentDir}
|
|
62
|
+
* is (below): `packages/cli/package.json` declares an `exports` map with a
|
|
63
|
+
* `./rkey` entry and no `.` entry (the CLI is consumed through its `bin`, not
|
|
64
|
+
* imported), so neither `blogwright` nor `blogwright/package.json` can be
|
|
65
|
+
* resolved through the `ModuleLoader` port - see that port's doc comment.
|
|
66
|
+
* Self-location is therefore a composition-root concern, not something
|
|
67
|
+
* `discover` (`plugins.ts`) can derive itself.
|
|
68
|
+
*
|
|
69
|
+
* A standalone function, not folded into {@link createContext}: `blogwright
|
|
70
|
+
* plugin list` dispatches before a context exists and still needs this
|
|
71
|
+
* value, and it is the one supplier every discovery-running path (plugin
|
|
72
|
+
* dispatch, `blogwright --help`, the init wizard, `plugin list`) passes as
|
|
73
|
+
* `discover`'s second argument.
|
|
74
|
+
*/
|
|
75
|
+
export declare function cliPackageDir(): string;
|
|
76
|
+
/**
|
|
77
|
+
* The running CLI's own declared version - the value `blogwright plugin add`
|
|
78
|
+
* (`plugin-commands.ts`) pins into the install spec, so a plugin and the CLI
|
|
79
|
+
* that dispatches it can never silently drift apart across two developers'
|
|
80
|
+
* checkouts.
|
|
81
|
+
*
|
|
82
|
+
* Read HERE, at the composition root, for exactly the reason
|
|
83
|
+
* {@link cliPackageDir} is resolved here: `blogwright`'s own `exports` map has
|
|
84
|
+
* no `.` entry, so neither the package nor its `package.json` can be reached
|
|
85
|
+
* through the `ModuleLoader` port, and this module is one of the few the
|
|
86
|
+
* `no-restricted-imports` rule lets touch `node:fs` at all. `plugin-commands.ts`
|
|
87
|
+
* receives the resolved string as DATA and never walks the filesystem for it -
|
|
88
|
+
* the same division `agentDir` already makes.
|
|
89
|
+
*
|
|
90
|
+
* Read on demand rather than at module load: `blogwright plugin list` and every
|
|
91
|
+
* built-in command share this module and none of them needs the value, so only
|
|
92
|
+
* the one command that pins a version pays for the read.
|
|
93
|
+
*/
|
|
94
|
+
export declare function cliVersion(): Promise<string>;
|
|
46
95
|
export interface ConfigSource {
|
|
47
96
|
env: string;
|
|
48
97
|
/** Repo root the default config candidates resolve against. */
|
|
@@ -50,12 +99,33 @@ export interface ConfigSource {
|
|
|
50
99
|
/** Explicit config file; when set it is the only candidate. */
|
|
51
100
|
configPath?: string | undefined;
|
|
52
101
|
}
|
|
53
|
-
/**
|
|
54
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Resolve the first config candidate that exists, in `configCandidates`'
|
|
104
|
+
* precedence. Throws, naming every candidate it looked for, when none does -
|
|
105
|
+
* the same message `loadConfig` has always raised on this path.
|
|
106
|
+
*
|
|
107
|
+
* Exported so `blogwright <plugin> init` (`plugin-commands.ts`) writes its
|
|
108
|
+
* spliced block into exactly the file `loadConfig` would read, rather than
|
|
109
|
+
* re-deriving the candidate list a second time.
|
|
110
|
+
*/
|
|
111
|
+
export declare function resolveConfigPath(fs: FileSystem, source: ConfigSource): Promise<string>;
|
|
112
|
+
/**
|
|
113
|
+
* Load and parse the first config candidate that exists, returning BOTH
|
|
114
|
+
* halves `parseConfigDocument` produces: the validated `config` every
|
|
115
|
+
* built-in command reads, and the `raw` document the dispatch path reads a
|
|
116
|
+
* plugin's own block out of. The candidate list and its precedence are
|
|
117
|
+
* unchanged - only the return type widens, so `createContext` can keep the
|
|
118
|
+
* raw half on {@link OpsContext.configDocument} instead of the file having to
|
|
119
|
+
* be read and parsed a second time at dispatch. Exported for tests.
|
|
120
|
+
*/
|
|
121
|
+
export declare function loadConfig(fs: FileSystem, source: ConfigSource): Promise<{
|
|
122
|
+
config: OpsConfig;
|
|
123
|
+
raw: Readonly<Record<string, unknown>>;
|
|
124
|
+
}>;
|
|
55
125
|
/**
|
|
56
126
|
* Build the runtime context: load config, resolve the account id, derive names, create
|
|
57
127
|
* clients, and load topology state from S3. The state bucket name is deterministic, which
|
|
58
|
-
* resolves the bootstrap chicken-and-egg. This is the composition root
|
|
128
|
+
* resolves the bootstrap chicken-and-egg. This is the composition root - the only place
|
|
59
129
|
* real adapters are constructed and wired.
|
|
60
130
|
*/
|
|
61
131
|
export declare function createContext(opts: ContextOptions): Promise<OpsContext>;
|
package/dist/context.js
CHANGED
|
@@ -1,53 +1,132 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { basename, join, resolve } from 'node:path';
|
|
2
3
|
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import { createClients, createNodeFileSystem, createNodeTerminal, deriveNames,
|
|
4
|
+
import { createClients, createNodeFileSystem, createNodeTerminal, deriveNames, findRepoRoot, parseConfigDocument, StateStore, } from 'blogwright-core';
|
|
4
5
|
import { createFetchPing } from './adapters/fetch-ping.js';
|
|
6
|
+
import { createNodeModuleLoader } from './adapters/node-module-loader.js';
|
|
5
7
|
import { createProcessVcs } from './adapters/process-vcs.js';
|
|
6
8
|
import { createLogger } from './logger.js';
|
|
7
9
|
/**
|
|
8
10
|
* The `app` tag value, by precedence: the explicit `config.app`, else the
|
|
9
|
-
* site's domain, else the repo directory name
|
|
11
|
+
* site's domain, else the repo directory name - always something a human can
|
|
10
12
|
* trace back to the project from a billing or resource listing.
|
|
11
13
|
*/
|
|
12
14
|
export function deriveAppTag(config, domain, repoRoot) {
|
|
13
15
|
return config.app ?? domain ?? basename(repoRoot);
|
|
14
16
|
}
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The directory holding the CLI's own `package.json` - `blogwright`'s package
|
|
19
|
+
* root. Located from `import.meta.url` the same way {@link OpsContext.agentDir}
|
|
20
|
+
* is (below): `packages/cli/package.json` declares an `exports` map with a
|
|
21
|
+
* `./rkey` entry and no `.` entry (the CLI is consumed through its `bin`, not
|
|
22
|
+
* imported), so neither `blogwright` nor `blogwright/package.json` can be
|
|
23
|
+
* resolved through the `ModuleLoader` port - see that port's doc comment.
|
|
24
|
+
* Self-location is therefore a composition-root concern, not something
|
|
25
|
+
* `discover` (`plugins.ts`) can derive itself.
|
|
26
|
+
*
|
|
27
|
+
* A standalone function, not folded into {@link createContext}: `blogwright
|
|
28
|
+
* plugin list` dispatches before a context exists and still needs this
|
|
29
|
+
* value, and it is the one supplier every discovery-running path (plugin
|
|
30
|
+
* dispatch, `blogwright --help`, the init wizard, `plugin list`) passes as
|
|
31
|
+
* `discover`'s second argument.
|
|
32
|
+
*/
|
|
33
|
+
export function cliPackageDir() {
|
|
34
|
+
// `new URL('..', …)` yields a trailing separator, unlike every other directory
|
|
35
|
+
// value in the CLI. join() and createRequire() tolerate it, but a caller
|
|
36
|
+
// writing `${cliPackageDir()}/x` would get a doubled separator - in the path
|
|
37
|
+
// and in any error message built from it. Normalise here so no caller has to
|
|
38
|
+
// remember. Four discovery-running paths (tasks 10, 11, 14, 17) consume this.
|
|
39
|
+
return resolve(fileURLToPath(new URL('..', import.meta.url)));
|
|
40
|
+
}
|
|
41
|
+
/** Narrow parsed JSON to an object before reading a field off it - no cast, no `any`. */
|
|
42
|
+
function isRecord(value) {
|
|
43
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The running CLI's own declared version - the value `blogwright plugin add`
|
|
47
|
+
* (`plugin-commands.ts`) pins into the install spec, so a plugin and the CLI
|
|
48
|
+
* that dispatches it can never silently drift apart across two developers'
|
|
49
|
+
* checkouts.
|
|
50
|
+
*
|
|
51
|
+
* Read HERE, at the composition root, for exactly the reason
|
|
52
|
+
* {@link cliPackageDir} is resolved here: `blogwright`'s own `exports` map has
|
|
53
|
+
* no `.` entry, so neither the package nor its `package.json` can be reached
|
|
54
|
+
* through the `ModuleLoader` port, and this module is one of the few the
|
|
55
|
+
* `no-restricted-imports` rule lets touch `node:fs` at all. `plugin-commands.ts`
|
|
56
|
+
* receives the resolved string as DATA and never walks the filesystem for it -
|
|
57
|
+
* the same division `agentDir` already makes.
|
|
58
|
+
*
|
|
59
|
+
* Read on demand rather than at module load: `blogwright plugin list` and every
|
|
60
|
+
* built-in command share this module and none of them needs the value, so only
|
|
61
|
+
* the one command that pins a version pays for the read.
|
|
62
|
+
*/
|
|
63
|
+
export async function cliVersion() {
|
|
64
|
+
const path = join(cliPackageDir(), 'package.json');
|
|
65
|
+
const parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
66
|
+
const version = isRecord(parsed) ? parsed.version : undefined;
|
|
67
|
+
if (typeof version !== 'string' || version.length === 0) {
|
|
68
|
+
throw new Error(`${path} declares no "version" - \`blogwright plugin add\` pins the installed plugin to ` +
|
|
69
|
+
"the running CLI's own version and has nothing to pin to");
|
|
70
|
+
}
|
|
71
|
+
return version;
|
|
72
|
+
}
|
|
73
|
+
/** Candidate config paths, in the precedence `loadConfig`/`resolveConfigPath` read them: an explicit `--config`, or `config/<env>.jsonc` then `ops.config.jsonc`. */
|
|
74
|
+
function configCandidates(source) {
|
|
75
|
+
return source.configPath
|
|
18
76
|
? [source.configPath]
|
|
19
77
|
: [
|
|
20
78
|
resolve(source.root, `config/${source.env}.jsonc`),
|
|
21
79
|
resolve(source.root, 'ops.config.jsonc'),
|
|
22
80
|
];
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Resolve the first config candidate that exists, in `configCandidates`'
|
|
84
|
+
* precedence. Throws, naming every candidate it looked for, when none does -
|
|
85
|
+
* the same message `loadConfig` has always raised on this path.
|
|
86
|
+
*
|
|
87
|
+
* Exported so `blogwright <plugin> init` (`plugin-commands.ts`) writes its
|
|
88
|
+
* spliced block into exactly the file `loadConfig` would read, rather than
|
|
89
|
+
* re-deriving the candidate list a second time.
|
|
90
|
+
*/
|
|
91
|
+
export async function resolveConfigPath(fs, source) {
|
|
92
|
+
const candidates = configCandidates(source);
|
|
23
93
|
for (const path of candidates) {
|
|
24
|
-
|
|
25
|
-
return
|
|
26
|
-
}
|
|
27
|
-
catch (err) {
|
|
28
|
-
if (!(err instanceof FileNotFoundError))
|
|
29
|
-
throw err;
|
|
30
|
-
}
|
|
94
|
+
if (await fs.exists(path))
|
|
95
|
+
return path;
|
|
31
96
|
}
|
|
32
|
-
throw new Error(`no config found for environment "${source.env}"
|
|
97
|
+
throw new Error(`no config found for environment "${source.env}" - looked for ${candidates.join(', ')}`);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Load and parse the first config candidate that exists, returning BOTH
|
|
101
|
+
* halves `parseConfigDocument` produces: the validated `config` every
|
|
102
|
+
* built-in command reads, and the `raw` document the dispatch path reads a
|
|
103
|
+
* plugin's own block out of. The candidate list and its precedence are
|
|
104
|
+
* unchanged - only the return type widens, so `createContext` can keep the
|
|
105
|
+
* raw half on {@link OpsContext.configDocument} instead of the file having to
|
|
106
|
+
* be read and parsed a second time at dispatch. Exported for tests.
|
|
107
|
+
*/
|
|
108
|
+
export async function loadConfig(fs, source) {
|
|
109
|
+
return parseConfigDocument(await fs.readText(await resolveConfigPath(fs, source)));
|
|
33
110
|
}
|
|
34
111
|
/**
|
|
35
112
|
* Build the runtime context: load config, resolve the account id, derive names, create
|
|
36
113
|
* clients, and load topology state from S3. The state bucket name is deterministic, which
|
|
37
|
-
* resolves the bootstrap chicken-and-egg. This is the composition root
|
|
114
|
+
* resolves the bootstrap chicken-and-egg. This is the composition root - the only place
|
|
38
115
|
* real adapters are constructed and wired.
|
|
39
116
|
*/
|
|
40
117
|
export async function createContext(opts) {
|
|
118
|
+
const fs = opts.ports?.fs ?? createNodeFileSystem();
|
|
41
119
|
const ports = {
|
|
42
|
-
fs
|
|
120
|
+
fs,
|
|
43
121
|
vcs: opts.ports?.vcs ?? createProcessVcs(),
|
|
44
122
|
terminal: opts.ports?.terminal ?? createNodeTerminal(),
|
|
45
123
|
ping: opts.ports?.ping ?? createFetchPing(),
|
|
124
|
+
loader: opts.ports?.loader ?? createNodeModuleLoader(),
|
|
46
125
|
};
|
|
47
126
|
const logger = createLogger(ports.terminal);
|
|
48
|
-
const agentDir =
|
|
127
|
+
const agentDir = join(cliPackageDir(), 'agent');
|
|
49
128
|
const root = await findRepoRoot(ports.fs);
|
|
50
|
-
const config = await loadConfig(ports.fs, {
|
|
129
|
+
const { config, raw: configDocument } = await loadConfig(ports.fs, {
|
|
51
130
|
env: opts.env,
|
|
52
131
|
root,
|
|
53
132
|
configPath: opts.configPath,
|
|
@@ -68,6 +147,7 @@ export async function createContext(opts) {
|
|
|
68
147
|
domain,
|
|
69
148
|
preview: opts.preview ?? false,
|
|
70
149
|
config,
|
|
150
|
+
configDocument,
|
|
71
151
|
names,
|
|
72
152
|
accountId,
|
|
73
153
|
clients,
|
package/dist/deploy.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ declare function manifestKey(hash: string): string;
|
|
|
19
19
|
/**
|
|
20
20
|
* Launch the builder MicroVM, retrying gateway errors (502/503/504) with a
|
|
21
21
|
* bounded backoff. The control plane can answer 502 for a short window right
|
|
22
|
-
* after the builder image was updated (fresh agent hash)
|
|
22
|
+
* after the builder image was updated (fresh agent hash) - an
|
|
23
23
|
* eventual-consistency gap that would otherwise fail every consumer's first
|
|
24
24
|
* deploy after a blogwright upgrade. Retrying is safe: the input's client
|
|
25
25
|
* token makes the launch idempotent. Exported for tests.
|
|
@@ -29,7 +29,7 @@ export declare function runMicrovmWithRetry(ctx: OpsContext, input: RunMicrovmIn
|
|
|
29
29
|
* Resolve the MicroVM build log group from bootstrapped state rather than re-deriving it.
|
|
30
30
|
* The image and the build-role IAM policy bake this name in at bootstrap, so if the derived
|
|
31
31
|
* name (`ctx.names.microvmLogGroup`) is renamed in code later, a deploy must still target
|
|
32
|
-
* the group the running stack actually logs to
|
|
32
|
+
* the group the running stack actually logs to - otherwise the VM's logs land in (or are
|
|
33
33
|
* denied to) a different group and `pollBuild` waits on an empty one.
|
|
34
34
|
*/
|
|
35
35
|
export declare function microvmLogGroup(ctx: OpsContext): string;
|
package/dist/deploy.js
CHANGED
|
@@ -7,8 +7,8 @@ function manifestKey(hash) {
|
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
9
|
* Nudge the MicroVM's endpoint to wake its event loop. A Firecracker-resumed process
|
|
10
|
-
* can sit idle with its poll timer pending until some I/O arrives; a connection here
|
|
11
|
-
* even one the agent's HTTP/1 server can't fully parse
|
|
10
|
+
* can sit idle with its poll timer pending until some I/O arrives; a connection here -
|
|
11
|
+
* even one the agent's HTTP/1 server can't fully parse - wakes the loop so the timer
|
|
12
12
|
* fires and the build starts. The wake-up, not the response, is the point: a missing
|
|
13
13
|
* endpoint or token means nothing to nudge, and a rejecting ping never fails the poll.
|
|
14
14
|
*/
|
|
@@ -26,7 +26,7 @@ function isGatewayError(err) {
|
|
|
26
26
|
/**
|
|
27
27
|
* Launch the builder MicroVM, retrying gateway errors (502/503/504) with a
|
|
28
28
|
* bounded backoff. The control plane can answer 502 for a short window right
|
|
29
|
-
* after the builder image was updated (fresh agent hash)
|
|
29
|
+
* after the builder image was updated (fresh agent hash) - an
|
|
30
30
|
* eventual-consistency gap that would otherwise fail every consumer's first
|
|
31
31
|
* deploy after a blogwright upgrade. Retrying is safe: the input's client
|
|
32
32
|
* token makes the launch idempotent. Exported for tests.
|
|
@@ -40,7 +40,7 @@ export async function runMicrovmWithRetry(ctx, input, delaysMs = RUN_RETRY_DELAY
|
|
|
40
40
|
const delay = delaysMs[attempt];
|
|
41
41
|
if (!isGatewayError(err) || delay === undefined)
|
|
42
42
|
throw err;
|
|
43
|
-
ctx.logger.warn(`MicroVM launch returned HTTP ${err.statusCode}
|
|
43
|
+
ctx.logger.warn(`MicroVM launch returned HTTP ${err.statusCode} - ` +
|
|
44
44
|
`retrying in ${Math.round(delay / 1000)}s (a just-updated builder image can lag)`);
|
|
45
45
|
await sleep(delay);
|
|
46
46
|
}
|
|
@@ -50,7 +50,7 @@ export async function runMicrovmWithRetry(ctx, input, delaysMs = RUN_RETRY_DELAY
|
|
|
50
50
|
* Resolve the MicroVM build log group from bootstrapped state rather than re-deriving it.
|
|
51
51
|
* The image and the build-role IAM policy bake this name in at bootstrap, so if the derived
|
|
52
52
|
* name (`ctx.names.microvmLogGroup`) is renamed in code later, a deploy must still target
|
|
53
|
-
* the group the running stack actually logs to
|
|
53
|
+
* the group the running stack actually logs to - otherwise the VM's logs land in (or are
|
|
54
54
|
* denied to) a different group and `pollBuild` waits on an empty one.
|
|
55
55
|
*/
|
|
56
56
|
export function microvmLogGroup(ctx) {
|
|
@@ -74,7 +74,7 @@ export function microvmLogGroup(ctx) {
|
|
|
74
74
|
*/
|
|
75
75
|
export async function pollBuild(ctx, hash, startTime, endpoint, token) {
|
|
76
76
|
const seen = new Set();
|
|
77
|
-
// Anchored at VM launch (startTime), like the VM's own maximumDuration
|
|
77
|
+
// Anchored at VM launch (startTime), like the VM's own maximumDuration -
|
|
78
78
|
// anchoring at poll start would keep polling a VM that is already dead for
|
|
79
79
|
// however long the RUNNING-wait consumed. One grace minute for log delivery.
|
|
80
80
|
const deadline = startTime + ctx.config.microvm.maxDurationSeconds * 1000 + 60_000;
|
|
@@ -109,7 +109,7 @@ export async function pollBuild(ctx, hash, startTime, endpoint, token) {
|
|
|
109
109
|
return result;
|
|
110
110
|
// Log delivery can lag or drop (e.g. the VM logging to a group the deploy isn't tailing).
|
|
111
111
|
// The agent writes build/changed/<hash>.json as its final step, so treat that artifact as
|
|
112
|
-
// an authoritative completion signal too
|
|
112
|
+
// an authoritative completion signal too - a successful build then can't hang until the
|
|
113
113
|
// deadline just because its logs never reached CloudWatch. runBuild clears any stale copy
|
|
114
114
|
// before launch, so its presence means *this* build finished.
|
|
115
115
|
if (await ctx.clients.s3.objectExists(ctx.names.bucket, `build/changed/${hash}.json`)) {
|
|
@@ -147,7 +147,7 @@ export async function runBuild(ctx, opts) {
|
|
|
147
147
|
ctx.logger.step(`running builder MicroVM for ${opts.hash}`);
|
|
148
148
|
// Launch first: if runMicrovm throws, no pending.json is left behind (a leaked job
|
|
149
149
|
// could otherwise be picked up during a later image bake and poison the snapshot).
|
|
150
|
-
// A MicroVM is ephemeral compute, so the client token is unique per launch
|
|
150
|
+
// A MicroVM is ephemeral compute, so the client token is unique per launch - keying it
|
|
151
151
|
// on the hash would make a re-deploy (or workflow re-run) of the same source idempotently
|
|
152
152
|
// return the ALREADY-TERMINATED original VM instead of launching a fresh one. Generated
|
|
153
153
|
// once here so a network retry of this single call still dedupes.
|
|
@@ -195,7 +195,7 @@ export async function runBuild(ctx, opts) {
|
|
|
195
195
|
finally {
|
|
196
196
|
// The pending-job cleanup must survive a terminate failure: a leaked
|
|
197
197
|
// pending.json is the poison-the-next-image-bake hazard the launch
|
|
198
|
-
// ordering above exists to avoid. A failed terminate is only logged
|
|
198
|
+
// ordering above exists to avoid. A failed terminate is only logged -
|
|
199
199
|
// the VM self-terminates at maxDuration, and the build outcome (already
|
|
200
200
|
// determined) must not be masked by a cleanup error.
|
|
201
201
|
await ctx.clients.microvms.terminateMicrovm(run.microvmId).catch((err) => {
|
|
@@ -251,17 +251,17 @@ export async function invalidateChanged(ctx, hash) {
|
|
|
251
251
|
}
|
|
252
252
|
}
|
|
253
253
|
if (!paths) {
|
|
254
|
-
ctx.logger.warn('no changed-paths manifest
|
|
254
|
+
ctx.logger.warn('no changed-paths manifest - invalidating everything (/*)');
|
|
255
255
|
await invalidateCloudFront(ctx, ['/*']);
|
|
256
256
|
return { mode: 'all', count: 0 };
|
|
257
257
|
}
|
|
258
258
|
let summary;
|
|
259
259
|
if (paths.length === 0) {
|
|
260
|
-
ctx.logger.ok('no content changed
|
|
260
|
+
ctx.logger.ok('no content changed - skipping CloudFront invalidation');
|
|
261
261
|
summary = { mode: 'none', count: 0 };
|
|
262
262
|
}
|
|
263
263
|
else if (paths.length > ctx.config.invalidationMaxPaths) {
|
|
264
|
-
ctx.logger.step(`${paths.length} paths changed (> cap)
|
|
264
|
+
ctx.logger.step(`${paths.length} paths changed (> cap) - invalidating everything (/*)`);
|
|
265
265
|
await invalidateCloudFront(ctx, ['/*']);
|
|
266
266
|
summary = { mode: 'all', count: paths.length };
|
|
267
267
|
}
|
package/dist/graph.d.ts
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
import type { ResourceNode, ResourceOutputs } from 'blogwright-core';
|
|
2
|
+
/**
|
|
3
|
+
* The structural minimum the engine below (`topoSort`, `applyGraph`,
|
|
4
|
+
* `destroyGraph`) actually reads off a node's context: a logger it calls
|
|
5
|
+
* `step`/`ok`/`warn` on, a way to persist state, and the state's resources
|
|
6
|
+
* map (`destroyGraph` deletes an entry from it). Exported so a caller
|
|
7
|
+
* running this engine over a different context - `OpsContext` (`context.ts`)
|
|
8
|
+
* and core's `PluginContext` (`blogwright-core`) both do already - knows
|
|
9
|
+
* exactly what that context must supply, without this module depending on
|
|
10
|
+
* either one. Neither `OpsContext` nor `PluginContext` is named here on
|
|
11
|
+
* purpose: this is the structural minimum both happen to satisfy, not a
|
|
12
|
+
* fixed supertype of them (see the doc comment on core's `ResourceNode` for
|
|
13
|
+
* why no such supertype is worth naming).
|
|
14
|
+
*/
|
|
15
|
+
export interface GraphContext {
|
|
16
|
+
logger: {
|
|
17
|
+
step(msg: string): void;
|
|
18
|
+
ok(msg: string): void;
|
|
19
|
+
warn(msg: string): void;
|
|
20
|
+
};
|
|
21
|
+
state: {
|
|
22
|
+
resources: Record<string, ResourceOutputs>;
|
|
23
|
+
};
|
|
24
|
+
save(): Promise<void>;
|
|
14
25
|
}
|
|
15
26
|
/** Topologically order nodes so dependencies come before dependents (Kahn's algorithm). */
|
|
16
|
-
export declare function topoSort(nodes: ResourceNode[]): ResourceNode[];
|
|
27
|
+
export declare function topoSort<Ctx>(nodes: ResourceNode<Ctx>[]): ResourceNode<Ctx>[];
|
|
17
28
|
/** Reconcile the graph in dependency order (create missing, update existing). */
|
|
18
|
-
export declare function applyGraph(nodes: ResourceNode[], ctx:
|
|
29
|
+
export declare function applyGraph<Ctx extends GraphContext>(nodes: ResourceNode<Ctx>[], ctx: Ctx): Promise<void>;
|
|
19
30
|
/** Tear down the graph in reverse dependency order. */
|
|
20
|
-
export declare function destroyGraph(nodes: ResourceNode[], ctx:
|
|
31
|
+
export declare function destroyGraph<Ctx extends GraphContext>(nodes: ResourceNode<Ctx>[], ctx: Ctx): Promise<void>;
|
package/dist/graph.js
CHANGED
|
@@ -77,7 +77,7 @@ export async function destroyGraph(nodes, ctx) {
|
|
|
77
77
|
await node.delete(ctx);
|
|
78
78
|
delete ctx.state.resources[node.id];
|
|
79
79
|
// The state lives in the bucket that is itself being deleted, so persisting it may
|
|
80
|
-
// fail (NoSuchBucket) once the bucket node is gone
|
|
80
|
+
// fail (NoSuchBucket) once the bucket node is gone - never let that abort teardown.
|
|
81
81
|
await ctx.save().catch(() => undefined);
|
|
82
82
|
ctx.logger.ok(`deleted ${node.title}`);
|
|
83
83
|
}
|
package/dist/init.d.ts
CHANGED
|
@@ -1,4 +1,38 @@
|
|
|
1
|
-
import { type FileSystem, type Terminal } from 'blogwright-core';
|
|
1
|
+
import { type FileSystem, type Plugin, type Terminal } from 'blogwright-core';
|
|
2
2
|
import type { Logger } from './logger.js';
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* One question, and how to ask it: shown text, a prefilled default, whether
|
|
5
|
+
* an empty answer must re-prompt, and an optional validator. Exported - and
|
|
6
|
+
* `ask` below along with it - because `plugin-commands.ts`'s `io.ask` (the
|
|
7
|
+
* surface an `init?(io)` contributor asks its own questions through) reuses
|
|
8
|
+
* this exact prompt/validate/retry loop rather than writing a second one; no
|
|
9
|
+
* plugin path may reach `node:readline` itself. Structurally the same shape
|
|
10
|
+
* as core's `PluginQuestion` (`blogwright-core`'s `plugin.ts`), so a
|
|
11
|
+
* contributor's question passes straight through with no conversion.
|
|
12
|
+
*/
|
|
13
|
+
export interface Question {
|
|
14
|
+
prompt: string;
|
|
15
|
+
defaultValue?: string | undefined;
|
|
16
|
+
required?: boolean | undefined;
|
|
17
|
+
validate?: ((answer: string) => string | undefined) | undefined;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Ask `q.prompt` over `terminal`, retrying up to `MAX_ATTEMPTS` times on a
|
|
21
|
+
* required-but-empty answer or a `validate` failure, and resolving with
|
|
22
|
+
* `undefined` for an unanswered optional question. Throws once every attempt
|
|
23
|
+
* is spent. The one prompt/validate/retry loop every wizard-shaped question -
|
|
24
|
+
* `blogwright init`'s own four and any plugin's `init?(io)` contributor -
|
|
25
|
+
* asks through.
|
|
26
|
+
*/
|
|
27
|
+
export declare function ask(terminal: Terminal, logger: Logger, q: Question): Promise<string | undefined>;
|
|
28
|
+
/**
|
|
29
|
+
* Run the wizard. Returns a process exit code; never throws for expected
|
|
30
|
+
* refusals (non-interactive, an existing config file). `plugins` is every
|
|
31
|
+
* plugin the caller has ALREADY discovered (`cli.ts`, over
|
|
32
|
+
* `DiscoveryPortsFactory`) - this function asks their questions and writes
|
|
33
|
+
* their blocks but never runs discovery itself. A plugin's `init(io)`
|
|
34
|
+
* contributor throwing propagates unchanged, rejecting this call before the
|
|
35
|
+
* single `fs.writeText` below ever runs, so the config file stays exactly
|
|
36
|
+
* what it was - absent, on this path - rather than a partial write.
|
|
37
|
+
*/
|
|
38
|
+
export declare function initSite(fs: FileSystem, terminal: Terminal, logger: Logger, plugins: readonly Plugin[], root?: string): Promise<number>;
|