diff --git a/apps/desk/scripts/production-config.mjs b/apps/desk/scripts/production-config.mjs index 7370ef3..87c5d92 100644 --- a/apps/desk/scripts/production-config.mjs +++ b/apps/desk/scripts/production-config.mjs @@ -19,6 +19,20 @@ export function createProductionConfig(source, env) { const queueName = required(env, 'ABLE_MEDIA_QUEUE_NAME', /^[a-z0-9][a-z0-9_-]{1,62}$/) const config = structuredClone(source) + // A deployment may run on a Worker whose name predates this one, and the + // Worker name is not customer-visible when custom domains front it. Renaming + // a Worker in place is not possible, so allow the name to be supplied like + // every other deployment identifier rather than moving domains, Access + // policy and email routing to a differently named Worker. Optional: the + // committed name is the default. + const workerName = env.ABLE_WORKER_NAME?.trim() + if (workerName) { + // A Worker name becomes a DNS label, so it may not start or end with a + // hyphen, and is limited to 63 characters. + if (!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(workerName)) throw new Error('ABLE_WORKER_NAME is invalid') + config.name = workerName + } + const database = config.d1_databases?.find((binding) => binding.binding === 'DB') const bucket = config.r2_buckets?.find((binding) => binding.binding === 'ATTACHMENTS') const producer = config.queues?.producers?.find((binding) => binding.binding === 'MEDIA_QUEUE') diff --git a/apps/desk/test/production-config.node.test.ts b/apps/desk/test/production-config.node.test.ts index d3f5c13..bbce10b 100644 --- a/apps/desk/test/production-config.node.test.ts +++ b/apps/desk/test/production-config.node.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest' import { createProductionConfig } from '../scripts/production-config.mjs' const source = { + name: 'able', d1_databases: [{ binding: 'DB', database_name: 'able', migrations_dir: 'migrations' }], r2_buckets: [{ binding: 'ATTACHMENTS' }], queues: { @@ -38,4 +39,20 @@ describe('production Wrangler configuration', () => { expect(() => createProductionConfig(source, { ...variables, ABLE_R2_BUCKET_NAME: '../bucket' })) .toThrow('ABLE_R2_BUCKET_NAME is missing or invalid') }) + + it('keeps the committed Worker name unless a deployment supplies its own', () => { + // A Worker cannot be renamed in place, so a deployment already serving + // custom domains from an older Worker name must be able to keep it. + expect(createProductionConfig(source, variables).name).toBe('able') + expect(createProductionConfig(source, { ...variables, ABLE_WORKER_NAME: 'abledesk' }).name).toBe('abledesk') + expect(createProductionConfig(source, { ...variables, ABLE_WORKER_NAME: ' ' }).name).toBe('able') + expect(source.name).toBe('able') + }) + + it('rejects a Worker name Cloudflare would not accept', () => { + for (const name of ['Not Valid', 'trailing-', '-leading', 'has_underscore', 'a'.repeat(64)]) { + expect(() => createProductionConfig(source, { ...variables, ABLE_WORKER_NAME: name })) + .toThrow('ABLE_WORKER_NAME is invalid') + } + }) }) diff --git a/apps/desk/wrangler.jsonc b/apps/desk/wrangler.jsonc index edd89c8..537303e 100644 --- a/apps/desk/wrangler.jsonc +++ b/apps/desk/wrangler.jsonc @@ -22,10 +22,20 @@ } ] }, + // Migration tags are an immutable contract: Cloudflare records which tags a + // Worker has applied, so an already-applied tag must keep its original + // contents. v1 therefore still names the class as it was first created, and + // the rename to Able is expressed as a new tag. A fresh install applies both + // in sequence and lands on AbleDeskAgent; a Worker that already applied v1 + // applies only v2 and keeps its Durable Object state. "migrations": [ { "tag": "v1_voice_demo", - "new_sqlite_classes": ["AbleDeskAgent"] + "new_sqlite_classes": ["AbleDeskVoiceDemoAgent"] + }, + { + "tag": "v2_rename_desk_agent", + "renamed_classes": [{ "from": "AbleDeskVoiceDemoAgent", "to": "AbleDeskAgent" }] } ], "d1_databases": [ diff --git a/docs/deployment.md b/docs/deployment.md index c24acd3..23040e0 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -41,7 +41,8 @@ Keep deployment-specific resource coordinates in private Workers Builds variable - `ABLE_D1_DATABASE_ID`: the production D1 database UUID; - `ABLE_D1_DATABASE_NAME`: the production D1 database name; - `ABLE_R2_BUCKET_NAME`: the production attachment bucket; -- `ABLE_MEDIA_QUEUE_NAME`: the production attachment-analysis queue. +- `ABLE_MEDIA_QUEUE_NAME`: the production attachment-analysis queue; +- `ABLE_WORKER_NAME`: optional; the Worker to deploy onto when it is not the committed default. A Worker cannot be renamed in place, so a deployment whose Worker predates a rename can keep serving from it instead of moving its custom domains, Access application and email routing to a differently named Worker. The Worker name is not customer-visible when custom domains front it. `npm run deploy:production` validates these values and creates an ignored, ephemeral Wrangler file for the build. It never writes the production coordinates into the public repository.