- Python 98.9%
- Shell 1.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| examples | ||
| tests | ||
| .gitignore | ||
| action.yaml | ||
| DESIGN.md | ||
| image_pr.py | ||
| README.md | ||
| requirements.txt | ||
| run.sh | ||
forgejo-k8s-image-pr
在 app 發布 image 後,向同一 Forgejo instance 的 Kubernetes repo 建立更新 PR。 Manifest 鎖定 digest,每次新版本建立獨立 PR;成功建立後關閉同部署目標的舊 bot PR, 不改寫舊 PR 內容或分支。合併與部署維持人工操作。
完整設定指南
設定順序為 runner → action 發布 → bot 權限 → app secrets → workflow → 發布驗收。
所有指令依所在環境分開執行。範例中的 forgejo.example.com、/path/to/... 與
<...> 都是佔位值,執行前請替換;workflow 的 uses URL 必須直接填入實際位置。
| 佔位值 | 請替換為 |
|---|---|
forgejo.example.com |
Forgejo instance 網域 |
<ssh-user>、<runner-host> |
Runner 主機的 SSH 登入帳號與位址 |
<runner-user> |
Runner 服務實際執行帳號,可能與 SSH 帳號不同 |
<action-owner> |
存放共用 actions 的使用者或 organization |
<manifest-owner>/<manifest-repo> |
Kubernetes manifests 的目標 repo |
/path/to/... |
對應 repo 的本機 checkout 路徑 |
Workflow 使用 app 作為 Deployment/container 範例名稱、default 作為 namespace;
請依目標 manifest 調整。
1. 補齊 runner 依賴
需求:Linux、Bash、Git、Python 3.10+、venv/pip,以及可存取 Forgejo HTTPS 與
Python 套件來源的網路。Build action 另外需要 Docker、flock、sha256sum、sed、tee。
以下安裝指令適用 Debian/Ubuntu;其他發行版請使用對應套件管理工具。 請先確認 runner 服務的執行帳號,後續驗證使用該帳號,而非 SSH 登入帳號。
ssh '<ssh-user>@<runner-host>'
sudo apt-get update
sudo apt-get install -y python3-venv
使用實際 runner 帳號驗證,套件只安裝到暫存 venv:
sudo -u '<runner-user>' -H bash <<'SH'
set -euo pipefail
check_dir=$(mktemp -d)
trap 'rm -rf -- "$check_dir"' EXIT
python3 -m venv "$check_dir/venv"
"$check_dir/venv/bin/python" -m pip install \
--disable-pip-version-check 'ruamel.yaml==0.19.1'
"$check_dir/venv/bin/python" -c \
'from ruamel.yaml import YAML; print("Python dependency OK")'
docker info --format '{{.ServerVersion}}'
SH
預期看到 Python dependency OK 與 Docker server 版本。安裝 venv 通常不需重啟 runner,
也不需要給 runner 服務帳號 sudo 權限。Runner 可設定以下 host executor label:
labels:
- 'docker-build:host'
確認 app repo 的 Actions 已啟用,且可使用這個 runner。
2. 確認 action 版本已發布
本指南使用:
| Action | 版本 | 功能 |
|---|---|---|
<action-owner>/forgejo-docker-build-push |
v1.1.0 |
提供 digest、image-ref outputs |
<action-owner>/forgejo-k8s-image-pr |
v1.0.0 |
建立 manifest PR |
若遠端已有這些版本,直接確認 tags,跳過建立與發布。不要覆寫已發布的 tag。 Action repo 必須能由 app workflow 下載;private repo 需確認跨 repo 讀取權限。
若自行託管 action,先在指定 owner 下建立空的 action repo,不要初始化 README。 以下在開發機執行;SSH URL 請使用 Forgejo clone 頁面提供的實際位址與連接埠:
cd /path/to/forgejo-k8s-image-pr
# 僅在尚未設定 origin 時執行:
git remote add origin 'ssh://[email protected]/<action-owner>/forgejo-k8s-image-pr.git'
# 確認 HEAD 是要發布的 commit;僅在 tag 尚不存在時建立。
git tag -a v1.0.0 -m "Release v1.0.0"
# 將 main 與 tag 分別推送。
git push -u origin main
git push origin refs/tags/v1.0.0
Build action 必須包含 digest outputs。確認 checkout 的 HEAD 是要發布且已驗證的 commit,
僅在 v1.1.0 尚不存在時建立:
cd /path/to/forgejo-docker-build-push
git tag -a v1.1.0 -m "Release v1.1.0: immutable image digest outputs"
git push origin main
git push origin refs/tags/v1.1.0
確認遠端版本:
git -C /path/to/forgejo-k8s-image-pr \
ls-remote --tags origin refs/tags/v1.0.0
git -C /path/to/forgejo-docker-build-push \
ls-remote --tags origin refs/tags/v1.1.0
也可在 workflow 固定已發布的完整 commit SHA。
3. 建立專用 bot 與 PAT
建立一般帳號,例如 k8s-deploy-bot。在 目標 manifests repo 的 Settings → Collaborators
給它 Write 權限,或用只包含此 repo 的 organization team 授權。
不需要 instance admin、organization owner 或 repo admin。確認目標 repo 已啟用 Pull Requests;
目標 repo 不必啟用 Actions,提出 PR 的流程在 app repo 執行。
登入 bot,進入 User Settings → Applications → Generate New Token:
| 設定 | 值 |
|---|---|
| 名稱 | k8s-manifest-pr |
| Repository access | All(public、private、limited) |
| Repository scope | Read and Write,即 write:repository |
| User scope | Read,即 read:user |
| 其他 scopes | 不開啟 |
目前 v1.0.0 會呼叫 /api/v1/user 確认 bot 身分,需要 read:user。不要選
Specific repositories:Forgejo 此模式不允許 user scope。 存取範圍由專用 bot 的帳號權限
限制,因此只授權它操作 目標 manifests repo。建立及關閉 PR 屬 repository scope,不需要 write:issue。
詳見 Forgejo 16 token scopes。
PAT 與 REGISTRY_TOKEN 分開,不把 PAT 放進 workflow、Actions Variable 或 Git remote URL。
4. 設定 main branch protection
建議在 目標 manifests repo 的 Settings → Branches/Branch protection,針對 main 設定:
| 項目 | 建議 |
|---|---|
| Direct push | 禁止,或只允許指定的人類維護者 |
| Force push | 禁止 |
| Required approvals | 1 |
| Merge allowlist | 只允許人類維護者或維護團隊 |
| Bot bypass | 不允許 |
Protection 只針對 main,bot 仍需可建立 bot/image-pr/*、更新 bot/image-state/*。
Push 與 merge 限制需分別設定,參考
Forgejo branch protection。
Branch protection 不是 action 能否執行的必要條件;尚未設定時 action 仍可提出 PR,
但 Git 平台不會強制保證 bot 只能經人工審核合併。未設定 protection 不會導致
Missing or invalid PR_IMAGE_REF。
5. 設定 app 的 variables/secrets
在 來源 app repo 的 Settings → Actions 設定:
| 類型 | 名稱 | 內容 |
|---|---|---|
| Variable | REGISTRY_USERNAME |
Registry PAT 擁有者帳號 |
| Secret | REGISTRY_TOKEN |
可推送該 app image repository 的 registry PAT |
| Secret | K8S_PR_TOKEN |
上一步建立的 bot PAT |
既有 user/organization 層級的 registry 設定若已正常運作可沿用;K8S_PR_TOKEN 可先放在
需要部署的 app repo。只在受信任的發布 workflow 提供此 secret,不提供給不受信任的 PR。
6. 接上 app workflow
將以下內容放在來源 app repo 的 .forgejo/workflows/build.yaml。
請替換 action URL、目標 repo 及 manifest 選擇條件。
name: Build and propose deployment
on:
push:
tags:
- 'v*.*.*'
concurrency:
group: publish-app-manifests
cancel-in-progress: false
jobs:
build-and-propose:
runs-on: docker-build
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Build and push
id: image
uses: https://forgejo.example.com/<action-owner>/[email protected]
with:
username: ${{ vars.REGISTRY_USERNAME }}
pat: ${{ secrets.REGISTRY_TOKEN }}
- name: Propose Kubernetes update
id: manifest
uses: https://forgejo.example.com/<action-owner>/[email protected]
with:
token: ${{ secrets.K8S_PR_TOKEN }}
repository: <manifest-owner>/<manifest-repo>
base-branch: main
manifest: apps/app.yaml
namespace: default
workload: app
container: app
image-ref: ${{ steps.image.outputs.image-ref }}
Build step 的 id: image 必須與 steps.image.outputs.image-ref 對應。 name 只是顯示名稱,
不會自動成為 step ID。如果改成 id: build,引用也必須改成 steps.build.outputs.image-ref。
manifest 是相對目標 repo 根目錄,填 apps/app.yaml,不是 k8s/apps/app.yaml。
Concurrency group 每個部署目標固定,不要包含 release tag,且保留 cancel-in-progress: false。
7. 發布與驗收
先提交、推送 workflow,再從包含新 workflow 的 commit 建立下一個未使用的 release tag:
cd /path/to/app
git add .forgejo/workflows/build.yaml
git commit -m "ci: propose Kubernetes image updates after release"
git push origin main
# 換成下一個未使用的正式版本;不要移動既有 tag。
release_tag='<next-release-tag>'
git tag "$release_tag"
git push origin "refs/tags/$release_tag"
驗收:
- Build step 成功推送 image;PR step 顯示
Manifest proposal: created。 - 目標 manifests repo 出現 bot PR,diff 僅修改指定 container 的 image 為
repository@sha256:…。 - PR 包含 source commit、發布 ref 與 workflow 連結。
- 保留第一個 PR 不合併,再發布內容不同的新版本:新 PR 建立後,舊 bot PR 被關閉, 但舊 PR 的本文與 commits 不變。
- 若 digest 相同,會沿用既有 PR;若 base 已是該 digest,會回報
unchanged。
修正 workflow 後,重跑舊 tag 的 job 仍會使用該 tag 的舊 workflow;必須發布包含修正的新 tag。
8. 人工合併與部署
審核 source commit、digest 與 workflow 結果後人工合併。在具備 cluster 存取權限的 管理環境更新 manifests checkout,再使用該 repo 原有的部署流程。
cd /path/to/manifests
git pull --ff-only
若目標 repo 使用原生 Deployment YAML,可依其操作慣例套用並觀察 rollout,例如:
kubectl apply -f apps/app.yaml
kubectl -n default rollout status deployment/app --timeout=120s
若 repo 使用加密 secrets、部署腳本或其他工具,應沿用其解密與套用程序;
本 action 只建立 PR,不提供或要求特定的 apply.sh。範例路徑、namespace 與
Deployment 名稱需與 workflow 選擇的目標一致。
若 image 是 private,cluster 需另外具備對應 namespace 的 registry 拉取憑證。 Runner 能 push 不代表 node 能 pull;不要將 PR bot token 當作 cluster registry credential。
Inputs 與 outputs
| Input | 說明 |
|---|---|
token |
必填,bot PAT |
repository |
必填,目標 owner/repo |
base-branch |
預設 main |
manifest |
必填,repo 相對 YAML 路徑 |
workload |
必填,Deployment 名稱 |
namespace |
預設 default,YAML 省略 namespace 也視為 default |
container |
必填,一般 container 名稱 |
image-ref |
必填,registry/path@sha256:<64 位小寫 hex>,不含 tag |
Outputs:pr-url、pr-number、status。
| Status | 意義 |
|---|---|
created |
新 PR 已建立;舊 PR 清理已完成 |
existing |
沿用同 run 或相同 digest 的 open PR |
unchanged |
Base 已是同 digest,不建立 PR,清理舊候選 PR |
closed |
同 run 的 PR 已關閉或合併,不重開 |
stale |
較新 run 已接手,不提出降版 |
沒有適用 PR 時 URL/number 為空。若新 run 只取得狀態預約、尚未成功建立替代 PR,
較舊 run 可能回傳 stale 但保留其現有候選 PR;避免新 run 失敗時丟失待審更新。
Action 失敗會讓 step 失敗,不輸出成功狀態。
更新與重試規則
- 僅支援
apps/v1Deployment 的spec.template.spec.containers。 - YAML parser 定位唯一目標,只替換 image scalar 的原始文字位置;註解、引號、CRLF、 其他文件及 sidecars 保持不變。找不到、重複、image repository 不符都失敗。
- 第一版不接受含 YAML anchors/aliases 的 manifest,避免隱含修改多個目標。
- Bot PR 需同時符合作者、marker、來源/目標資訊、head repo/branch 與 base branch。 人工 PR 與其他 app PR 不會被關閉。
- PR 分支
bot/image-pr/<target hash>/<run id>建立後不改寫。 - 狀態分支
bot/image-state/<target hash>用 Git compare-and-swap 紀錄最新 run number、 digest 與候選是否準備完成;包含相同 digest 的 no-op,避免舊 run 重跑提出降版。 - 狀態分支是 bot metadata,不含 manifests,不應部署。不要刪除或手動改寫它;
k8s 的部署 workflow 若未來建立,應只監聽
main。 - 一旦 run 預約成功,即使後續 API 失敗也不降低 run 順序;重試該 run 或觸發新 run 修復。 同 run 的 digest 固定,若重新 build 得到不同內容,需以新的 workflow run 發布。
- 建立新 PR 失敗保留舊 PR。關閉舊 PR 失敗保留新 PR 並報錯,重試補做清理。
- PR API 回應遺失時先查詢既有結果,避免重複建立。人工關閉的同 run PR 不自動重開。
- 使用固定 concurrency group,不能包含 release tag,且
cancel-in-progress: false。 Git 狀態更新與 Forgejo PR API 並非同一交易;不要移除 workflow 排隊設定。
Credentials 只經環境與暫存 askpass helper 傳遞,不寫入 remote URL、Git 全域設定或 log。 Action 不使用 cluster credentials,不會執行目標 repo 的程式碼或 Git hooks。
保留 registry 中已部署、待審與所需回復版本的 digest。若需回復舊內容,以新的發布 run 明確提出舊 digest;不能重跑已被較新 run 淘汰的 run 來降版。
常見問題與排查
Missing or invalid PR_IMAGE_REF
代表 PR action 沒收到有效的 image reference。若 build/push 已成功,先檢查 build step
是否有 id: image,以及兩個 step 是否在同一個 job:
- name: Build and push
id: image
uses: https://forgejo.example.com/<action-owner>/[email protected]
with:
username: ${{ vars.REGISTRY_USERNAME }}
pat: ${{ secrets.REGISTRY_TOKEN }}
- name: Propose Kubernetes update
uses: https://forgejo.example.com/<action-owner>/[email protected]
with:
# 其餘必填 inputs 同完整範例。
image-ref: ${{ steps.image.outputs.image-ref }}
接著確認:build action 使用含 digest 輸出的版本、build step 沒被 if 跳過,以及 output
引用的 ID/名稱拼字正確。不同 job 不能直接用 steps.image,需先映射為 job output,
再透過 needs.<job>.outputs 傳遞。
必要時在 build 後暫時加一個檢查 step;只顯示非敏感 image reference,不印任何 token:
- name: Check image output
shell: bash
env:
BUILT_IMAGE_REF: ${{ steps.image.outputs.image-ref }}
run: |
test -n "$BUILT_IMAGE_REF"
printf 'image-ref=%s\n' "$BUILT_IMAGE_REF"
修正 workflow 後發布新的 tag。此錯誤與 branch protection 或 Docker credential warning 無關。
Docker 警告 credentials stored unencrypted
docker login 未使用 credential helper 時會提示 config.json 以可還原形式保存 credential。
Login Succeeded 表示登入已成功,這個 warning 本身不會導致 PR action 失敗。
Build action 使用獨立暫存 DOCKER_CONFIG,正常結束或可捕捉的中斷會登出並清除該目錄,
不寫入 runner 永久 Docker config。若主機崩潰或程序遭 SIGKILL,cleanup 可能來不及執行;
檢查殘留時只處理已結束 job 的暫存目錄,不刪除其他執行中 job 的 credential。
其他錯誤
| 現象 | 檢查/處理方式 |
|---|---|
ensurepip is not available |
在 runner 安裝 python3-venv,並以服務帳號驗證 |
| Action 下載失敗 | 遠端 repo、tag 是否存在;private action 是否可供來源 repo 讀取 |
| Job 一直排隊 | Runner 在線且來源 repo 能使用 docker-build label |
/user 回傳 403 |
PAT 需 read:user;v1.0.0 不使用 Specific repositories token |
| API 回傳 401/403/404 | 確認 PAT 有效、bot repo 權限、token scope、repo 名稱;private repo 無權限也可能回傳 404 |
| Git push 或建立 PR 失敗 | Bot 需 Write 與 write:repository;protection 不應擋住 bot 分支;目標 repo 啟用 PR |
Expected exactly one matching container |
檢查 manifest、namespace、Deployment、container 名稱;避免重複目標 |
Existing image repository does not match |
新舊 image 的 registry/repository 必須一致;registry 遷移先人工更新 |
| YAML anchors/aliases 不支援 | 改成明確的 image scalar;本 action 不會展開 alias 修改多處 |
stale |
有較新 run 已接手;以新發布 run 提出更新,不重跑舊 run 降版 |
| 同 run 有不同 digest | 重建得到不同內容,需新的 workflow run;不改寫同 run 的既有 PR 分支 |
| 新 PR 已建立但 job 失敗 | 可能關閉舊 PR 或寫入狀態失敗;相同 digest 重試可補做,不必手動刪分支 |
| 同 run 的 PR 已人工關閉 | 不會自動重開;要重新提出請用新 run |
ImagePullBackOff |
檢查 node 到 registry 的連線、拉取憑證與 digest 是否仍存在 |
每個部署目標只有一個來源發布 workflow;保留固定 concurrency group。
不要為排除 stale 而刪除 bot/image-state/*,那會失去防止舊版本覆蓋的依據。
開發與驗證
python3 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
.venv/bin/python -m unittest discover -s tests -v
bash -n run.sh
測試使用暫存 bare Git remote 與模擬 Forgejo API,不需要 PAT、不會碰遠端 repo。 正式啟用前,仍需以測試 repo/bot 驗證 instance API、branch protection 與實際 image 拉取。 完整設計與限制見 DESIGN.md。