Forgejo Git platform deployment (Helm + Kustomize + ArgoCD)
  • Go Template 75.2%
  • Shell 24.8%
Find a file
2026-05-20 10:02:00 +02:00
charts/forgejo-17.0.1/forgejo feat: initial Forgejo deployment repo (Phase 1.1) 2026-05-06 10:02:26 +02:00
k8up-backup feat: add k8up backup (daily schedule + secrets backup) 2026-05-06 23:15:59 +02:00
resources chore(deps): update alpine docker tag to v3.23 (#5) 2026-05-19 17:54:44 +02:00
.gitignore chore(repo): ignore local worktree directory 2026-05-07 21:51:11 +02:00
.gitlab-ci.yml feat: initial Forgejo deployment repo (Phase 1.1) 2026-05-06 10:02:26 +02:00
.sops.yaml feat: initial Forgejo deployment repo (Phase 1.1) 2026-05-06 10:02:26 +02:00
kustomization.yaml feat(forgejo): daily sync of archive flag + description to gitlab.com 2026-05-07 13:27:01 +02:00
kustomize-build.sh chore: add renovate annotations for Docker image tracking in kustomize-build.sh 2026-05-20 10:02:00 +02:00
README.md feat(forgejo): default repositories to squash merges 2026-05-07 22:14:18 +02:00
renovate.json chore: update renovate preset path after org move 2026-05-07 15:04:10 +02:00
values.yaml fix(forgejo): force ENABLE_BASIC_AUTHENTICATION=true to override stale app.ini 2026-05-08 16:16:44 +02:00

Forgejo (git.xarif.de)

Forgejo v15.0.1 (Chart 17.0.1) — primary Git platform. Helm + Kustomize + ArgoCD. Auth: OIDC via Authelia. SSH: port 22 (LoadBalancer, rootless 22→2222 remap).

Global Mirror Hook

WHY: Every push to a public repo mirrors to gitlab.com/xarif/<org>/<repo> for disaster recovery.

HOW: Symlink in each repo's hooks/post-receive.d/mirror-git-repo → ConfigMap-mounted script.

New repos: Git's init.templateDir ([git.config] in app.ini) propagates the symlink automatically at git init --bare time. No cron, no manual intervention.

Existing repos: postStart lifecycle hook backfills missing symlinks on every pod start.

Helm chart limitation: Chart v17 ignores lifecycle values key — applied via Kustomize strategic merge patch (resources/patch-lifecycle.yaml).

Mirror State Sync

WHY: Mirror hook fires only on git push, so archive/unarchive and description edits do not propagate. Forgejo has no archive webhook event (verified in gitea/modules/webhook/type.gorepository event covers only created/deleted/transferred). Polling reconcile is the only option.

HOW: Daily CronJob (0 3 * * *) iterates /api/v1/repos/search, compares archived flag and description against gitlab.com/xarif/<org>/<repo>, patches gitlab.com when state diverges. Forgejo is source of truth; orphan gitlab.com repos are never modified.

Skips: private Forgejo repos (consistent with the hook), and 404 on gitlab.com (will be created on next push).

Idempotent: re-run with no drift reports synced=0.

Manual run: ArgoCD prunes Jobs created via kubectl create job --from=cronjob/... (no ownerReference → unmanaged). Use a kubectl run Pod instead — Pods without app.kubernetes.io/instance: forgejo are not pruned:

kubectl run -n forgejo mirror-state-sync-test --rm -i --restart=Never --image=alpine:3.21 \
  --overrides='{"apiVersion":"v1","spec":{"containers":[{"name":"mirror-state-sync-test","image":"alpine:3.21","command":["/bin/sh","-c"],"args":["set -eu; apk add --no-cache --quiet bash curl jq > /dev/null; exec /scripts/sync-mirror-state.sh"],"stdin":true,"stdinOnce":true,"tty":false,"volumeMounts":[{"name":"script","mountPath":"/scripts","readOnly":true},{"name":"mirror-secrets","mountPath":"/mirror-secrets","readOnly":true}]}],"volumes":[{"name":"script","configMap":{"name":"forgejo-mirror-state-sync","defaultMode":493}},{"name":"mirror-secrets","secret":{"secretName":"forgejo-mirror-secrets"}}],"restartPolicy":"Never"}}'

Default Merge Style

New repositories default to Create squash commit via [repository.pull-request] DEFAULT_MERGE_STYLE = squash.

Existing repositories are not changed automatically. To backfill them, run:

bash resources/backfill-default-merge-style.sh --dry-run
bash resources/backfill-default-merge-style.sh

The helper reads admin credentials from Kubernetes Secret forgejo-admin-user in namespace forgejo and updates only repositories whose default_merge_style is not already squash.

Key files

  • values.yaml — Helm values: server config, OIDC, extraVolumes (hook script + secrets), [git.config] init.templateDir
  • resources/mirror-git-repo.sh — Post-receive hook: checks visibility via Forgejo API, creates gitlab.com project if missing, force-pushes
  • resources/sync-mirror-state.sh — Daily reconcile script: paginated walk through Forgejo repos, archives/unarchives gitlab.com via POST /projects/:id/(un)archive, updates description via PUT /projects/:id
  • resources/cronjob-mirror-state-sync.yaml — CronJob (alpine:3.21, daily 03:00); mounts forgejo-mirror-secrets and sync-script ConfigMap
  • resources/patch-lifecycle.yaml — Kustomize patch: postStart waits for ConfigMap mount, creates template dir, backfills symlinks
  • resources/secret-mirror.yaml — SOPS: forgejo-token (read:repository), remote-gitlab-token (api), remote-gitlab-push-token (write_repository)
  • resources/secret-admin-user.yaml — SOPS: bootstrap admin (xarif-admin)
  • resources/secret-oidc.yaml — SOPS: Authelia OIDC client

Path mapping

Forgejo <org>/<repo> → gitlab.com xarif/<org>/<repo> (1:1, no transformation). On first mirror (new repo), the hook creates the gitlab.com project, pushes, and protects main with allow_force_push=true. Description on gitlab.com always reads "This project is a mirror of https://git.xarif.de/<org>/<repo>" — set on creation by the hook, kept in sync by the reconcile CronJob.