diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..f193c2c --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,149 @@ +# Per ADR-0028 (CI/CD migration from Gitea Actions to GitLab CE). +# Thin YAML — orchestration lives in package.json scripts (ci:check, +# ci:catalogue-drift, ci:audit, ci:commits, ci:perf, ci:gzip-budgets) +# and Nx targets. Any change to gate behaviour belongs in those +# scripts, not in this file — see ADR-0015 §"Thin pipeline YAML…" +# (principle preserved by ADR-0028 at the migration boundary). +# +# Phase 2 of ADR-0028: ships alongside .gitea/workflows/ci.yml. Both +# pipelines must remain in parity until cutover (Phase 3), at which +# point the Gitea workflow gets deleted. + +include: + # GitLab built-in scanners — replace the manual Trivy + gitleaks + # install in .gitea/workflows/ci.yml. Both add their own jobs to + # the pipeline; their default stage is `test`, which is mapped in + # `stages:` below. + - template: Jobs/SAST.gitlab-ci.yml + - template: Jobs/Secret-Detection.gitlab-ci.yml + +stages: + - check + - test + - commits + - perf + - a11y + +workflow: + rules: + # MR pipelines — covers GitLab MR review flow once it lands. + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + # Direct pushes — covers post-merge runs on main today, and Phase 2 + # parity testing on feature branches mirrored from Gitea. To be + # tightened to `$CI_COMMIT_BRANCH == "main"` at the Phase 3 cutover + # once GitLab is the source of truth. + - if: $CI_PIPELINE_SOURCE == "push" + +# Shared shape for jobs that need Node + pnpm. Hidden (`.` prefix) so +# GitLab does not try to execute it. Inheriting jobs `extends: .node-job`. +.node-job: + image: node:24-bookworm + cache: + key: + files: + - pnpm-lock.yaml + paths: + - .pnpm-store/ + before_script: + - corepack enable + - corepack prepare pnpm@10.33.4 --activate + - pnpm config set store-dir "$CI_PROJECT_DIR/.pnpm-store" + +check: + extends: .node-job + stage: check + # `nx affected` needs a base SHA to diff against. Mirrors the + # equivalent step in .gitea/workflows/ci.yml — same logic, GitLab + # variable names. Replaces nrwl/nx-set-shas (GitHub-only). + script: + - | + if [ "$CI_PIPELINE_SOURCE" = "merge_request_event" ]; then + git fetch origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" --depth=100 + export NX_BASE=$(git merge-base HEAD "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME") + else + export NX_BASE="HEAD~1" + fi + export NX_HEAD="HEAD" + - pnpm install --frozen-lockfile + - pnpm ci:check + # Catalogue-vs-code drift gate per ADR-0025 §"Confirmation". The + # script's own unit tests run first — if they fail the gate itself + # is broken and the actual catalogue check would be noise. + - pnpm ci:catalogue-drift:test + - pnpm ci:catalogue-drift + +audit: + extends: .node-job + stage: test + # npm-advisory check against pnpm-lock.yaml. Trivy's dependency-vuln + # role from the Gitea workflow is now covered by the SAST include + # (which includes Dependency Scanning analyzers); `pnpm audit` stays + # in-pipeline as defense in depth against the npm advisory DB + # specifically. + script: + - pnpm install --frozen-lockfile + - pnpm ci:audit + +commits: + extends: .node-job + stage: commits + # MRs opened by Renovate (apf-portal-bot) carry commit messages from + # a vetted Conventional-Commits template — running commitlint on + # them is tautological. Per ADR-0017 amendment. + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" && $GITLAB_USER_LOGIN != "apf-portal-bot" + variables: + # Default `GIT_DEPTH: 20` is too shallow to diff against + # origin/main on long-running branches. Full clone so commitlint + # can walk all the way back. + GIT_DEPTH: 0 + script: + - git fetch origin main + - pnpm install --frozen-lockfile + - COMMIT_LINT_FROM=origin/main pnpm ci:commits + +perf: + extends: .node-job + stage: perf + # Lighthouse CI drives a real Chrome instance. Playwright's image is + # the canonical "Node + browsers" CI image and is already on our + # roadmap for the ADR-0016 axe-core e2e — single image covers both + # uses once the a11y suite lands. + image: mcr.microsoft.com/playwright:v1.55.0-jammy + # Skip on Renovate MRs (same rationale as commits + the perf signal + # on a dep bump is essentially zero). Push events on `main` still + # run perf — we catch regressions immediately post-merge, not + # pre-merge. Per ADR-0017 amendment. + rules: + - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "main" + - if: $CI_PIPELINE_SOURCE == "merge_request_event" && $GITLAB_USER_LOGIN != "apf-portal-bot" + script: + - pnpm install --frozen-lockfile + - pnpm ci:perf + artifacts: + when: always + paths: + - .lighthouseci/ + expire_in: 30 days + +a11y: + extends: .node-job + stage: a11y + # Placeholder until the e2e a11y suite (axe-core via Playwright, per + # ADR-0016) is wired with the first real screens. The job exists so + # branch protection can require it from day one — currently no-ops + # with a clear message. + script: + - echo "a11y gate placeholder - axe-core via Playwright wires up with the first real screens (ADR-0016)." + +# Place SAST + Secret Detection in the `test` stage to mirror the +# Gitea workflow's grouping (where Trivy + gitleaks + audit all +# lived in the `scan` job). Override does not change behaviour — +# only stage placement. The included templates set sensible defaults +# (run on default branch + MRs, full repo on default branch / diff +# on MRs); revisited in Phase 3 if pipeline duration becomes a pain. +sast: + stage: test + +secret_detection: + stage: test