1#!/usr/bin/env node
Run by nix run .#deploy after release-check.mjs and before wrangler deploy: the Worker's storage, made to exist and brought up to date first.
resources.mjs <wrangler> <worker dir>
<wrangler> is the deploy's 1Password-fed wrangler (`jevstrudel-wrangler
<dir> <args…>`); <worker dir> is the deploy's temporary copy of worker/, whose wrangler.json this rewrites. For the production config's (top level)d1_databaseswithout adatabase_id: finds the database bydatabase_name, creating it if the account has none, and writes its id into the copy. Then applies its migrations (d1 migrations apply --remote), so the Worker about to be published never runs against a schema older than its code.kv_namespaceswithout anid: finds the namespace by title,<worker name>-<binding, lowercased, _ as ->(the title wrangler's own automatic provisioning gives it, so either finds the other's), creating it if missing, and writes its id into the copy.r2_buckets: an R2 binding names its bucket (bucket_name, a name, not an account's id), so there is no id to write; the bucket is created if the account has none by that name, sincewrangler deployrefuses a binding to a bucket that does not exist.
Everything is looked up by name each run, so it is idempotent, and the
repo's wrangler.json holds no account's ids: a first deploy to an empty
account creates what it needs, and every later one finds it. wrangler's
automatic provisioning at deploy time is not enough by itself: it
creates a database during wrangler deploy, after which it is too late
to migrate it; and a KV title it created in a deploy that then failed is
taken (KV titles are unique per account) but bound to no Worker, which
its next attempt does not look for.
Nothing here prints a credential: the wrapper reads the token itself, and only ids and names pass through this process.
wrangler's own automatic-provisioning title (autoProvisionedResourceName).
42export const kvTitle = (worker, binding) => `${worker}-${binding.toLowerCase().replaceAll('_', '-')}`;
The JSON array a list command printed, from the first line that opens one (past any banner; a "[WARNING]" line goes to stderr, but is not taken for the list even if it did not).
47export function parseList(out, what) { 48 const start = out.search(/^\[\s*(\{|\])/m); 49 if (start < 0) throw new Error(`${what}: no JSON list in wrangler's output`); 50 const list = JSON.parse(out.slice(start)); 51 if (!Array.isArray(list)) throw new Error(`${what}: not a list`); 52 return list; 53}
The bucket names wrangler r2 bucket list printed. It has no --json
(wrangler 4.132.0): each bucket is a block of label: value lines, the
first name:.
wrangler(args, { capture }) runs the deploy's wrangler in the copy and
returns its stdout when capturing. config is the copy's wrangler.json,
parsed; it is updated in place and returned.
65export function provision(config, wrangler, log = console.error) { 66 const databases = config.d1_databases ?? []; 67 const pendingD1 = databases.filter((db) => !db.database_id); 68 if (pendingD1.length) { 69 const list = () => 70 parseList(wrangler(['d1', 'list', '--json'], { capture: true }), 'd1 list').filter( 71 (d) => typeof d?.name === 'string' && typeof d?.uuid === 'string', 72 ); 73 let existing = list(); 74 for (const db of pendingD1) { 75 if (!existing.some((d) => d.name === db.database_name)) { 76 log(`resources: creating the D1 database ${db.database_name}`); 77 wrangler(['d1', 'create', db.database_name, '--update-config=false']); 78 existing = list(); 79 } 80 const found = existing.find((d) => d.name === db.database_name); 81 if (!found) throw new Error(`D1 database ${db.database_name} is still missing after creating it`); 82 db.database_id = found.uuid; 83 log(`resources: ${db.binding} is D1 ${db.database_name} (${found.uuid})`); 84 } 85 } 86 87 const pendingKV = (config.kv_namespaces ?? []).filter((ns) => !ns.id); 88 if (pendingKV.length) { 89 const list = () => 90 parseList(wrangler(['kv', 'namespace', 'list'], { capture: true }), 'kv namespace list').filter( 91 (n) => typeof n?.title === 'string' && typeof n?.id === 'string', 92 ); 93 let existing = list(); 94 for (const ns of pendingKV) { 95 const title = kvTitle(config.name, ns.binding); 96 if (!existing.some((n) => n.title === title)) { 97 log(`resources: creating the KV namespace ${title}`); 98 wrangler(['kv', 'namespace', 'create', title, '--update-config=false']); 99 existing = list(); 100 } 101 const found = existing.find((n) => n.title === title); 102 if (!found) throw new Error(`KV namespace ${title} is still missing after creating it`); 103 ns.id = found.id; 104 log(`resources: ${ns.binding} is KV ${title} (${found.id})`); 105 } 106 } 107 108 const buckets = (config.r2_buckets ?? []).filter((b) => b.bucket_name); 109 if (buckets.length) { 110 let existing = bucketNames(wrangler(['r2', 'bucket', 'list'], { capture: true })); 111 for (const b of buckets) { 112 if (!existing.includes(b.bucket_name)) { 113 log(`resources: creating the R2 bucket ${b.bucket_name}`); 114 wrangler(['r2', 'bucket', 'create', b.bucket_name, '--update-config=false']); 115 existing = bucketNames(wrangler(['r2', 'bucket', 'list'], { capture: true })); 116 } 117 if (!existing.includes(b.bucket_name)) throw new Error(`R2 bucket ${b.bucket_name} is still missing after creating it`); 118 log(`resources: ${b.binding} is R2 ${b.bucket_name}`); 119 } 120 } 121 return config; 122}
After the ids are in the copy's config: each database's pending migrations, on the real database. wrangler lists them and asks before applying when run from a terminal.
133if (import.meta.url === pathToFileURL(process.argv[1]).href) { 134 const [bin, dir] = process.argv.slice(2); 135 if (!bin || !dir) { 136 console.error('usage: resources.mjs <wrangler> <worker dir>'); 137 process.exit(2); 138 } 139 const path = join(dir, 'wrangler.json'); 140 const wrangler = (args, { capture = false } = {}) => 141 execFileSync(bin, [dir, ...args], { 142 encoding: 'utf8', 143 stdio: ['inherit', capture ? 'pipe' : 'inherit', 'inherit'], 144 env: { ...process.env, WRANGLER_SEND_METRICS: 'false' }, 145 }); 146 const config = provision(JSON.parse(readFileSync(path, 'utf8')), wrangler); 147 writeFileSync(path, JSON.stringify(config, null, 2)); 148 migrate(config, wrangler); 149}