feat(gitea): upgrade 1.27.3 -> 28.3.0 #142

Merged
jakman85 merged 1 commits from feat/gitea-28 into main 2026-11-08 05:41:34 +00:00
Owner

PRE-MERGE OPERATOR CHECKLIST

  • Take a Longhorn snapshot and backup of gitea/gitea-data, and confirm the off-cluster backup (backup.policy: daily-offcluster) is recent. The 1.27 -> 28 DB migration (SQLite on that PVC) is one-way. I could not read the backup age from my worktree, so it is UNVERIFIED.
  • Merge in a quiet window. Gitea is the merge gate, the CI host and prod's Flux GitRepository source. The Deployment is strategy: Recreate, so while the new pod migrates and starts there is no Gitea at all: no CI runs, no source fetches, no git pushes. Do not queue other merges behind this one.
  • No /approve-override is needed. No sensitive path (infra/gitea/approval-policy.json) is touched; the PR changes catalog/, apps/wave-0/, renovate.json5 and decisions/ only.

POST-MERGE CHECK

  • GET https://gitea.home.keithjackson.dev/api/v1/version returns 28.3.0. Until then the apps-wave-0 Kustomization has not rolled.
  • Sign in through Keycloak SSO in a private window. The pod log should also show [gitea-oidc] login source 'keycloak' registered and verified from the postStart hook, and /data/gitea-oidc-bootstrap.log should have no new FAILED line.
  • A PR gate run (gate / gate (pull_request)) completes on the incluster-dind runner. Opening or re-pushing any PR will start one.
  • Optional: the startup log has no [security] ALLOWED_HOST_LIST / egress warnings.

Rollback

Restore gitea-data from the pre-upgrade snapshot or backup, with the matching 1.27.3 image. Do NOT simply revert the image tag. 28 migrates the DB schema forward, and 1.27.3 running against a migrated DB is not a supported state. Nothing reverts the image automatically:
update.autoRollbackOnFailedHealth: true only ever renders an annotation on a Flux ImagePolicy, and apps/ is rendered --no-image-automation (ci/render-golden.sh), so no ImagePolicy exists. The container has no probes, and a Deployment never self-reverts. I kept the flag (R-UPD-03) and added a comment in the catalog saying it is inert for this service and why.

What this changes

File Change
catalog/gitea.yaml image gitea/gitea:1.27.3 -> gitea/gitea:28.3.0; update.range ">=1.22.0 <2.0.0" -> ">=28.0.0 <29.0.0" (the schema takes free-form range strings); env changes below
apps/wave-0/gitea.yaml re-rendered with the canonical render.py --all catalog -o apps --wave-layout --no-image-automation in the toolbox. Only gitea changed
renovate.json5 gitea allowedVersions <2.0.0 -> <29.0.0, so Renovate proposes 28.x patches and minors (never auto-merged: gitea is on the blast-radius list) and never 29. The description no longer calls 28.x a stale tag. Checked with renovate-config-validator at 44.167.0 (the tag renovate.yaml pins): Config validated successfully
decisions/2026-11-07-gitea-28.md handoff entry

The runner (apps/wave-1/gitea-actions.yaml) and dind are not touched; they get separate PRs.

28.3.0 is the newest stable 28.x tag on both Docker Hub (28.0.0, 28.1.0, 28.2.0, 28.3.0) and GitHub (v28.0.0..v28.3.0), checked 2026-11-07.

Breaking changes and how each is handled

Sources: release notes https://blog.gitea.com/release-of-28.0.0/ , https://github.com/go-gitea/gitea/releases ; config reference https://docs.gitea.com/administration/config-cheat-sheet and custom/conf/app.example.ini at v28.3.0; upgrade guide https://docs.gitea.com/installation/upgrade-from-gitea . Claims marked (source) were checked against the v28.3.0 Go source.

  1. Egress policy for outbound HTTP (https://github.com/go-gitea/gitea/pull/39426): [security] EGRESS_MODE (lax by default, or strict) and [security] ALLOWED_HOST_LIST. In lax mode, public targets are allowed and private, loopback or CGNAT targets need an allow entry. (source) For those policies, a hostname entry does not admit a private address. WithLocalNeedsIPAllow requires an IP or CIDR entry, or the built-in private / loopback (modules/egress/policy/policy.go).
    • Keycloak SSO is not affected. (source) egress.NewSecurityPolicy has exactly two callers: the OAuth2 avatar fetch (routers/web/auth/oauth.go; [oauth2_client] UPDATE_AVATAR is off here) and OpenID 2.0 (modules/auth/openid, unused). The OIDC login source is goth's openidConnect.New(...) with its default HTTP client (services/auth/source/oauth2/providers_openid.go), so it never reaches the egress check. keycloak.${BASE_DOMAIN} resolving to the private Gateway address in-cluster therefore does not matter to it.
    • Webhooks and migrations/mirrors are covered, but this instance has 0 repo, org or system webhooks and 0 mirrors (measured against the live API, 2026-11-07). 1.27 also already refused private webhook targets by default, so nothing that works today is newly denied.
    • Actions uses: fetching (DEFAULT_ACTIONS_URL=github) is done by the runner, not by Gitea, so it is unaffected.
    • Handled by: declaring GITEA__security__EGRESS_MODE=lax with no allow list, on purpose. A hostname entry would not help for a private IP, and a CIDR entry would only widen webhook SSRF reach for no current consumer. The catalog comment says to add the narrowest CIDR (Gateway /32) only if a real private webhook target appears. This differs from the brief, which expected an allow-list entry. The source shows the premise does not hold for 28.3.0.
  2. [server] DOMAIN is ignored (https://github.com/go-gitea/gitea/pull/39400). The domain and the SSH domain come from ROOT_URL, which was already set. (source) modules/setting/server.go no longer reads it; the only remaining DOMAIN key is [session]. Grepping the repo found no other reader of GITEA__server__DOMAIN; bootstrap/gitea/bootstrap-gitea.sh sets only ROOT_URL and SSH_PORT. Handled by: removing the env line. The old value that environment-to-ini already wrote into app.ini stays there and is inert.
  3. Actions run retention (https://github.com/go-gitea/gitea/pull/38855): completed runs, with their jobs, logs and artifacts, are deleted after 400 days by the cleanup_action_runs cron. (source) modules/setting/actions.go: defaultRunRetentionDays = 400. The spec repo has no CI-history retention requirement. Handled by: declaring GITEA__actions__RUN_RETENTION_DAYS=400 (upstream's value, now explicit).
  4. Self-registration is disabled by default (#39400; 1.27 defaulted it open). (source) OAuth2 auto-registration (routers/web/auth/oauth.go) depends on ALLOW_ONLY_INTERNAL_REGISTRATION and ENABLE_AUTO_REGISTRATION, not on DISABLE_REGISTRATION, so Keycloak auto-provisioning keeps working. Handled by: declaring GITEA__service__DISABLE_REGISTRATION=true. Accounts now arrive only through Keycloak or the CLI.
  5. Git >= 2.25.0 required (#39131). The official image is alpine 3.24 with distro git (Dockerfile at v28.3.0), so no action is needed.
  6. postStart hook CLI. (source) The commands are unchanged at v28.3.0:
    • admin user list --admin prints ID Username Email IsActive IsAdmin 2FA Type, so awk '{print $2}' is still the username.
    • admin auth list prints ID Name Type Enabled, so $1/$2 are still right.
    • add-oauth and update-oauth keep --name --provider --key --secret --auto-discover-url --scopes --group-claim-name --admin-group --id.
    • admin user create keeps --username --password --email --admin --must-change-password.
    • curl and su-exec are still in the image, and the s6 setup still runs environment-to-ini (so the GITEA__* env vars still apply).
    • Handled by: no change.
  7. Actions workflow evaluation (#39358): job-level if: is evaluated before matrix expansion, fail-fast is enforced, and private reusable workflows are restricted. No workflow under .gitea/workflows/ uses matrix or strategy, so nothing changes.
  8. Live notifications move to a WebSocket at /-/ws (#36965). If the Gateway does not forward the upgrade, notifications fall back to polling, so this is not a break. UNVERIFIED on the Envoy Gateway.
  9. Startup migration under Flux: the container has no liveness probe, so a slow migration cannot be killed part-way by kubelet. apps-wave-0 has timeout: 5m, wait: true. A migration longer than that would mark the wave unhealthy for a while, but it would not interrupt the migration.

Local gate (toolbox, committed tree 8a9baa9)

DEV_KUBECONFIG=/dev/null ENGINE=<.git-mount shim> ./infra/devcontainer/dev.sh run bash -c './ci/gate.sh' -> rc 3 (UNDETERMINED), not green.
spec-immutability pass · decisions-log pass (76 entries; 75 on main + this one, which shows the gate saw the commit) · lint pass · l0 pass · render-golden pass · l1 pass (3/3) · approval-policy pass (N/A offline) · gitea-settings SKIPPED (no SETTINGS_API_TOKEN; the only stage that did not run) · conformance L2/L3 SKIPPED (no cluster, Phase 0).

Not verified

  • Nothing booted 28.3.0 against a copy of the production DB. Migration duration and success are UNVERIFIED.
  • The age of the latest backup (see the checklist).
  • L2/L3 conformance does not run in CI.

🤖 Generated with Claude Code

## PRE-MERGE OPERATOR CHECKLIST - [x] **Take a Longhorn snapshot and backup of `gitea/gitea-data`**, and confirm the off-cluster backup (`backup.policy: daily-offcluster`) is recent. The 1.27 -> 28 DB migration (SQLite on that PVC) is **one-way**. I could not read the backup age from my worktree, so it is UNVERIFIED. - [ ] **Merge in a quiet window.** Gitea is the merge gate, the CI host and prod's Flux `GitRepository` source. The Deployment is `strategy: Recreate`, so while the new pod migrates and starts there is no Gitea at all: no CI runs, no source fetches, no git pushes. Do not queue other merges behind this one. - [ ] No `/approve-override` is needed. No sensitive path (`infra/gitea/approval-policy.json`) is touched; the PR changes `catalog/`, `apps/wave-0/`, `renovate.json5` and `decisions/` only. ## POST-MERGE CHECK - [ ] `GET https://gitea.home.keithjackson.dev/api/v1/version` returns `28.3.0`. Until then the apps-wave-0 Kustomization has not rolled. - [ ] Sign in through **Keycloak SSO** in a private window. The pod log should also show `[gitea-oidc] login source 'keycloak' registered and verified` from the postStart hook, and `/data/gitea-oidc-bootstrap.log` should have no new `FAILED` line. - [ ] A PR gate run (`gate / gate (pull_request)`) completes on the `incluster-dind` runner. Opening or re-pushing any PR will start one. - [ ] Optional: the startup log has no `[security] ALLOWED_HOST_LIST` / egress warnings. ## Rollback **Restore `gitea-data` from the pre-upgrade snapshot or backup, with the matching 1.27.3 image. Do NOT simply revert the image tag.** 28 migrates the DB schema forward, and 1.27.3 running against a migrated DB is not a supported state. Nothing reverts the image automatically: `update.autoRollbackOnFailedHealth: true` only ever renders an annotation on a Flux `ImagePolicy`, and `apps/` is rendered `--no-image-automation` (`ci/render-golden.sh`), so no ImagePolicy exists. The container has no probes, and a Deployment never self-reverts. I kept the flag (R-UPD-03) and added a comment in the catalog saying it is inert for this service and why. ## What this changes | File | Change | |---|---| | `catalog/gitea.yaml` | image `gitea/gitea:1.27.3` -> `gitea/gitea:28.3.0`; `update.range` `">=1.22.0 <2.0.0"` -> `">=28.0.0 <29.0.0"` (the schema takes free-form range strings); env changes below | | `apps/wave-0/gitea.yaml` | re-rendered with the canonical `render.py --all catalog -o apps --wave-layout --no-image-automation` in the toolbox. Only gitea changed | | `renovate.json5` | gitea `allowedVersions` `<2.0.0` -> `<29.0.0`, so Renovate proposes 28.x patches and minors (never auto-merged: gitea is on the blast-radius list) and never 29. The description no longer calls 28.x a stale tag. Checked with `renovate-config-validator` at **44.167.0** (the tag `renovate.yaml` pins): `Config validated successfully` | | `decisions/2026-11-07-gitea-28.md` | handoff entry | The runner (`apps/wave-1/gitea-actions.yaml`) and dind are not touched; they get separate PRs. 28.3.0 is the newest stable 28.x tag on both Docker Hub (`28.0.0`, `28.1.0`, `28.2.0`, `28.3.0`) and GitHub (`v28.0.0`..`v28.3.0`), checked 2026-11-07. ## Breaking changes and how each is handled Sources: release notes https://blog.gitea.com/release-of-28.0.0/ , https://github.com/go-gitea/gitea/releases ; config reference https://docs.gitea.com/administration/config-cheat-sheet and `custom/conf/app.example.ini` at `v28.3.0`; upgrade guide https://docs.gitea.com/installation/upgrade-from-gitea . Claims marked **(source)** were checked against the v28.3.0 Go source. 1. **Egress policy for outbound HTTP** (https://github.com/go-gitea/gitea/pull/39426): `[security] EGRESS_MODE` (`lax` by default, or `strict`) and `[security] ALLOWED_HOST_LIST`. In lax mode, public targets are allowed and private, loopback or CGNAT targets need an allow entry. **(source)** For those policies, a *hostname* entry does not admit a private address. `WithLocalNeedsIPAllow` requires an IP or CIDR entry, or the built-in `private` / `loopback` (`modules/egress/policy/policy.go`). - **Keycloak SSO is not affected. (source)** `egress.NewSecurityPolicy` has exactly two callers: the OAuth2 *avatar* fetch (`routers/web/auth/oauth.go`; `[oauth2_client] UPDATE_AVATAR` is off here) and OpenID 2.0 (`modules/auth/openid`, unused). The OIDC login source is goth's `openidConnect.New(...)` with its default HTTP client (`services/auth/source/oauth2/providers_openid.go`), so it never reaches the egress check. `keycloak.${BASE_DOMAIN}` resolving to the private Gateway address in-cluster therefore does not matter to it. - **Webhooks and migrations/mirrors** are covered, but this instance has **0** repo, org or system webhooks and **0** mirrors (measured against the live API, 2026-11-07). 1.27 also already refused private webhook targets by default, so nothing that works today is newly denied. - **Actions `uses:` fetching** (`DEFAULT_ACTIONS_URL=github`) is done by the runner, not by Gitea, so it is unaffected. - **Handled by:** declaring `GITEA__security__EGRESS_MODE=lax` with **no allow list**, on purpose. A hostname entry would not help for a private IP, and a CIDR entry would only widen webhook SSRF reach for no current consumer. The catalog comment says to add the narrowest CIDR (Gateway `/32`) only if a real private webhook target appears. *This differs from the brief, which expected an allow-list entry. The source shows the premise does not hold for 28.3.0.* 2. **`[server] DOMAIN` is ignored** (https://github.com/go-gitea/gitea/pull/39400). The domain and the SSH domain come from `ROOT_URL`, which was already set. **(source)** `modules/setting/server.go` no longer reads it; the only remaining `DOMAIN` key is `[session]`. Grepping the repo found no other reader of `GITEA__server__DOMAIN`; `bootstrap/gitea/bootstrap-gitea.sh` sets only ROOT_URL and SSH_PORT. **Handled by:** removing the env line. The old value that `environment-to-ini` already wrote into `app.ini` stays there and is inert. 3. **Actions run retention** (https://github.com/go-gitea/gitea/pull/38855): completed runs, with their jobs, logs and artifacts, are deleted after 400 days by the `cleanup_action_runs` cron. **(source)** `modules/setting/actions.go`: `defaultRunRetentionDays = 400`. The spec repo has no CI-history retention requirement. **Handled by:** declaring `GITEA__actions__RUN_RETENTION_DAYS=400` (upstream's value, now explicit). 4. **Self-registration is disabled by default** (#39400; 1.27 defaulted it open). **(source)** OAuth2 auto-registration (`routers/web/auth/oauth.go`) depends on `ALLOW_ONLY_INTERNAL_REGISTRATION` and `ENABLE_AUTO_REGISTRATION`, not on `DISABLE_REGISTRATION`, so Keycloak auto-provisioning keeps working. **Handled by:** declaring `GITEA__service__DISABLE_REGISTRATION=true`. Accounts now arrive only through Keycloak or the CLI. 5. **Git >= 2.25.0 required** (#39131). The official image is alpine 3.24 with distro git (`Dockerfile` at v28.3.0), so no action is needed. 6. **postStart hook CLI. (source)** The commands are unchanged at v28.3.0: - `admin user list --admin` prints `ID Username Email IsActive IsAdmin 2FA Type`, so `awk '{print $2}'` is still the username. - `admin auth list` prints `ID Name Type Enabled`, so `$1`/`$2` are still right. - `add-oauth` and `update-oauth` keep `--name --provider --key --secret --auto-discover-url --scopes --group-claim-name --admin-group --id`. - `admin user create` keeps `--username --password --email --admin --must-change-password`. - `curl` and `su-exec` are still in the image, and the s6 `setup` still runs `environment-to-ini` (so the `GITEA__*` env vars still apply). - **Handled by:** no change. 7. **Actions workflow evaluation** (#39358): job-level `if:` is evaluated before matrix expansion, `fail-fast` is enforced, and private reusable workflows are restricted. No workflow under `.gitea/workflows/` uses `matrix` or `strategy`, so nothing changes. 8. **Live notifications move to a WebSocket at `/-/ws`** (#36965). If the Gateway does not forward the upgrade, notifications fall back to polling, so this is not a break. UNVERIFIED on the Envoy Gateway. 9. **Startup migration under Flux:** the container has no liveness probe, so a slow migration cannot be killed part-way by kubelet. apps-wave-0 has `timeout: 5m, wait: true`. A migration longer than that would mark the wave unhealthy for a while, but it would not interrupt the migration. ## Local gate (toolbox, committed tree `8a9baa9`) `DEV_KUBECONFIG=/dev/null ENGINE=<.git-mount shim> ./infra/devcontainer/dev.sh run bash -c './ci/gate.sh'` -> **rc 3 (UNDETERMINED), not green.** spec-immutability **pass** · decisions-log **pass** (76 entries; 75 on main + this one, which shows the gate saw the commit) · lint **pass** · l0 **pass** · render-golden **pass** · l1 **pass** (3/3) · approval-policy **pass** (N/A offline) · gitea-settings **SKIPPED** (no `SETTINGS_API_TOKEN`; the only stage that did not run) · conformance L2/L3 **SKIPPED** (no cluster, Phase 0). ## Not verified - Nothing booted 28.3.0 against a copy of the production DB. Migration duration and success are UNVERIFIED. - The age of the latest backup (see the checklist). - L2/L3 conformance does not run in CI. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
gitea-admin added the needs-reviewblast-radius labels 2026-11-08 04:59:34 +00:00
gitea-admin added 1 commit 2026-11-08 04:59:35 +00:00
feat(gitea): upgrade 1.27.3 -> 28.3.0
gate / gate (pull_request) Successful in 3m13s
gate / conformance (pull_request) Skipped
8a9baa92bc
Gitea dropped the "1." prefix; 28.x is the successor of 1.27.
- image 28.3.0, update.range >=28.0.0 <29.0.0, Renovate ceiling <29.0.0
- drop GITEA__server__DOMAIN (no longer read in 28, ROOT_URL is the source)
- declare RUN_RETENTION_DAYS=400, EGRESS_MODE=lax (no allow list: the OIDC
  login source is not gated by the egress policy in 28.3.0), and
  DISABLE_REGISTRATION=true (28's new default)
- document autoRollbackOnFailedHealth as inert (no ImagePolicy is rendered);
  rollback is a PVC restore because the DB migration is one-way

Co-Authored-By: Claude Opus 5.5 <[email protected]>
jakman85 merged commit 048254bfaf into main 2026-11-08 05:41:34 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Home-Network/home-network-k3s-impl#142