# Gitea Actions workflow — build/test on every push to main. Gitea Actions uses the same # workflow syntax as GitHub Actions (a compatible runner), so this is portable if the canonical # remote ever needs to move CI providers, without being GitHub Actions itself or requiring a # paid CI provider. Runs against the canonical Gitea repo only — see docs/PHASE4_PRODUCTION_INFRASTRUCTURE.md # §18 for why this never touches the GitHub kubekosh fork. # # Deploy this file to .gitea/workflows/build-and-test.yml at the repo root to activate it (kept # under infra/ci/ in this PR so it's reviewed alongside the rest of the infra package first, # per the same "build now, activate deliberately" posture as everything else server-dependent # in this phase — enabling Gitea Actions on the instance is itself a one-time admin step that # may not be done yet). name: build-and-test on: push: branches: [main] pull_request: branches: [main] jobs: regression: runs-on: ubuntu-latest services: postgres: image: postgres:16-alpine env: POSTGRES_PASSWORD: postgres ports: ["5432:5432"] options: >- --health-cmd "pg_isready -U postgres" --health-interval 10s --health-timeout 5s --health-retries 10 steps: - uses: actions/checkout@v4 - name: Set up Node uses: actions/setup-node@v4 with: node-version: "20" - name: Install dependencies (all first-party services) run: | for d in backend frontend platform/api platform/gateway platform/provisioner platform/lab-router; do (cd "$d" && npm ci) done - name: Syntax-check every service run: | for d in backend frontend/src platform/api platform/gateway platform/provisioner platform/lab-router; do find "$d" -name '*.js' -not -path '*/node_modules/*' -exec node --check {} \; done - name: Lab Host Router unit tests (Phase 4.1 — fast, no Docker daemon needed) run: (cd platform/lab-router && npm test) - name: Build frontend run: (cd frontend && npm run build) - name: Provisioner migrations against the service Postgres run: | cd platform/provisioner PLATFORM_DB_URL=postgres://postgres:postgres@localhost:5432/postgres \ node -e "require('./src/db').runMigrations().then(m=>console.log('Applied:',m.join(', ')))" - name: Platform API migrations (depends on Provisioner's, same order as production) run: | cd platform/api PLATFORM_DB_URL=postgres://postgres:postgres@localhost:5432/postgres \ node -e "require('./src/db').runMigrations().then(m=>console.log('Applied:',m.join(', ')))" - name: Validate infra configs (Phase 4 local tests — no hardware needed) run: | ./infra/scripts/validate-compose.sh ./infra/firewall/validate-firewall.sh # The real k3s-boot-dependent e2e suites (platform/test/e2e-phase*.js) need a full Docker # Compose stack with --privileged Lab containers, which a shared CI runner typically # can't grant safely — they stay a LOCAL/staging pre-promotion step # (infra/scripts/staging-smoke-test.sh), not this job. See docs/PHASE4_PRODUCTION_INFRASTRUCTURE.md §18. # Phase 4.1 §24 — free/open-source security scanning, no paid tooling, no signup required for # any of these. Severity policy (docs/SECURITY_SCANNING.md): CRITICAL/HIGH findings fail the # build; MEDIUM/LOW are reported but never block a merge — a build that's permanently red over # an unrelated low-severity transitive dependency finding trains everyone to ignore red builds. security-scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "20" - name: npm audit (dependency vulnerabilities) — fails only on high/critical run: | FAIL=0 for d in backend frontend platform/api platform/gateway platform/provisioner platform/lab-router; do echo "== $d ==" (cd "$d" && npm audit --audit-level=high) || FAIL=1 done exit $FAIL - name: Secret scanning (gitleaks — free, no account needed) run: docker run --rm -v "$PWD:/repo" zricethezav/gitleaks:latest detect --source=/repo --no-git -v - name: Dockerfile linting (hadolint — free) run: | FAIL=0 for f in Dockerfile platform/api/Dockerfile platform/gateway/Dockerfile platform/provisioner/Dockerfile platform/egress-proxy/Dockerfile platform/lab-router/Dockerfile; do echo "== $f ==" docker run --rm -i hadolint/hadolint < "$f" || FAIL=1 done exit $FAIL # -S error, not warning — matches docs/SECURITY_SCANNING.md's severity policy. Real run # against this tree at -S warning surfaces only style nits (unused loop vars, a missing # `cd ... || exit` guard) on scripts that are otherwise correct; failing the build on # those trains everyone to ignore the check. - name: Shell linting (shellcheck — free, standard) run: | sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck find infra -name '*.sh' -exec shellcheck -S error {} \+ - name: Container image scanning (trivy — free, no account needed) run: | docker build -q -t rootnode-scan/platform-api -f platform/api/Dockerfile . docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy:latest image \ --severity HIGH,CRITICAL --exit-code 1 rootnode-scan/platform-api build-images: needs: [regression, security-scan] if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Build and tag images run: ./infra/scripts/ci/tag-and-push.sh env: REGISTRY: ${{ vars.REGISTRY }} REGISTRY_USER: ${{ secrets.REGISTRY_USER }} REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} deploy-staging: needs: build-images if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Deploy to staging (automatic) run: | cd infra/ansible ansible-playbook site.yml -i inventory/staging.ini \ -e "image_tag=staging-${GITHUB_SHA::12}" \ --vault-password-file <(echo "$VAULT_PASSWORD") env: VAULT_PASSWORD: ${{ secrets.ANSIBLE_VAULT_PASSWORD }} - name: Staging smoke test run: ./infra/scripts/staging-smoke-test.sh # Production is NEVER automatic — a human runs infra/scripts/deploy-production.sh after # reviewing the staging smoke test result, per the explicit "manual approval step" preference. # No job here does that; see docs/RUNBOOK_NORMAL_DEPLOY.md for the actual command.