diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1fd9f16 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,87 @@ +name: CI + +# The checks this repository already had, run on every push. skill-check.mjs is +# the substantial one: it loads the real module graph through Vite, so it tests +# the pipeline rather than a mock of it. + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: npm + + - run: npm ci + + - name: Lint + run: npm run lint + + - name: Checks + # Prints "N/M checks passed" and exits non-zero on any failure. The count + # is asserted below, because a suite that silently stopped running most + # of itself still prints a pass line for whatever did run. + run: npm test | tee /tmp/check.log + + - name: Fail if the suite shrank + # A file that stops being reached — an import that throws, a section + # quietly skipped — reduces the count without failing anything. The + # number going DOWN is the signal; raise the floor when it goes up. + run: | + python3 - <<'PY' + import re, sys + log = open('/tmp/check.log').read() + m = re.findall(r'(\d+)/(\d+) checks passed', log) + if not m: + sys.exit("no check count in the output; did the suite run at all?") + passed, total = (int(x) for x in m[-1]) + print(f"{passed}/{total} checks passed") + if passed != total: + sys.exit(f"{total - passed} checks failed") + FLOOR = 900 + if total < FLOOR: + sys.exit(f"only {total} checks ran, expected at least {FLOOR} — " + "the suite is smaller than it was, which usually means part " + "of it stopped being reached rather than being deleted") + PY + + - name: Production build + # The build inlines VITE_* at build time and there is no runtime config, + # so a bundle built without these calls a relative /api/v1 that nginx + # serves as a static file — a login POST then answers 405. Building with + # them here is what proves the arguments still reach the bundle. + env: + VITE_API_BASE_URL: https://mcp.krowforce.com/api/v1 + VITE_AGENT_API: https://mcp.krowforce.com/api/v1 + run: | + npm run build + if ! grep -rq "mcp.krowforce.com/api/v1" dist/assets/*.js; then + echo "the API origin did not reach the bundle; the build args are not being inlined" + exit 1 + fi + echo "API origin is baked into the bundle" + + - name: The backend fixture is in step with this seed + # seed.json is generated from src/api/seed.js. Only runs where the + # backend is checked out beside this repo; says so rather than passing + # quietly when it is not. + run: | + if [ -d ../krow-backend ]; then + npm run seed:check + else + echo "krow-backend is not checked out beside this repo; fixture drift not checked here." + echo "The backend's own CI checks it from the other side." + fi