## 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
17 KiB
status, date, decision-makers, tags
| status | date | decision-makers | tags | ||
|---|---|---|---|---|---|
| accepted | 2026-05-26 | R&D Lead |
|
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-gitlabis 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 + gitleaksplumbing in.gitea/workflows/ci.ymlinto one tool with documented blocking thresholds — less inline YAML to maintain, fewer "official action paywalled" workarounds. act_runnermaturity ceiling. It works, but it's a thin layer overactand has been the source of repeated setup-time friction (paywalledgitleaks/gitleaks-action@v2, github.com clone fallbacks inaquasecurity/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, andvm-gitlabis 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:
- on-prem hosting (compliance);
- enterprise-grade MR review affordances (team scaling);
- native security scanning that consolidates the current
Trivy + gitleakssetup; - a docker-native CI executor (GitLab Runner with the Docker executor) replacing
act_runner; - 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.jsonscripts (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'scommit signingpolicy 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 + gitleakssetup 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 + tarboilerplate) 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 runningpnpm 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-gitlabis 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:
chore(gitlab): mirror repos + bootstrap groups + branch protection + Renovate (no CI yet)ci(gitlab): land .gitlab-ci.yml alongside .gitea/workflows/ — parallel runchore(gitlab): cutover — flip remotes in docs, drop .gitea/workflows + infra/ci-runnerschore(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).