iT邦幫忙

2026 iThome 鐵人賽

DAY 24
0

! 本篇文章將會介紹 進度追蹤:30 天作戰地圖,期望大家都能不架任何監控平台,用狀態檔和幾十行 bash 一眼看清完賽進度 :D

昨天(D23)把重試鏈、備稿、告警三層保命機制補完之後,這套系統已經會自己出貨、自己擦屁股、自己叫人了。但每晚看著 Discord 跳出綠色告警,我心裡還是有一個問題:它現在到底跑得怎麼樣? 已經打到第幾天、還剩幾發彈藥、斷更的風險離我多遠?

標準答案是裝個 Grafana 之類的監控儀表板。但這個系列的主題是耍廢——能讀檔案就不要架服務。而且真相其實早就躺在 state/ 目錄裡了,今天只是把它畫出來。

本篇目標

讀完這篇你會學到:

  • 系統裡已經存在的兩種狀態檔:published.log 和 published-*.flag 各自記了什麼、誰在寫、誰在讀
  • 用 20 行 bash 把狀態檔變成進度條與 30 格作戰地圖
  • 儀表板的第一課:先驗證儀表板本身,再相信它說的話

環境準備

延續 D22 的管線,跑了一段時間之後,state/ 裡已經有東西:

$ ls state/
published-20260912.flag   # d02 發佈成功的存在證明(空檔案,只看有沒有)
published-20260913.flag
# ...(一天一個,到昨天為止共 22 個)
published.log             # 每次成功發佈追加一行:日期|天數|文章 URL

其他需求非常樸素:

  • macOS 內建 bash 與 BSD date,不用另外裝任何東西
  • config.sh 裡的 START_DATE 與 TOTAL_DAYS——進度腳本直接沿用,不另設常數

主要內容

步驟一:真相早就寫好了——狀態檔盤點

自動化系統最怕「為了做儀表板,再養一套系統」。好消息是這條管線從第一天起就是「事件發生當下順手記一筆」的設計。publish.sh 在確認發佈成功(輪詢到 URL 變成 /articles/10xxxx)之後做了兩件事:

# publish.sh:發佈成功當下順手記帳(append-only)
touch "$ROOT/state/published-$(date +%Y%m%d).flag"                # 存在證明:給機器看
echo "$(date +%F)|${DD}|${AFTER}" >> "$ROOT/state/published.log"  # 明細:給人看

兩個檔案,兩種讀者:

  • flag 給機器讀:20:15 的 republish job 重跑 publish.sh 時,開頭的冪等守衛看到 flag 存在就直接跳過(D23 講過的重跑安全保證)。它只回答一個是非題:「今天發過了沒?」
  • log 給人讀:一行代表一次成功發佈,附日期與文章網址,回答的是「哪天發了什麼、發到哪裡」。

設計原則一句話:append-only、事實單一來源。「發佈成功」這個事件發生時寫一次,之後不管是冪等檢查、補發判斷,還是今天的進度儀表板,都只是換個角度讀同一份事實,不需要另外維護一套「進度系統」。

另一條路是反過來做:直接去 iThelp 的系列頁爬自己發過幾篇。能動,但代價是儀表板從此依賴外部網站的頁面結構——網站一改版、尖峰時段一變慢,儀表板就跟著抖。狀態檔在自己家裡,讀它零網路、零依賴、零風險。這就是我說「真相早就寫好了」的意思。

步驟二:20 行 bash 畫出進度條

有了狀態檔,儀表板就只是字串處理。progress.sh 的核心:

#!/bin/bash
# progress.sh:讀狀態檔、畫進度。純唯讀,跑幾次都不會弄髒系統
ROOT="/Users/benben/ai/automations/ironman"
source "$ROOT/config.sh"   # START_DATE、TOTAL_DAYS 從這裡來

# 今天是第幾天(與 generate.sh 同一套算法)
TODAY=$(date +%Y-%m-%d)
DAY=$(( ($(date -j -f "%Y-%m-%d" "$TODAY" +%s) \
       - $(date -j -f "%Y-%m-%d" "$START_DATE" +%s)) / 86400 + 1 ))

# 已發佈天數:published.log 一行 = 一次成功發佈
DONE=$(wc -l < "$ROOT/state/published.log" | tr -d ' ')
PCT=$(( DONE * 100 / TOTAL_DAYS ))

# 進度條:20 格,█=走過的路 ░=剩下的賽程
FILLED=$(( DONE * 20 / TOTAL_DAYS ))
BAR=$(printf '█%.0s' $(seq 1 "$FILLED"))$(printf '░%.0s' $(seq $((FILLED+1)) 20))
echo "鐵人賽進度:${BAR} ${DONE}/${TOTAL_DAYS} 天(${PCT}%)"

輸出長這樣:

鐵人賽進度:██████████████░░░░░░ 22/30 天(73%)

兩個「沿用而非重造」的細節:算天數的公式跟 generate.sh 完全同一套(START_DATE 的 epoch 差除以 86400 加 1),TOTAL_DAYS 也是直接 source config.sh。儀表板和被儀表的系統必須共用同一份常數,否則遲早出現「儀表板以為賽期是 35 天」這種平行宇宙。

小小小測驗:你知道為什麼腳本算出來是 22 天,但這個系列明明已經發了 23 篇嗎?(答案在踩坑記錄第一條,這個坑我踩得非常踏實 :D)

步驟三:30 格作戰地圖——把 outline 疊上來

進度條回答「還剩多少」,但鐵人賽真正的問題是「哪幾天打下了、今天在哪、接下來是什麼」。所以再往下畫一張 30 格的地圖:

# 30 格作戰地圖:● 已發佈、◉ 今天、· 未來的戰場
MAP=""
for d in $(seq 1 "$TOTAL_DAYS"); do
  if grep -q "|$(printf '%02d' "$d")|" "$ROOT/state/published.log"; then MAP+="●"
  elif (( d == DAY )); then                                            MAP+="◉"
  else                                                                 MAP+="·"
  fi
done
echo "作戰地圖:${MAP}"

# 今天與明天的主題(outline.md 格式:DD|標題|摘要)
TDD=$(printf '%02d' $(( DAY + 1 )))
awk -F'|' -v d="$DD"  '$1==d {print "今天(d" $1 "):" $2}' "$ROOT/outline.md"
awk -F'|' -v d="$TDD" '$1==d {print "明天(d" $1 "):" $2}' "$ROOT/outline.md"

完整輸出(就是寫這篇的當下跑出來的):

鐵人賽進度:██████████████░░░░░░ 22/30 天(73%)
作戰地圖:·●●●●●●●●●●●●●●●●●●●●●◉······
今天(d24):進度追蹤:30 天作戰地圖
明天(d25):日誌與稽核:自動化系統的黑盒子

一眼看完戰況:22 個 ● 是已經打下的城,◉ 是今天——也就是此刻正在生成的這篇,剩下六個 · 是還沒來的戰場。至於最左邊那個孤零零的 · 是歷史遺跡,踩坑記錄見。

順帶一提,「今天(d24)/明天(d25)」那兩行不是裝飾。outline.md 是整個系統的唯一事實來源,地圖直接從它抓標題,所以「今天該寫什麼、明天會是什麼」我也不用另外記——大綱改了,地圖自動跟著變。

這支腳本純唯讀:不寫檔、不碰網路、跑幾次輸出都一樣,跟 publish 那種有副作用的腳本完全相反,所以可以隨手跑、跑心安的。想每天在 Discord 看戰況的話,接上 D13 的通知腳本一行就搞定:

# 發文 job 收工前順手回報進度(discord-notify.sh 見 D13)
MSG="$(/bin/bash "$ROOT/scripts/progress.sh")"
"$NOTIFY" -t "鐵人賽進度" -s ok "$MSG"

常見問題 / 踩坑記錄

  • Q:為什麼進度條顯示 22/30,但系列明明已經發了 23 篇?
    A:因為 published.log 這個機制是 d02 才加上去的——第一天發文時還沒有狀態檔,d01 就這樣消失在歷史裡(就是地圖最左邊那個 ·)。狀態檔跟著系統一起演化,起點常常不齊。解法很簡單:手動回補一行(echo "2026-09-11|01|<文章URL>" >> state/published.log),或者知道誤差存在、心裡有數。真正的教訓是:儀表板做好後,第一件事是拿已知事實驗證它,不然你只是在看一部畫得很漂亮的科幻小說。

  • Q:為什麼讀 published.log,不直接 ls articles/*.md | wc -l 數文章檔案?
    A:因為它們是兩種真相。articles/ 下的 .md 是「生成成功」的證據,不是「發佈成功」的證據——生成成功但發佈失敗的日子,檔案在、文章卻沒出去。更別提 state/ 裡還躺著 quarantine-d09-*.md 這種被機敏掃描隔離的檔案,直接數檔案一定會被騙。真相分層:articles=生成、flag=發佈與否、log=發佈+URL。要看哪一層真相,就讀哪一層的檔案。

  • Q:腳本裡的 date -j -f 是什麼寫法?網路上的範例都是 date -d?
    A:這是 macOS 的坑。內建的是 BSD date,解析日期字串要寫 date -j -f "%Y-%m-%d" "$TODAY" +%s;Linux(GNU date)才是 date -d "$TODAY" +%s,照抄網路範例會直接噴錯。順帶一提這也是 D11 的延伸:如果哪天想把進度腳本也交給 launchd 排程,記得它一樣沒有你的 shell rc,PATH 要自己掛。

小結

  • 狀態檔是唯一事實:發佈成功當下 append 一行 log、touch 一個 flag,儀表板只是換角度讀它,不要養第二套真相
  • 20 行 bash 就夠:BSD date 算天數、grep 對照 log 畫 30 格地圖,常數直接 source config.sh,跟主系統共用
  • 先驗證儀表板本身:d01 沒進 log 的教訓——不知道儀表板哪裡不準,比沒有儀表板更危險

明日預告

下一篇我們要介紹 日誌與稽核:自動化系統的黑盒子,敬請期待!進度儀表板看的是「現在」,明天往回看——logs/ 裡那幾十個檔案怎麼分層、flight recorder 思維怎麼讓事後除錯不靠通靈。

參考資料:

有任何疑問但沒有 iT 邦幫忙帳號,或是想匿名提問?
歡迎到 https://dev.benben.me/q/Z5442T 提問或加油打氣,沒意外的話會在完賽之後一起回答 :D


上一篇
23 備援設計:斷更即淘汰的保命機制
下一篇
25 日誌與稽核:自動化系統的黑盒子
系列文
自我耍廢組:全自動化の鐵人 共 26 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言