670f6303fe
## 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
159 lines
17 KiB
Markdown
159 lines
17 KiB
Markdown
---
|
||
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).
|