# Per ADR-0015 (CI/CD on Gitea Actions). # Thin YAML — orchestration lives in package.json scripts (ci:check, # ci:audit, ci:commits, ci:perf) and Nx targets. Any change to gate # behaviour belongs in those scripts, not in this file. # # `cache: 'pnpm'` is intentionally NOT enabled on actions/setup-node # below: act_runner's built-in GitHub-Actions-cache server is bound on # the runner container and is unreachable from job containers (which # run on Docker's default network, not the runners' compose network). # Each request burns a ~2 min ETIMEDOUT on restore + another on save, # for zero hit rate. Once the runner network/cache config is fixed # (tracked in infra/README.md "Cache server"), re-add `cache: 'pnpm'` # here. name: CI on: pull_request: branches: [main] push: branches: [main] jobs: check: runs-on: [self-hosted, on-prem] steps: - uses: actions/checkout@v6 with: fetch-depth: 0 # Derive NX_BASE / NX_HEAD for `nx affected`. Replaces # nrwl/nx-set-shas@v4, which is GitHub-only (it queries the GitHub # API to find the last successful workflow run, returning 404 on # Gitea). HEAD~1 is a reasonable approximation for push events on # a squash-merge trunk; pull_request uses the merge-base with the # target branch. - name: Derive Nx affected base and head shell: bash run: | # `actions/checkout@v4` with fetch-depth: 0 already pulls every # branch and tag, so origin/ is present locally — no # extra `git fetch` is needed (and `--depth=0` is invalid: git # requires a positive integer). if [ "${{ github.event_name }}" = "pull_request" ]; then echo "NX_BASE=$(git merge-base HEAD origin/${{ github.base_ref }})" >> "$GITHUB_ENV" else echo "NX_BASE=HEAD~1" >> "$GITHUB_ENV" fi echo "NX_HEAD=HEAD" >> "$GITHUB_ENV" - uses: pnpm/action-setup@v6 - uses: actions/setup-node@v6 with: node-version-file: '.nvmrc' - run: pnpm install --frozen-lockfile - run: pnpm ci:check scan: runs-on: [self-hosted, on-prem] steps: - uses: actions/checkout@v6 - uses: pnpm/action-setup@v6 - uses: actions/setup-node@v6 with: node-version-file: '.nvmrc' - run: pnpm install --frozen-lockfile - run: pnpm ci:audit # Dependency vulnerability scan. Trivy is a Go binary, not an npm # package, so it cannot live in package.json scripts as cleanly # as audit/lint do. # # We deliberately avoid `aquasecurity/trivy-action`. On cache # miss (our act_runner cache server is unreachable from job # containers — see infra/README.md "Cache server (deferred)"), # the action falls back to `git clone github.com/aquasecurity/ # trivy` to fetch its install script, using `actions/checkout` # which defaults `with.token` to `${{ github.token }}` (Gitea's # auto-token, useless for github.com). The clone hits the # anonymous github.com rate limit and fails with "could not # read Username". Passing GITHUB_TOKEN as an env var doesn't # help — actions/checkout reads it from `inputs.token`, not env. # # Direct curl + tar is simpler, predictable, and gives us an # explicit version pin instead of `@master`. GITHUBCOM_TOKEN is # passed to handle the github.com rate limit on the release # download in the worst case (release artefacts are usually # unmetered, but auth is free insurance). - name: Install Trivy env: # Bump deliberately when a security advisory or new feature # warrants it. Renovate cannot manage this pin out of the # box (it is not a package-manager-tracked dep); a custom # regex manager could be added later if the cadence justifies # it. For now, manual updates from # https://github.com/aquasecurity/trivy/releases. TRIVY_VERSION: '0.70.0' GITHUB_TOKEN: ${{ secrets.GITHUBCOM_TOKEN }} run: | curl -sfL \ -H "Authorization: Bearer ${GITHUB_TOKEN}" \ -o /tmp/trivy.tar.gz \ "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz" tar -xzf /tmp/trivy.tar.gz -C /usr/local/bin trivy trivy --version - name: Run Trivy run: | trivy fs \ --ignore-unfixed \ --skip-dirs node_modules \ --exit-code 1 \ --severity CRITICAL,HIGH \ . # Secret scan, same reasoning (gitleaks is a Go binary). - uses: gitleaks/gitleaks-action@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} commits: # PRs opened by Renovate (apf-portal-bot) carry commit messages # generated from a vetted Conventional-Commits template — running # commitlint on them is tautological. Per ADR-0017 amendment. if: github.event_name == 'pull_request' && github.event.pull_request.user.login != 'apf-portal-bot' runs-on: [self-hosted, on-prem] steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - uses: pnpm/action-setup@v6 - uses: actions/setup-node@v6 with: node-version-file: '.nvmrc' - run: pnpm install --frozen-lockfile - run: COMMIT_LINT_FROM=origin/main pnpm ci:commits perf: # Skip the Lighthouse run on PRs opened by Renovate (apf-portal-bot): # the per-PR perf signal on a dep bump is essentially zero (no # routes yet, bundle is the static placeholder), and the Lighthouse # round-trip burns several minutes per PR. Push events on `main` # still run perf — we catch regressions immediately post-merge, # not pre-merge. Per ADR-0017 amendment. if: github.event_name != 'pull_request' || github.event.pull_request.user.login != 'apf-portal-bot' runs-on: [self-hosted, on-prem] # Lighthouse CI drives a real Chrome instance; the default act runner # image (catthehacker/ubuntu:act-22.04) ships without one. The :full # variant adds Chrome, Firefox, and the GUI-test toolchain — pinned # to the same Ubuntu 22.04 base as the default labels for parity. container: image: catthehacker/ubuntu:full-22.04 steps: - uses: actions/checkout@v6 - uses: pnpm/action-setup@v6 - uses: actions/setup-node@v6 with: node-version-file: '.nvmrc' - run: pnpm install --frozen-lockfile - run: pnpm ci:perf - uses: actions/upload-artifact@v7 if: always() with: name: lighthouseci-report path: .lighthouseci/ retention-days: 30 a11y: runs-on: [self-hosted, on-prem] steps: - uses: actions/checkout@v6 - uses: pnpm/action-setup@v6 - uses: actions/setup-node@v6 with: node-version-file: '.nvmrc' - run: pnpm install --frozen-lockfile # 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 - it # currently no-ops with a clear message. - run: echo "a11y gate placeholder - axe-core via Playwright wires up with the first real screens (ADR-0016)."