Files
apf_portal/.gitea/workflows/ci.yml
T
Julien Gautier 7adc6e33f5
CI / commits (pull_request) Successful in 2m9s
CI / check (pull_request) Successful in 2m31s
CI / scan (pull_request) Failing after 3m17s
CI / a11y (pull_request) Successful in 1m41s
CI / perf (pull_request) Successful in 3m1s
fix(ci): replace gitleaks-action with manual install
`gitleaks/gitleaks-action@v2` is now paywalled for organisations:
the action errors out with `🛑 missing gitleaks license. Go grab one
at gitleaks.io and store it as a GitHub Secret named
GITLEAKS_LICENSE.` Worse, it cannot reliably detect personal-vs-org
on Gitea (the GitHub API contract differs), so it defaults to
license enforcement and the scan fails. The gitleaks binary itself
remains MIT-licensed and free.

Mirror the pattern we just adopted for Trivy in #45: drop the
wrapper, install the binary directly via curl + tar from the GitHub
release, run the CLI. This:

- removes a third-party action dependency we did not need;
- pins the gitleaks version explicitly;
- harmonises with the Trivy step that lives next to it.

Apply the same change to `.gitea/workflows/security-scheduled.yml`
in the same PR — it had both broken integrations (`trivy-action
@master` plus `gitleaks-action@v2`) and was waiting silently to
fail at next Monday's cron.

Per-PR gitleaks scan uses `--no-git --source .` (working tree only)
since the scan job uses a shallow checkout; the weekly scheduled
job switches to a full clone (`fetch-depth: 0`) and runs gitleaks
in deep-history mode (default), which is the value-add of the
scheduled job over the per-PR gate.

`--redact` is added on both invocations so any matched secret is
masked in the CI log itself (no leak via the log artefact).

Also drop `cache: 'pnpm'` from `actions/setup-node` in the
scheduled workflow — we already removed it from ci.yml in #8 (the
act_runner cache server is unreachable from job containers; every
restore burns ~2 min ETIMEDOUT for zero hits). Consistency.
2026-05-07 19:25:54 +02:00

215 lines
9.3 KiB
YAML

# 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/<base_ref> 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
# `--scanners vuln`: limit Trivy to vulnerability scanning. Its
# secret scanner false-positives on demo RSA keys embedded in
# the README/fixtures of cryptographic npm packages (which
# land under .pnpm-store/), and we already have gitleaks below
# as the dedicated secret-scan gate. Trivy's intent in this
# job, per ADR-0015, was always "dependency vulnerability
# scan" — restoring that scope.
run: |
trivy fs \
--scanners vuln \
--ignore-unfixed \
--skip-dirs node_modules \
--exit-code 1 \
--severity CRITICAL,HIGH \
.
# Secret scan. Same install pattern as Trivy: gitleaks is a Go
# binary, and the official `gitleaks/gitleaks-action@v2` wrapper
# is now paywalled for organisations (a GITLEAKS_LICENSE secret
# from gitleaks.io is required, otherwise the action errors out
# with `🛑 missing gitleaks license`). The binary itself stays
# MIT-licensed and free — installing it directly bypasses the
# wrapper and gives us version pinning for free.
- name: Install gitleaks
env:
# Bump deliberately. Same caveat as TRIVY_VERSION above —
# not Renovate-tracked out of the box. Releases:
# https://github.com/gitleaks/gitleaks/releases.
GITLEAKS_VERSION: '8.21.0'
GITHUB_TOKEN: ${{ secrets.GITHUBCOM_TOKEN }}
run: |
curl -sfL \
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
-o /tmp/gitleaks.tar.gz \
"https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
tar -xzf /tmp/gitleaks.tar.gz -C /usr/local/bin gitleaks
gitleaks version
- name: Run gitleaks
# `--no-git --source .` scans the working tree only. The scan
# job uses a shallow checkout, so a git-history scan would not
# see beyond HEAD anyway; the weekly security-scheduled
# workflow does the deep history scan with a full clone.
# `--redact` masks any matched secret in the log output so we
# do not leak it via the CI logs themselves.
run: |
gitleaks detect \
--no-git \
--source . \
--redact \
--exit-code 1
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)."