Files
apf_portal/docs/decisions/0028-migrate-cicd-and-git-hosting-to-gitlab.md
T
Julien Gautier a706e3ddc4
CI / scan (pull_request) Successful in 3m2s
CI / commits (pull_request) Successful in 3m12s
CI / check (pull_request) Successful in 3m23s
CI / a11y (pull_request) Successful in 3m10s
Docs site / build (pull_request) Successful in 3m44s
CI / perf (pull_request) Successful in 6m27s
docs(adr-0028): accept CI/CD + git hosting migration to GitLab
ADR-0028 shipped as proposed in #226. No open questions on drivers,
considered options, or the 4-phase migration sequence - promote to
accepted to unblock Phase 1 (mirror-and-bootstrap).

ADR-0015 gets a status-update blockquote at the top calling out that
the platform choice "Gitea Actions" is superseded by ADR-0028 while
the architectural principles in the rest of ADR-0015 (trunk-based +
squash, all-gates-blocking, thin YAML, on-prem runners, signed
commits, Conventional Commits) carry over unchanged. ADR-0015's
frontmatter status stays accepted - this is the canonical MADR way
to handle partial supersession without rewriting the decision record.

CLAUDE.md CI/CD architecture bullet rewritten to reflect the
accepted-but-not-yet-implemented state. Roll-up bumped to 0001 -> 0028
accepted. Side-fix: the bullet referenced a non-existent `ci:scan`
script; replaced with the real names (ci:catalogue-drift, ci:audit,
ci:perf, ci:gzip-budgets).

No code changes, no ci:check impact.
2026-05-26 10:49:39 +02:00

17 KiB
Raw Blame History

status, date, decision-makers, tags
status date decision-makers tags
accepted 2026-05-26 R&D Lead
infrastructure
process

Migrate CI/CD + git hosting from Gitea to GitLab self-hosted

Context and Problem Statement

ADR-0015 chose Gitea Actions as the v1 CI/CD platform, explicitly framed as "level-2 implementation; will be superseded by a GitLab migration ADR within 6-18 months". That window opens now: the infra team has provisioned vm-gitlab at 10.100.201.10, the team is about to scale beyond a single dev, and several Gitea-specific friction points have surfaced and are documented inline in .gitea/workflows/ci.yml (no Trivy / gitleaks official action — manual install; GitHub-API-bound action defaults that 404 against Gitea — actions/checkout token fallback; act_runner discovery quirks). The decision recorded here formalises the platform shift.

This ADR is decision-only — the actual migration ships across four follow-up PRs in §"Migration sequence" below.

Decision Drivers

  • ADR-0015's explicit commitment. The platform choice was always known to be temporary; we're inside the planned 6-18 month window.
  • vm-gitlab is already provisioned. No infra wait. The host the migration targets is up.
  • Team scaling. v1 is solo-developer; adding contributors needs better MR review affordances (inline suggestions, draft MRs, threaded discussions, reviewer assignment rules, MR templates). GitLab is materially better here than Gitea.
  • Built-in security scanning. GitLab CE ships native SAST + dependency scanning + secret detection. Consolidates today's manual Trivy + gitleaks plumbing in .gitea/workflows/ci.yml into one tool with documented blocking thresholds — less inline YAML to maintain, fewer "official action paywalled" workarounds.
  • act_runner maturity ceiling. It works, but it's a thin layer over act and has been the source of repeated setup-time friction (paywalled gitleaks/gitleaks-action@v2, github.com clone fallbacks in aquasecurity/trivy-action, missing standard runner image features). GitLab Runner with the Docker executor is the canonical CI execution model for the target platform.
  • Operational support. GitLab is the infra team's preferred git/CI platform — operating it long-term has organisational support, Gitea does not.
  • Future Container Registry consumer. When the BFF / SPA / docs site eventually ship Docker images (post-Phase-3), GitLab's built-in Container Registry is a natural target — currently no place to publish.

Considered Options

  • Option A — Status quo (Gitea + act_runner). Discarded: ADR-0015's commitment is explicit, friction is accumulating, and vm-gitlab is already provisioned. Inaction would be the deviation, not the default.
  • Option B — GitLab CE self-hosted on vm-gitlab (chosen). On-prem (HDS / GDPR / likely ASVS L3 posture preserved), team's standard, immediate availability.
  • Option C — Forgejo self-hosted. Forgejo is Gitea's actively-maintained fork with better open-source governance. UX-identical to today; would keep Gitea Actions on forgejo-runner. Considered but discarded: same feature gaps as Gitea (MR review affordances, native scanning), and the infra team's choice is GitLab — picking Forgejo would deviate without closing the friction.
  • Option D — Cloud SaaS (GitLab SaaS / GitHub Enterprise Cloud). APF processes health + financial data; on-prem is materially preferable for the compliance posture. Cloud not pursued.

Decision Outcome

Chosen option: B — GitLab CE self-hosted on vm-gitlab, because it is the only option that combines:

  1. on-prem hosting (compliance);
  2. enterprise-grade MR review affordances (team scaling);
  3. native security scanning that consolidates the current Trivy + gitleaks setup;
  4. a docker-native CI executor (GitLab Runner with the Docker executor) replacing act_runner;
  5. immediate availability — no infra wait.

What carries over from ADR-0015 (unchanged)

The architectural principles of ADR-0015 are preserved verbatim. Only the implementation host changes:

  • Trunk-based development with squash-merge.
  • Branch protection on main, all CI gates blocking.
  • Thin pipeline YAML — orchestration logic lives in package.json scripts (ci:check, ci:catalogue-drift, ci:audit, ci:commits, ci:perf, ci:gzip-budgets) and Nx targets, runnable locally. This is the load-bearing call from ADR-0015 — it was made specifically so the CI platform could swap with low cost. ADR-0028 cashes in that bet.
  • Self-hosted runners on the on-prem network.
  • Required reviewer count = 0 in v1, raised to ≥1 once a second contributor joins.
  • Signed commits recommended (revisited at this migration — see §"Signed commits" below).
  • Conventional Commits validated locally (hook) AND in CI (defense in depth).

What changes

Aspect Before (ADR-0015) After (ADR-0028)
Git host git.unespace.com (Gitea) <vm-gitlab> (GitLab CE)
Pipeline file .gitea/workflows/ci.yml .gitlab-ci.yml
Runner act_runner (Docker Compose on dev host) GitLab Runner with Docker executor on vm-gitlab
Dependency vuln scan Trivy (manual install + trivy-action workaround) GitLab Dependency Scanning (built-in)
Secret scan gitleaks (manual install + paywalled-action workaround) GitLab Secret Detection (built-in)
Bot account apf-portal-bot (Renovate, Gitea) apf-portal-bot (Renovate, GitLab) — same name, new auth token
MR review Gitea PRs GitLab MRs with templates, draft, threaded, suggestions
Conventional Commits pnpm ci:commits against origin/main unchanged — same script, different default-branch ref name if needed

Migration sequence

This ADR ships decision-only. The migration is implemented across four PRs after acceptance:

Phase PR Effect
0 This ADR Decision recorded; ADR-0015's "Gitea Actions" implementation choice flagged as superseded by §"Decision Outcome" here.
1 mirror-and-bootstrap git push --mirror gitlab from each repo (apf_portal, apf-ai-service proto vendoring, later cascade/acteurs_plus when needed). GitLab side: groups, projects, branch protection (mirror Gitea's), MR templates, deploy keys, Renovate reconfigured for GitLab. No .gitlab-ci.yml yet — Gitea pipelines continue to gate.
2 gitlab-ci-pipeline .gitlab-ci.yml lands alongside .gitea/workflows/ci.ymlboth run in parallel for ~1 calendar week to validate parity. Replace the manual Trivy + gitleaks install in the scan job with GitLab's SAST.gitlab-ci.yml + Secret-Detection.gitlab-ci.yml includes. GitLab Runner started + registered on vm-gitlab.
3 cutover Remotes flip in CLAUDE.md, READMEs, docs/setup/01-dev-debian-vm-setup.md §8.3 hand-off block. .gitea/workflows/ deleted, infra/ci-runners.compose.yml deleted, the three infra/data/runner-*/ directories deleted (or moved out of the repo). Gitea moves to read-only / archive (not decommissioned — old PR URLs in commit messages stay resolvable as historical artefacts).
4 cleanup Stale Gitea references swept across docs (docs/decisions/0015-… annotated, this ADR amended if anything drifted during 1-3). RENOVATE_PLATFORM, GITHUB_TOKEN secret naming, and the workflow's apf-portal-bot exclusion rule reviewed.

Phases 1-4 run on the user's calendar; this ADR doesn't pin dates.

Signed commits

ADR-0015 §"Signed commits recommended, revisited at GitLab migration" — that revisit happens here. The recommendation:

  • Once GitLab is the active host, enable required signed commits on main (GitLab's commit signing policy on the project's protected branch). GitLab supports both GPG and SSH signing keys.
  • Pair with GnuPG agent forwarding (covered in docs/setup/01-dev-debian-vm-setup.md §8.5) so dev VM operations work without holding a private signing key on the VM.
  • The apf-portal-bot (Renovate) service account needs a dedicated signing key — to be issued at PR 1 (mirror-and-bootstrap).

Consequences

  • Good, because ADR-0015's anticipated migration arrives within the committed window — the ADR record stays honest, no zombie commitments.
  • Good, because MR review affordances improve materially (inline suggestions / drafts / threaded discussions / required-reviewer rules) — relevant from the moment a second contributor joins.
  • Good, because GitLab's built-in scanning consolidates the dual Trivy + gitleaks setup into one configured tool with opinionated defaults. ADR-0015's "two paywalled-action workarounds" section of .gitea/workflows/ci.yml (~30 lines of inline rationale + curl + tar boilerplate) goes away.
  • Good, because GitLab Runner is mature and well-documented relative to act_runner; removes a class of friction we've hit during runner ops.
  • Good, because Container Registry is built-in — when the project eventually ships Docker images, a natural target exists without a new infra ask.
  • Bad, because every dev must update remote URLs on every clone (git remote set-url origin git@…). Single-line operation but a coordination point at cutover.
  • Bad, because existing references to Gitea PRs in commit messages and ADRs (#213, #217, #219, …) become 404s if Gitea is decommissioned. Mitigation: keep Gitea in read-only / archive mode rather than decommissioning — long-term cost is low (one VM at idle).
  • Bad, because Renovate needs reconfiguration. Mitigation: Renovate has first-class GitLab support; the config translation is documented and lands in PR 1.
  • Bad, because GitLab CE itself is a heavier piece of software to operate (Postgres + Redis + Sidekiq + nginx + …) than Gitea. Mitigation: infra team owns the platform's operability — same posture as vm-dev, just a different scope.
  • Neutral, because the gates, scripts, and Nx orchestration are unchanged. Thin-YAML-over-portable-scripts was made specifically so the CI platform could swap with low cost. The bet pays off here.
  • Neutral, because licensing: GitLab CE (the free / open-source edition) covers everything we need for v1 (CI/CD, MRs, Container Registry, basic SAST / Dependency / Secret scanning). The "Ultimate" tier features (advanced SAST, security dashboard, compliance pipelines) are paid — revisited if and when the compliance bar lands them in scope.

Confirmation

  • Parity validation. Phase 2 runs both pipelines in parallel on every PR for ~1 calendar week. Sign-off requires: every Gitea-Actions gate has its GitLab-CI counterpart, every commit that passes Gitea also passes GitLab, no false negatives observed.
  • Performance check. GitLab CI's pipeline view shows total runtime. Acceptance bar: GitLab pipeline ≤ 1.5× current Gitea pipeline (the act_runner setup currently shares the dev host's warm Docker cache; GitLab Runner has a small per-job container-start tax — expected, not blocking).
  • Renovate sanity check. Renovate creates at least one MR on GitLab successfully before phase 3 (cutover) — chosen MR being a small dependency bump on a non-critical lib so it's safe to merge or close on either platform.
  • Hooks parity. The current pre-commit suite (husky + lint-staged + commitlint) is platform-agnostic — verified at PR 2 by running pnpm prepare && git commit --allow-empty -m 'test' on a fresh clone from GitLab.

Pros and Cons of the Options

Option B — GitLab CE self-hosted on vm-gitlab (chosen)

  • Good, because: see "Decision Outcome" above.
  • Good, because GitLab is the infra team's preferred platform — operational support exists long-term.
  • Good, because Container Registry is built-in (potential consumer when shipping Docker images).
  • Good, because GitLab Pages is built-in — could replace the standalone VitePress hosting in ADR-0022 if we ever want to consolidate (decision-only — not in scope for this ADR).
  • Bad, because GitLab CE is heavier to operate than Gitea. Mitigation: infra team ownership.
  • Neutral, because licensing (CE covers v1 needs).

Option A — Status quo (Gitea)

  • Good, because: zero migration cost today.
  • Bad, because ADR-0015 commitment to migrate.
  • Bad, because: friction points (act_runner setup tax, MR review primitives, scan tool plumbing) compound as the team grows.
  • Bad, because: vm-gitlab is already provisioned — staying on Gitea wastes the infra investment and leaves a parallel host to operate.

Option C — Forgejo self-hosted

  • Good, because: governance / fork direction is more aligned with open-source norms than Gitea.
  • Good, because: migration from Gitea is trivial (Forgejo is a drop-in).
  • Bad, because: same feature gaps as Gitea (MR review primitives, native scanning) — Forgejo is largely a same-feature-set fork with better governance, not a more capable product.
  • Bad, because: infra team's choice is GitLab — Forgejo would be a per-project deviation.

Option D — Cloud SaaS

  • Good, because: zero operational burden for the platform itself.
  • Bad, because: APF's data classification (health + financial) plus likely ASVS L3 makes on-prem materially preferable.
  • Bad, because: corp egress policies likely require ingress / egress rules to cloud SaaS we don't have today.

More Information

Relationship to ADR-0015. ADR-0028 supersedes the specific implementation choice "Gitea Actions" in ADR-0015 §"Decision Outcome". The architectural principles in the rest of ADR-0015 (trunk-based + squash-merge, branch protection, all gates blocking, thin YAML over portable scripts, on-prem runners, signed commits, Conventional Commits, defense-in-depth) carry over unchanged. ADR-0015's frontmatter status stays accepted; a note at its top points at ADR-0028 for the platform shift.

Renumbering. This ADR takes number 0028. The previously-reserved placeholder for "Pléiades + Acteurs+ syncs + facet schemas" — referenced in ADR-0026 and ADR-0027 as ADR-0028 — shifts to ADR-0029. The (#) placeholder links in those two ADRs are updated in this PR so the chain stays consistent.

Phasing recap. Decision-only. Implementation across four PRs:

  1. chore(gitlab): mirror repos + bootstrap groups + branch protection + Renovate (no CI yet)
  2. ci(gitlab): land .gitlab-ci.yml alongside .gitea/workflows/ — parallel run
  3. chore(gitlab): cutover — flip remotes in docs, drop .gitea/workflows + infra/ci-runners
  4. chore(gitlab): cleanup — sweep stale references + finalise signed-commit policy

Follow-up ADRs.

  • ADR-0029 — Pléiades + Acteurs+ + cascade syncs + facet schemas. Previously numbered 0028 in ADR-0026 / ADR-0027 / CLAUDE.md. Renumbered in this PR's diff. Content unchanged from the original placeholder description.
  • A future ADR — required signed commits. Optional, only if §"Signed commits" above turns out to need its own decision record (e.g. if APF's RSSI lands a specific policy on signing keys, key escrow, or revocation).