From 315727f8d7031de74a421faa2bc2556186e8791b Mon Sep 17 00:00:00 2001 From: Morrow Contributors Date: Thu, 6 Aug 2026 21:49:27 +0800 Subject: [PATCH] Make the rename deployable onto an existing Worker Two things stood between the renamed suite and a deployment that already exists. Migration tags are an immutable contract. The rename rewrote the contents of v1_voice_demo, which Cloudflare has already recorded as applied on any deployed Worker: the tag would never re-run, the class would never be created under its new name, and the AbleDeskAgent binding would resolve to nothing. v1 is restored to the class as first created and the rename is expressed as a new v2_rename_desk_agent tag. A fresh install applies both and lands on AbleDeskAgent; a Worker that already applied v1 applies only v2 and keeps its Durable Object state. A Worker also cannot be renamed in place. A deployment already serving custom domains, an Access application and email routing from a Worker named before this rename should keep serving from it rather than moving all of that to a new name. ABLE_WORKER_NAME supplies the name the same way every other deployment coordinate is supplied, so the committed default stays "able" and nothing deployment-specific enters source. The name is validated as the DNS label it becomes: no leading or trailing hyphen, 63 characters at most. --- apps/desk/scripts/production-config.mjs | 14 ++++++++++++++ apps/desk/test/production-config.node.test.ts | 17 +++++++++++++++++ apps/desk/wrangler.jsonc | 12 +++++++++++- docs/deployment.md | 3 ++- 4 files changed, 44 insertions(+), 2 deletions(-) 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.