一鍵發版背後:三個 repo 的分工
發版流程,Docker build,版本管理
Ricky-
07 月 09,2026

一個按鈕,牽動三個 repo

fleet-console 的服務列表上,每個服務右邊都有一顆「發版」按鈕。 點下去會跳出三個選項——修補、小改版、大改版——每個選項旁邊直接顯示算好的版本號。

從使用者的角度,這是一次點擊。從系統的角度,這次點擊會依序穿過三個 repo, 而每個 repo 都刻意不知道另外兩個在做什麼。這篇文章要拆開的就是這條路徑。

三個 repo 的職責

  • fleet-console(發版 UI):只送出「要往哪個方向加」,不碰版本號。
  • clelereve_ops_backend(部署後端):讀 git tag 自動加一、build image、push registry、打 tag。
  • 目標服務(如 clelereve_blog_front):在 Dockerfile 用 ARG 被動接收版本號。

前端只送一個字串

前端那顆按鈕背後的程式碼短得有點反高潮:

async function doRelease(bump: 'major' | 'minor' | 'patch') {
  const r = await fetch(`/api/services/${releaseSvc.id}/release`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ bump, by: me?.name }),
  })
  const { id } = await r.json()
  setLogId(id ?? null)
}

整個 request body 只有兩個欄位:bump 說要往哪個位數加,by 記錄是誰按的。 沒有版本號。

這是刻意的。前端算不出正確的版本號——它不知道 registry 上最新的 tag 是什麼, 也不知道剛才有沒有人從別的瀏覽器發過版。如果讓前端算,兩個人同時開著頁面時就會算出同一個號碼。 版本號的唯一真相來源是 git tag,而 git tag 只有部署後端摸得到。

後端才是真正做事的人

clelereve_ops_backend 收到 bump 之後,第一件事是去讀最新的 git tag, 解析出 x.y.z,然後照 semver 的規則加一:

const bumpVer = ([a, b, c]: [number, number, number], bump: Bump): string =>
  bump === 'major' ? `${a + 1}.0.0`
  : bump === 'minor' ? `${a}.${b + 1}.0`
  : `${a}.${b}.${c + 1}`

注意 major 會把後兩位歸零、minor 會把最後一位歸零。 這條規則寫在後端的一行三元運算式裡,而不是散在前端的三顆按鈕上。

算出版本號之後,跑一次 docker build,把版本號當作 build argument 灌進去:

const buildArgs = [
  'build', ...df,
  '--build-arg', `APP_VERSION=${ver}`,
  '-t', imageVer,
  ...(latest ? ['-t', `${spec.image}:latest`] : []),
  spec.buildContext,
]

這裡有個容易被忽略的細節:--build-arg 是無條件帶的。 如果目標服務的 Dockerfile 沒有宣告 ARG APP_VERSION,Docker 只會忽略它,不會報錯。 這讓部署後端可以用同一套指令對付所有服務,不必為「這個服務要不要版本號」寫分支。

build 成功才 push,push 成功才打 tag。順序不能顛倒——先打 tag 再 build, build 失敗你就得到一個指向不存在 image 的 tag:

if (await run('docker', ['push', imageVer], onChunk) !== 0) {
  onChunk?.('✗ push 失敗\n')
  return { ok: false, version: tag }
}
// 4. git tag + push(都成功才打 tag)
if (await run('git', ['-C', spec.gitDir, 'tag', '-a', tag, '-m', tag], onChunk) !== 0) {
  onChunk?.('✗ git tag 失敗(可能已存在)\n')
  return { ok: false, version: tag }
}

目標服務只負責「收」

第三個 repo 是被發版的對象。它要做的只有一件事:在 Dockerfile 裡宣告自己接受這個參數。

ARG APP_VERSION=dev
ENV NEXT_PUBLIC_APP_VERSION=$APP_VERSION
RUN npm run build

這裡的行號順序是有意義的。Next.js 的 NEXT_PUBLIC_*build 時被 inline 進 bundle 的,所以 ENV 必須寫在 RUN npm run build 之前,否則 build 當下讀不到,版本號永遠是 dev

Django 那邊就寬鬆得多,因為它是 runtime 讀環境變數:

APP_VERSION = os.environ.get("APP_VERSION", "dev")

位置不拘,甚至可以在 docker run -e APP_VERSION=1.2.3 時當場覆蓋。 同一個 --build-arg,在前後端的行為完全不同——這件事值得單獨寫一篇。

「發版」和「重新部署」不是同一件事

這是我自己一開始沒分清楚、後來吃過虧的地方。兩個按鈕長得很像,做的事天差地遠:

  • 發版docker builddocker pushgit tag。 產生一個新的 image 版本,會動到 registry 和 git。
  • 重新部署docker pulldocker rm -fdocker run -d。 拉一個已經存在的 tag 起容器,完全不碰 registry 內容。

部署那條路徑的核心其實只有一行:

docker pull ${ref} && (docker rm -f ${container} || true) && \
docker run -d --name ${container} ${runArgs} ${envFlags} ${ref}

rm -f 後面掛 || true,是因為第一次部署時容器根本不存在, rm 會回非零值把整條指令中斷掉。

分清楚之後,很多決策就自然了:改了程式碼要「發版」;只是想回滾到舊版本, 或者換個環境變數重啟,那是「重新部署」,選一個既有的 tag 就好,不需要浪費一次 build。

為什麼要拆成三個 repo

你當然可以把這三件事塞進一個 script。我一開始就是這樣。問題在於權責邊界會跟著消失:

  • 前端一旦能決定版本號,就有人會手動填一個「暫時的」版本號上去。
  • 目標服務一旦知道 registry 位址,它就跟你的部署基礎設施綁死了。
  • 部署後端一旦知道「blog 前端要跑 npm build」,每加一個服務就要改它一次。

現在的切法讓每個 repo 都只依賴一個很窄的介面:前端依賴 { bump } 這個字串、目標服務依賴 ARG APP_VERSION 這個名字。 介面窄,就代表你可以單獨換掉任何一邊。

下一篇

image 都 push 上去了,容器也 docker run 起來了, docker ps 看起來一切正常。然後我打開瀏覽器, 前端完全連不到 API——而錯誤訊息說的是 CORS。

問題其實出在請求根本沒走到容器裡。下一篇談 nginx 怎麼在 Docker 網路裡找到你的容器。

Tags:
一鍵發版Docker版本管理


相關文章
從零開始製作一個ip查詢網站-第一章 React 開發環境的準備
chris - 2025-11-24T11:28:35Z
從零開始打造 IP 查詢網站,本章節教你如何準備 React 開發環境,包括安裝 Node.js、使用 Vite 建立專案、安裝依賴套件並啟動開發伺服器。詳細說明每個步驟與常見坑點,幫助初學者快速上手...
把流程寫進工具,而不是寫進腦袋
ricky - 2026-07-09T02:05:00Z
版本號手改的日子裡,我踩過三種坑,而它們都不是技術問題,是知識存放位置的問題。從自動化、預設值到「移除選項」,談談一人工作室為什麼也需要工程文化,以及工具擋不住的那一類錯。
現實的挑戰:為了生存,我不得不開始接案
ricky - 2025-05-01T14:28:41Z
創業的夢想雖然美好,但當你真正踏入這個領域時,現實的挑戰與費用壓力讓人無法忽視。在這篇文章中,分享了從開設公司到開始接案的心路歷程。從一開始的迷茫與不安,到逐步找到穩定的收入來源,接案成為了生存下去的...
訂閱電子報
隨時掌握最新的創業故事與設計開發過程
內容包含來自麒航團隊的日常分享、專業知識與工具推薦,助你了解更多創業背後的思維與技術。
麒航私房推薦
Clelereve Blog LogoClelereve Blog LogoClelereve Blog Logo
廣告區
Clelereve Blog Logo
聯絡我們
任何合作或問題請洽:
clelereve@gmail.com
Follow us
Copyright © 2025 blog.clelereve.com - All Rights Reserved
Frontend Version: -- | Backend Version: --