Vercel 的 cron 只跑正式版那個部署:alias 不等於 production 的一次踩坑
目錄
Vercel 的 cron 只在專案「目前的正式版」那個部署上跑,不是網域指到的那個。如果你的專案關了自動綁網域,用 vercel --prod 部署再手動 vercel alias set,網站會變新版,但排程還留在舊的正式版上,新加的 cron 不會跑,舊的 cron 跑的是舊程式。解法是 vercel promote,再用 API 確認網域、正式版、排程三個指到同一個部署。我們 promote 之後還遇到一個副作用:自動綁網域被系統打開,之後推 Git 正式分支就會直接上線。
以下是我們 11 月 2 日在 ultralab.tw 實際遇到的情況,附查法和檢查程式。
症狀:頁面是新的,排程是舊的
11 月 2 日我們上了一版,裡面加了一條新的 cron:每天寄一封培養信給剛領取工具包的人。部署流程照舊:vercel --prod --yes --archive=tgz,等它 Ready,再對 ultralab.tw 和 www 各跑一次 vercel alias set。
上線後的驗證都過了:新頁面 200,直接打那支 cron 的端點(帶 dry run 參數)也回 200,函式本身是好的。
問題出在排程清單。新版的 vercel.json 寫了四條 cron,Vercel 實際登記的只有兩條,而且那兩條打的是 9 月 3 日那個部署的網址。回頭一查,11 月 1 日那版加的另一條週報排程也一樣沒登記上,當時也沒發現。
說白了:從 9 月 3 日到 11 月 2 日這兩個月,我們的部署都只換了網域,網站是新版,但 Vercel 認定的正式版一直停在 9/3。排程跟著正式版走,新的不會跑,舊的那兩條打的一直是 9/3 部署上的程式,這段期間要是改過它們,改動也不會生效。
這是很典型的靜默失敗:沒有錯誤、沒有告警,只是該發生的事沒發生。
為什麼 alias 不等於 production?
Vercel 的專案裡有兩件事是分開的:
- 網域指到哪個部署:
vercel alias set改的是這個。 - 專案的正式版(current production deployment)是哪個:cron 看的是這個。
平常兩者是一起動的。專案開著「自動綁正式網域」(API 欄位叫 autoAssignCustomDomains)的時候,正式部署一 Ready,Vercel 就會把正式網域綁上去,同時把它設成正式版。
我們的專案這個開關是關的。關著的時候,vercel --prod 產生的部署只是一個「還沒上線的正式建置」,你再手動 alias,網域就過去了,但正式版的指標沒動。
這個開關怎麼關掉的,我們沒查到紀錄。Vercel 文件寫的是:做過 Instant Rollback 之後,會自動關掉自動綁網域;要恢復,就 promote 一個部署。另一種情況是有人為了先驗再上線,自己去設定裡關掉。做過其中一件就值得查。
怎麼查:三個 ID 要一樣
要比的是三個部署 ID:
- 網域現在指到哪個部署:
/v4/aliases/<網域>的deploymentId - 專案的正式版:
/v9/projects/<專案>的targets.production.id - 排程登記在哪個部署:同一個回應裡的
crons.deploymentId,crons.definitions裡每條的host也會是那個部署的網址
出事那天,網域是 11/2 的新部署,正式版和排程都是 9/3 那個。只比後兩個會以為沒事,一定要把網域也拉進來。
我們手上的 Vercel CLI 50.25.2 沒有 vercel crons ls(回「not a valid subcommand」;Vercel 文件已經有 vercel crons 指令,新版 CLI 可能可以直接用),所以直接打 API。下面這支用 Node 18 以上內建的 fetch,不用裝套件:
// check-cron-target.mjs
// 用法:VERCEL_TOKEN=xxx node check-cron-target.mjs <專案名> <團隊 slug> <正式網域>
const [project, team, domain] = process.argv.slice(2)
const api = async (path) => {
const res = await fetch(`https://api.vercel.com${path}?slug=${team}`, {
headers: { Authorization: `Bearer ${process.env.VERCEL_TOKEN}` },
})
if (!res.ok) { console.error('API', res.status, path); process.exit(2) }
return res.json()
}
const p = await api(`/v9/projects/${project}`)
const a = await api(`/v4/aliases/${domain}`)
const ids = {
domain: a.deploymentId, // 網域現在指到哪個部署
production: p.targets?.production?.id, // Vercel 認定的正式版
cron: p.crons?.deploymentId, // 排程登記在哪個部署上
}
console.log(ids, 'autoAssign:', p.autoAssignCustomDomains)
for (const d of p.crons?.definitions || []) console.log(' ', d.schedule.padEnd(12), d.path, '@', d.host)
if (new Set(Object.values(ids).filter(Boolean)).size > 1) {
console.error('三個不是同一個部署:網域、正式版、排程有人落後')
process.exit(1)
}
三個一致就 exit 0,不一致 exit 1,token 錯 exit 2,可以直接接進部署腳本的最後一步。我們對自己的專案跑過一次一致的情況,再用假資料跑過「網域是新的、正式版和排程是舊的」那種情況,會正確 exit 1。
怎麼修:promote,但先看清楚會開哪些排程
修法本身只有一行:
vercel promote <部署網址> --yes --scope=<團隊 slug>
--scope 我們是被逼著加的:不帶的時候 CLI 回「Deployment belongs to a different team」,帶上團隊就正常。
但我們沒有馬上按下去。promote 會一次啟用那個部署 vercel.json 裡所有的 cron,而新版裡除了培養信,還有那條沒登記上的週報:排程寫的是 UTC 的 0 3 * * 0,也就是每週日上午 11 點(台北時間),會真的寄信給訂閱者,下一次是 11 月 8 日。那不在這次上線的範圍。
所以我們的做法是:先從 vercel.json 拿掉週報那條,重新部署一版,再 promote 那一版。promote 完再用 API 查一次,三個 ID 一致,排程是新版的三條,週報不在裡面。
如果你也遇到,promote 之前先把新版的 vercel.json 的 crons 整段看一遍,問自己每一條現在開起來會發生什麼事。之前一直沒登記上的排程,一開就會照時間表跑,不會先問你。
副作用:自動綁網域被打開了
當天晚上再部署一次時,我們發現行為變了:vercel --prod 一 Ready,網域就自動綁過去了,log 裡直接出現 Aliased。我們照舊再跑 promote,回的是 409「already the current production deployment」,代表 --prod 已經讓它成為正式版。
用 API 一查,專案的 autoAssignCustomDomains 變成 true,而 autoAssignCustomDomainsUpdatedBy 是 system。Vercel 文件寫的是「Instant Rollback 之後 promote 會恢復自動綁網域」,沒有明講一般的 promote 一定會;我們的情況跟這個描述對得上,但你的專案要自己查一次。
對日常部署這是好事,以後排程會跟著 --prod 走,不用再手動 promote。但它也帶來一個風險:這個專案還連著一個 GitHub repo,正式分支是 master,而那個 repo 的 master 因為一些歷史原因停在幾個月前,我們的主線在另一個遠端。自動綁網域打開之後,只要有人推那條 master,Vercel 就會建正式部署並直接把網域換過去,等於用幾個月前的舊碼蓋掉正式站。
我們目前靠 git 的 pre-push hook 擋住對那條分支的推送。你的專案如果也是「CLI 部署為主、Git 連結還留著」,promote 之後記得確認專案連的正式分支是不是你真的要上線的程式;不是的話,斷開 Git 連結,或改正式分支。
我們現在的部署收尾
每次上正式之後固定做兩件事:
- 用 API 查網域、正式版、排程三個 ID 要一致。之前是手動打 API 查,上面那支是我們把它整理成腳本的版本。
- 看
crons.definitions的條數和路徑,跟vercel.json一樣才算數。
再加一個建議:有新增或改動的 cron,等它第一次排定的時間過了,去看那個路徑的 log 有沒有真的被呼叫。登記上了不代表跑得起來。
這類「服務看起來好好的,但該做的事沒做」的問題,我們之前整理過一篇自動化工具的靜默失敗與偵測,排程沒跑就是其中最安靜的一種。
常見問題
Vercel 的 cron job 為什麼沒有執行?
先查排程登記在哪個部署上。Vercel 的 cron 只在專案目前的正式版(current production deployment)上執行,如果專案關了「自動綁正式網域」,vercel --prod 再手動 vercel alias set,網域會指到新版,但正式版還是舊的,排程就照舊版的 vercel.json 跑舊版程式。用 API 查專案的 targets.production 和 crons.deploymentId,跟網域現在指到的部署比一下就知道。
vercel alias set 和 vercel promote 差在哪?
alias set 只改「這個網域指到哪個部署」。promote 是把某個部署設成專案的正式版,排程、正式環境的判定都跟著它走。所以網站看起來是新版,不代表 Vercel 認定的正式版是新版。
promote 之後要注意什麼?
兩件事。第一,promote 會一次啟用那個部署 vercel.json 裡所有的 cron,有不想開的(例如會真的寄信給訂閱者的週報),要先拿掉再部署。第二,我們的專案在 promote 之後「自動綁正式網域」被系統打開了,之後專案連結的 Git 正式分支一有推送,網域就會直接換過去,promote 完要確認這個開關和那條分支是不是你要的。