diff --git a/.changeset/stale-cache-recovery.md b/.changeset/stale-cache-recovery.md new file mode 100644 index 00000000..b8c9367d --- /dev/null +++ b/.changeset/stale-cache-recovery.md @@ -0,0 +1,5 @@ +--- +"twilio-run": patch +--- + +Automatically recover from stale `.twiliodeployinfo` cache entries during deploy. When a cached service SID references a service that was deleted externally (via Console or API), the deploy now detects the 20404 error, clears the stale cache entry, and retries the deployment instead of failing with a confusing error message. diff --git a/packages/twilio-run/src/commands/deploy.ts b/packages/twilio-run/src/commands/deploy.ts index 62034037..0c6c135d 100644 --- a/packages/twilio-run/src/commands/deploy.ts +++ b/packages/twilio-run/src/commands/deploy.ts @@ -20,6 +20,7 @@ import { } from '../flags'; import { printConfigInfo, printDeployedResources } from '../printers/deploy'; import { HttpError, saveLatestDeploymentData } from '../serverless-api/utils'; +import { clearDeployInfoCache } from '../utils/deployInfoCache'; import { getDebugFunction, getOraSpinner, @@ -123,7 +124,31 @@ export async function handler( client.on('status-update', (evt) => { spinner.text = evt.message + '\n'; }); - const result = await client.deployLocalProject(config); + + let result; + try { + result = await client.deployLocalProject(config); + } catch (deployErr) { + // If the cached service SID is stale (service was deleted externally), + // clear the cache and retry without the stale SID + const isStaleService = config.serviceSid && deployErr.code === 20404; + if (isStaleService) { + const accountSid = config.username.startsWith('AC') + ? config.username + : externalCliOptions?.accountSid; + logger.warn( + `Cached service ${config.serviceSid} not found (it may have been deleted). Clearing cache and retrying.` + ); + if (accountSid) { + clearDeployInfoCache(config.cwd, accountSid, config.region); + } + config.serviceSid = undefined; + result = await client.deployLocalProject(config); + } else { + throw deployErr; + } + } + spinner.text = 'Serverless project successfully deployed\n'; spinner.succeed(); printDeployedResources(config, result, config.outputFormat); diff --git a/packages/twilio-run/src/utils/deployInfoCache.ts b/packages/twilio-run/src/utils/deployInfoCache.ts index 9218b18d..d9618211 100644 --- a/packages/twilio-run/src/utils/deployInfoCache.ts +++ b/packages/twilio-run/src/utils/deployInfoCache.ts @@ -58,6 +58,30 @@ export function getDeployInfoCache( return {}; } +export function clearDeployInfoCache( + baseDir: string, + username: string, + region: string = 'us1', + deployInfoCacheFileName: string = '.twiliodeployinfo' +): void { + const fullPath = path.resolve(baseDir, deployInfoCacheFileName); + debug('Clearing stale deploy info cache entry for %s:%s', username, region); + const currentDeployInfoCache = getDeployInfoCache( + baseDir, + deployInfoCacheFileName + ); + + delete currentDeployInfoCache[`${username}:${region}`]; + delete currentDeployInfoCache[username]; + + try { + const data = JSON.stringify(currentDeployInfoCache, null, '\t'); + fs.writeFileSync(fullPath, data, 'utf8'); + } catch (err) { + debug('Failed to clear deploy info cache entry'); + } +} + export function updateDeployInfoCache( baseDir: string, username: string,