name: CI on: push: branches: [main] pull_request: branches: [main] jobs: build-and-test: name: lint • test • smoke (Node ${{ matrix.node }}) runs-on: ubuntu-latest strategy: fail-fast: false matrix: # Node 20 is EOL (since 2025-10); we target current maintained LTS lines. node: ['22.x', '24.x'] steps: - name: Checkout uses: actions/checkout@v7 - name: Use Node ${{ matrix.node }} uses: actions/setup-node@v6 with: node-version: ${{ matrix.node }} cache: npm - name: Install dependencies run: npm ci - name: Format check run: npm run format:check - name: Type check (JSDoc + checkJs per-file) run: npm run typecheck - name: Lint run: npm run lint - name: Test run: npm test - name: Smoke compile (CLI end-to-end) run: | printf 'Buat ruang:\n "Hello CI"\n' > smoke.pjs node src/cli/index.js compile smoke.pjs --stdout > /dev/null rm -f smoke.pjs - name: Compile examples (E4) run: | for f in examples/*.pjs; do echo "Compiling $f..." node src/cli/index.js compile "$f" --stdout > /dev/null done - name: Compile multi-page example run: | for f in examples/multi-page/src/pages/*.pjs; do echo "Compiling $f..." node src/cli/index.js compile "$f" --stdout > /dev/null done - name: Compile demo apps (todo-app + dashboard-app) run: | # todo-app echo "Compiling todo-app..." node src/cli/index.js compile examples/todo-app/index.pjs --stdout > /dev/null # dashboard-app (SPA with auth) for f in examples/dashboard-app/index.pjs examples/dashboard-app/pages/*.pjs; do echo "Compiling $f..." node src/cli/index.js compile "$f" --stdout > /dev/null done readme-link-check: name: docs • README tests/* link integrity runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v7 # Anti-drift gate (F-01): every `(tests/...)` path referenced in README.md # MUST resolve to a real file. A renamed/removed/phantom test path FAILS # the build so the docs can never silently drift from the suite again. # Mirrors the user-specified one-liner but uses an explicit loop so a # missing file is GUARANTEED to exit non-zero (xargs swallows that). - name: Verify README tests/* links resolve run: | missing=0 while read -r f; do [ -z "$f" ] && continue if [ -f "$f" ]; then echo "ok $f" else echo "BROKEN $f" missing=$((missing + 1)) fi done < <(grep -oE '\(tests/[^)]+\)' README.md | sed 's/[()]//g' | sort -u) echo "broken links: $missing" if [ "$missing" -ne 0 ]; then echo "::error::README references $missing missing tests/* path(s)" exit 1 fi coverage: name: coverage • gate ≥80% lines (Node 22) runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v7 - name: Use Node 22 uses: actions/setup-node@v6 with: node-version: '22.x' cache: npm - name: Install dependencies run: npm ci # Runs `vitest run --coverage`. The per-module thresholds in # vitest.config.js (statements/branches/functions/lines) act as a hard # gate — the job FAILS if coverage regresses below them. This is what # makes "production ready" objectively verifiable rather than a claim. - name: Test with coverage (enforces thresholds) run: npm run coverage - name: Upload coverage report (lcov + html) if: always() uses: actions/upload-artifact@v7 with: name: coverage-report path: coverage/ retention-days: 14 - name: Coverage summary to job log if: always() run: | if [ -f coverage/coverage-summary.json ]; then node -e "const c=require('./coverage/coverage-summary.json').total;console.log('Lines: '+c.lines.pct+'% | Stmts: '+c.statements.pct+'% | Branch: '+c.branches.pct+'% | Funcs: '+c.functions.pct+'%')" >> "$GITHUB_STEP_SUMMARY" fi mutation: name: mutation • Stryker (resolver + analyzer) runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v7 - name: Use Node 22 uses: actions/setup-node@v6 with: node-version: '22.x' cache: npm - name: Install dependencies run: npm ci # Mutation testing proves the test ASSERTIONS catch regressions — not just # that lines executed. Scoped to the DSL semantic core (resolver + # analyzer) via stryker.config.json. The `break` threshold (45, just # below the measured 49.72% baseline) makes the job FAIL only if the # mutation score REGRESSES below it, so a PR that weakens assertions on # these high-risk modules is caught automatically. Raise `break` toward # `low`/`high` as survived mutants are killed over time. - name: Run Stryker mutation testing run: npm run mutation - name: Upload mutation report (html + json) if: always() uses: actions/upload-artifact@v7 with: name: mutation-report path: reports/mutation/ retention-days: 14 - name: Mutation score to job log if: always() run: | if [ -f reports/mutation/mutation.json ]; then node -e "const r=require('./reports/mutation/mutation.json');const f=Object.values(r.files);let killed=0,total=0;for(const x of f){for(const m of x.mutants){total++;if(m.status==='Killed')killed++;}}const score=total?((killed/(total-Object.values(r.files).reduce((a,x)=>a+x.mutants.filter(m=>['NoCoverage','Ignored','RuntimeError','CompileError'].includes(m.status)).length,0)))*100):0;console.log('Mutation: '+killed+'/'+total+' mutants killed')" >> \"$GITHUB_STEP_SUMMARY\" || true fi audit: name: supply-chain • npm audit (high+) runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v7 - name: Use Node 22 uses: actions/setup-node@v6 with: node-version: '22.x' cache: npm - name: Install dependencies run: npm ci # Runtime is zero-dependency, but the dev/build toolchain (vitest, eslint, # prettier, stryker, type defs) is not. This gate FAILS the build on a # known High/Critical advisory so supply-chain risk is surfaced on every # push/PR rather than discovered at release time. Dependabot opens the # follow-up upgrade PRs. - name: npm audit (fail on high or critical) run: npm audit --audit-level=high - name: Audit summary to job log if: always() run: | npm audit --json > npm-audit.json || true if [ -f npm-audit.json ]; then node -e "try{const a=require('./npm-audit.json').metadata.vulnerabilities;console.log('Vulns — critical:'+a.critical+' high:'+a.high+' moderate:'+a.moderate+' low:'+a.low)}catch(e){console.log('audit summary unavailable')}" >> "$GITHUB_STEP_SUMMARY" fi