VercelCron部署自動化

Vercel 的 cron 只跑正式版那個部署:alias 不等於 production 的一次踩坑

· · 12 分鐘閱讀
目錄
  1. 症狀:頁面是新的,排程是舊的
  2. 為什麼 alias 不等於 production?
  3. 怎麼查:三個 ID 要一樣
  4. 怎麼修:promote,但先看清楚會開哪些排程
  5. 副作用:自動綁網域被打開了
  6. 我們現在的部署收尾

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 的專案裡有兩件事是分開的:

  1. 網域指到哪個部署:vercel alias set 改的是這個。
  2. 專案的正式版(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 連結,或改正式分支。

我們現在的部署收尾

每次上正式之後固定做兩件事:

  1. 用 API 查網域、正式版、排程三個 ID 要一致。之前是手動打 API 查,上面那支是我們把它整理成腳本的版本。
  2. 看 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 完要確認這個開關和那條分支是不是你要的。

每週 AI 自動化實戰筆記

不廢話,只有能直接用的東西。Prompt 模板、自動化 SOP、技術拆解。

加入一人公司實驗室

免費資源包、每日建造日誌、可以對話的 AI Agent。一群用 AI 武裝自己的獨立開發者社群。

想自己動手試試?

UltraProbe 免費、免註冊,掃一次網站就知道 Google 與 AI 找不找得到你。