fj.doany.io。git のホスティングと Forgejo Actions。GitHub Actions の課金を避けて、CI をこのクラスタで回すために置いた。
| ファイル | 中身 |
|---|---|
| forgejo.yaml | Forgejo 本体(最新を追う)。chart は使わない(理由はファイルの先頭) |
| postgres.yaml | DB。k8up が pg_dump を取る |
| runner.yaml | Runner(v13)+ docker(dind)。runs-on: ubuntu-latest をそのまま拾う |
| httproute.yaml | 公開経路。SSH は出さない(clone / push は HTTPS + トークン) |
| forgejo-secrets.yaml | Infisical から Secret 3 つ |
| argocd-creds.yaml | ArgoCD が Forgejo のリポジトリを読むための資格情報 |
バックアップは ../k8up/schedules.yaml(毎日 14:45 UTC)。
-
Infisical に 2 つ入れる(同期より先に)
/forgejo/forgejo-db:postgres-password── 英数字だけのランダム(openssl rand -hex 32など)。 postgres は最初の起動でしかパスワードを設定しないので、後から変えるなら DB 側も変える/forgejo/forgejo-oauth:key=b0fa498f-7e6a-4fe1-a1c6-16fbbb6f397e(Main のクライアント ID。秘密ではなく bootstrap/auth にも平文で書いてある)、secret=${prod.auth.auth-secrets.oidc-client-secret}(値は写さず参照)
-
Entra のアプリ登録 Main のリダイレクト URI に
https://fj.doany.io/user/oauth2/entra/callback(pulumi/entra.ts にある) -
main にマージ → ArgoCD が同期
-
管理者を作る(1 度だけ)。Entra のメールと同じにしておくと、Entra でログインしたときに自動で紐づいて管理者になる。 パスワードは捨てる(表示させない)。組織
doaとチームmembers(読み取り・すべてのリポジトリ)も作るkubectl -n forgejo exec deploy/forgejo-web -c forgejo -- \ gitea admin user create --admin --username info --email info@doany.io --random-password >/dev/null
members を作る前にログインした人は、次のログインで入る
-
Runner の秘密を入れる: Infisical
/forgejo/forgejo-runnerにrunner-secret= 16 進 40 文字のランダム。 値は画面に出さずに作って貼る(WSL ならopenssl rand -hex 20 | tr -d '\n' | clip.exe。貼ったらクリップボードは消す)。 管理画面での操作は要らない。Forgejo と Runner の Pod が入れ替わり、Forgejo の init が同じ秘密で Runner を登録し (forgejo.yaml の setup.sh の 5)、Runner は同じ秘密から uuid / token を出して繋ぐ(runner.yaml)。/admin/actions/runnersにforgejo-runnerが「Idle」で出れば済み。 秘密を変えると別の Runner として登録される(UUID は秘密の先頭 16 文字から作る)。古い方は一覧で消す
方針: プライベートは GitHub に置かない。 GitHub Actions の課金はプライベートにだけ掛かるので、 公開リポジトリ(gitops・blog など)は GitHub のままでよい。
Forgejo が唯一の置き場になる。 バックアップは k8up(R2)の 1 日 1 回だけなので、手元の clone も捨てないこと。
ArgoCD が読むトークンは Infisical /argocd/forgejo-repo-creds(argocd-creds.yaml)。
スコープは repository・organization・issue の読み取り。 issue が無いと ApplicationSet が
token does not have at least one of required scope(s): [read:issue] で止まる。
ApplicationSet は GitHub 側と別の repos-forgejo(同居させると、Forgejo が空のクラスタで GitHub 側まで 1 本も作られない)。
リポジトリごとに:
- Forgejo の「新しい移行」で組織
doaの下に取り込む(Issue / PR / リリースも)。非公開のまま入り、チーム members が読める - ワークフローを Forgejo 向けに直す(
.github/workflows/のままで読まれる)- イメージは
ghcr.io/danything/<name>→fj.doany.io/doa/<name>。push はワークフローのsecrets.GITHUB_TOKEN(Forgejo のトークン)で通る - クラスタが pull できるように、アプリの namespace に
imagePullSecretsを足す(Forgejo のread:packageトークンを Infisical からkubernetes.io/dockerconfigjsonで) - claude-review(GitHub App)のワークフローは消す。Forgejo では動かない
- イメージは
- Forgejo で CI とイメージの push が通るのを確かめる
- GitHub のリポジトリを消す。両方に
deploy/argocd.yamlがあると Application 名がぶつかる (ApplicationSet がreposとrepos-forgejoに分かれているので、2 つが同じ Application を奪い合う)。 ApplicationSet はpreserveResourcesOnDeletion: trueなので、Application が作り直されても Pod や PVC は消えない - ghcr.io の古いパッケージを消す
| 公開リポジトリ | 非公開リポジトリ | fork・PR | push・マージ | 管理画面 | |
|---|---|---|---|---|---|
| ログインしていない人 | 見える | 見えない | ─ | ─ | ─ |
| テナントの人(Entra、チーム members) | 見える | 読める | できる | できない | ─ |
| 管理者(Forgejo の管理画面で付ける) | 全部 | 全部 | できる | できる | できる |
- テナントのゲストも tid が同じなので members に入る。 外したくなったら Forgejo 専用のアプリ登録を作り、 「割り当てが必要」にして人ごとに割り当てる(Main の設定を変えると、ロールを割り当てていない人が Main の後ろのアプリ全部から締め出される。 ../../docs/entra.md)
- Entra のアプリロール admin は Forgejo の管理者に連動しない(グループのクレームを tid に使っているため)。管理者は管理画面で付け外しする
- fork からの PR のワークフローは、管理者が承認するまで走らない(読み取り権限だけの人は承認が要る。Forgejo の既定)。 Runner は privileged なので、中身を見ずに承認しないこと
- Runner の docker は privileged。namespace の PSA が
privilegedなのはこのため - dind のイメージ置き場は
emptyDir。Pod が入れ替わると次のジョブで pull し直す - Cilium は vxlan なので dind の MTU を 1400 にしてある。1500 に戻すと大きい pull が途中で止まる
公開にしてはいけない。 パッケージ(コンテナイメージ)の読み取り権限はリポジトリではなく組織の公開範囲で決まり、
公開の組織だと非公開リポジトリのイメージでも匿名で pull できる(services/packages/perm.go の
HasOrgOrUserVisible)。
ワークフローの自動トークン(GITHUB_TOKEN)ではパッケージに書けない(Forgejo 15。services/packages/perm.go に
TODO: ActionUser permission check とあり、Actions のユーザーは組織のメンバー扱いにならない)。
組織 doa の Actions のシークレット REGISTRY_TOKEN(bot ユーザー forgejo-bot の write:package だけのトークン)で
docker login fj.doany.io -u forgejo-bot する。下の承認済みインテグレーションで秘密を無くせることは確かめたが、
forgejo-bot のものは管理の手段が無いので見送っている(下の表)。
CI から Forgejo の API・git・レジストリに長く生きる秘密なしで入る仕組み。外から来た JWT のクレームを、ユーザーごとに
登録したルールで確かめ、合えばそのユーザーとして(登録したスコープとリポジトリの範囲で)通す。
ドキュメント。設定(app.ini)は要らない
([authorized_integration] の既定で外の発行者に取りに行ける)。既定では取りに行けるのは外のホストだけで、
クラスタの中・プライベートの IP・ループバックには行かない(ALLOW_LOCALNETWORKS が false。services/auth/authorized_integration.go
の initAuthorizedIntegrationHTTPClient)。リダイレクトも追わず、jwks_uri は発行者と同じホストに限る。
もっと絞るなら FORGEJO__authorized_integration__ALLOWED_DOMAINS: token.actions.githubusercontent.com
(Forgejo Actions の JWT は中で確かめるので、この一覧に要らない)。
- 誰として動くか: インテグレーションの持ち主。push・PR・マージの予約も持ち主がしたことになるので、
付属の actions トークンと違ってワークフローも起きる(
services/actions/notifier_helper.goはIsActions()のときだけ止める) - 渡し方: API は
Authorization: Bearer <JWT>(token <JWT>でも通る)。git とレジストリは Basic のパスワードに JWT (ユーザー名は見ない。routers/web/web.goのbuildGitAuthGroup、routers/api/packages/api.goのContainerRoutes) - JWT の寿命: Forgejo Actions のものは 1 時間(
[actions].ID_TOKEN_EXPIRATION_TIME)。レジストリのトークンもそれに縛られる - リポジトリを絞ると、スコープは repository / issue しか選べず、管理者の権限は効かない(
services/authz/access_token.go)。 レジストリ(package)と組織の操作は「すべて」で作る - Forgejo Actions のジョブは
enable-openid-connect: trueで JWT を取る。fork からの PR では取れない。 JWT のworkflowはname:ではなくファイル名(run.WorkflowID。GitHub とは違う)。 再利用ワークフロー(uses: ./...)のジョブは、呼ばれる側のファイルの設定が効き、JWT のworkflowは呼ぶ側のファイル名になる - 境界は main に書ける人: main のワークフローを書き換えられる人は、その JWT で持ち主として書ける(シークレットのときと同じ)。 doa の main に書けるのは bots と Owners だけ(members は読み取り)
| 状態 | 持ち主 | 名前 | 発行者 | クレームのルール | 範囲・スコープ | aud の置き場 | 代わりに消したもの |
|---|---|---|---|---|---|---|---|
| 使っている | yui | repo-config | GitHub Actions | GitHub の 5ym/repo-config、refs/heads/main、forgejo-settings.yml、push / schedule / workflow_dispatch |
すべて・write:organization write:repository |
GitHub の 5ym/repo-config の変数 FORGEJO_AUDIENCE |
repo-config の secrets FORGEJO_TOKEN と yui のトークン repo-config-settings |
| 見送り | forgejo-bot | doa-registry | Forgejo Actions | doa の todoroku・tamasagashi・worklog-cloud、refs/heads/main、publish.yml / fetch-latest.yml、push / workflow_dispatch / schedule |
すべて・write:package |
組織 doa の変数 REGISTRY_AUDIENCE |
(今は組織の REGISTRY_TOKEN) |
| 見送り | forgejo-bot | todoroku-deploy | Forgejo Actions | todoroku、refs/heads/main、publish.yml、push / workflow_dispatch |
todoroku だけ・write:repository |
todoroku の変数 DEPLOY_AUDIENCE |
(今は todoroku の DEPLOY_TOKEN) |
**forgejo-bot の 2 つは見送った。**forgejo-bot のものは 16.0.5 では画面にも CLI・API にも一覧・変更・削除の手段が無く
(forgejo-bot は画面にログインできない。ENABLE_INTERNAL_SIGNIN: "false")、止めるには DB の authorized_integration の行を触ることになる。
v17 以降で管理の手段が揃ったら、表のルールで forgejo admin user create-authorized-integration --username forgejo-bot … を本番で打ち、
出力の audience(u:<ユーザー ID>:<UUID>。秘密ではない)を表の変数に入れて、ワークフローの PR
(doa/tamasagashi#106・doa/todoroku#368・doa/worklog-cloud#148。閉じてある)を出し直す。急いで止めるなら forgejo-bot を bots チームから外す
(prohibit_login はレジストリを止めない)。doa の ID は 2、リポジトリ ID は todoroku 1・tamasagashi 3・worklog-cloud 4(変更の CLI が無いので、足すときは作り直し)。
yui のもの(repo-config)は画面(設定 → 承認済みインテグレーション → 汎用 JWT)で作った。発行者は https://token.actions.githubusercontent.com、
クレームの値は文字列(5ym/repo-config の repository_id は 1315588536、repository_owner_id は 13718335)。
アプリ側に imagePullSecrets は要らない(../../talos/README.md「fj.doany.io の資格情報」)。
組織単位でマージ方法を縛る設定は Forgejo に無い(15 時点)。既定のマージ方法は forgejo.yaml の
DEFAULT_MERGE_STYLE、許すマージ方法はリポジトリごと。リポジトリを作ったり取り込んだりしたら、これを当てる:
curl -X PATCH -H "Authorization: token $T" -H 'Content-Type: application/json' \
-d '{"allow_merge_commits":false,"allow_rebase":false,"allow_rebase_explicit":false,"allow_fast_forward_only_merge":false,"allow_squash_merge":true,"default_merge_style":"squash","default_delete_branch_after_merge":true}' \
https://fj.doany.io/api/v1/repos/doa/<name>