fix(ci): stop trusting GET /v13 alias for production domain ownership
CI short-circuited "already aliased" when v13 listed pt.jorgesolla.com.br on a fresh --prod deploy while the domain still pointed at an older Current. Verify only via GET /v2/.../aliases and /v4/aliases?domain=. Also drop invalid --yes on vercel alias set (CLI 55).
This commit is contained in:
parent
b5303bd8d6
commit
fded303fe6
4 changed files with 126 additions and 21 deletions
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
|
|
@ -265,8 +265,9 @@ jobs:
|
|||
# pt.jorgesolla.com.br on the previous Current — promote + verify here.
|
||||
node scripts/vercel-ensure-production-alias.mjs "$deploy_url"
|
||||
# Docs: promote staged production, then alias (Git Integration is OFF by design).
|
||||
vercel promote "$deploy_url" --token="$VERCEL_TOKEN" --scope="$VERCEL_ORG_ID" --yes || true
|
||||
vercel alias set "$deploy_url" pt.jorgesolla.com.br --token="$VERCEL_TOKEN" --scope="$VERCEL_ORG_ID" --yes
|
||||
vercel promote "$deploy_url" --token="$VERCEL_TOKEN" --scope="$VERCEL_ORG_ID" || true
|
||||
# `--yes` is only valid for `alias rm`, not `alias set` (CLI 55).
|
||||
vercel alias set "$deploy_url" pt.jorgesolla.com.br --token="$VERCEL_TOKEN" --scope="$VERCEL_ORG_ID"
|
||||
|
||||
requeue:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
|||
5
.github/workflows/vercel-promote.yml
vendored
5
.github/workflows/vercel-promote.yml
vendored
|
|
@ -67,5 +67,6 @@ jobs:
|
|||
fi
|
||||
node scripts/vercel-ensure-production-alias.mjs "$ref"
|
||||
# Docs: when Auto-assign is/was OFF, prefer `vercel promote` then `alias set`.
|
||||
vercel promote "$ref" --token="$VERCEL_TOKEN" --scope="$VERCEL_ORG_ID" --yes || true
|
||||
vercel alias set "$ref" pt.jorgesolla.com.br --token="$VERCEL_TOKEN" --scope="$VERCEL_ORG_ID" --yes
|
||||
vercel promote "$ref" --token="$VERCEL_TOKEN" --scope="$VERCEL_ORG_ID" || true
|
||||
# `--yes` is only valid for `alias rm`, not `alias set` (CLI 55).
|
||||
vercel alias set "$ref" pt.jorgesolla.com.br --token="$VERCEL_TOKEN" --scope="$VERCEL_ORG_ID"
|
||||
|
|
|
|||
|
|
@ -35,6 +35,23 @@ export const aliasesIncludeHost = (aliases, expectedHost) => {
|
|||
return aliases.some((alias) => normalizeHostname(alias) === want)
|
||||
}
|
||||
|
||||
/**
|
||||
* Vercel deployment ids appear as `dpl_…` or bare ids depending on the endpoint.
|
||||
* @param {string | null | undefined} a
|
||||
* @param {string | null | undefined} b
|
||||
*/
|
||||
export const sameDeploymentId = (a, b) => {
|
||||
const left = String(a ?? '')
|
||||
.trim()
|
||||
.replace(/^dpl_/i, '')
|
||||
.toLowerCase()
|
||||
const right = String(b ?? '')
|
||||
.trim()
|
||||
.replace(/^dpl_/i, '')
|
||||
.toLowerCase()
|
||||
return Boolean(left) && left === right
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a CLI stdout URL / inspect path / bare id into something the
|
||||
* deployments GET endpoint accepts (`dpl_…`, short id, or hostname).
|
||||
|
|
@ -610,17 +627,31 @@ export const ensureProductionCustomDomain = async ({
|
|||
deploymentId: deployment.id,
|
||||
fetchImpl,
|
||||
})
|
||||
if (aliasesIncludeHost(hosts, expectedHost)) return true
|
||||
if (aliasesIncludeHost(hosts, expectedHost)) {
|
||||
return { onDeployment: true, hosts, domainTarget: null }
|
||||
}
|
||||
const domainTarget = await fetchDomainAliasTarget({
|
||||
token,
|
||||
teamId,
|
||||
domain: expectedHost,
|
||||
fetchImpl,
|
||||
})
|
||||
return domainTarget?.deploymentId === deployment.id
|
||||
return {
|
||||
onDeployment: sameDeploymentId(domainTarget?.deploymentId, deployment.id),
|
||||
hosts,
|
||||
domainTarget,
|
||||
}
|
||||
}
|
||||
|
||||
if (aliasesIncludeHost(deployment.aliases, expectedHost) || (await hostAlreadyOnDeployment())) {
|
||||
// Do NOT trust GET /v13 `alias` — it can list the production hostname on a
|
||||
// fresh --prod deploy even when the custom domain still points at an older
|
||||
// Current (observed: ensure short-circuited and left pt.jorgesolla.com.br stale).
|
||||
const ownership = await hostAlreadyOnDeployment()
|
||||
note(
|
||||
`domain ${expectedHost} → ${ownership.domainTarget?.deploymentId ?? '(via deployment aliases)'} ` +
|
||||
`(this=${deployment.id}; listed=${JSON.stringify(ownership.hosts)})`,
|
||||
)
|
||||
if (ownership.onDeployment) {
|
||||
note(`already aliased to ${expectedHost}`)
|
||||
return {
|
||||
alreadyAssigned: true,
|
||||
|
|
@ -632,6 +663,11 @@ export const ensureProductionCustomDomain = async ({
|
|||
steps,
|
||||
}
|
||||
}
|
||||
if (ownership.domainTarget?.deploymentId) {
|
||||
note(
|
||||
`${expectedHost} still on ${ownership.domainTarget.deploymentId} — will promote/alias onto ${deployment.id}`,
|
||||
)
|
||||
}
|
||||
|
||||
let promoted = false
|
||||
try {
|
||||
|
|
@ -673,7 +709,8 @@ export const ensureProductionCustomDomain = async ({
|
|||
|
||||
let verified = false
|
||||
for (let attempt = 0; attempt < 10; attempt += 1) {
|
||||
if (await hostAlreadyOnDeployment()) {
|
||||
const check = await hostAlreadyOnDeployment()
|
||||
if (check.onDeployment) {
|
||||
verified = true
|
||||
break
|
||||
}
|
||||
|
|
@ -681,21 +718,10 @@ export const ensureProductionCustomDomain = async ({
|
|||
}
|
||||
|
||||
if (!verified) {
|
||||
const hosts = await listDeploymentAliasHosts({
|
||||
token,
|
||||
teamId,
|
||||
deploymentId: deployment.id,
|
||||
fetchImpl,
|
||||
})
|
||||
const domainTarget = await fetchDomainAliasTarget({
|
||||
token,
|
||||
teamId,
|
||||
domain: expectedHost,
|
||||
fetchImpl,
|
||||
})
|
||||
const check = await hostAlreadyOnDeployment()
|
||||
throw new Error(
|
||||
`After alias assign, ${expectedHost} is still not on deployment ${deployment.id}. ` +
|
||||
`deployment aliases=${JSON.stringify(hosts)}; domain points to=${JSON.stringify(domainTarget)}; ` +
|
||||
`deployment aliases=${JSON.stringify(check.hosts)}; domain points to=${JSON.stringify(check.domainTarget)}; ` +
|
||||
`aliasApiBody=${JSON.stringify(assignResult.body)}. ` +
|
||||
`Check Vercel → Settings → Domains: Git Branch must be empty, and Environments → Production → Auto-assign Custom Production Domains must be ON.`,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
normalizeHostname,
|
||||
parseDeploymentRef,
|
||||
PRODUCTION_CUSTOM_DOMAIN,
|
||||
sameDeploymentId,
|
||||
} from '../../scripts/lib/vercel-production-alias.mjs'
|
||||
|
||||
const jsonResponse = (ok: boolean, status: number, payload: unknown) =>
|
||||
|
|
@ -36,6 +37,82 @@ describe('vercelProductionAlias', () => {
|
|||
expect(aliasesIncludeHost(['jorgesolla-solla.vercel.app'], 'pt.jorgesolla.com.br')).toBe(false)
|
||||
})
|
||||
|
||||
it('sameDeploymentId ignores dpl_ prefix', () => {
|
||||
expect(sameDeploymentId('dpl_abc', 'abc')).toBe(true)
|
||||
expect(sameDeploymentId('dpl_abc', 'dpl_abc')).toBe(true)
|
||||
expect(sameDeploymentId('dpl_abc', 'dpl_other')).toBe(false)
|
||||
})
|
||||
|
||||
it('ensureProductionCustomDomain does not trust GET /v13 alias alone', async () => {
|
||||
// v13 lists the custom host (false positive observed in CI) while the domain
|
||||
// alias target still points at an older deployment — must promote/alias.
|
||||
let aliasHosts = ['jorgesolla-solla.vercel.app']
|
||||
let domainTargetId = 'dpl_old'
|
||||
const fetchImpl = (async (url: string | URL | Request, init?: RequestInit) => {
|
||||
const href = String(url)
|
||||
const method = init?.method ?? 'GET'
|
||||
if (href.includes('/v9/projects/') && href.includes('/domains') && method === 'GET') {
|
||||
return jsonResponse(true, 200, {
|
||||
domains: [{ name: 'pt.jorgesolla.com.br', verified: true }],
|
||||
})
|
||||
}
|
||||
if (href.includes('/v9/projects/') && method === 'GET') {
|
||||
return jsonResponse(true, 200, { autoAssignCustomDomains: true })
|
||||
}
|
||||
if (href.includes('/v13/deployments/')) {
|
||||
return jsonResponse(true, 200, {
|
||||
id: 'dpl_new',
|
||||
// Misleading: listed even though domain still points at dpl_old.
|
||||
alias: ['pt.jorgesolla.com.br', 'jorgesolla-solla.vercel.app'],
|
||||
readyState: 'READY',
|
||||
})
|
||||
}
|
||||
if (href.includes('/v2/deployments/') && href.includes('/aliases') && method === 'GET') {
|
||||
return jsonResponse(true, 200, {
|
||||
aliases: aliasHosts.map((alias) => ({ alias, uid: alias, created: '' })),
|
||||
})
|
||||
}
|
||||
if (href.includes('/v4/aliases') && method === 'GET') {
|
||||
return jsonResponse(true, 200, {
|
||||
aliases: [
|
||||
{
|
||||
alias: 'pt.jorgesolla.com.br',
|
||||
deploymentId: domainTargetId,
|
||||
uid: 'a1',
|
||||
created: '',
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
if (href.includes('/promote/') && method === 'POST') {
|
||||
return jsonResponse(true, 200, {})
|
||||
}
|
||||
if (href.includes('/aliases') && method === 'POST') {
|
||||
aliasHosts = ['jorgesolla-solla.vercel.app', 'pt.jorgesolla.com.br']
|
||||
domainTargetId = 'dpl_new'
|
||||
return jsonResponse(true, 200, {
|
||||
alias: 'pt.jorgesolla.com.br',
|
||||
uid: 'alias_1',
|
||||
oldDeploymentId: 'dpl_old',
|
||||
created: new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
throw new Error(`unexpected fetch ${method} ${href}`)
|
||||
}) as typeof fetch
|
||||
|
||||
const result = await ensureProductionCustomDomain({
|
||||
token: 'tok',
|
||||
projectId: 'prj_x',
|
||||
teamId: 'team_x',
|
||||
deploymentRef: 'dpl_new',
|
||||
fetchImpl,
|
||||
sleepImpl: async () => {},
|
||||
})
|
||||
|
||||
expect(result.alreadyAssigned).toBe(false)
|
||||
expect(result.aliased).toBe(true)
|
||||
})
|
||||
|
||||
it('parseDeploymentRef accepts inspect paths and vercel.app hosts', () => {
|
||||
expect(
|
||||
parseDeploymentRef('https://vercel.com/solla/jorgesolla/CQkmCejZSvRoFtYP8hHbjzP3pc1L'),
|
||||
|
|
|
|||
Loading…
Reference in a new issue