Files
profilarr/.github/workflows/ci.yml
2026-05-25 06:37:15 +09:30

358 lines
10 KiB
YAML

name: CI
on:
push:
branches: ['hotfix/*']
paths-ignore:
['docs/**', '*.md', '!SECURITY.md', '.github/issue_template/**', '.github/assets/**']
pull_request:
branches: [develop]
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: read
jobs:
ci-scope:
name: Detect CI Scope
runs-on: ubuntu-latest
outputs:
full_ci: ${{ steps.push.outputs.full_ci || steps.filter.outputs.full_ci }}
steps:
- id: push
if: github.event_name != 'pull_request'
run: echo "full_ci=true" >> "$GITHUB_OUTPUT"
- id: filter
if: github.event_name == 'pull_request'
uses: dorny/paths-filter@v4
with:
predicate-quantifier: every
filters: |
full_ci:
- '**'
- '!docs/**'
- '!*.md'
- '!LICENSE'
- '!.github/issue_template/**'
- '!.github/assets/**'
docs-only:
name: Docs-only CI
needs: [ci-scope]
if: ${{ needs.ci-scope.outputs.full_ci != 'true' }}
runs-on: ubuntu-latest
steps:
- name: Report skipped CI
run: |
echo "Docs-only change detected." >> "$GITHUB_STEP_SUMMARY"
echo "Build, lint, type-check, tests, Semgrep, integration, and E2E jobs were intentionally skipped." >> "$GITHUB_STEP_SUMMARY"
# ─── Build ──────────────────────────────────────────────────────────────
build:
name: Build
needs: [ci-scope]
if: ${{ needs.ci-scope.outputs.full_ci == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Cache deps
uses: actions/cache@v5
with:
path: |
~/.cache/deno
node_modules
key: deno-${{ runner.os }}-${{ hashFiles('deno.lock') }}
restore-keys: deno-${{ runner.os }}-
- run: deno install --node-modules-dir
- name: Build
run: deno task build
- name: Upload build artifact
uses: actions/upload-artifact@v7
with:
name: profilarr-build
path: |
dist/build/profilarr
dist/build/server.js
dist/build/static/
retention-days: 1
# ─── Quality gates (all run in parallel with build) ─────────────────────
lint:
name: Lint
needs: [ci-scope]
if: ${{ needs.ci-scope.outputs.full_ci == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Cache deps
uses: actions/cache@v5
with:
path: |
~/.cache/deno
node_modules
key: deno-${{ runner.os }}-${{ hashFiles('deno.lock') }}
restore-keys: deno-${{ runner.os }}-
- run: deno install --node-modules-dir
- name: Lint
run: deno task lint
type-check:
name: Type Check
needs: [ci-scope]
if: ${{ needs.ci-scope.outputs.full_ci == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- uses: actions/setup-node@v6
with:
node-version: 22
- name: Cache deps
uses: actions/cache@v5
with:
path: |
~/.cache/deno
node_modules
key: deno-${{ runner.os }}-${{ hashFiles('deno.lock') }}
restore-keys: deno-${{ runner.os }}-
- run: deno install --node-modules-dir
- name: Sync SvelteKit types
run: npx svelte-kit sync
- name: Type check
run: deno task check
unit:
name: Unit Tests
needs: [ci-scope]
if: ${{ needs.ci-scope.outputs.full_ci == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Cache deps
uses: actions/cache@v5
with:
path: |
~/.cache/deno
node_modules
key: deno-${{ runner.os }}-${{ hashFiles('deno.lock') }}
restore-keys: deno-${{ runner.os }}-
- run: deno install --node-modules-dir
- name: Run unit tests
run: deno task test
semgrep:
name: Semgrep
needs: [ci-scope]
if: ${{ needs.ci-scope.outputs.full_ci == 'true' }}
runs-on: ubuntu-latest
container:
image: semgrep/semgrep
steps:
- uses: actions/checkout@v6
- name: Run Semgrep
run: |
semgrep scan \
--config tests/scan/semgrep/ \
--config p/default \
--config p/typescript \
--config p/javascript \
--config p/owasp-top-ten \
--config p/nodejs \
--config p/security-audit \
--config p/csharp \
--error
# ─── Tests that need the binary ─────────────────────────────────────────
integration:
name: Integration Tests
needs: [ci-scope, build]
if: ${{ needs.ci-scope.outputs.full_ci == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- uses: actions/setup-node@v6
with:
node-version: 22
- name: Cache deps
uses: actions/cache@v5
with:
path: |
~/.cache/deno
node_modules
key: deno-${{ runner.os }}-${{ hashFiles('deno.lock') }}
restore-keys: deno-${{ runner.os }}-
- run: deno install --node-modules-dir
# Dedicated cache for the FFI plug binaries (sqlite, bcrypt). The main
# `Cache deps` covers ~/.cache/deno but is shared across jobs and gets
# overwritten by Build (which never populates plug). This cache uses a
# separate key so it survives independently. Without this, the 30+
# parallel integration spec processes race to download plug binaries
# into the same path on a cold cache and corrupt each other.
- name: Cache FFI plug
id: plug-cache
uses: actions/cache@v5
with:
path: ~/.cache/deno/plug
key: deno-plug-${{ runner.os }}-${{ hashFiles('deno.lock') }}-v1
- name: Pre-warm FFI plug
if: steps.plug-cache.outputs.cache-hit != 'true'
run: |
deno eval --ext=ts '
import { hash } from "@felix/bcrypt";
import { Database } from "@db/sqlite";
await hash("warmup");
new Database(":memory:").close();
'
- name: Download build artifact
uses: actions/download-artifact@v8
with:
name: profilarr-build
path: dist/build
- name: Make binary executable
run: chmod +x dist/build/profilarr
- name: Cache Docker images
id: docker-cache
uses: actions/cache@v5
with:
path: /tmp/docker-images.tar
key: docker-images-${{ hashFiles('tests/integration/auth/docker-compose.yml') }}
- name: Load cached Docker images
if: steps.docker-cache.outputs.cache-hit == 'true'
run: docker load -i /tmp/docker-images.tar
- name: Pull and save Docker images
if: steps.docker-cache.outputs.cache-hit != 'true'
run: |
docker pull ghcr.io/navikt/mock-oauth2-server:3.0.1
docker pull caddy:2-alpine
docker pull nginx:alpine
docker save ghcr.io/navikt/mock-oauth2-server:3.0.1 caddy:2-alpine nginx:alpine -o /tmp/docker-images.tar
- name: Run integration tests
run: deno task test integration
e2e-auth:
name: E2E Tests
needs: [ci-scope, build]
if: ${{ needs.ci-scope.outputs.full_ci == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- uses: actions/setup-node@v6
with:
node-version: 22
- name: Cache deps
uses: actions/cache@v5
with:
path: |
~/.cache/deno
node_modules
key: deno-${{ runner.os }}-${{ hashFiles('deno.lock') }}
restore-keys: deno-${{ runner.os }}-
- run: deno install --node-modules-dir
- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@v5
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('deno.lock') }}
- name: Install Playwright browsers + system deps
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: npx playwright install --with-deps chromium
- name: Install Playwright system deps only (browsers cached)
if: steps.playwright-cache.outputs.cache-hit == 'true'
run: npx playwright install-deps chromium
- name: Download build artifact
uses: actions/download-artifact@v8
with:
name: profilarr-build
path: dist/build
- name: Make binary executable
run: chmod +x dist/build/profilarr
- name: Cache Docker images
id: docker-cache
uses: actions/cache@v5
with:
path: /tmp/docker-images.tar
key: docker-images-${{ hashFiles('tests/integration/auth/docker-compose.yml') }}
- name: Load cached Docker images
if: steps.docker-cache.outputs.cache-hit == 'true'
run: docker load -i /tmp/docker-images.tar
- name: Pull and save Docker images
if: steps.docker-cache.outputs.cache-hit != 'true'
run: |
docker pull ghcr.io/navikt/mock-oauth2-server:3.0.1
docker pull caddy:2-alpine
docker pull nginx:alpine
docker save ghcr.io/navikt/mock-oauth2-server:3.0.1 caddy:2-alpine nginx:alpine -o /tmp/docker-images.tar
- name: Run E2E auth tests
run: deno task test e2e auth