包包baobaolin.com
date
entry
017
topic
tooling
rev

部署完跑一次全站比對:用內容驗收,不用狀態碼

我在第一篇裡用三句話帶過這件事。它值得展開,因為那支腳本是我唯一信任的「部署成功」定義。

靜態站部署完,怎麼知道它真的好了?最常見的答案是打幾個網址看看有沒有 200。這個答案的問題在於,兩種最常見的壞法都回 200

  • 目錄 key 缺失——CloudFront 的 SPA fallback 把找不到的路徑換成首頁,狀態碼 200,內容是錯的
  • 部署根本沒生效——舊版還在,狀態碼 200,內容是舊的

只看狀態碼,這兩種都測不到。要測到它們,得比對內容

用 title 當指紋

不需要比對整頁。<title> 就夠了,理由是它同時滿足三個條件:每一頁都不一樣、位置固定好抽、而且不會因為快取版本號或時間戳而變動。

流程只有三步:

  1. 走訪本機 build 產物裡的每個 index.html,抽出 title
  2. 把檔案路徑換算成線上網址
  3. 請求那個網址,抽出 title,比對
#!/usr/bin/env bash
set -uo pipefail          # 注意沒有 -e,見下方說明
BASE="https://example.com"
DIST="dist"
fail=0

title_of() {   # 從 stdin 抽第一個 <title>
  tr '\n' ' ' | sed -n 's/.*<title>\(.*\)<\/title>.*/\1/p' | head -n1
}

while IFS= read -r f; do
  rel="${f#"$DIST"/}"; rel="${rel%index.html}"      # posts/foo/
  url="$BASE/$rel"
  want=$(title_of < "$f")
  got=$(curl -fsS --max-time 10 "$url" | title_of)

  if [ "$want" != "$got" ]; then
    printf 'MISMATCH %s\n  want: %s\n  got : %s\n' "$url" "$want" "$got"
    fail=$((fail + 1))
  fi
done < <(find "$DIST" -name index.html)

[ "$fail" -eq 0 ] && echo "OK: all pages match" || echo "$fail page(s) wrong" >&2
exit $(( fail > 0 ))

三個刻意的決定

沒有 set -e驗證腳本的價值在於跑完,然後給你一份完整的壞掉清單。第一個不符就中止,你只會知道有問題,不會知道有多少、在哪裡——那樣還得再跑一次才能規劃修補。

失敗時印出 want 和 got。這兩行直接告訴你是哪一種壞法:got 是首頁的標題就是目錄 key 問題,got 是這一頁的標題就是部署沒生效。診斷資訊要在錯誤訊息裡,不要留給下一輪調查。

curl -f 讓 4xx/5xx 直接算失敗。title 比對負責抓「回 200 但內容錯」,狀態碼失敗則由 -f 接住——兩種都要涵蓋。

再加一條:build 的身分

title 比對能證明「這一頁的內容對」,但如果一篇舊文章的內容從頭到尾沒變,它也會通過——即使這次部署完全沒生效。補上一個從 build 產生的識別碼就能封住這個缺口:

<meta name="build-sha" content="dea001c" />
EXPECTED=$(git rev-parse --short HEAD)
ACTUAL=$(curl -fsS "$BASE/" | sed -n 's/.*name="build-sha" content="\([^"]*\)".*/\1/p')
[ "$ACTUAL" = "$EXPECTED" ] || { echo "stale build: $ACTUAL != $EXPECTED" >&2; exit 1; }

這跟後端的 /version 是同一件事的靜態站版本:那個值必須由 build 產生,不能由人維護。

什麼時候跑

兩個時機,理由不同。

部署腳本的最後一步。放在 invalidation 之後——太早跑會讀到邊緣節點的舊快取,得到假的失敗,而假失敗比沒有檢查更糟,因為它會訓練你忽略這個檢查。

每天排一次。這條比較不直覺,但更重要:有些壞法是後來才發生的。下一次 sync --delete 會把上一輪補好的目錄 key 再刪一次,而那次部署的當下是通過的。只在部署時檢查,你抓不到這種。

我遇過的最貴一次是這樣:AWS session 在補目錄 key 的迴圈跑到一半過期,前面的檔案都同步完成,幾個頁面直接消失。腳本用了 set -e,所以連後面的 invalidation 都沒跑。當下沒有人發現,因為沒有東西在比對內容。

不要比對整頁

一個容易走過頭的方向是「乾脆比對整份 HTML 的雜湊」。不要——線上的頁面幾乎一定會跟本機有細微差異:注入的分析腳本、資產檔名的雜湊、伺服器加的註解。整頁比對會每天給你一堆假警報,而假警報的下場是這個檢查被關掉。

指紋要選穩定且有鑑別力的東西。title 是最好的起點;有需要再加上 <h1> 或 canonical,但每加一項就多一分假警報的機率。

如果只記得一件事

「部署完成」應該是一個可以驗證的斷言,不是腳本沒噴錯的副作用。 而在靜態站上,唯一夠強的驗證是把線上內容抓回來跟你手上那份比。三十行的事。

修訂紀錄

  1. 首次發布