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: vet: name: Build & Vet 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 restore-keys: | ${{ runner.os }}-go- - name: Fix node toolcache path for Post-step cleanup run: | mkdir -p /opt/hostedtoolcache/node/22.23.1/x64/bin ln -sf /usr/local/bin/node /opt/hostedtoolcache/node/22.23.1/x64/bin/node - name: Build run: go build ./... working-directory: backend - name: Vet (dev tags) run: go vet -tags "test,dev" ./... working-directory: backend - name: Vet (prod tags) run: go vet -tags "test,!dev" ./... working-directory: backend test: name: Tests (${{ matrix.label }}) needs: [vet, 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 - label: prod gotags: test,!dev verbose: "" 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: | mkdir -p /opt/hostedtoolcache/node/22.23.1/x64/bin ln -sf /usr/local/bin/node /opt/hostedtoolcache/node/22.23.1/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 ${{ matrix.verbose }} ./... env: POSTGRES_HOST: postgres TEST_DB_HOST: postgres race: name: Race (${{ matrix.label }}) needs: [vet, 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: | mkdir -p /opt/hostedtoolcache/node/22.23.1/x64/bin ln -sf /usr/local/bin/node /opt/hostedtoolcache/node/22.23.1/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 ./... env: POSTGRES_HOST: postgres TEST_DB_HOST: postgres CGO_ENABLED: "1" vulns: name: Go vulnerabilities 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: | mkdir -p /opt/hostedtoolcache/node/22.23.1/x64/bin ln -sf /usr/local/bin/node /opt/hostedtoolcache/node/22.23.1/x64/bin/node - name: Go vulnerability scan working-directory: backend run: | go install golang.org/x/vuln/cmd/govulncheck@latest 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: | mkdir -p /opt/hostedtoolcache/node/22.23.1/x64/bin ln -sf /usr/local/bin/node /opt/hostedtoolcache/node/22.23.1/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-build: name: Frontend build (gate) 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: | mkdir -p /opt/hostedtoolcache/node/22.23.1/x64/bin ln -sf /usr/local/bin/node /opt/hostedtoolcache/node/22.23.1/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-qc: name: Frontend QC (${{ matrix.task }}) needs: [frontend-build] runs-on: ubuntu-latest defaults: run: shell: sh strategy: matrix: include: - task: typecheck cmd: npm run check - task: lint cmd: npm run lint - task: audit cmd: npm audit --audit-level=info steps: - uses: actions/checkout@v4 - name: Fix node toolcache path for Post-step cleanup run: | mkdir -p /opt/hostedtoolcache/node/22.23.1/x64/bin ln -sf /usr/local/bin/node /opt/hostedtoolcache/node/22.23.1/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 }}