前言:Tags (173) 的紅燈海

打開 Jenkins 的 Multibranch Pipeline 頁面,映入眼簾的是一整排紅色叉叉——173 個 tag,幾乎全是失敗的歷史建置記錄。這些 tag 從 dev-0.0.74+143 一路排到 dev-0.0.74+181,光是同一個版本就堆了 39 個。

三個 repo(Flutter、Strapi、Vue)加起來超過 1000 個 tag。這不只是視覺上的噪音,更是 Jenkins 每次 scan 都要花時間處理的負擔。


核心觀念:Jenkins 上的 Tag 不是 Jenkins 的

第一個直覺可能是「到 Jenkins 上刪」,但這是錯的。

Jenkins Multibranch Pipeline 的運作方式是定期掃描 Git repository,把發現的 branch 和 tag 當作獨立的 pipeline 來建置。換句話說,Jenkins 頁面上看到的 tag 就是 Git remote 上的 tag——Jenkins 只是一面鏡子。

Mermaid Diagram

為什麼不能從 Jenkins 刪? 因為即使你在 Jenkins UI 手動移除某個 tag 的建置記錄,下一次 scan 時 Jenkins 發現 remote 上 tag 還在,又會重新建立。要斬草除根,必須從 Git remote 下手。


清理策略:保留最新,刪除冗餘

以 Flutter repo 為例,dev-0.0.74 版本產生了 39 個 tag(從 +143+181),代表這個版本被建置了 39 次。但真正有意義的只有最後一個成功的版本。

清理策略很直覺:每個版本系列只保留最新的一個 tag,其餘全刪。

第一步:產生刪除清單

關鍵是如何自動識別「同一版本系列」並找出最新的 tag。以下腳本會遍歷所有 dev tag,找出每個版本前綴(如 dev-0.0.74)下除了最新以外的所有 tag:

git tag -l "dev-*" | while read tag; do
  # 提取版本前綴(去掉 +buildNumber)
  prefix=$(echo "$tag" | sed 's/+[0-9]*$//')

  # 找出同前綴的所有 tag,按 build number 排序
  siblings=$(git tag -l "${prefix}+*" | sort -t'+' -k2 -n)
  count=$(echo "$siblings" | wc -l | tr -d ' ')

  # 只有存在多個 tag 時,才標記非最新的為待刪除
  if [ "$count" -gt 1 ]; then
    latest=$(echo "$siblings" | tail -1)
    if [ "$tag" != "$latest" ]; then
      echo "$tag"
    fi
  fi
done | sort -u > /tmp/tags_to_delete.txt

為什麼先產生清單再刪? 這是防禦性做法。刪除 remote tag 是不可逆的操作,先輸出清單讓自己確認,比直接 pipe 到 git push 安全得多。

第二步:批次刪除遠端與本地

確認清單沒問題後,一次性刪除:

# 刪除遠端 tag(關鍵步驟)
cat /tmp/tags_to_delete.txt \
  | sed 's|^|:refs/tags/|' \
  | xargs git push origin

# 刪除本地 tag
cat /tmp/tags_to_delete.txt | xargs git tag -d

sed 's|^|:refs/tags/|' 這行把每個 tag 名稱轉換成 :refs/tags/tag-name 的格式——這是 git push 的刪除語法。冒號前面為空代表「用空內容推送到這個 ref」,等於刪除。

第三步:觸發 Jenkins 重新掃描

刪完 tag 後 Jenkins 不會立刻反映變化,需要手動觸發:

進入 Jenkins job → 點擊左側的 Scan Multibranch Pipeline Now

如果有設定 webhook(GitLab/GitHub),push 事件會自動觸發掃描,不需要手動操作。


不同 Repo 的清理差異

實務上三個 repo 的 tag 命名方式不同,清理策略也要調整:

RepoTag 格式特徵策略
Flutterdev-0.0.74+181版本+build number每版本保留最新
Strapidev-0.212遞增版號全刪 / 保留最新 N 個
Vuedev-0.308遞增版號全刪 / 保留最新 N 個

Strapi 和 Vue 的 tag 沒有 +buildNumber 的重複問題,但 307 個遞增版號也是一種浪費。這種情況可以改用「保留最新 N 個」的策略:

# 保留最新 10 個 dev tag,其餘刪除
git tag -l "dev-*" | sort -V > /tmp/all_dev.txt
total=$(wc -l < /tmp/all_dev.txt | tr -d ' ')
keep=10
delete=$((total - keep))
head -${delete} /tmp/all_dev.txt > /tmp/tags_to_delete.txt

sort -V 是版本排序(version sort),會正確處理 dev-0.9 排在 dev-0.10 前面的情況,比字典排序準確。


實際清理成果

三個 repo 的清理結果:

Repo清理前清理後刪除數
Flutter (dev/stg/prod)30841267
Strapi (dev/stg/prod)54640506
Vue (dev/stg/prod)59241551
合計14461221324

清完之後 Jenkins 的 Tags 頁面從一片紅海變成乾淨的清單,scan 速度也明顯加快。


未來預防:要不要關掉 Discover Tags?

Jenkins Multibranch Pipeline 預設會開啟 Discover Tags,這是那些 tag 出現在 Jenkins 上的原因。如果你的部署流程不依賴 Jenkins 自動建置 tag,可以直接關掉:

Branch Sources → Behaviours → Discover Tags → 設為 None

但如果你的 CI/CD 就是靠 git tag 觸發部署(像我們用 dev-* / stg-* / prod-* tag 來區分環境),那 Discover Tags 必須保留。這種情況下,定期清理就是必要的維護工作。

一個實用的做法是在 CI/CD pipeline 裡加一個定期清理的 job,自動刪除超過 30 天的 dev tag。這樣就不需要手動大掃除了。


結語

Git tag 的累積是一個典型的「技術債悄悄長大」的案例——每次部署只加一個 tag,看似無害,但幾個月後就是上千個。關鍵的認知是:Jenkins 只是 Git 的鏡像,要清理鏡中的倒影,必須清理真實的物件。