No description
  • Python 98.9%
  • Shell 1.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-03 21:11:14 +08:00
examples format 2026-10-03 17:18:24 +08:00
tests feat: propose digest-pinned Kubernetes image updates 2026-10-03 17:17:29 +08:00
.gitignore feat: propose digest-pinned Kubernetes image updates 2026-10-03 17:17:29 +08:00
action.yaml format 2026-10-03 17:18:24 +08:00
DESIGN.md feat: propose digest-pinned Kubernetes image updates 2026-10-03 17:17:29 +08:00
image_pr.py feat: propose digest-pinned Kubernetes image updates 2026-10-03 17:17:29 +08:00
README.md update README.md 2026-10-03 21:11:14 +08:00
requirements.txt feat: propose digest-pinned Kubernetes image updates 2026-10-03 17:17:29 +08:00
run.sh feat: propose digest-pinned Kubernetes image updates 2026-10-03 17:17:29 +08:00

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/v1 Deployment 的 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。