Files
apf_portal/docs/decisions/0028-migrate-cicd-and-git-hosting-to-gitlab.md
T
julien 670f6303fe
CI / check (push) Successful in 2m0s
CI / commits (push) Has been skipped
CI / scan (push) Successful in 2m22s
CI / a11y (push) Successful in 3m11s
CI / perf (push) Successful in 6m15s
Docs site / build (push) Successful in 5m11s
docs(adr-0028): accept CI/CD + git hosting migration to GitLab (#227)
## Summary

Promotes [ADR-0028](docs/decisions/0028-migrate-cicd-and-git-hosting-to-gitlab.md) from `proposed` to `accepted`. Shipped as `proposed` in [#226](#226); no open question left on either drivers, considered options, or the 4-phase migration sequence. Same shape as [#219](#219) (ADR-0026 + ADR-0027 acceptance).

Once merged, **Phase 1** of the migration (`mirror-and-bootstrap` — repos pushed to GitLab, branch protection / MR templates / deploy keys / Renovate reconfig) is unblocked.

## What lands

| File | Change |
| --- | --- |
| `docs/decisions/0028-migrate-cicd-and-git-hosting-to-gitlab.md` | Frontmatter `status: proposed → accepted`. |
| `docs/decisions/0015-cicd-gitea-actions.md` | **Status-update note** added at the top, right under the title — calls out that the platform choice "Gitea Actions" is superseded by ADR-0028, while the rest of ADR-0015's architectural principles (trunk-based + squash, all-gates-blocking, thin YAML, on-prem runners, signed commits, Conventional Commits) carry over unchanged. ADR-0015 stays `accepted`. |
| `docs/decisions/README.md` | ADR-0028 row: `proposed → accepted`. |
| `CLAUDE.md` | Roll-up bumped to `ADRs 0001 → 0028 accepted` with a one-sentence explanation that ADR-0028 supersedes only ADR-0015's platform choice. **CI/CD architecture bullet rewritten** to reflect the accepted-but-not-yet-implemented state — "Gitea Actions today, migrating to GitLab CE on `vm-gitlab` per ADR-0028 (4-phase rollout in follow-up PRs)". The architectural detail (gates list, thin YAML, signed commits) was preserved; only the platform headline and the act_runner→GitLab Runner line are touched. Also corrected: `ci:scan` (which doesn't exist as a script) → the real script names (`ci:catalogue-drift`, `ci:audit`, `ci:perf`, `ci:gzip-budgets`). |

## Notes for the reviewer

- **No new Architecture bullet for ADR-0028 itself.** The decision is a *platform shift* for the existing CI/CD architecture, not a new architectural concern — so it folds into the existing ADR-0015 bullet rather than adding a sibling.
- **The annotation pattern on ADR-0015** (status-update blockquote at the top) is the canonical MADR way to handle partial supersession without changing the frontmatter status. The architectural principles are still accepted; only the platform implementation moves. A future reader hitting ADR-0015 first sees the redirect immediately.
- **The CLAUDE.md script-list correction** is a side-fix — `ci:scan` is not a real script name; the actual gates are `ci:check`, `ci:catalogue-drift`, `ci:audit`, `ci:commits`, `ci:perf`, `ci:gzip-budgets`. Updated in the same touch since the bullet was being rewritten anyway.
- **No code changes**, so no `pnpm ci:check` impact. `pnpm exec prettier --check` clean on the four touched files.

## Test plan

- [x] `pnpm exec prettier --check` — clean on the four touched files.
- [x] ADR-0015 → ADR-0028 cross-reference resolves (the new blockquote link).
- [x] `ADRs 0001 → 0028 accepted` matches reality (`grep '^status: ' docs/decisions/*.md` shows everything below 0029 as `accepted`).
- [ ] **Review focus** — the ADR-0015 status-update note phrasing, the CLAUDE.md CI/CD bullet rewrite (especially the "carry over" wording), the roll-up sentence about partial supersession.

## What's next (post-merge)

1. **Phase 1 — `mirror-and-bootstrap`** — `git push --mirror gitlab` for `apf_portal` and the proto vendoring in `apf-ai-service`. 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. Ops work primarily; the only PR-shaped output is a Renovate config update on the apf-portal repo if its host detection changes.
2. **Phase 2 — `gitlab-ci-pipeline`** — `.gitlab-ci.yml` lands alongside `.gitea/workflows/ci.yml`. Both pipelines run in parallel for ~1 calendar week. GitLab Runner registered on `vm-gitlab`.
3. **Phase 3 — `cutover`** — remotes flip in CLAUDE.md, READMEs, `docs/setup/01-dev-debian-vm-setup.md` §8.3. `.gitea/workflows/` + `infra/ci-runners.compose.yml` deleted, Gitea read-only / archive.
4. **Phase 4 — `cleanup`** — stale references sweep, required signed-commits on `main` enabled.

In parallel — once you've finished walking through the dev VM bootstrap — the paused **ADR-0027 PR 1** (Region / Delegation / Structure Prisma schema + inline seed) and **ADR-0026 PR 1** (Person / User / UserScope schema + provisioner) can ship on Gitea; they're decoupled from the migration and don't need to wait for it.

---------

Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #227
2026-05-26 10:54:55 +02:00

159 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
status: accepted
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).