blogwright-core 0.3.3 → 0.4.0-beta.1
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 +1 -1
- package/dist/adapters/node-terminal.d.ts +2 -2
- package/dist/adapters/node-terminal.js +1 -1
- package/dist/aws/cloudfront.d.ts +4 -4
- package/dist/aws/cloudfront.js +6 -6
- package/dist/aws/credentials.d.ts +1 -1
- package/dist/aws/credentials.js +1 -1
- package/dist/aws/endpoint.d.ts +22 -2
- package/dist/aws/endpoint.js +21 -2
- package/dist/aws/errors.js +1 -1
- package/dist/aws/iam.d.ts +1 -1
- package/dist/aws/iam.js +1 -1
- package/dist/aws/logs.d.ts +24 -4
- package/dist/aws/logs.js +19 -9
- package/dist/aws/microvms.d.ts +1 -1
- package/dist/aws/microvms.js +2 -2
- package/dist/aws/route53.d.ts +1 -1
- package/dist/aws/route53.js +2 -2
- package/dist/aws/signer.d.ts +2 -2
- package/dist/aws/signer.js +7 -6
- package/dist/aws/sts.d.ts +1 -1
- package/dist/aws/sts.js +1 -1
- package/dist/aws/xml.d.ts +2 -2
- package/dist/aws/xml.js +2 -2
- package/dist/clients.d.ts +11 -1
- package/dist/clients.js +3 -1
- package/dist/colors.js +1 -1
- package/dist/config.d.ts +47 -4
- package/dist/config.js +29 -28
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/plugin.d.ts +360 -0
- package/dist/plugin.js +85 -0
- package/dist/repo-root.d.ts +3 -0
- package/dist/repo-root.js +7 -1
- package/dist/state.d.ts +24 -3
- package/dist/state.js +38 -11
- package/dist/util.d.ts +23 -0
- package/dist/util.js +23 -0
- package/package.json +1 -1
package/dist/state.d.ts
CHANGED
|
@@ -9,14 +9,35 @@ export interface OpsState {
|
|
|
9
9
|
}
|
|
10
10
|
export declare function emptyState(env: string): OpsState;
|
|
11
11
|
/**
|
|
12
|
-
* S3-backed topology state
|
|
13
|
-
*
|
|
12
|
+
* S3-backed topology state: the single source of truth for what has been provisioned.
|
|
13
|
+
*
|
|
14
|
+
* An unscoped store (three constructor arguments) keys `s3://<bucket>/state/<env>.json`.
|
|
15
|
+
* This is on-disk identity for every environment that already exists - it must never move,
|
|
16
|
+
* or every existing site's state becomes unreadable.
|
|
17
|
+
*
|
|
18
|
+
* A store scoped to a plugin (the fourth argument) keys
|
|
19
|
+
* `s3://<bucket>/state/<env>.<plugin>.json` instead, so a plugin's resources are recorded
|
|
20
|
+
* separately from the site's: `blogwright <plugin> destroy` never touches, and never
|
|
21
|
+
* discards, the site's own record of what exists.
|
|
22
|
+
*
|
|
23
|
+
* Scoping changes the key, not the bucket - a scoped and an unscoped store for the same
|
|
24
|
+
* environment are constructed over the same `names.bucket` (see
|
|
25
|
+
* `packages/cli/src/context.ts:134`) and both objects live side by side under that
|
|
26
|
+
* bucket's `state/` prefix. That prefix is not itself a safety boundary: the site's own
|
|
27
|
+
* bucket node empties it wholesale before deleting the bucket
|
|
28
|
+
* (`deletePrefix(ctx.names.bucket, '')`, `packages/cli/src/nodes.ts:66`), which would take
|
|
29
|
+
* every `state/<env>.<plugin>.json` with it. Keeping the two records genuinely independent
|
|
30
|
+
* is therefore a CLI-level policy, not something this class enforces: `blogwright destroy`
|
|
31
|
+
* is expected to refuse while any `state/<env>.<plugin>.json` exists, naming that plugin's
|
|
32
|
+
* `blogwright <plugin> destroy --yes`, so plugins are always torn down before the site that
|
|
33
|
+
* hosts them. `StateStore` itself is a store, not a policy - it does not add that guard.
|
|
14
34
|
*/
|
|
15
35
|
export declare class StateStore {
|
|
16
36
|
private readonly s3;
|
|
17
37
|
private readonly bucket;
|
|
18
38
|
private readonly env;
|
|
19
|
-
|
|
39
|
+
private readonly key;
|
|
40
|
+
constructor(s3: S3Client, bucket: string, env: string, scope?: string);
|
|
20
41
|
load(): Promise<OpsState>;
|
|
21
42
|
save(state: OpsState): Promise<void>;
|
|
22
43
|
delete(): Promise<void>;
|
package/dist/state.js
CHANGED
|
@@ -1,27 +1,54 @@
|
|
|
1
1
|
export function emptyState(env) {
|
|
2
2
|
return { version: 1, env, updatedAt: undefined, resources: {} };
|
|
3
3
|
}
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
/** Namespace shape a plugin manifest declares - reused here so a scope can't smuggle a `/` or `..` into the key. */
|
|
5
|
+
const SCOPE_PATTERN = /^[a-z0-9-]+$/;
|
|
6
|
+
function stateKey(env, scope) {
|
|
7
|
+
return scope === undefined ? `state/${env}.json` : `state/${env}.${scope}.json`;
|
|
6
8
|
}
|
|
7
9
|
/**
|
|
8
|
-
* S3-backed topology state
|
|
9
|
-
*
|
|
10
|
+
* S3-backed topology state: the single source of truth for what has been provisioned.
|
|
11
|
+
*
|
|
12
|
+
* An unscoped store (three constructor arguments) keys `s3://<bucket>/state/<env>.json`.
|
|
13
|
+
* This is on-disk identity for every environment that already exists - it must never move,
|
|
14
|
+
* or every existing site's state becomes unreadable.
|
|
15
|
+
*
|
|
16
|
+
* A store scoped to a plugin (the fourth argument) keys
|
|
17
|
+
* `s3://<bucket>/state/<env>.<plugin>.json` instead, so a plugin's resources are recorded
|
|
18
|
+
* separately from the site's: `blogwright <plugin> destroy` never touches, and never
|
|
19
|
+
* discards, the site's own record of what exists.
|
|
20
|
+
*
|
|
21
|
+
* Scoping changes the key, not the bucket - a scoped and an unscoped store for the same
|
|
22
|
+
* environment are constructed over the same `names.bucket` (see
|
|
23
|
+
* `packages/cli/src/context.ts:134`) and both objects live side by side under that
|
|
24
|
+
* bucket's `state/` prefix. That prefix is not itself a safety boundary: the site's own
|
|
25
|
+
* bucket node empties it wholesale before deleting the bucket
|
|
26
|
+
* (`deletePrefix(ctx.names.bucket, '')`, `packages/cli/src/nodes.ts:66`), which would take
|
|
27
|
+
* every `state/<env>.<plugin>.json` with it. Keeping the two records genuinely independent
|
|
28
|
+
* is therefore a CLI-level policy, not something this class enforces: `blogwright destroy`
|
|
29
|
+
* is expected to refuse while any `state/<env>.<plugin>.json` exists, naming that plugin's
|
|
30
|
+
* `blogwright <plugin> destroy --yes`, so plugins are always torn down before the site that
|
|
31
|
+
* hosts them. `StateStore` itself is a store, not a policy - it does not add that guard.
|
|
10
32
|
*/
|
|
11
33
|
export class StateStore {
|
|
12
34
|
s3;
|
|
13
35
|
bucket;
|
|
14
36
|
env;
|
|
15
|
-
|
|
37
|
+
key;
|
|
38
|
+
constructor(s3, bucket, env, scope) {
|
|
16
39
|
this.s3 = s3;
|
|
17
40
|
this.bucket = bucket;
|
|
18
41
|
this.env = env;
|
|
42
|
+
if (scope !== undefined && !SCOPE_PATTERN.test(scope)) {
|
|
43
|
+
throw new Error(`state store scope must be lowercase alphanumeric/dashes, got "${scope}" for s3://${bucket}`);
|
|
44
|
+
}
|
|
45
|
+
this.key = stateKey(env, scope);
|
|
19
46
|
}
|
|
20
47
|
async load() {
|
|
21
48
|
// getObjectText returns undefined only when the object/bucket does not exist (a fresh
|
|
22
|
-
// environment). A present-but-corrupt document must NOT be silently treated as empty
|
|
23
|
-
// that would cause duplicate-resource creation
|
|
24
|
-
const text = await this.s3.getObjectText(this.bucket,
|
|
49
|
+
// environment). A present-but-corrupt document must NOT be silently treated as empty -
|
|
50
|
+
// that would cause duplicate-resource creation - so let a parse error surface.
|
|
51
|
+
const text = await this.s3.getObjectText(this.bucket, this.key);
|
|
25
52
|
// Strictly undefined: a present-but-empty (zero-byte) state object is
|
|
26
53
|
// corruption, not a fresh environment, and must hit the guard below.
|
|
27
54
|
if (text === undefined)
|
|
@@ -30,14 +57,14 @@ export class StateStore {
|
|
|
30
57
|
return JSON.parse(text);
|
|
31
58
|
}
|
|
32
59
|
catch (err) {
|
|
33
|
-
throw new Error(
|
|
60
|
+
throw new Error(`${this.key} in s3://${this.bucket} is not valid JSON - refusing to proceed with empty state`, { cause: err });
|
|
34
61
|
}
|
|
35
62
|
}
|
|
36
63
|
async save(state) {
|
|
37
64
|
state.updatedAt = new Date().toISOString();
|
|
38
|
-
await this.s3.putObject(this.bucket,
|
|
65
|
+
await this.s3.putObject(this.bucket, this.key, JSON.stringify(state, null, 2), 'application/json');
|
|
39
66
|
}
|
|
40
67
|
async delete() {
|
|
41
|
-
await this.s3.deleteObject(this.bucket,
|
|
68
|
+
await this.s3.deleteObject(this.bucket, this.key).catch(() => undefined);
|
|
42
69
|
}
|
|
43
70
|
}
|
package/dist/util.d.ts
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fixed timestamp every reproducible zip in this repo stamps its entries
|
|
3
|
+
* with, so identical input bytes always produce identical archive bytes.
|
|
4
|
+
*
|
|
5
|
+
* **Constructed from local parts on purpose, and the obvious `Date.UTC` form is
|
|
6
|
+
* a bug.** A zip's DOS timestamp is *local* time, and `fflate` reads it with
|
|
7
|
+
* `getFullYear()`/`getMonth()`/`getDate()`, so a `Date` fixed in UTC lands on a
|
|
8
|
+
* different local date in every zone. Two things follow, and this repo shipped
|
|
9
|
+
* both:
|
|
10
|
+
*
|
|
11
|
+
* - `new Date('1980-01-01T00:00:00Z')` is 1979-12-31 local anywhere west of
|
|
12
|
+
* Greenwich, and `fflate` throws `date not in range 1980-2099` outright -
|
|
13
|
+
* so `blogwright bootstrap`, `deploy` and `analytics bootstrap` all failed
|
|
14
|
+
* for most of the Americas while passing in CI, which runs `TZ=UTC`.
|
|
15
|
+
* - Even where it did not throw, the encoded timestamp differed by zone, so
|
|
16
|
+
* the archive was *not* reproducible - the property the constant exists to
|
|
17
|
+
* provide. The crash was hiding that.
|
|
18
|
+
*
|
|
19
|
+
* A local-constructed 1980-01-02 is 1980-01-02 in every zone by construction:
|
|
20
|
+
* in range everywhere, and byte-identical everywhere. The second of January
|
|
21
|
+
* rather than the first so that no offset can push it below the 1980 floor.
|
|
22
|
+
*/
|
|
23
|
+
export declare const REPRODUCIBLE_ZIP_MTIME: Date;
|
|
1
24
|
export declare function sleep(ms: number): Promise<void>;
|
|
2
25
|
export interface RetryOptions {
|
|
3
26
|
attempts?: number;
|
package/dist/util.js
CHANGED
|
@@ -1,4 +1,27 @@
|
|
|
1
1
|
import { isRetryable } from './aws/errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* The fixed timestamp every reproducible zip in this repo stamps its entries
|
|
4
|
+
* with, so identical input bytes always produce identical archive bytes.
|
|
5
|
+
*
|
|
6
|
+
* **Constructed from local parts on purpose, and the obvious `Date.UTC` form is
|
|
7
|
+
* a bug.** A zip's DOS timestamp is *local* time, and `fflate` reads it with
|
|
8
|
+
* `getFullYear()`/`getMonth()`/`getDate()`, so a `Date` fixed in UTC lands on a
|
|
9
|
+
* different local date in every zone. Two things follow, and this repo shipped
|
|
10
|
+
* both:
|
|
11
|
+
*
|
|
12
|
+
* - `new Date('1980-01-01T00:00:00Z')` is 1979-12-31 local anywhere west of
|
|
13
|
+
* Greenwich, and `fflate` throws `date not in range 1980-2099` outright -
|
|
14
|
+
* so `blogwright bootstrap`, `deploy` and `analytics bootstrap` all failed
|
|
15
|
+
* for most of the Americas while passing in CI, which runs `TZ=UTC`.
|
|
16
|
+
* - Even where it did not throw, the encoded timestamp differed by zone, so
|
|
17
|
+
* the archive was *not* reproducible - the property the constant exists to
|
|
18
|
+
* provide. The crash was hiding that.
|
|
19
|
+
*
|
|
20
|
+
* A local-constructed 1980-01-02 is 1980-01-02 in every zone by construction:
|
|
21
|
+
* in range everywhere, and byte-identical everywhere. The second of January
|
|
22
|
+
* rather than the first so that no offset can push it below the 1980 floor.
|
|
23
|
+
*/
|
|
24
|
+
export const REPRODUCIBLE_ZIP_MTIME = new Date(1980, 0, 2);
|
|
2
25
|
export function sleep(ms) {
|
|
3
26
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4
27
|
}
|