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






今天分享 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 分鐘執行五步:
- Checkout dawsonwang.com(先不展開 submodule,等下用自己的 SSH key)
- 裝 SSH deploy key(從
DAY100_DEPLOY_KEYsecret 載入),並把 HTTPS clone URL rewrite 成 SSH——這樣才能讀私有的 100Days - 比對指針:
OLD_SHA = git rev-parse :100days(當前 submodule 釘住的 SHA)NEW_SHA = git ls-remote refs/heads/main(upstream 最新 SHA)- 一樣 → 整個 job 直接跳過後面兩步
- Bump submodule:
git submodule update --init --remote 100days - 以
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 一行都不用動。
少一個元件,就少一個會壞的東西。