name: CI description: Runs Go tests, race detection, vulnerability scanning, and frontend quality checks. on: push: branches: - main - develop pull_request: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: POSTGRES_USER: myuser POSTGRES_PASSWORD: mypassword POSTGRES_DB: mydb jobs: secrets-scan: name: Secrets scan runs-on: ubuntu-latest defaults: run: shell: sh steps: - uses: actions/checkout@v4 - name: Fix node toolcache path for Post-step cleanup run: | NODE_MAJOR=$(node -e "console.log(process.version.slice(1).split('.')[0])") mkdir -p /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin ln -sf $(which node) /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin/node - uses: actions/setup-go@v5 with: go-version: "1.26" cache: false - name: Install git (required by gitleaks) run: apk add --no-cache git - name: Install gitleaks # Pinned to a released version (supply-chain: never install @latest). run: go install github.com/gitleaks/gitleaks/v8@v8.30.1 - name: Detect secrets run: gitleaks detect --source . --verbose --no-banner env-docs-check: name: Env docs check runs-on: ubuntu-latest defaults: run: shell: sh steps: - uses: actions/checkout@v4 - name: Install python3 run: apk add --no-cache python3 - name: Check env var documentation run: python3 scripts/check-env-docs.py go-build: name: Go build runs-on: ubuntu-latest defaults: run: shell: sh steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: "1.26" cache: false - name: Cache Go modules uses: actions/cache@v4 with: path: | ~/go/pkg/mod ~/.cache/go-build key: ${{ runner.os }}-go-${{ hashFiles('backend/go.sum') }}-build restore-keys: | ${{ runner.os }}-go- - name: Fix node toolcache path for Post-step cleanup run: | NODE_MAJOR=$(node -e "console.log(process.version.slice(1).split('.')[0])") mkdir -p /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin ln -sf $(which node) /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin/node - name: Build run: go build ./... working-directory: backend go-vet-dev: name: Go vet (dev) needs: [go-build] runs-on: ubuntu-latest defaults: run: shell: sh steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: "1.26" cache: false - name: Cache Go modules uses: actions/cache@v4 with: path: | ~/go/pkg/mod ~/.cache/go-build key: ${{ runner.os }}-go-${{ hashFiles('backend/go.sum') }}-vet-dev restore-keys: | ${{ runner.os }}-go- - name: Fix node toolcache path for Post-step cleanup run: | NODE_MAJOR=$(node -e "console.log(process.version.slice(1).split('.')[0])") mkdir -p /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin ln -sf $(which node) /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin/node - name: Vet (dev tags) run: go vet -tags "test,dev" ./... working-directory: backend go-vet-prod: name: Go vet (prod) needs: [go-build] runs-on: ubuntu-latest defaults: run: shell: sh steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: "1.26" cache: false - name: Cache Go modules uses: actions/cache@v4 with: path: | ~/go/pkg/mod ~/.cache/go-build key: ${{ runner.os }}-go-${{ hashFiles('backend/go.sum') }}-vet-prod restore-keys: | ${{ runner.os }}-go- - name: Fix node toolcache path for Post-step cleanup run: | NODE_MAJOR=$(node -e "console.log(process.version.slice(1).split('.')[0])") mkdir -p /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin ln -sf $(which node) /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin/node - name: Vet (prod tags) run: go vet -tags "test,!dev" ./... working-directory: backend go-lint: name: golangci-lint needs: [go-build] runs-on: ubuntu-latest defaults: run: shell: sh steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: "1.26" cache: false - name: Cache Go modules uses: actions/cache@v4 with: path: | ~/go/pkg/mod ~/.cache/go-build key: ${{ runner.os }}-go-${{ hashFiles('backend/go.sum') }}-lint restore-keys: | ${{ runner.os }}-go- - name: Fix node toolcache path for Post-step cleanup run: | NODE_MAJOR=$(node -e "console.log(process.version.slice(1).split('.')[0])") mkdir -p /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin ln -sf $(which node) /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin/node - name: golangci-lint run: | go install github.com/golangci/golangci-lint/cmd/golangci-lint@v2.12.2 golangci-lint run ./... --timeout 5m working-directory: backend go-staticcheck-dev: name: Staticcheck (dev) needs: [go-build] runs-on: ubuntu-latest defaults: run: shell: sh steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: "1.26" cache: false - name: Cache Go modules uses: actions/cache@v4 with: path: | ~/go/pkg/mod ~/.cache/go-build key: ${{ runner.os }}-go-${{ hashFiles('backend/go.sum') }}-staticcheck-dev restore-keys: | ${{ runner.os }}-go- - name: Fix node toolcache path for Post-step cleanup run: | NODE_MAJOR=$(node -e "console.log(process.version.slice(1).split('.')[0])") mkdir -p /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin ln -sf $(which node) /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin/node - name: Staticcheck (dev tags) run: | go install honnef.co/go/tools/cmd/staticcheck@2026.1 staticcheck -tags "test,dev" ./... working-directory: backend go-staticcheck-prod: name: Staticcheck (prod) needs: [go-build] runs-on: ubuntu-latest defaults: run: shell: sh steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: "1.26" cache: false - name: Cache Go modules uses: actions/cache@v4 with: path: | ~/go/pkg/mod ~/.cache/go-build key: ${{ runner.os }}-go-${{ hashFiles('backend/go.sum') }}-staticcheck-prod restore-keys: | ${{ runner.os }}-go- - name: Fix node toolcache path for Post-step cleanup run: | NODE_MAJOR=$(node -e "console.log(process.version.slice(1).split('.')[0])") mkdir -p /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin ln -sf $(which node) /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin/node - name: Staticcheck (prod tags) run: | go install honnef.co/go/tools/cmd/staticcheck@2026.1 staticcheck -tags "test,!dev" ./... working-directory: backend go-gosec-dev: name: Security scan (dev) needs: [go-build] runs-on: ubuntu-latest defaults: run: shell: sh steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: "1.26" cache: false - name: Cache Go modules uses: actions/cache@v4 with: path: | ~/go/pkg/mod ~/.cache/go-build key: ${{ runner.os }}-go-${{ hashFiles('backend/go.sum') }}-gosec-dev restore-keys: | ${{ runner.os }}-go- - name: Fix node toolcache path for Post-step cleanup run: | NODE_MAJOR=$(node -e "console.log(process.version.slice(1).split('.')[0])") mkdir -p /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin ln -sf $(which node) /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin/node - name: gosec (dev tags) run: | go install github.com/securego/gosec/v2/cmd/gosec@v2.27.1 gosec -severity medium -tags "test,dev" ./... working-directory: backend go-gosec-prod: name: Security scan (prod) needs: [go-build] runs-on: ubuntu-latest defaults: run: shell: sh steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: "1.26" cache: false - name: Cache Go modules uses: actions/cache@v4 with: path: | ~/go/pkg/mod ~/.cache/go-build key: ${{ runner.os }}-go-${{ hashFiles('backend/go.sum') }}-gosec-prod restore-keys: | ${{ runner.os }}-go- - name: Fix node toolcache path for Post-step cleanup run: | NODE_MAJOR=$(node -e "console.log(process.version.slice(1).split('.')[0])") mkdir -p /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin ln -sf $(which node) /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin/node - name: gosec (prod tags) run: | go install github.com/securego/gosec/v2/cmd/gosec@v2.27.1 gosec -severity medium -tags "test,!dev" ./... working-directory: backend go-mod-tidy: name: go mod tidy needs: [go-build] runs-on: ubuntu-latest defaults: run: shell: sh steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: "1.26" cache: false - name: Cache Go modules uses: actions/cache@v4 with: path: | ~/go/pkg/mod ~/.cache/go-build key: ${{ runner.os }}-go-${{ hashFiles('backend/go.sum') }}-tidy restore-keys: | ${{ runner.os }}-go- - name: Fix node toolcache path for Post-step cleanup run: | NODE_MAJOR=$(node -e "console.log(process.version.slice(1).split('.')[0])") mkdir -p /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin ln -sf $(which node) /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin/node - name: go mod tidy check run: | cp go.sum go.sum.bak cp go.mod go.mod.bak go mod tidy diff -q go.sum go.sum.bak && diff -q go.mod go.mod.bak && echo "go.mod and go.sum up to date" || { echo "go.mod or go.sum out of date — run 'go mod tidy' and commit"; exit 1; } rm -f go.sum.bak go.mod.bak working-directory: backend test: timeout-minutes: 30 name: Tests (${{ matrix.label }}) needs: [secrets-scan, go-vet-dev, go-vet-prod, go-lint, go-staticcheck-dev, go-staticcheck-prod, go-gosec-dev, go-gosec-prod, go-mod-tidy, vulns] runs-on: ubuntu-latest defaults: run: shell: sh services: postgres: image: postgres:16-alpine env: POSTGRES_USER: myuser POSTGRES_PASSWORD: mypassword POSTGRES_DB: mydb options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 strategy: matrix: include: - label: dev gotags: test,dev verbose: -v coverflags: -coverprofile=coverage.out -covermode=atomic - label: prod gotags: test,!dev verbose: "" coverflags: -coverprofile=coverage.out -covermode=atomic steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: "1.26" cache: false - name: Cache Go modules uses: actions/cache@v4 with: path: | ~/go/pkg/mod ~/.cache/go-build key: ${{ runner.os }}-go-${{ hashFiles('backend/go.sum') }}-test-${{ matrix.label }} restore-keys: | ${{ runner.os }}-go- - name: Fix node toolcache path for Post-step cleanup run: | NODE_MAJOR=$(node -e "console.log(process.version.slice(1).split('.')[0])") mkdir -p /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin ln -sf $(which node) /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin/node - name: Install psql client run: apk add --no-cache postgresql-client - name: Wait for Postgres run: | for i in $(seq 1 30); do pg_isready -h postgres -U myuser && break sleep 1 done - name: Create test database run: PGPASSWORD=mypassword psql -h postgres -U myuser -d mydb -c "CREATE DATABASE crussell_test_db;" 2>/dev/null || true - name: Run tests working-directory: backend run: go test -tags "${{ matrix.gotags }}" -count=1 -timeout 120s ${{ matrix.verbose }} ${{ matrix.coverflags }} ./... env: GO_TESTING: "1" DAV_SKIP_INIT: "1" POSTGRES_HOST: postgres TEST_DB_HOST: postgres - name: Report coverage working-directory: backend run: | if [ -f coverage.out ]; then COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//') echo "Total coverage: $COVERAGE%" go tool cover -func=coverage.out | grep -E "total|handler" else echo "No coverage file generated" fi - name: Check coverage minimum if: matrix.label == 'dev' working-directory: backend run: | if [ -f coverage.out ]; then COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//') if awk "BEGIN {exit !($COVERAGE < 60)}"; then echo "FAIL: Coverage $COVERAGE% is below 60% minimum" exit 1 fi echo "PASS: Coverage $COVERAGE% meets 60% minimum" fi - name: Upload coverage artifact if: matrix.label == 'dev' run: | if [ -f backend/coverage.out ]; then echo "Coverage report available at backend/coverage.out" echo "Total coverage: $(go tool cover -func=backend/coverage.out 2>/dev/null | grep total | awk '{print $3}')" fi race: timeout-minutes: 30 name: Race (${{ matrix.label }}) needs: [secrets-scan, go-vet-dev, go-vet-prod, go-lint, go-staticcheck-dev, go-staticcheck-prod, go-gosec-dev, go-gosec-prod, go-mod-tidy, vulns] runs-on: ubuntu-latest defaults: run: shell: sh services: postgres: image: postgres:16-alpine env: POSTGRES_USER: myuser POSTGRES_PASSWORD: mypassword POSTGRES_DB: mydb options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 strategy: matrix: include: - label: dev gotags: test,dev - label: prod gotags: test,!dev steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: "1.26" cache: false - name: Cache Go modules uses: actions/cache@v4 with: path: | ~/go/pkg/mod ~/.cache/go-build key: ${{ runner.os }}-go-${{ hashFiles('backend/go.sum') }}-race-${{ matrix.label }} restore-keys: | ${{ runner.os }}-go- - name: Fix node toolcache path for Post-step cleanup run: | NODE_MAJOR=$(node -e "console.log(process.version.slice(1).split('.')[0])") mkdir -p /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin ln -sf $(which node) /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin/node - name: Install psql + build-base run: apk add --no-cache postgresql-client build-base - name: Wait for Postgres run: | for i in $(seq 1 30); do pg_isready -h postgres -U myuser && break sleep 1 done - name: Create test database run: PGPASSWORD=mypassword psql -h postgres -U myuser -d mydb -c "CREATE DATABASE crussell_test_db;" 2>/dev/null || true - name: Run race detector working-directory: backend run: go test -tags "${{ matrix.gotags }}" -race -count=1 -timeout 480s ./... env: GO_TESTING: "1" DAV_SKIP_INIT: "1" POSTGRES_HOST: postgres TEST_DB_HOST: postgres CGO_ENABLED: "1" vulns: name: Go vulnerabilities needs: [go-build] runs-on: ubuntu-latest defaults: run: shell: sh steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: "1.26" cache: false - name: Cache Go modules uses: actions/cache@v4 with: path: | ~/go/pkg/mod ~/.cache/go-build key: ${{ runner.os }}-go-${{ hashFiles('backend/go.sum') }}-vulns restore-keys: | ${{ runner.os }}-go- - name: Fix node toolcache path for Post-step cleanup run: | NODE_MAJOR=$(node -e "console.log(process.version.slice(1).split('.')[0])") mkdir -p /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin ln -sf $(which node) /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin/node - name: Go vulnerability scan working-directory: backend run: | go install golang.org/x/vuln/cmd/govulncheck@v1.1.4 govulncheck ./... frontend-deps: name: Frontend deps check runs-on: ubuntu-latest defaults: run: shell: sh steps: - uses: actions/checkout@v4 - name: Fix node toolcache path for Post-step cleanup run: | NODE_MAJOR=$(node -e "console.log(process.version.slice(1).split('.')[0])") mkdir -p /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin ln -sf $(which node) /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin/node - name: Cache npm dependencies uses: actions/cache@v4 with: path: | ~/.npm frontend/node_modules key: ${{ runner.os }}-npm-${{ hashFiles('frontend/package-lock.json') }} restore-keys: | ${{ runner.os }}-npm- - name: Install dependencies run: cd frontend && npm ci - name: Check for in-range dependency updates run: | cd frontend && npm outdated --json 2>&1 | node -e " const chunks = []; process.stdin.on('data', c => chunks.push(c)); process.stdin.on('end', () => { const raw = Buffer.concat(chunks).toString().trim(); if (!raw) { console.log('No dependencies to check'); process.exit(0); } const data = JSON.parse(raw); const updates = Object.entries(data).filter(([_, v]) => v.current !== v.wanted); if (updates.length) { console.log('In-range updates available — run \"npm update\" locally and commit:'); updates.forEach(([k, v]) => console.log(' ' + k + ': ' + v.current + ' -> ' + v.wanted)); process.exit(1); } console.log('All dependencies up to date within semver range'); }); " - name: Check for stale overrides run: | cd frontend && node -e " const { execSync } = require('child_process'); const pkg = JSON.parse(require('fs').readFileSync('package.json', 'utf8')); const overrides = pkg.overrides || {}; const keys = Object.keys(overrides); if (!keys.length) { console.log('No overrides configured'); process.exit(0); } function parseMin(range) { const v = range.replace(/^[\^~>=<]*/, '').split('.').map(Number); return { major: v[0]||0, minor: v[1]||0, patch: v[2]||0 }; } function gte(a, b) { if (a.major !== b.major) return a.major > b.major; if (a.minor !== b.minor) return a.minor > b.minor; return a.patch >= b.patch; } let stale = []; for (const key of keys) { const explain = execSync('npm explain ' + key + ' 2>/dev/null || true').toString(); const was = explain.match(/\(was \"([^\"]+)\"\)/); if (!was) { const found = explain.includes('node_modules/' + key); console.log(key + ': ' + (found ? 'override active' : 'not in tree')); continue; } const parentRange = was[1]; const overrideTarget = overrides[key]; if (gte(parseMin(parentRange), parseMin(overrideTarget))) { stale.push(key + ' (parent requires ' + parentRange + ', override is ' + overrideTarget + ')'); } } if (stale.length) { console.log('Stale overrides detected — remove from package.json:'); stale.forEach(s => console.log(' ' + s)); process.exit(1); } console.log('All overrides appear necessary'); " frontend-deps-major: name: Frontend major deps continue-on-error: true runs-on: ubuntu-latest defaults: run: shell: sh steps: - uses: actions/checkout@v4 - name: Fix node toolcache path for Post-step cleanup run: | NODE_MAJOR=$(node -e "console.log(process.version.slice(1).split('.')[0])") mkdir -p /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin ln -sf $(which node) /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin/node - name: Restore npm dependencies uses: actions/cache@v4 with: path: | ~/.npm frontend/node_modules key: ${{ runner.os }}-npm-${{ hashFiles('frontend/package-lock.json') }} restore-keys: | ${{ runner.os }}-npm- - name: Install dependencies run: cd frontend && npm ci - name: Check for major updates (non-blocking) run: | cd frontend && npm outdated --json 2>&1 | node -e " const chunks = []; process.stdin.on('data', c => chunks.push(c)); process.stdin.on('end', () => { const raw = Buffer.concat(chunks).toString().trim(); if (!raw) { console.log('No dependencies to check'); process.exit(0); } const data = JSON.parse(raw); const major = Object.entries(data).filter(([_, v]) => v.wanted !== v.latest); if (major.length) { console.log('Major (out-of-range) updates available — review carefully:'); major.forEach(([k, v]) => console.log(' ' + k + ': ' + v.current + ' (wanted: ' + v.wanted + ') -> latest: ' + v.latest)); // TODO: vite 8 + svelte-vite 7 now install clean via npm ci. // Remove --legacy-peer-deps from local npm install workflow once // @sveltejs/vite-plugin-svelte-inspector catches up. process.exit(0); } else { console.log('All dependencies within semver range, no major updates'); process.exit(0); } }); " knip: name: Knip needs: [frontend-deps] runs-on: ubuntu-latest defaults: run: shell: sh steps: - uses: actions/checkout@v4 - name: Fix node toolcache path for Post-step cleanup run: | NODE_MAJOR=$(node -e "console.log(process.version.slice(1).split('.')[0])") mkdir -p /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin ln -sf $(which node) /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin/node - name: Restore npm dependencies uses: actions/cache@v4 with: path: | ~/.npm frontend/node_modules key: ${{ runner.os }}-npm-${{ hashFiles('frontend/package-lock.json') }} restore-keys: | ${{ runner.os }}-npm- - name: Install dependencies run: cd frontend && npm ci - name: Run knip run: cd frontend && npx knip frontend-a11y: name: Frontend a11y check needs: [frontend-deps] runs-on: ubuntu-latest defaults: run: shell: sh steps: - uses: actions/checkout@v4 - name: Fix node toolcache path for Post-step cleanup run: | NODE_MAJOR=$(node -e "console.log(process.version.slice(1).split('.')[0])") mkdir -p /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin ln -sf $(which node) /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin/node - name: Restore npm dependencies uses: actions/cache@v4 with: path: | ~/.npm frontend/node_modules key: ${{ runner.os }}-npm-${{ hashFiles('frontend/package-lock.json') }} restore-keys: | ${{ runner.os }}-npm- - name: Install dependencies run: cd frontend && npm ci - name: Check a11y run: cd frontend && npm run lint:a11y frontend-build: name: Frontend build runs-on: ubuntu-latest defaults: run: shell: sh steps: - uses: actions/checkout@v4 - name: Fix node toolcache path for Post-step cleanup run: | NODE_MAJOR=$(node -e "console.log(process.version.slice(1).split('.')[0])") mkdir -p /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin ln -sf $(which node) /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin/node - name: Cache npm dependencies uses: actions/cache@v4 with: path: | ~/.npm frontend/node_modules key: ${{ runner.os }}-npm-${{ hashFiles('frontend/package-lock.json') }} restore-keys: | ${{ runner.os }}-npm- - name: Install dependencies run: cd frontend && npm ci - name: Build run: cd frontend && npm run build frontend-svelte-strict: name: Svelte strict check needs: [frontend-qc] runs-on: ubuntu-latest defaults: run: shell: sh steps: - uses: actions/checkout@v4 - name: Fix node toolcache path for Post-step cleanup run: | NODE_MAJOR=$(node -e "console.log(process.version.slice(1).split('.')[0])") mkdir -p /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin ln -sf $(which node) /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin/node - name: Restore npm dependencies uses: actions/cache@v4 with: path: | ~/.npm frontend/node_modules key: ${{ runner.os }}-npm-${{ hashFiles('frontend/package-lock.json') }} restore-keys: | ${{ runner.os }}-npm- - name: Install dependencies run: cd frontend && npm ci - name: svelte-kit sync run: cd frontend && npx svelte-kit sync - name: svelte-check (fail on warnings) run: cd frontend && npx svelte-check --tsconfig ./tsconfig.json --fail-on-warnings frontend-qc: name: Frontend QC (${{ matrix.task }}) needs: [frontend-build, knip] runs-on: ubuntu-latest defaults: run: shell: sh strategy: matrix: include: - task: typecheck cmd: npm run check - task: lint cmd: npm run lint - task: test cmd: npm test - task: audit cmd: npm audit --audit-level=info steps: - uses: actions/checkout@v4 - name: Fix node toolcache path for Post-step cleanup run: | NODE_MAJOR=$(node -e "console.log(process.version.slice(1).split('.')[0])") mkdir -p /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin ln -sf $(which node) /opt/hostedtoolcache/node/${NODE_MAJOR}.0.0/x64/bin/node - name: Restore npm dependencies uses: actions/cache@v4 with: path: | ~/.npm frontend/node_modules key: ${{ runner.os }}-npm-${{ hashFiles('frontend/package-lock.json') }} restore-keys: | ${{ runner.os }}-npm- - name: Install dependencies run: cd frontend && npm ci - name: Run QC task run: cd frontend && ${{ matrix.cmd }} docker-compose-check: name: Docker compose check runs-on: ubuntu-latest defaults: run: shell: sh steps: - uses: actions/checkout@v4 - name: Install docker CLI run: apk add --no-cache docker-cli docker-compose - name: Create env file for compose validation # compose.yml reads ./.env at the REPO ROOT (env_file paths resolve # relative to the compose file — compose.yml:25), not backend/.env. # A backend/.env file would leave the interpolation vars unset and the # config validation would not exercise the real file the stack uses. run: cp .env.example .env - name: Validate compose.yml run: docker compose -f compose.yml config --quiet nginx-check: name: Nginx config check runs-on: ubuntu-latest defaults: run: shell: sh steps: - uses: actions/checkout@v4 - name: Install nginx run: apk add --no-cache nginx - name: Validate nginx config run: | apk add --no-cache openssl mkdir -p /etc/nginx/http.d /etc/nginx/certs cp $(pwd)/nginx/conf.d/default.conf /etc/nginx/http.d/default.conf # Create dummy TLS certs so nginx -t can resolve ssl_certificate paths openssl req -x509 -nodes -days 1 -newkey rsa:2048 -keyout /etc/nginx/certs/privkey.pem -out /etc/nginx/certs/fullchain.pem -subj "/CN=localhost" 2>/dev/null # Add compose service names so nginx -t can resolve upstreams echo "127.0.0.1 backend sabredav" >> /etc/hosts nginx -t