@rexezuge/tooling 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +297 -2
- package/dist/eslint.d.ts +120 -0
- package/dist/eslint.d.ts.map +1 -0
- package/dist/eslint.js +551 -0
- package/dist/eslint.js.map +1 -0
- package/dist/functions/pages-proxy.d.ts +113 -0
- package/dist/functions/pages-proxy.d.ts.map +1 -0
- package/dist/functions/pages-proxy.js +131 -0
- package/dist/functions/pages-proxy.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/scripts/backup/d1-target.d.ts +35 -0
- package/dist/scripts/backup/d1-target.d.ts.map +1 -0
- package/dist/scripts/backup/d1-target.js +31 -0
- package/dist/scripts/backup/d1-target.js.map +1 -0
- package/dist/scripts/backup/destination-config.d.ts +61 -0
- package/dist/scripts/backup/destination-config.d.ts.map +1 -0
- package/dist/scripts/backup/destination-config.js +57 -0
- package/dist/scripts/backup/destination-config.js.map +1 -0
- package/dist/scripts/backup/encrypt-backup.d.ts +35 -0
- package/dist/scripts/backup/encrypt-backup.d.ts.map +1 -0
- package/dist/scripts/backup/encrypt-backup.js +98 -0
- package/dist/scripts/backup/encrypt-backup.js.map +1 -0
- package/dist/scripts/backup/naming.d.ts +44 -0
- package/dist/scripts/backup/naming.d.ts.map +1 -0
- package/dist/scripts/backup/naming.js +58 -0
- package/dist/scripts/backup/naming.js.map +1 -0
- package/dist/scripts/check-god-files.d.ts +164 -0
- package/dist/scripts/check-god-files.d.ts.map +1 -0
- package/dist/scripts/check-god-files.js +271 -0
- package/dist/scripts/check-god-files.js.map +1 -0
- package/dist/scripts/ensure-spa-shell-stub.d.ts +3 -0
- package/dist/scripts/ensure-spa-shell-stub.d.ts.map +1 -0
- package/dist/scripts/ensure-spa-shell-stub.js +30 -0
- package/dist/scripts/ensure-spa-shell-stub.js.map +1 -0
- package/dist/scripts/init-secrets.d.ts +63 -0
- package/dist/scripts/init-secrets.d.ts.map +1 -0
- package/dist/scripts/init-secrets.js +240 -0
- package/dist/scripts/init-secrets.js.map +1 -0
- package/dist/scripts/lib/cli-args.d.ts +78 -0
- package/dist/scripts/lib/cli-args.d.ts.map +1 -0
- package/dist/scripts/lib/cli-args.js +116 -0
- package/dist/scripts/lib/cli-args.js.map +1 -0
- package/dist/scripts/lib/github-actions.d.ts +26 -0
- package/dist/scripts/lib/github-actions.d.ts.map +1 -0
- package/dist/scripts/lib/github-actions.js +38 -0
- package/dist/scripts/lib/github-actions.js.map +1 -0
- package/dist/scripts/lib/wrangler-table.d.ts +46 -0
- package/dist/scripts/lib/wrangler-table.d.ts.map +1 -0
- package/dist/scripts/lib/wrangler-table.js +99 -0
- package/dist/scripts/lib/wrangler-table.js.map +1 -0
- package/dist/scripts/migrations-lock.d.ts +3 -0
- package/dist/scripts/migrations-lock.d.ts.map +1 -0
- package/dist/scripts/migrations-lock.js +46 -0
- package/dist/scripts/migrations-lock.js.map +1 -0
- package/dist/scripts/prepare-wrangler-config.d.ts +3 -0
- package/dist/scripts/prepare-wrangler-config.d.ts.map +1 -0
- package/dist/scripts/prepare-wrangler-config.js +50 -0
- package/dist/scripts/prepare-wrangler-config.js.map +1 -0
- package/dist/scripts/spa-shell.d.ts +41 -0
- package/dist/scripts/spa-shell.d.ts.map +1 -0
- package/dist/scripts/spa-shell.js +155 -0
- package/dist/scripts/spa-shell.js.map +1 -0
- package/dist/scripts/validate-locales.d.ts +26 -0
- package/dist/scripts/validate-locales.d.ts.map +1 -0
- package/dist/scripts/validate-locales.js +350 -0
- package/dist/scripts/validate-locales.js.map +1 -0
- package/dist/scripts/verify-migrations.d.ts +62 -0
- package/dist/scripts/verify-migrations.d.ts.map +1 -0
- package/dist/scripts/verify-migrations.js +302 -0
- package/dist/scripts/verify-migrations.js.map +1 -0
- package/dist/scripts/verify-spa-shell.d.ts +3 -0
- package/dist/scripts/verify-spa-shell.d.ts.map +1 -0
- package/dist/scripts/verify-spa-shell.js +53 -0
- package/dist/scripts/verify-spa-shell.js.map +1 -0
- package/dist/scripts/wrangler-config/cli.d.ts +22 -0
- package/dist/scripts/wrangler-config/cli.d.ts.map +1 -0
- package/dist/scripts/wrangler-config/cli.js +51 -0
- package/dist/scripts/wrangler-config/cli.js.map +1 -0
- package/dist/scripts/wrangler-config/patches.d.ts +51 -0
- package/dist/scripts/wrangler-config/patches.d.ts.map +1 -0
- package/dist/scripts/wrangler-config/patches.js +140 -0
- package/dist/scripts/wrangler-config/patches.js.map +1 -0
- package/dist/scripts/wrangler-config/resources.d.ts +70 -0
- package/dist/scripts/wrangler-config/resources.d.ts.map +1 -0
- package/dist/scripts/wrangler-config/resources.js +290 -0
- package/dist/scripts/wrangler-config/resources.js.map +1 -0
- package/dist/scripts/wrangler-config/types.d.ts +103 -0
- package/dist/scripts/wrangler-config/types.d.ts.map +1 -0
- package/dist/scripts/wrangler-config/types.js +49 -0
- package/dist/scripts/wrangler-config/types.js.map +1 -0
- package/dist/test/integration-migrations.d.ts +167 -0
- package/dist/test/integration-migrations.d.ts.map +1 -0
- package/dist/test/integration-migrations.js +171 -0
- package/dist/test/integration-migrations.js.map +1 -0
- package/dist/test/mocks/cloudflare-workers.d.ts +106 -0
- package/dist/test/mocks/cloudflare-workers.d.ts.map +1 -0
- package/dist/test/mocks/cloudflare-workers.js +90 -0
- package/dist/test/mocks/cloudflare-workers.js.map +1 -0
- package/dist/vite.d.ts +117 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +125 -0
- package/dist/vite.js.map +1 -0
- package/dist/vitest-web.d.ts +73 -0
- package/dist/vitest-web.d.ts.map +1 -0
- package/dist/vitest-web.js +72 -0
- package/dist/vitest-web.js.map +1 -0
- package/dist/vitest.d.ts +92 -0
- package/dist/vitest.d.ts.map +1 -0
- package/dist/vitest.js +128 -0
- package/dist/vitest.js.map +1 -0
- package/package.json +58 -3
- package/src/eslint.test.ts +175 -0
- package/src/eslint.ts +640 -0
- package/src/functions/pages-proxy.test.ts +72 -0
- package/src/functions/pages-proxy.ts +187 -0
- package/src/github/actions/retry-step/action.yml +39 -0
- package/src/github/actions/setup-env/action.yml +20 -0
- package/src/github/dependabot.yml +30 -0
- package/src/github/workflows/backup-main.yml +46 -0
- package/src/github/workflows/continuous-deployment.yml +188 -0
- package/src/github/workflows/continuous-integration.yml +259 -0
- package/src/github/workflows/scheduled-version-update.yml +38 -0
- package/src/github/workflows/upstream-sync.yml +56 -0
- package/src/index.ts +42 -0
- package/src/scripts/backup/backup-rules.test.ts +105 -0
- package/src/scripts/backup/d1-target.ts +54 -0
- package/src/scripts/backup/destination-config.ts +92 -0
- package/src/scripts/backup/encrypt-backup.ts +107 -0
- package/src/scripts/backup/naming.ts +62 -0
- package/src/scripts/check-god-files.test.ts +131 -0
- package/src/scripts/check-god-files.ts +327 -0
- package/src/scripts/ensure-spa-shell-stub.ts +34 -0
- package/src/scripts/init-secrets.ts +265 -0
- package/src/scripts/lib/cli-args.ts +154 -0
- package/src/scripts/lib/github-actions.ts +41 -0
- package/src/scripts/lib/wrangler-table.ts +105 -0
- package/src/scripts/migrations-lock.ts +52 -0
- package/src/scripts/prepare-wrangler-config.ts +51 -0
- package/src/scripts/spa-shell.test.ts +91 -0
- package/src/scripts/spa-shell.ts +179 -0
- package/src/scripts/validate-locales.test.ts +89 -0
- package/src/scripts/validate-locales.ts +380 -0
- package/src/scripts/verify-migrations.test.ts +71 -0
- package/src/scripts/verify-migrations.ts +364 -0
- package/src/scripts/verify-spa-shell.ts +56 -0
- package/src/scripts/wrangler-config/cli.ts +51 -0
- package/src/scripts/wrangler-config/patches.ts +157 -0
- package/src/scripts/wrangler-config/resources.ts +330 -0
- package/src/scripts/wrangler-config/types.ts +113 -0
- package/src/test/integration-migrations.test.ts +169 -0
- package/src/test/integration-migrations.ts +267 -0
- package/src/test/mocks/cloudflare-workers.ts +115 -0
- package/src/vite.test.ts +83 -0
- package/src/vite.ts +202 -0
- package/src/vitest-web.ts +109 -0
- package/src/vitest.ts +185 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Create the Secrets Store entries declared in `wrangler.jsonc`, generating a fresh
|
|
4
|
+
* value for each one that does not exist yet.
|
|
5
|
+
*
|
|
6
|
+
* Provenance: Edge-Sonic's `scripts/deploy/init-secrets.ts` and Durable-DAV's
|
|
7
|
+
* `scripts/init-secrets.ts`, converged. Both record the same two defects that hid
|
|
8
|
+
* a broken deploy for a whole release, and both fixes are carried over.
|
|
9
|
+
*
|
|
10
|
+
* ### Why this script exists at all
|
|
11
|
+
*
|
|
12
|
+
* A `secrets_store_secrets` binding fails the deploy unless the named secret
|
|
13
|
+
* already exists inside the named store. Nothing creates it: Cloudflare will not
|
|
14
|
+
* generate a value, and the template cannot carry one. So this script is the only
|
|
15
|
+
* thing standing between a fresh account and a deploy that cannot succeed.
|
|
16
|
+
*
|
|
17
|
+
* ### The two defects that hid a broken deploy
|
|
18
|
+
*
|
|
19
|
+
* **The generator list was stale.** Durable-DAV's copy carried an allowlist of
|
|
20
|
+
* exact names from the repository it was inherited from, all deleted when the repo
|
|
21
|
+
* was renamed, so the very first iteration threw `Unknown secret` and never created
|
|
22
|
+
* anything. So the rule here is the secret's **name shape**, not a list of names:
|
|
23
|
+
* `*-encryption-key` gets a generated AES-GCM key and `*-signing-secret` gets 32
|
|
24
|
+
* random bytes. Adding a secret to the template is then a one-line change needing
|
|
25
|
+
* no matching edit here, which is the property the exact-name list did not have.
|
|
26
|
+
*
|
|
27
|
+
* **The throw was swallowed.** The original ended in `main().catch(console.error)`.
|
|
28
|
+
* `console.error` returns `undefined` and never sets an exit status, so the process
|
|
29
|
+
* exited **0** and CI marked the step green — the failure only surfaced one step
|
|
30
|
+
* later, as an unexplainable `wrangler deploy` failure. Hence: the whole plan is
|
|
31
|
+
* validated **before** any Cloudflare call, and a rejection reaches the runner as
|
|
32
|
+
* a non-zero exit with a workflow annotation.
|
|
33
|
+
*
|
|
34
|
+
* ### Create-if-absent is a correctness property, not laziness
|
|
35
|
+
*
|
|
36
|
+
* An existing secret is left untouched. A replication encryption key seals every
|
|
37
|
+
* remote target's credential; regenerating it on a redeploy would strand them all
|
|
38
|
+
* as undecryptable. Rotation has to be a deliberate act with a re-encrypt path,
|
|
39
|
+
* not a side effect of deploying.
|
|
40
|
+
*
|
|
41
|
+
* Requires a materialized `wrangler.jsonc` — run
|
|
42
|
+
* `scripts/prepare-wrangler-config.ts` first. Fails rather than skipping when the
|
|
43
|
+
* config is missing, because a step reporting "Secret initialization complete"
|
|
44
|
+
* having created nothing is worse than a named error.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import { execFileSync } from 'node:child_process';
|
|
48
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
49
|
+
import { parse } from 'jsonc-parser';
|
|
50
|
+
import { fail } from './lib/github-actions';
|
|
51
|
+
import { isEmptySecretsStoreListing, parseWranglerNames } from './lib/wrangler-table';
|
|
52
|
+
import { CONFIG_PATH, type WranglerConfig } from './wrangler-config/types';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The secret names the AES-GCM key generator produces, matched by **shape**.
|
|
56
|
+
*
|
|
57
|
+
* Keyed on the suffix because the alternative is the exact-name map that went
|
|
58
|
+
* stale. See the header: a template naming `*-encryption-key` is then a one-line
|
|
59
|
+
* change with no matching edit here, and a new secret breaks the deploy loudly
|
|
60
|
+
* instead of silently.
|
|
61
|
+
*/
|
|
62
|
+
const ENCRYPTION_KEY_SUFFIX = '-encryption-key';
|
|
63
|
+
|
|
64
|
+
/** The secret names the random signing secret generator produces. */
|
|
65
|
+
const SIGNING_SECRET_SUFFIX = '-signing-secret';
|
|
66
|
+
|
|
67
|
+
/** A 256-bit AES-GCM key, base64-encoded — the shape the worker's crypto layer accepts. */
|
|
68
|
+
async function generateAESGCMKey(): Promise<string> {
|
|
69
|
+
const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
|
|
70
|
+
const exported = await crypto.subtle.exportKey('raw', key);
|
|
71
|
+
// Chunked rather than spread: `String.fromCharCode(...bytes)` passes 32 arguments,
|
|
72
|
+
// which is fine today and is a stack overflow the day the key is longer.
|
|
73
|
+
let binary = '';
|
|
74
|
+
for (const byte of new Uint8Array(exported)) {
|
|
75
|
+
binary += String.fromCodePoint(byte);
|
|
76
|
+
}
|
|
77
|
+
return btoa(binary);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A 256-bit random signing secret for internal self-calls, hex-encoded. */
|
|
81
|
+
function generateSigningSecret(): Promise<string> {
|
|
82
|
+
let hex = '';
|
|
83
|
+
for (const byte of crypto.getRandomValues(new Uint8Array(32))) {
|
|
84
|
+
hex += byte.toString(16).padStart(2, '0');
|
|
85
|
+
}
|
|
86
|
+
return Promise.resolve(hex);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The generator for a secret name, chosen by the name's shape.
|
|
91
|
+
*
|
|
92
|
+
* A name matching neither is still refused rather than given a guess: the generator
|
|
93
|
+
* is not the only thing that has to be right, and an unusable value that looks
|
|
94
|
+
* provisioned is worse than a failure.
|
|
95
|
+
*/
|
|
96
|
+
function generatorFor(secretName: string): () => Promise<string> {
|
|
97
|
+
if (secretName.endsWith(ENCRYPTION_KEY_SUFFIX)) return generateAESGCMKey;
|
|
98
|
+
if (secretName.endsWith(SIGNING_SECRET_SUFFIX)) return generateSigningSecret;
|
|
99
|
+
throw new Error(
|
|
100
|
+
`Unknown secret: ${secretName}. Name it '*-encryption-key' for a generated 32-byte AES-GCM key, ` +
|
|
101
|
+
`or '*-signing-secret' for 32 random bytes, and teach this script the shape if neither fits.`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Whether this script can produce a value for `secretName`. */
|
|
106
|
+
export function canProvisionSecret(secretName: string): boolean {
|
|
107
|
+
try {
|
|
108
|
+
generatorFor(secretName);
|
|
109
|
+
return true;
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Runs a wrangler subcommand, optionally feeding `input` on stdin.
|
|
117
|
+
*
|
|
118
|
+
* Arguments are passed as an argv array rather than interpolated into a shell
|
|
119
|
+
* string, so a value containing shell metacharacters cannot alter the command.
|
|
120
|
+
*/
|
|
121
|
+
function wrangler(args: readonly string[], input?: string): string {
|
|
122
|
+
try {
|
|
123
|
+
// eslint-disable-next-line sonarjs/no-os-command-from-path
|
|
124
|
+
return execFileSync('pnpm', ['exec', 'wrangler', ...args], {
|
|
125
|
+
encoding: 'utf8',
|
|
126
|
+
stdio: [input === undefined ? 'ignore' : 'pipe', 'pipe', 'pipe'],
|
|
127
|
+
...(input !== undefined && { input }),
|
|
128
|
+
});
|
|
129
|
+
} catch (error: unknown) {
|
|
130
|
+
const maybeProcessError = error as { stdout?: string | Buffer; stderr?: string | Buffer; message?: string };
|
|
131
|
+
const stdout: string = maybeProcessError.stdout ? maybeProcessError.stdout.toString() : '';
|
|
132
|
+
const stderr: string = maybeProcessError.stderr ? maybeProcessError.stderr.toString() : '';
|
|
133
|
+
throw new Error(`Command failed: pnpm exec wrangler ${args.join(' ')}\n${stdout}${stderr || maybeProcessError.message || ''}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function parseWranglerConfig(): WranglerConfig {
|
|
138
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`${CONFIG_PATH} not found. Run scripts/prepare-wrangler-config.ts first — it is what creates the file this script reads.`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
// `parse` is error-tolerant and returns a partial object, so an empty result means
|
|
144
|
+
// malformed JSON rather than "no secrets declared". Treating that as an empty list
|
|
145
|
+
// would report success having created nothing.
|
|
146
|
+
const config = parse(readFileSync(CONFIG_PATH, 'utf8')) as WranglerConfig;
|
|
147
|
+
if (config.secrets_store_secrets === undefined && Object.keys(config).length === 0) {
|
|
148
|
+
throw new Error(`${CONFIG_PATH} is empty or not valid JSONC. Run scripts/prepare-wrangler-config.ts to regenerate it.`);
|
|
149
|
+
}
|
|
150
|
+
return config;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** One secret to create, as the plan names it. */
|
|
154
|
+
interface PlannedSecret {
|
|
155
|
+
storeId: string;
|
|
156
|
+
secretName: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Turn the config's bindings into a validated work list.
|
|
161
|
+
*
|
|
162
|
+
* Every entry is checked *before* the first Cloudflare call, so an
|
|
163
|
+
* unprovisionable secret fails the step in milliseconds instead of after some other
|
|
164
|
+
* secret has already been created.
|
|
165
|
+
*/
|
|
166
|
+
export function planSecrets(config: WranglerConfig): PlannedSecret[] {
|
|
167
|
+
const plan: PlannedSecret[] = [];
|
|
168
|
+
for (const [index, secret] of (config.secrets_store_secrets ?? []).entries()) {
|
|
169
|
+
const label = secret.binding ?? `index ${index}`;
|
|
170
|
+
const storeId = secret.store_id;
|
|
171
|
+
const secretName = secret.secret_name;
|
|
172
|
+
if (storeId === undefined || storeId === '' || secretName === undefined || secretName === '') {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`secrets_store_secrets[${index}] ("${label}") needs both store_id and secret_name; ` +
|
|
175
|
+
'store_id is normally filled in by scripts/prepare-wrangler-config.ts.',
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
if (!canProvisionSecret(secretName)) {
|
|
179
|
+
throw new Error(`No generator for secret "${secretName}" (binding "${label}").`);
|
|
180
|
+
}
|
|
181
|
+
plan.push({ storeId, secretName });
|
|
182
|
+
}
|
|
183
|
+
return plan;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* List the secret names present in a store.
|
|
188
|
+
*
|
|
189
|
+
* Returns `undefined` when the listing itself failed, which is deliberately
|
|
190
|
+
* distinct from an empty set: a transient CLI failure must not be read as "the
|
|
191
|
+
* secret is missing" and cause a duplicate insert over a live value. An empty store
|
|
192
|
+
* *is* a confirmed absence, so it maps to an empty set rather than `undefined`.
|
|
193
|
+
*
|
|
194
|
+
* The name is taken exactly. `output.includes(secretName)` is not a membership test:
|
|
195
|
+
* `my-replication-encryption-key-2` satisfies a search for
|
|
196
|
+
* `my-replication-encryption-key`, so a store holding only the suffixed entry would
|
|
197
|
+
* report the unsuffixed one as already present and it would never be created.
|
|
198
|
+
*/
|
|
199
|
+
function listSecretNames(storeId: string): Set<string> | undefined {
|
|
200
|
+
let output: string;
|
|
201
|
+
try {
|
|
202
|
+
output = wrangler(['secrets-store', 'secret', 'list', storeId, '--remote']);
|
|
203
|
+
} catch (error: unknown) {
|
|
204
|
+
if (isEmptySecretsStoreListing(error)) {
|
|
205
|
+
console.log(`Secrets Store ${storeId} is empty`);
|
|
206
|
+
return new Set();
|
|
207
|
+
}
|
|
208
|
+
console.warn(`Unable to list secrets in store ${storeId}:`, error instanceof Error ? error.message : 'unknown error');
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
return parseWranglerNames(output);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function createSecret(storeId: string, secretName: string, secretValue: string): void {
|
|
215
|
+
console.log(`Creating secret: ${secretName}`);
|
|
216
|
+
// The value goes in on stdin, never as an argv entry: an argv is visible to every
|
|
217
|
+
// process on the runner through `ps`, and the previous
|
|
218
|
+
// `echo "${value}" | wrangler … ${name}` also broke on `"`, `$`, backticks and
|
|
219
|
+
// newlines.
|
|
220
|
+
wrangler(['secrets-store', 'secret', 'create', storeId, '--name', secretName, '--scopes', 'workers', '--remote'], secretValue);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function main(): Promise<void> {
|
|
224
|
+
console.log('Initializing Cloudflare secrets...');
|
|
225
|
+
const config = parseWranglerConfig();
|
|
226
|
+
const plan = planSecrets(config);
|
|
227
|
+
|
|
228
|
+
if (plan.length === 0) {
|
|
229
|
+
throw new Error('wrangler.jsonc declares no secrets_store_secrets entries, so there is nothing to initialize.');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const listedByStore = new Map<string, Set<string> | undefined>();
|
|
233
|
+
|
|
234
|
+
for (const { storeId, secretName } of plan) {
|
|
235
|
+
if (!listedByStore.has(storeId)) {
|
|
236
|
+
listedByStore.set(storeId, listSecretNames(storeId));
|
|
237
|
+
}
|
|
238
|
+
const existing = listedByStore.get(storeId);
|
|
239
|
+
if (existing === undefined) {
|
|
240
|
+
// Refuse to guess: creating a secret we could not read back risks overwriting
|
|
241
|
+
// a live value with a freshly generated one.
|
|
242
|
+
throw new Error(
|
|
243
|
+
`Could not list secrets in store ${storeId}; refusing to create ${secretName} without confirming it is absent.`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
if (existing.has(secretName)) {
|
|
247
|
+
console.log(`Secret ${secretName} already exists; leaving it untouched.`);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
createSecret(storeId, secretName, await generatorFor(secretName)());
|
|
251
|
+
existing.add(secretName);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
console.log('Secret initialization complete');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Top-level await rather than `.catch(console.error)`, so a rejected `main` surfaces
|
|
258
|
+
// as a stack trace with its cause intact and the process exits non-zero. Either way
|
|
259
|
+
// the exit status has to be right: this runs in the deploy pipeline, and swallowing
|
|
260
|
+
// the rejection reported a failed key bootstrap as a green step.
|
|
261
|
+
try {
|
|
262
|
+
await main();
|
|
263
|
+
} catch (error: unknown) {
|
|
264
|
+
fail(error instanceof Error ? `Secret initialization failed: ${error.message}` : String(error));
|
|
265
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal flag parsing for the operator scripts.
|
|
3
|
+
*
|
|
4
|
+
* Provenance: ported from Edge-Sonic's `scripts/lib/cli-args.ts`, which is the
|
|
5
|
+
* only typed arg parser in the family — every other repo reads `process.argv`
|
|
6
|
+
* with `includes('--flag')` inline.
|
|
7
|
+
*
|
|
8
|
+
* The shape is generic: a declared set of value flags and boolean flags, so an
|
|
9
|
+
* unknown or valueless flag is rejected **by name** rather than silently ignored.
|
|
10
|
+
* A mistyped `--write` must not degrade into "ran with no flags" — which is what
|
|
11
|
+
* an inline `argv.includes()` does, and what turns a migration-lock invocation
|
|
12
|
+
* that misspelled the flag into a read-only run that reports success.
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* const flags = parseFlags(process.argv.slice(2), {
|
|
18
|
+
* boolean: ['write', 'dry-run'],
|
|
19
|
+
* value: ['locales-dir', 'root'],
|
|
20
|
+
* });
|
|
21
|
+
* if (isSet(flags, 'write')) { ... }
|
|
22
|
+
* const dir = valueOf(flags, 'locales-dir');
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parsed flags: value flags map to their string, boolean flags to `true`.
|
|
28
|
+
*/
|
|
29
|
+
export type ParsedFlags = Map<string, string | true>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Declares which flags take a value and which are bare switches.
|
|
33
|
+
*/
|
|
34
|
+
export interface FlagSpec {
|
|
35
|
+
/**
|
|
36
|
+
* `--name` takes a value, e.g. `--to new@example.com`.
|
|
37
|
+
*/
|
|
38
|
+
value?: readonly string[];
|
|
39
|
+
/**
|
|
40
|
+
* `--dry-run` takes no value.
|
|
41
|
+
*/
|
|
42
|
+
boolean?: readonly string[];
|
|
43
|
+
/**
|
|
44
|
+
* Short form → long form, e.g. `{ h: 'help' }`.
|
|
45
|
+
*/
|
|
46
|
+
alias?: Readonly<Record<string, string>>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parses `--flag value` and `--boolean` pairs out of `argv`.
|
|
51
|
+
*
|
|
52
|
+
* @throws when a value flag is missing its value, when a flag is repeated, or
|
|
53
|
+
* when an undeclared flag is seen. Each case names the offending flag.
|
|
54
|
+
*/
|
|
55
|
+
export function parseFlags(argv: readonly string[], spec: FlagSpec): ParsedFlags {
|
|
56
|
+
const valueFlags = new Set(spec.value);
|
|
57
|
+
const booleanFlags = new Set(spec.boolean);
|
|
58
|
+
const aliases = spec.alias ?? {};
|
|
59
|
+
const flags: ParsedFlags = new Map();
|
|
60
|
+
|
|
61
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
62
|
+
const arg = argv[index] as string;
|
|
63
|
+
if (!arg.startsWith('-')) {
|
|
64
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
65
|
+
}
|
|
66
|
+
const raw = arg.replace(/^--?/, '');
|
|
67
|
+
const name = aliases[raw] ?? raw;
|
|
68
|
+
|
|
69
|
+
if (booleanFlags.has(name)) {
|
|
70
|
+
if (flags.has(name)) {
|
|
71
|
+
throw new Error(`--${name} was given more than once`);
|
|
72
|
+
}
|
|
73
|
+
flags.set(name, true);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!valueFlags.has(name)) {
|
|
78
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
79
|
+
}
|
|
80
|
+
if (flags.has(name)) {
|
|
81
|
+
throw new Error(`--${name} was given more than once`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const value = argv[index + 1];
|
|
85
|
+
// A value that starts with `-` is treated as a missing value so that
|
|
86
|
+
// `--to --dry-run` reports the real problem instead of capturing the flag.
|
|
87
|
+
if (value === undefined || value.startsWith('-')) {
|
|
88
|
+
throw new Error(`--${name} requires a value`);
|
|
89
|
+
}
|
|
90
|
+
flags.set(name, value);
|
|
91
|
+
index += 1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return flags;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Reads a value flag, or `undefined` when it was not supplied.
|
|
99
|
+
*/
|
|
100
|
+
export function valueOf(flags: ParsedFlags, name: string): string | undefined {
|
|
101
|
+
const value = flags.get(name);
|
|
102
|
+
return typeof value === 'string' ? value : undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Reads a boolean flag, defaulting to `false`.
|
|
107
|
+
*/
|
|
108
|
+
export function isSet(flags: ParsedFlags, name: string): boolean {
|
|
109
|
+
return flags.get(name) === true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Split `argv` into its positional arguments and the rest, so a caller can accept
|
|
114
|
+
* `tsx script.ts <dir>` alongside `--root <dir>`.
|
|
115
|
+
*
|
|
116
|
+
* A token that follows a value flag *is* that flag's value, not a positional: the
|
|
117
|
+
* naive `argv.filter((arg) => !arg.startsWith('-'))` takes `.` out of
|
|
118
|
+
* `--root . --fail-on-hard` and then `parseFlags` reports `--root requires a value`.
|
|
119
|
+
* The value flags are passed in rather than inferred, because only the caller knows
|
|
120
|
+
* which of its declared flags take one.
|
|
121
|
+
*
|
|
122
|
+
* @throws when more than `expected` positionals are given, naming them: a second
|
|
123
|
+
* directory is far more likely to be a mistyped flag than an intended argument.
|
|
124
|
+
*/
|
|
125
|
+
export function splitPositional(
|
|
126
|
+
argv: readonly string[],
|
|
127
|
+
valueFlags: readonly string[],
|
|
128
|
+
expected = 1,
|
|
129
|
+
): { positional: string[]; rest: string[] } {
|
|
130
|
+
const takes = new Set(valueFlags);
|
|
131
|
+
const positional: string[] = [];
|
|
132
|
+
const rest: string[] = [];
|
|
133
|
+
|
|
134
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
135
|
+
const arg = argv[index] as string;
|
|
136
|
+
if (arg.startsWith('-')) {
|
|
137
|
+
rest.push(arg);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const previous = argv[index - 1] as string | undefined;
|
|
141
|
+
const isFlagValue = previous !== undefined && previous.startsWith('-') && takes.has(previous.replace(/^--?/, ''));
|
|
142
|
+
if (isFlagValue) {
|
|
143
|
+
rest.push(arg);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
positional.push(arg);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (positional.length > expected) {
|
|
150
|
+
throw new Error(`Expected at most ${expected} positional argument(s), got: ${positional.join(', ')}`);
|
|
151
|
+
}
|
|
152
|
+
return { positional, rest };
|
|
153
|
+
}
|
|
154
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal GitHub Actions step helpers shared by the CI scripts.
|
|
3
|
+
*
|
|
4
|
+
* Provenance: ported from Edge-Sonic's `scripts/lib/github-actions.ts`, the only
|
|
5
|
+
* repo that factored these out. The scripts deliberately avoid `@actions/*`
|
|
6
|
+
* dependencies so they stay runnable with plain `tsx` from a local shell, which
|
|
7
|
+
* is how they are tested.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { appendFileSync } from 'node:fs';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Append `name=value` to the step's output file. No-op outside Actions.
|
|
14
|
+
*/
|
|
15
|
+
export function setOutput(name: string, value: string): void {
|
|
16
|
+
const outputFile = process.env.GITHUB_OUTPUT;
|
|
17
|
+
if (!outputFile) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
appendFileSync(outputFile, `${name}=${value}\n`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Emit a workflow-command annotation so the failure is visible in the run log.
|
|
25
|
+
*/
|
|
26
|
+
export function logError(message: string): void {
|
|
27
|
+
process.stderr.write(`::error::${message}\n`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Report a fatal error and exit non-zero.
|
|
32
|
+
*
|
|
33
|
+
* The annotation is written *before* exiting so the reason survives in the run
|
|
34
|
+
* log. This is the whole point of the helper: the failure mode it exists to
|
|
35
|
+
* prevent is a script that ends in `.catch(console.error)`, which reports success
|
|
36
|
+
* while every side effect it was supposed to perform did not happen.
|
|
37
|
+
*/
|
|
38
|
+
export function fail(message: string): never {
|
|
39
|
+
logError(message);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parsers for the human-readable tables `wrangler` prints.
|
|
3
|
+
*
|
|
4
|
+
* Provenance: ported from Edge-Sonic's `scripts/lib/wrangler-table.ts`, which was
|
|
5
|
+
* factored out of two scripts that had each grown their own copy.
|
|
6
|
+
*
|
|
7
|
+
* `wrangler secrets-store …` renders through `cli-table3`, which draws a
|
|
8
|
+
* box-drawing frame and separates cells with U+2502 (`│`) padded by exactly one
|
|
9
|
+
* space. Columns are therefore *not* space-aligned, and a parser written for
|
|
10
|
+
* aligned output silently matches nothing — which reads as "the resource is
|
|
11
|
+
* absent" and makes provisioning create a duplicate.
|
|
12
|
+
*
|
|
13
|
+
* ### Why membership is exact and never a substring
|
|
14
|
+
*
|
|
15
|
+
* Durable-DAV's `init-secrets.ts` used `output.includes(secretName)`, and that is
|
|
16
|
+
* the defect this port replaces: a store holding both
|
|
17
|
+
* `durable-dav-replication-encryption-key` and a `…-key-backup` would satisfy a
|
|
18
|
+
* substring search for the first, so the real secret was reported as already
|
|
19
|
+
* present and never created. A substring match is not a membership test.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Cell separator emitted by `cli-table3`.
|
|
24
|
+
*/
|
|
25
|
+
const COLUMN_SEPARATOR = String.fromCodePoint(0x25_02);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Horizontal rule drawn by `cli-table3`; a row made of these is not data.
|
|
29
|
+
*/
|
|
30
|
+
const RULE_CHARACTER = String.fromCodePoint(0x25_00);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Header row of every `wrangler … list` table.
|
|
34
|
+
*/
|
|
35
|
+
const HEADER_FIRST_CELL = 'Name';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Extracts the data rows of a `wrangler` table, dropping the frame, the header,
|
|
39
|
+
* and any surrounding log lines.
|
|
40
|
+
*
|
|
41
|
+
* Rows are returned in print order with cells trimmed and empty cells removed,
|
|
42
|
+
* so a blank optional column (`Comment`, typically unset) does not shift the
|
|
43
|
+
* columns a caller indexes into.
|
|
44
|
+
*/
|
|
45
|
+
export function parseWranglerTableRows(output: string): string[][] {
|
|
46
|
+
const rows: string[][] = [];
|
|
47
|
+
for (const line of output.split('\n')) {
|
|
48
|
+
// The frame rows use U+253C (`┼`) at the joints rather than U+2502, so they
|
|
49
|
+
// are already excluded here; the rule-character guard below additionally
|
|
50
|
+
// covers borders that happen to contain a separator.
|
|
51
|
+
if (!line.includes(COLUMN_SEPARATOR)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const cells: string[] = line
|
|
56
|
+
.split(COLUMN_SEPARATOR)
|
|
57
|
+
.map((cell) => cell.trim())
|
|
58
|
+
.filter(Boolean);
|
|
59
|
+
const firstCell = cells[0];
|
|
60
|
+
if (firstCell === undefined || cells.length < 2) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
// The header row and the horizontal rules are frame, not data.
|
|
64
|
+
if (firstCell === HEADER_FIRST_CELL || firstCell.includes(RULE_CHARACTER)) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
rows.push(cells);
|
|
68
|
+
}
|
|
69
|
+
return rows;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The exact names in the first column of a `wrangler secrets-store secret list`
|
|
74
|
+
* table.
|
|
75
|
+
*
|
|
76
|
+
* Exact values rather than a substring search — see the header.
|
|
77
|
+
*/
|
|
78
|
+
export function parseWranglerNames(output: string): Set<string> {
|
|
79
|
+
const names = new Set<string>();
|
|
80
|
+
for (const row of parseWranglerTableRows(output)) {
|
|
81
|
+
const name = row[0];
|
|
82
|
+
if (name !== undefined) names.add(name);
|
|
83
|
+
}
|
|
84
|
+
return names;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* wrangler exits non-zero when a Secrets Store holds no secrets
|
|
89
|
+
* (`secrets-store secret list` throws this `FatalError`), which is
|
|
90
|
+
* indistinguishable by exit code from a genuine failure.
|
|
91
|
+
*/
|
|
92
|
+
const EMPTY_STORE_MESSAGE = 'List request returned no secrets';
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* True when a failed `secrets-store secret list` means the store is empty
|
|
96
|
+
* rather than the listing having failed.
|
|
97
|
+
*
|
|
98
|
+
* Callers must treat this as a confirmed absence; every other failure has to
|
|
99
|
+
* stay indistinguishable from "unknown", or a transient CLI error would be read
|
|
100
|
+
* as permission to create a secret that already holds a live value.
|
|
101
|
+
*/
|
|
102
|
+
export function isEmptySecretsStoreListing(error: unknown): boolean {
|
|
103
|
+
const message: string = error instanceof Error ? error.message : String(error);
|
|
104
|
+
return message.includes(EMPTY_STORE_MESSAGE);
|
|
105
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Record newly-added migration files in `migrations/migrations.lock.json`.
|
|
4
|
+
*
|
|
5
|
+
* Provenance: Edge-Sonic's `scripts/migrations/verify-migrations.ts --write`,
|
|
6
|
+
* which is the only repo that splits the two operations into two commands. The
|
|
7
|
+
* other repos have one script with a flag; the split is kept here because the two
|
|
8
|
+
* calls have different audiences and different blast radii — `verify-migrations`
|
|
9
|
+
* runs in CI and changes nothing, while this one is an operator action that writes
|
|
10
|
+
* a file into version control.
|
|
11
|
+
*
|
|
12
|
+
* The rules live once, in `verify-migrations.ts`, so the two entrypoints cannot
|
|
13
|
+
* disagree. `--write` is add-only: it records unlocked files, refreshes the
|
|
14
|
+
* baseline's own entry, and drops entries the baseline absorbed. It never touches
|
|
15
|
+
* another existing entry, so an incremental migration that has drifted still
|
|
16
|
+
* fails afterwards — re-baselining one means deleting its entry by hand, which
|
|
17
|
+
* shows up in the diff.
|
|
18
|
+
*
|
|
19
|
+
* There is deliberately no `--force`, because a write that could bless a drift
|
|
20
|
+
* without reading the drift is the operator blessing their own edit, and they are
|
|
21
|
+
* by definition the one making it.
|
|
22
|
+
*
|
|
23
|
+
* Usage, from the repo root:
|
|
24
|
+
*
|
|
25
|
+
* ```bash
|
|
26
|
+
* pnpm exec tsx scripts/migrations-lock.ts # ./migrations
|
|
27
|
+
* pnpm exec tsx scripts/migrations-lock.ts path/to/migrations
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
import { fileURLToPath } from 'node:url';
|
|
32
|
+
import { LOCK_NAME, verifyMigrations } from './verify-migrations';
|
|
33
|
+
|
|
34
|
+
const REPOSITORY_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
35
|
+
const migrationsDir = process.argv[2] === undefined ? path.join(REPOSITORY_ROOT, 'migrations') : path.resolve(process.argv[2]);
|
|
36
|
+
|
|
37
|
+
const report = verifyMigrations(migrationsDir, { write: true });
|
|
38
|
+
|
|
39
|
+
for (const problem of report.problems) {
|
|
40
|
+
console.error(`FAIL: ${problem.problem}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log(
|
|
44
|
+
`${report.lock === null ? 'left' : 'wrote'} ${path.join(migrationsDir, LOCK_NAME)} ` +
|
|
45
|
+
`(${report.files.length} migration(s) recorded)`,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// The lock is re-checked by the caller: `verify-migrations.ts` re-reads from disk,
|
|
49
|
+
// so running it after this command reports the same verdict CI will. A `--write`
|
|
50
|
+
// that could not resolve everything still exits non-zero here, which is why an
|
|
51
|
+
// incremental `edited` finding does not vanish just because someone wrote.
|
|
52
|
+
process.exit(report.problems.length === 0 ? 0 : 1);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Materialize `wrangler.jsonc` and provision what it refers to.
|
|
4
|
+
*
|
|
5
|
+
* Provenance: Durable-DAV's `scripts/prepare-wrangler-config.ts` and Edge-Sonic's
|
|
6
|
+
* `scripts/deploy/prepare-wrangler-config.ts`, converged. The five ordered calls
|
|
7
|
+
* live in `wrangler-config/`: read or copy the config, refuse one older than the
|
|
8
|
+
* template requires, apply the caller's patches, then create whatever resources it
|
|
9
|
+
* names and write their real ids over the placeholders. This file is only the
|
|
10
|
+
* order; it has nothing else to do.
|
|
11
|
+
*
|
|
12
|
+
* Run by the deploy job before `wrangler deploy`, and by the backup workflow
|
|
13
|
+
* before `wrangler d1 export` — so the database a backup covers is resolved from
|
|
14
|
+
* the same config the deploy targets, with no second `D1_DATABASE_ID` secret to
|
|
15
|
+
* keep in sync.
|
|
16
|
+
*
|
|
17
|
+
* The one thing it does beyond calling is publish what
|
|
18
|
+
* `provisionWranglerResources` created, which the backup workflow reads to refuse
|
|
19
|
+
* exporting a database that did not exist until this run.
|
|
20
|
+
*
|
|
21
|
+
* Usage, from the repo root:
|
|
22
|
+
*
|
|
23
|
+
* ```bash
|
|
24
|
+
* pnpm exec tsx scripts/prepare-wrangler-config.ts
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* Environment:
|
|
28
|
+
*
|
|
29
|
+
* - `WRANGLER_JSONC` — the whole config, as a repository variable. When set it is
|
|
30
|
+
* written verbatim instead of copying the template.
|
|
31
|
+
* - `WRANGLER_PATCH_JSON` — top-level keys merged into the config.
|
|
32
|
+
* - `WRANGLER_VARS_PATCH_JSON` — `vars` entries merged into the config.
|
|
33
|
+
*/
|
|
34
|
+
import { setOutput } from './lib/github-actions';
|
|
35
|
+
import { applyTopLevelPatch, applyVarsPatch, ensureMinimumConfigVersion, prepareConfigFile } from './wrangler-config/patches';
|
|
36
|
+
import { provisionWranglerResources } from './wrangler-config/resources';
|
|
37
|
+
|
|
38
|
+
prepareConfigFile();
|
|
39
|
+
ensureMinimumConfigVersion();
|
|
40
|
+
applyTopLevelPatch();
|
|
41
|
+
applyVarsPatch();
|
|
42
|
+
const created = provisionWranglerResources();
|
|
43
|
+
|
|
44
|
+
// Published as a step output rather than only printed. The backup workflow needs
|
|
45
|
+
// to know whether the D1 database *existed* or was created moments ago: a fresh one
|
|
46
|
+
// holds no user data, and uploading that empty dump every night is a false sense
|
|
47
|
+
// of safety. The alternative — re-reading the placeholder id out of
|
|
48
|
+
// `wrangler.jsonc` — is unreachable, because `provisionWranglerResources` patches
|
|
49
|
+
// the placeholder away before the reader ever runs.
|
|
50
|
+
setOutput('created', created.join(','));
|
|
51
|
+
console.log('Wrangler configuration is ready.');
|