ddd05994e0
CI / commits (pull_request) Successful in 3m41s
CI / scan (pull_request) Successful in 3m51s
CI / check (pull_request) Successful in 4m2s
CI / a11y (pull_request) Successful in 3m18s
Docs site / build (pull_request) Successful in 3m43s
CI / perf (pull_request) Successful in 7m28s
ADR-0015 framed Gitea Actions as a level-2 implementation explicitly slated for a GitLab migration within 6-18 months. That window opens now: vm-gitlab is provisioned, the team is about to scale, and the Gitea-specific friction points documented inline in .gitea/workflows/ ci.yml (paywalled gitleaks-action, github.com-bound trivy-action, manual curl+tar install dance) are accumulating maintenance tax. ADR-0028 records the platform shift. Architectural principles from ADR-0015 (trunk-based + squash, all-gates-blocking, thin YAML over portable scripts in pnpm + Nx, on-prem runners, Conventional Commits) carry over unchanged - only host / pipeline-file grammar / runner type / scan tooling change. Decision-only; 4-phase migration (mirror, parallel pipelines, cutover, cleanup) lands in follow-up PRs. Renumbering: ADR-0028 was a placeholder reference for the future Pleiades + Acteurs+ sync ADR. That ADR shifts to 0029; the 25 link references across ADR-0026 and ADR-0027 update in lockstep. CLAUDE.md roll-up adjusted accordingly. Status: proposed. Accept will be a separate small PR per the established pattern (see #219 for ADR-0026/0027 acceptance).
159 lines
17 KiB
Markdown
159 lines
17 KiB
Markdown
---
|
||
status: proposed
|
||
date: 2026-05-26
|
||
decision-makers: R&D Lead
|
||
tags: [infrastructure, process]
|
||
---
|
||
|
||
# Migrate CI/CD + git hosting from Gitea to GitLab self-hosted
|
||
|
||
## Context and Problem Statement
|
||
|
||
[ADR-0015](0015-cicd-gitea-actions.md) 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.yml` — **both 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](0022-docs-site-vitepress.md) 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"](0015-cicd-gitea-actions.md). 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](0026-person-user-portal-data-model.md) and [ADR-0027](0027-portal-side-organisational-hierarchy.md) 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](0026-person-user-portal-data-model.md) / [ADR-0027](0027-portal-side-organisational-hierarchy.md) / `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).
|