DAY 143 · 2026-05-23

文章發完,個人站自動跟著上線

SLIDES · 6
Day 143 slide 1Day 143 slide 2Day 143 slide 3Day 143 slide 4Day 143 slide 5Day 143 slide 6
1 / 6

今天分享 100days 跟 dawsonwang.com 怎麼串起來。

100days 是我本地的專案目錄,每一天的文章、相關的 AI pipeline 都在這。後來作了自己的官網 dawsonwang.com,動機很單純——哪一天社群帳號被封鎖了,內容至少還在自己手上。

兩個 repo 目的不同,但又共用同一批文章內容,於是要解的問題是:

文章發到 100days 之後,怎麼讓 dawsonwang.com 自動跟著上線?

最後選了一條最小耦合的設計:100Days 完全不知道 dawsonwang.com 存在,所有同步邏輯只活在 dawsonwang.com 自己這邊,靠 GitHub Actions 每 15 分鐘輪一次(劇透:實際是 1~3 小時,後面會講)。100Days 的發布腳本一行都沒改、沒有任何 webhook、也沒有 Vercel deploy hook URL 藏在程式碼裡。

設計:兩 repo + git submodule

  • andrew54068/100Days(私有 repo)= 內容真實來源,所有 content/dayNN/ 都在這
  • andrew54068/dawsonwang.com(Astro on Vercel)= 對外站,把 100Days 整個當 git submodule 掛在自己的 100days/ 目錄底下

Astro build 的時候從 submodule 讀 markdown,把 slides 圖片拷到 public/,然後正常產靜態站。Vercel 本來就 watch dawsonwang.com 的 main branch,只要這個 repo 有新 commit,就會自動 build & deploy——這條 Vercel 內建的行為,是整套設計能這麼乾淨的關鍵。

剩下唯一要解的事:讓 submodule pointer 在 100Days 有新 commit 時自動 bump,並推回 dawsonwang.com。一旦做到這件事,Vercel 自然就會接手。

核心:一個 cron workflow

dawsonwang.com/.github/workflows/sync-100days.yml

on:
  schedule:
    - cron: '*/15 * * * *'   # 每 15 分鐘輪一次
  workflow_dispatch:          # 也可手動觸發

concurrency:
  group: sync-100days
  cancel-in-progress: false   # 兩個 cron 撞期不互殺

每 15 分鐘執行五步:

  1. Checkout dawsonwang.com(先不展開 submodule,等下用自己的 SSH key)
  2. 裝 SSH deploy key(從 DAY100_DEPLOY_KEY secret 載入),並把 HTTPS clone URL rewrite 成 SSH——這樣才能讀私有的 100Days
  3. 比對指針
    • OLD_SHA = git rev-parse :100days(當前 submodule 釘住的 SHA)
    • NEW_SHA = git ls-remote refs/heads/main(upstream 最新 SHA)
    • 一樣 → 整個 job 直接跳過後面兩步
  4. Bump submodulegit submodule update --init --remote 100days
  5. github-actions[bot] 身份 commit + push
    chore(content): bump 100days submodule (OLD → NEW)
    

第 5 步那個 push 就是 Vercel 的觸發點。整鏈走完理論上最壞 15 分鐘——對個人站每日一次的更新頻率綽綽有餘。

為什麼選 pull 不選 push

最直覺的設計是 push-based:100Days 發完後 fire webhook 通知 dawsonwang.com,0 秒延遲。但這條路要 100Days 存對方 token、要知道對方存在;以後想多開 mirror(英文版、newsletter),100Days 又要再塞 webhook,對方砍掉換新的也要跟著改。

Pull 反過來:100Days 完全不知道誰在訂閱它,要加 N 個 mirror 就讓每個 mirror 自己長 cron,互不干擾;要關掉同步 disable 一個 workflow 就好。代價是 15 分鐘延遲,但個人站沒人在等這 15 分鐘。

為什麼第 3 步是關鍵

沒有 SHA 比對,cron 每跑一次就無腦 bump 一次,commit log 跟 Vercel build 都會被灌爆。加了比對之後整個 workflow 變 idempotent——沒新東西就走完前 3 步結束,0 commit、0 build。對 cron 來說這條是必要的設計。

等等,這個 cron 真的有每 15 分鐘跑嗎

寫到這裡我順手拉了實際 workflow runs 紀錄,嚇到——

2026-05-23 22:12:44
2026-05-23 20:40:26  ← 上一次相隔 1h 32min
2026-05-23 19:33:39  ← 1h 7min
2026-05-23 18:03:28  ← 1h 30min
2026-05-23 16:03:04  ← 2h
2026-05-23 13:46:52  ← 2h 16min
2026-05-23 10:03:45  ← 3h 43min
2026-05-23 07:55:44  ← 2h 8min

cron: '*/15 * * * *' 寫得清清楚楚,實際間隔卻是 1~3 小時,幾乎沒有一次接近 15 分鐘。

這不是 bug,是 GitHub Actions 把 schedule 跟其他觸發放在不同的 queue:

觸發類型 Queue 延遲
push / workflow_dispatch / repository_dispatch 事件 queue(高優先) 秒級
schedule(cron) 共享背景 queue(低優先) 看全球負載,高頻 cron 會被 skip 不補跑

官方文件原話:"scheduled workflows may be delayed during periods of high load"。community 共識也很一致:*/15 在 GH Actions 上跑不準是常態,不是 bug

慢的不是 GitHub Actions 本身,是 schedule 那條 queue。換掉觸發來源,瓶頸就消失。

那我會怎麼換

既然家裡那台 MBP 本來就 always-on(跟 Vexa 共處一機那台),最直接的解法是把 cron 從 GitHub 搬到本地——一個 launchd plist 每 1~2 分鐘跑這段 bash:

#!/bin/bash
NEW=$(gh api repos/andrew54068/100Days/commits/main --jq .sha)
OLD=$(gh api repos/andrew54068/dawsonwang.com/contents/100days --jq .sha)
[ "$NEW" != "$OLD" ] && gh workflow run sync-100days.yml -R andrew54068/dawsonwang.com

延遲從幾小時掉到 1~2 分鐘。100Days repo 一行都不用動,sync workflow 仍是接收方——只是觸發來源從 GH schedule 換成本地 cron 經由 workflow_dispatch fire。

一句話總結

兩個 repo + 一個 submodule + 一個觸發器。沒有 webhook、沒有 deploy hook、沒有 access token 從 100Days 那邊飛過來。整套的耦合面只剩 dawsonwang.com 這邊一個 workflow——關了它,同步就停,100Days 那邊不會察覺。

GH schedule 教我的事:把同步邏輯集中在一邊的好處,不只是 100Days 保持乾淨——換觸發來源也只動一個 workflow,sync 邏輯跟 100Days repo 一行都不用動。

少一個元件,就少一個會壞的東西。

延伸閱讀
看完整 165 篇 →