blogwright 0.4.0-beta.2 → 0.4.0-beta.4

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.
@@ -1,3 +1,3 @@
1
1
  {
2
- "hash": "b8c4ae39e46a"
2
+ "hash": "0163ca230569"
3
3
  }
@@ -3,7 +3,7 @@ import type { OpsContext } from './context.js';
3
3
  import { type GraphContext } from './graph.js';
4
4
  import { type StatusEntry } from './render.js';
5
5
  /** Create the full infrastructure graph. */
6
- export declare function bootstrap(ctx: OpsContext): Promise<void>;
6
+ export declare function bootstrap(ctx: OpsContext, nodes?: ResourceNode<OpsContext>[]): Promise<void>;
7
7
  /**
8
8
  * Refuse to destroy the site while any plugin's own state object still
9
9
  * exists in the bucket - §State → Scoped state stores' "`blogwright
@@ -22,8 +22,8 @@ export declare function bootstrap(ctx: OpsContext): Promise<void>;
22
22
  *
23
23
  * Reads the bucket, not the plugin registry, so the refusal holds even for
24
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
25
+ * themselves, not `createContext`, sharing the listing with the post-bootstrap
26
+ * warning while plugin discovery stays lazy - and ahead of
27
27
  * `clearRunningMicrovms`/`destroyGraph` in each, so a refusal has zero side
28
28
  * effects: nothing is terminated, nothing is deleted.
29
29
  *
package/dist/commands.js CHANGED
@@ -49,14 +49,15 @@ function siteBaseUrl(ctx) {
49
49
  return typeof cf === 'string' ? `https://${cf}` : undefined;
50
50
  }
51
51
  /** Create the full infrastructure graph. */
52
- export async function bootstrap(ctx) {
52
+ export async function bootstrap(ctx, nodes = buildNodes(ctx)) {
53
53
  ctx.logger.info(colors.bold(`Bootstrapping "${ctx.env}" (bucket ${ctx.names.bucket})`));
54
54
  // The state bucket must exist before anything else can persist state.
55
- await applyGraph(buildNodes(ctx), ctx);
55
+ await applyGraph(nodes, ctx);
56
56
  ctx.logger.ok(`bootstrap complete for "${ctx.env}"`);
57
57
  const domain = ctx.state.resources['cloudfront-distribution']?.domainName;
58
58
  if (typeof domain === 'string')
59
59
  ctx.logger.info(`Site will be served at https://${domain}`);
60
+ await warnAboutScopedState(ctx);
60
61
  }
61
62
  /**
62
63
  * The prefix every state object - scoped or not - is filed under
@@ -85,6 +86,27 @@ function scopedStateScopes(env, objects) {
85
86
  .map((key) => key.slice(prefix.length, -'.json'.length));
86
87
  return [...new Set(scopes)].sort();
87
88
  }
89
+ /** Listing is strict; each caller decides whether a failure can be recovered. */
90
+ async function listScopedStateScopes(ctx) {
91
+ const objects = await ctx.clients.s3.listObjects(ctx.names.bucket, STATE_PREFIX);
92
+ return scopedStateScopes(ctx.env, objects);
93
+ }
94
+ /** A never-bootstrapped plugin has no scoped key, so cannot produce a warning. */
95
+ async function warnAboutScopedState(ctx) {
96
+ let scopes;
97
+ try {
98
+ scopes = await listScopedStateScopes(ctx);
99
+ }
100
+ catch (err) {
101
+ ctx.logger.warn(`could not check plugin state for "${ctx.env}" at s3://${ctx.names.bucket}/${STATE_PREFIX} ` +
102
+ `(bootstrap succeeded): ${String(err)}`);
103
+ return;
104
+ }
105
+ for (const scope of scopes) {
106
+ ctx.logger.warn(`plugin state for "${scope}" still exists in "${ctx.env}"; ` +
107
+ `run \`blogwright ${scope} bootstrap ${ctx.env}\` to reconcile its resources`);
108
+ }
109
+ }
88
110
  /**
89
111
  * Refuse to destroy the site while any plugin's own state object still
90
112
  * exists in the bucket - §State → Scoped state stores' "`blogwright
@@ -103,8 +125,8 @@ function scopedStateScopes(env, objects) {
103
125
  *
104
126
  * Reads the bucket, not the plugin registry, so the refusal holds even for
105
127
  * 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
128
+ * themselves, not `createContext`, sharing the listing with the post-bootstrap
129
+ * warning while plugin discovery stays lazy - and ahead of
108
130
  * `clearRunningMicrovms`/`destroyGraph` in each, so a refusal has zero side
109
131
  * effects: nothing is terminated, nothing is deleted.
110
132
  *
@@ -134,9 +156,9 @@ function scopedStateScopes(env, objects) {
134
156
  * `destroy()` run over the entire production graph.
135
157
  */
136
158
  export async function assertNoScopedState(ctx) {
137
- let objects;
159
+ let scopes;
138
160
  try {
139
- objects = await ctx.clients.s3.listObjects(ctx.names.bucket, STATE_PREFIX);
161
+ scopes = await listScopedStateScopes(ctx);
140
162
  }
141
163
  catch (err) {
142
164
  // No bucket, no state objects - see this function's doc comment. Matched
@@ -152,7 +174,6 @@ export async function assertNoScopedState(ctx) {
152
174
  return;
153
175
  throw err;
154
176
  }
155
- const scopes = scopedStateScopes(ctx.env, objects);
156
177
  if (scopes.length === 0)
157
178
  return;
158
179
  // The remedy MUST name the environment. `runPlugin` resolves a plugin
package/dist/nodes.js CHANGED
@@ -788,7 +788,13 @@ const GITHUB_OIDC_THUMBPRINT = '6938fd4d98bab03faadb97b34396831e3780aea1';
788
788
  /**
789
789
  * IAM role a GitHub Actions workflow assumes via OIDC - to deploy/destroy previews
790
790
  * (preview stack, any ref) or to deploy production (main branch only, plus CloudFront
791
- * invalidation and read access to the PDS credentials secret).
791
+ * invalidation).
792
+ *
793
+ * The role carries the site's own grants and nothing else. A plugin needing the deploy
794
+ * role to reach one of *its* resources attaches its own **named** inline policy to this
795
+ * same role from its own graph, reconciled by its own `<plugin> bootstrap` - so this
796
+ * document never names a plugin's resource, and `applyOidcRole` rewriting it wholesale
797
+ * on every `blogwright bootstrap` cannot disturb the plugin's.
792
798
  */
793
799
  function githubOidcRoleNode(preview) {
794
800
  const roleName = (ctx) => ctx.names.githubRole;
@@ -879,33 +885,6 @@ export function oidcRolePolicyStatements(ctx) {
879
885
  Action: ['cloudfront:CreateInvalidation'],
880
886
  Resource: String(output(ctx, 'cloudfront-distribution').arn),
881
887
  });
882
- if (ctx.config.pds) {
883
- // The post-deploy PDS sync reads the OAuth secret and writes it back:
884
- // refresh tokens are single-use, so every sync persists the rotated
885
- // session (PutSecretValue via the upsert helper, which tries CreateSecret
886
- // first when the secret is missing).
887
- //
888
- // The `??` default duplicates the pds plugin's own `resolvePdsSecretName`
889
- // deliberately, and must NOT be replaced by an import of it: this
890
- // resource graph carries no plugin knowledge, which is the whole point of
891
- // the plugin owning its own node (`packages/pds/src/nodes.ts`). Core
892
- // stopped defaulting `pds.secretName`, so without this default the
893
- // template literal would happily interpolate `undefined` into a live IAM
894
- // policy - `applyOidcRole` rewrites this whole document on every
895
- // `blogwright bootstrap`, so a wrong ARN here is a broken permission and
896
- // not a test failure. Task 59 deletes this statement together with the
897
- // `ctx.config.pds` branch around it; the duplication lives only until
898
- // then.
899
- statements.push({
900
- Effect: 'Allow',
901
- Action: [
902
- 'secretsmanager:GetSecretValue',
903
- 'secretsmanager:PutSecretValue',
904
- 'secretsmanager:CreateSecret',
905
- ],
906
- Resource: `arn:aws:secretsmanager:${ctx.config.region}:${ctx.accountId}:secret:${ctx.config.pds.secretName ?? `${ctx.config.siteName}/atproto`}-*`,
907
- });
908
- }
909
888
  }
910
889
  return statements;
911
890
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blogwright",
3
- "version": "0.4.0-beta.2",
3
+ "version": "0.4.0-beta.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "blogwright": "./dist/bin.js",
@@ -17,8 +17,8 @@
17
17
  }
18
18
  },
19
19
  "dependencies": {
20
- "blogwright-core": "0.4.0-beta.2",
21
- "blogwright-pds": "0.4.0-beta.2",
20
+ "blogwright-core": "0.4.0-beta.4",
21
+ "blogwright-pds": "0.4.0-beta.4",
22
22
  "fflate": "^0.8.3"
23
23
  },
24
24
  "devDependencies": {