Spaces:
Runtime error
Runtime error
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| types: [opened, synchronize, reopened, ready_for_review] | |
| workflow_dispatch: | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| env: | |
| CI_NODE_VERSION: "24" | |
| CI_NODE_24_VERSION: "24" | |
| CI_NODE_26_VERSION: "26" | |
| jobs: | |
| changes: | |
| name: Change Classification | |
| runs-on: ubuntu-latest | |
| outputs: | |
| code: ${{ steps.classify.outputs.code }} | |
| docs: ${{ steps.classify.outputs.docs }} | |
| i18n: ${{ steps.classify.outputs.i18n }} | |
| workflow: ${{ steps.classify.outputs.workflow }} | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 | |
| with: | |
| persist-credentials: false | |
| fetch-depth: 0 | |
| - id: classify | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| if [ "$EVENT_NAME" != "pull_request" ]; then | |
| { | |
| echo "code=true" | |
| echo "docs=true" | |
| echo "i18n=true" | |
| echo "workflow=true" | |
| } >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| code=false | |
| docs=false | |
| i18n=false | |
| workflow=false | |
| git diff --name-only "$BASE_SHA" "$HEAD_SHA" > changed-files.txt | |
| while IFS= read -r file; do | |
| case "$file" in | |
| .github/workflows/*|.zizmor.yml) | |
| workflow=true | |
| code=true | |
| ;; | |
| docs/*|*.md) | |
| docs=true | |
| ;; | |
| src/i18n/*|src/i18n/messages/*|scripts/i18n/*|config/i18n.json) | |
| i18n=true | |
| code=true | |
| ;; | |
| src/*|open-sse/*|bin/*|electron/*|tests/*|scripts/*|package.json|package-lock.json|tsconfig*.json|next.config.*|vitest*.config.*|playwright.config.*) | |
| code=true | |
| ;; | |
| db/*|config/*) | |
| code=true | |
| ;; | |
| *) | |
| code=true | |
| ;; | |
| esac | |
| done < changed-files.txt | |
| { | |
| echo "code=$code" | |
| echo "docs=$docs" | |
| echo "i18n=$i18n" | |
| echo "workflow=$workflow" | |
| } >> "$GITHUB_OUTPUT" | |
| lint: | |
| name: Lint | |
| runs-on: ubuntu-latest | |
| env: | |
| # tsx gates below (known-symbols, route-guard-membership) import modules that | |
| # open SQLite on load; provide DB env so a fresh CI DB initializes cleanly. | |
| JWT_SECRET: ci-lint-secret-with-sufficient-length-for-validation | |
| API_KEY_SECRET: ci-lint-api-key-secret-long | |
| DISABLE_SQLITE_AUTO_BACKUP: "true" | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.CI_NODE_VERSION }} | |
| cache: npm | |
| - uses: ./.github/actions/npm-ci-retry | |
| - run: npm run check:node-runtime | |
| - run: npm run audit:deps | |
| - run: npm run lint | |
| - run: npm run check:cycles | |
| - run: npm run check:route-validation:t06 | |
| - run: npm run check:any-budget:t11 | |
| - run: npm run check:provider-consistency | |
| - run: npm run check:fetch-targets | |
| - run: npm run check:deps | |
| - run: npm run check:file-size | |
| - run: npm run check:error-helper | |
| - run: npm run check:migration-numbering | |
| - run: npm run check:public-creds | |
| - run: npm run check:db-rules | |
| - run: npm run check:known-symbols | |
| - run: npm run check:route-guard-membership | |
| - run: npm run check:test-discovery | |
| - run: npm run check:tracked-artifacts | |
| - run: npm run check:lockfile | |
| - run: npm run check:licenses | |
| # check:docs-sync is run by the docs-sync-strict job (via check:docs-all) and the | |
| # husky pre-commit hook; the standalone copy here was redundant (ROI dedup). | |
| - run: npm run typecheck:core | |
| # typecheck:noimplicit:core is a forward-looking gate (noImplicitAny). | |
| # Run informationally for now β many pre-existing call sites still need | |
| # explicit annotations; track in a dedicated follow-up. | |
| - run: npm run typecheck:noimplicit:core | |
| continue-on-error: true | |
| quality-gate: | |
| name: Quality Ratchet | |
| runs-on: ubuntu-latest | |
| needs: test-coverage | |
| # Run even when test-coverage was SKIPPED/FAILED (e.g. a single flaky Coverage | |
| # Shard breaks the shardβcoverageβratchet chain). The DETERMINISTIC ratchets | |
| # (eslint / complexity / cognitive-complexity / duplication / codeql) do NOT need | |
| # the coverage artifact and MUST still run so cycle drift is measured on the | |
| # release PR β otherwise a flake silently skips the whole gate (incident: v3.8.36 | |
| # release PR #4854, where the drift cascade only surfaced post-merge in #5029). | |
| # The coverage.* metrics degrade gracefully: the download is continue-on-error and | |
| # the ratchet runs with --allow-missing, so absent coverage is skipped, not failed. | |
| if: ${{ !cancelled() }} | |
| # security-events: read lets the CodeQL ratchet read open code-scanning alerts | |
| # via `gh api .../code-scanning/alerts`. contents: read keeps checkout working. | |
| permissions: | |
| contents: read | |
| security-events: read | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.CI_NODE_VERSION }} | |
| cache: npm | |
| - uses: ./.github/actions/npm-ci-retry | |
| # Coverage mergeada (coverage-summary.json) p/ o ratchet de cobertura. | |
| # continue-on-error: o artifact pode nΓ£o existir se a job test-coverage foi | |
| # SKIPPED (shard flaky). Nesse caso collect-metrics pula coverage.* (ausente sem | |
| # erro) e o ratchet roda com --allow-missing β as mΓ©tricas determinΓsticas | |
| # (eslint/complexity/cognitive) seguem BLOQUEANTES. | |
| - uses: actions/download-artifact@v8 | |
| continue-on-error: true | |
| with: | |
| name: coverage-report | |
| path: coverage/ | |
| - run: npm run quality:collect | |
| # Catraca: falha se qualquer mΓ©trica regredir vs quality-baseline.json (commitado). | |
| # Hoje: contagem de warnings do ESLint. Fase 4 estende com cobertura (lida do | |
| # coverage mergeado). Tamanho de arquivo e duplicaΓ§Γ£o tΓͺm gates dedicados. | |
| # --allow-missing: pula mΓ©tricas do baseline ausentes do collect (coverage.* quando | |
| # o artifact nΓ£o veio) em vez de falhar β mantΓ©m os gates determinΓsticos ativos. | |
| - name: Ratchet check | |
| run: node scripts/quality/check-quality-ratchet.mjs --allow-missing --summary .artifacts/quality-ratchet.md | |
| # Fase 6A.5: require-tighten β BLOQUEANTE (promovido de advisory no fim do ciclo | |
| # v3.8.27). Falha quando uma mΓ©trica MELHOROU sem o baseline ter sido apertado no | |
| # mesmo PR (forΓ§a capturar ganhos permanentes). As mΓ©tricas coverage.* carregam | |
| # tightenSlack para o gap anti-flake (CI mergeado > baseline) nΓ£o disparar falso- | |
| # positivo. Verificado limpo (exit 0) no tip de release/v3.8.27 com a cobertura | |
| # mergeada == baseline (ver a nota _require_tighten_flip_blocking em | |
| # config/quality/quality-baseline.json). | |
| - name: Require-tighten (blocking) | |
| run: node scripts/quality/check-quality-ratchet.mjs --allow-missing --require-tighten | |
| # Catraca de duplicaΓ§Γ£o (jscpd@4 sobre src+open-sse). Roda neste job (paralelo) | |
| # para nΓ£o pesar no caminho crΓtico do lint. | |
| - name: Duplication ratchet | |
| run: npm run check:duplication | |
| - name: Complexity ratchet | |
| run: npm run check:complexity | |
| # Fase 7 INT: dead-code, cognitive-complexity, type-coverage promovidos de | |
| # advisory (quality-extended) para BLOQUEANTES aqui. Os 3 leem seus baseline | |
| # de quality-baseline.json e saem 1 em regressΓ£o. | |
| - name: Dead-code ratchet (knip) | |
| run: npm run check:dead-code | |
| - name: Cognitive complexity ratchet (sonarjs) | |
| run: npm run check:cognitive-complexity | |
| - name: Type coverage ratchet | |
| run: npm run check:type-coverage | |
| - name: Compression budget ratchet (F2.4 / N4) | |
| run: npm run check:compression-budget | |
| # CodeQL alerts ratchet β BLOQUEANTE (promovido de advisory na v3.8.26). | |
| # LΓͺ metrics.codeqlAlerts.value de quality-baseline.json e sai 1 SOMENTE numa | |
| # regressΓ£o real (alertas abertos > baseline). Falha de mediΓ§Γ£o (gh/auth/api) | |
| # Γ© skip gracioso com exit 0 β security-events:read no job-level permissions. | |
| - name: CodeQL alerts ratchet (blocking) | |
| run: npm run check:codeql-ratchet | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Append summary | |
| if: always() | |
| run: cat .artifacts/quality-ratchet.md >> "$GITHUB_STEP_SUMMARY" | |
| - name: Upload ratchet report | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: quality-ratchet | |
| path: .artifacts/quality-ratchet.md | |
| if-no-files-found: warn | |
| # Phase 7/8 extended quality gates β MIXED (Etapa 2, v3.8.26). The job no longer | |
| # carries a job-level continue-on-error: the three ratchet-blocking steps below | |
| # (Secret scan / Workflow lint / Bundle size, all passing --ratchet) FAIL the job | |
| # on a measured regression vs config/quality/quality-baseline.json. The remaining | |
| # steps stay ADVISORY via step-level continue-on-error (scanner install, | |
| # vuln/osv ratchet, OpenAPI/oasdiff breaking-change, circular-deps/dpdm): they | |
| # depend on external binaries/state that may legitimately self-skip, so they must | |
| # never block. The blocking gates themselves SKIP (exit 0) when their binary/plugin | |
| # is absent β only a measured regression on the SAME metric the baseline froze | |
| # blocks. The CodeQL ratchet was PROMOTED to the quality-gate job (v3.8.26). | |
| # SonarQube needs SONAR_TOKEN/SONAR_HOST_URL secrets. | |
| quality-extended: | |
| name: Quality Gates (Extended) | |
| runs-on: ubuntu-latest | |
| steps: | |
| # fetch-depth: 0 β the OpenAPI breaking-change gate (oasdiff) reads the base | |
| # spec via `git show <base_ref>:docs/openapi.yaml`; a shallow clone | |
| # would lack the base ref and the gate would self-skip (base-unresolved). | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| fetch-depth: 0 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.CI_NODE_VERSION }} | |
| cache: npm | |
| - uses: ./.github/actions/npm-ci-retry | |
| # Dead-code, cognitive-complexity, type-coverage foram promovidos ao job | |
| # quality-gate (bloqueante) na Fase 7 INT β nΓ£o rodam aqui para evitar duplo custo. | |
| - name: Circular deps (dpdm; advisory) | |
| continue-on-error: true | |
| run: npm run check:circular-deps | |
| # BLOCKING ratchet (Etapa 2): bundleSize must not regress vs the baseline | |
| # (gzip via @size-limit/file, installed by `npm ci`). --ratchet exits 1 on a | |
| # measured regression; it SKIPs (exit 0) when the size-limit plugin/build is | |
| # absent (a non-comparable measurement never blocks). | |
| - name: Bundle size (ratchet, blocking) | |
| run: npm run check:bundle-size -- --ratchet | |
| # CodeQL ratchet foi PROMOVIDO a BLOQUEANTE no job quality-gate (v3.8.26) β | |
| # nΓ£o roda aqui para evitar duplo run/duplo report. | |
| # Install the advisory security scanners so the gates below actually run | |
| # (they self-skip when the binaries are absent). Robustness lessons baked in: | |
| # β’ `go install β¦/gitleaks/v8@latest` produces a binary WITHOUT the version | |
| # ldflags gitleaks needs (and often fails) β avoided. | |
| # β’ `curl β¦api.github.com/β¦/releases/latest` is UNAUTHENTICATED and | |
| # rate-limited to 60 req/hr/IP; when throttled it returns an empty body, | |
| # so the asset URL resolves to nothing and the install silently no-ops β | |
| # every gate then self-skips and the metric is never produced. We instead | |
| # use `gh release download`, which is preinstalled on GitHub runners and | |
| # authenticated via GITHUB_TOKEN (5000 req/hr) β robust under load. | |
| # β’ actionlint keeps its official download script; zizmor stays on pipx. | |
| # We `set +e` (no single failure aborts the step), ALWAYS export $GITHUB_PATH | |
| # at the end, and print diagnostics so the next CI run proves exactly what | |
| # installed. The job is continue-on-error too, so an install hiccup never | |
| # blocks the build. | |
| - name: Install advisory security scanners (gitleaks/osv/actionlint/zizmor) | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set +e | |
| mkdir -p "$HOME/.local/bin" | |
| # gitleaks β download latest linux x64 tarball via gh (authed), extract binary | |
| rm -rf /tmp/gl && mkdir -p /tmp/gl | |
| gh release download --repo gitleaks/gitleaks --pattern '*linux_x64.tar.gz' --dir /tmp/gl | |
| tar -xzf /tmp/gl/*linux_x64.tar.gz -C "$HOME/.local/bin" gitleaks | |
| # osv-scanner β download latest linux amd64 bare binary via gh (authed) | |
| rm -rf /tmp/osv && mkdir -p /tmp/osv | |
| gh release download --repo google/osv-scanner --pattern '*linux_amd64' --dir /tmp/osv | |
| install -m 0755 /tmp/osv/*linux_amd64 "$HOME/.local/bin/osv-scanner" | |
| # actionlint β official download script | |
| bash <(curl -fsSL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) latest "$HOME/.local/bin" | |
| # zizmor β PyPI (pipx preferred, pip --user fallback); lands in ~/.local/bin | |
| pipx install zizmor || pip install --user zizmor | |
| # oasdiff β download latest linux amd64 tarball via gh (authed), extract binary | |
| rm -rf /tmp/oasd && mkdir -p /tmp/oasd | |
| gh release download --repo oasdiff/oasdiff --pattern '*linux_amd64.tar.gz' --dir /tmp/oasd | |
| tar -xzf /tmp/oasd/*linux_amd64.tar.gz -C "$HOME/.local/bin" oasdiff | |
| # ALWAYS export the bin dir (even if any step above failed) | |
| echo "$HOME/.local/bin" >> "$GITHUB_PATH" | |
| # diagnostics β prove what installed on the next CI run | |
| ls -la "$HOME/.local/bin" | |
| "$HOME/.local/bin/gitleaks" version || true | |
| "$HOME/.local/bin/actionlint" -version || true | |
| "$HOME/.local/bin/osv-scanner" --version || true | |
| "$HOME/.local/bin/oasdiff" --version || true | |
| zizmor --version || true | |
| # BLOCKING ratchet (Etapa 2): secretFindings must not regress vs the baseline. | |
| # --ratchet exits 1 on a measured regression; it SKIPs (exit 0) when gitleaks | |
| # is absent (a missing binary never blocks). | |
| - name: Secret scan (gitleaks, ratchet, blocking) | |
| run: npm run check:secrets -- --ratchet | |
| # BLOCKING ratchet (v3.8.27 cycle-end): vulnCount must not regress vs the | |
| # baseline. --ratchet exits 1 on a measured regression (measured > baseline); | |
| # it SKIPs (exit 0) when osv-scanner is absent or osv.dev is unreachable (a | |
| # missing/failed measurement never blocks). See the CVE-variance note in | |
| # docs/security/SUPPLY_CHAIN.md β a newly-disclosed CVE on an unchanged dep can | |
| # red this gate; the fix is to bump the dep or re-baseline metrics.vulnCount. | |
| - name: Vulnerability ratchet (osv-scanner, ratchet, blocking) | |
| run: npm run check:vuln-ratchet -- --ratchet | |
| # BLOCKING ratchet (Etapa 2): zizmorFindings must not regress vs the baseline. | |
| # ONLY zizmor is ratcheted β actionlint findings are reported, not blocking. | |
| # --ratchet exits 1 on a measured zizmor regression; it SKIPs (exit 0) when | |
| # zizmor is absent (a missing binary never blocks). | |
| - name: Workflow lint (actionlint+zizmor, ratchet, blocking) | |
| run: npm run check:workflows -- --ratchet | |
| # OpenAPI breaking-change detection (oasdiff). Diffs the PR's public API | |
| # contract (docs/openapi.yaml) against the base branch's spec. | |
| # BLOCKING ratchet (Fase 9 Onda 0): reads metrics.openapiBreaking.value and | |
| # exits 1 ONLY on a measured regression (count > baseline). It SKIPs (exit 0) | |
| # when oasdiff is absent or the base spec can't be resolved β a missing | |
| # measurement never blocks. BASE_REF is read by the script from the env | |
| # (never interpolated into a shell body) β workflow-injection-safe. | |
| - name: OpenAPI breaking-change (oasdiff, ratchet, blocking) | |
| env: | |
| BASE_REF: ${{ github.base_ref }} | |
| run: npm run check:openapi-breaking -- --ratchet | |
| docs-sync-strict: | |
| name: Docs Sync (Strict) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.CI_NODE_VERSION }} | |
| cache: npm | |
| - uses: ./.github/actions/npm-ci-retry | |
| - run: npm run check:docs-all | |
| # Previously-orphaned contract gates (existed as files, never wired anywhere). | |
| # All exit 0 today: cli-i18n is a hard gate, openapi-coverage is a ratchet | |
| # (floor ~36), openapi-security-tiers is advisory (Hard Rules #15/#17). | |
| - name: CLI i18n consistency | |
| run: npm run check:cli-i18n | |
| - name: OpenAPI route coverage (ratchet) | |
| run: npm run check:openapi-coverage | |
| - name: OpenAPI security-tier consistency (advisory) | |
| run: npm run check:openapi-security-tiers | |
| - name: OpenAPI spec paths resolve to real routes (anti-hallucination) | |
| run: npm run check:openapi-routes | |
| - name: Doc /api refs resolve to real routes (anti-hallucination) | |
| run: npm run check:docs-symbols | |
| - name: i18n translation drift (warn) | |
| run: node scripts/i18n/check-translation-drift.mjs --warn | |
| docs-lint: | |
| name: Docs Lint (prose β advisory) | |
| runs-on: ubuntu-latest | |
| # Advisory (warning-first): prose/markdown style must not block merges while the | |
| # existing doc corpus is brought up to style. Promote to blocking once it converges. | |
| continue-on-error: true | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.CI_NODE_VERSION }} | |
| cache: npm | |
| - name: markdownlint (docs + root, advisory) | |
| run: npx --yes markdownlint-cli2 "docs/**/*.md" "*.md" "!docs/i18n" "!docs/research" || true | |
| - name: Vale prose lint (Microsoft style, advisory) | |
| # Non-fatal: a Vale/reviewdog setup error must not turn this advisory job red. | |
| continue-on-error: true | |
| uses: errata-ai/vale-action@reviewdog | |
| with: | |
| files: docs | |
| fail_on_error: false | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| i18n-ui-coverage: | |
| name: i18n UI Coverage | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.CI_NODE_VERSION }} | |
| cache: npm | |
| - uses: ./.github/actions/npm-ci-retry | |
| - run: node scripts/i18n/check-ui-keys-coverage.mjs --threshold=65 | |
| i18n-matrix: | |
| name: Build language matrix | |
| runs-on: ubuntu-latest | |
| outputs: | |
| langs: ${{ steps.langs.outputs.langs }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| - id: langs | |
| run: | | |
| LANG_DIR="src/i18n/messages" | |
| LANGS=$(ls "$LANG_DIR"/*.json | xargs -n1 basename | sed 's/.json$//' | grep -v '^en$' | jq -R . | jq -s . | jq -c .) | |
| echo "langs=${LANGS}" >> "$GITHUB_OUTPUT" | |
| i18n: | |
| name: i18n Validation | |
| runs-on: ubuntu-latest | |
| continue-on-error: true | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| lang: ${{ fromJson(needs.i18n-matrix.outputs.langs) }} | |
| needs: i18n-matrix | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| - name: Validate ${{ matrix.lang }} | |
| env: | |
| # Pass the matrix value via env (never interpolate ${{ ... }} straight | |
| # into the run: script body) so the shell receives a variable, not | |
| # inlined text β zizmor template-injection mitigation. Named MATRIX_LANG | |
| # to avoid clobbering the POSIX `LANG` locale variable. | |
| MATRIX_LANG: ${{ matrix.lang }} | |
| run: | | |
| python3 scripts/i18n/validate_translation.py quick -l "$MATRIX_LANG" > result.txt | |
| - name: Upload result | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: i18n-${{ matrix.lang }} | |
| path: result.txt | |
| pr-test-policy: | |
| name: PR Test Policy | |
| if: ${{ github.event_name == 'pull_request' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| fetch-depth: 0 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.CI_NODE_VERSION }} | |
| - name: Fetch base branch | |
| run: git fetch --no-tags origin "${GITHUB_BASE_REF}" --depth=1 | |
| - name: Validate source changes include tests | |
| run: node scripts/check/check-pr-test-policy.mjs --summary-file .artifacts/pr-test-policy.md | |
| # Anti test-masking: flag net assert removal / new assert.ok(true) in changed tests. | |
| - name: Detect test-masking (weakened assertions) | |
| run: npm run check:test-masking | |
| # Evidence-in-PR-body (Hard Rule #18 mechanized): claims of "tests pass" must carry output. | |
| - name: Require evidence in PR body | |
| run: npm run check:pr-evidence | |
| env: | |
| PR_BODY: ${{ github.event.pull_request.body }} | |
| - name: Publish PR test policy summary | |
| if: always() | |
| run: | | |
| if [ -f .artifacts/pr-test-policy.md ]; then | |
| cat .artifacts/pr-test-policy.md >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| build: | |
| name: Build | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| if: ${{ github.event_name != 'pull_request' || needs.changes.outputs.code == 'true' }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.CI_NODE_VERSION }} | |
| cache: npm | |
| - uses: ./.github/actions/npm-ci-retry | |
| - run: npm run check:node-runtime | |
| - name: Cache Next.js build cache | |
| uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 | |
| with: | |
| path: .build/next/cache | |
| key: nextjs-${{ runner.os }}-node-${{ env.CI_NODE_VERSION }}-${{ hashFiles('package-lock.json') }}-${{ hashFiles('src/**/*', 'open-sse/**/*', 'db/**/*', 'next.config.mjs', 'tsconfig*.json', 'postcss.config.*', 'tailwind.config.*') }} | |
| restore-keys: | | |
| nextjs-${{ runner.os }}-node-${{ env.CI_NODE_VERSION }}-${{ hashFiles('package-lock.json') }}- | |
| - run: npm run build | |
| - name: Archive Next.js build for downstream jobs | |
| # Use tar so the archive preserves paths relative to CWD (.build/next/...). | |
| # upload-artifact path-stripping is ambiguous when exclude patterns are used; | |
| # an explicit tar avoids the double-nesting issue (.build/next/next/...). | |
| # Keep standalone/node_modules intact: package/electron jobs consume the | |
| # Next-traced standalone tree and must not replace it with root node_modules. | |
| run: | | |
| tar -czf /tmp/e2e-build.tar.gz \ | |
| --exclude='.build/next/cache' \ | |
| .build/next | |
| - name: Upload Next.js build for downstream jobs | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: next-build | |
| path: /tmp/e2e-build.tar.gz | |
| retention-days: 1 | |
| package-artifact: | |
| name: Package Artifact | |
| runs-on: ubuntu-latest | |
| needs: build | |
| env: | |
| JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.CI_NODE_VERSION }} | |
| cache: npm | |
| - uses: ./.github/actions/npm-ci-retry | |
| - run: npm run check:node-runtime | |
| - name: Download Next.js build artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: next-build | |
| path: /tmp/ | |
| - name: Extract Next.js build artifact | |
| run: | | |
| tar -xzf /tmp/e2e-build.tar.gz | |
| # build:cli consumes the downloaded .build/next standalone artifact and assembles dist/; | |
| # it only rebuilds if the downloaded standalone artifact is missing. | |
| - run: npm run build:cli | |
| - name: Assert dist/server.js exists | |
| run: test -f dist/server.js || (echo "dist/server.js missing β build:cli did not assemble correctly" && exit 1) | |
| - run: npm run check:pack-artifact | |
| electron-package-smoke: | |
| name: Electron Package Smoke | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 25 | |
| needs: build | |
| env: | |
| JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation | |
| CSC_IDENTITY_AUTO_DISCOVERY: "false" | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.CI_NODE_VERSION }} | |
| cache: npm | |
| - uses: ./.github/actions/npm-ci-retry | |
| - run: npm run check:node-runtime | |
| - name: Download Next.js build artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: next-build | |
| path: /tmp/ | |
| - name: Extract Next.js build artifact | |
| run: | | |
| tar -xzf /tmp/e2e-build.tar.gz | |
| - name: Install Electron dependencies | |
| working-directory: electron | |
| run: npm install --no-audit --no-fund | |
| - name: Pack Electron app | |
| working-directory: electron | |
| run: npm run pack | |
| - name: Smoke packaged Electron app | |
| env: | |
| ELECTRON_SMOKE_TIMEOUT_MS: 60000 | |
| run: xvfb-run -a npm run electron:smoke:packaged | |
| test-unit: | |
| name: Unit Tests (${{ matrix.shard }}/8) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| needs: build | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shard: [1, 2, 3, 4, 5, 6, 7, 8] | |
| env: | |
| JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation | |
| API_KEY_SECRET: ci-test-api-key-secret-long | |
| DISABLE_SQLITE_AUTO_BACKUP: "true" | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.CI_NODE_VERSION }} | |
| cache: npm | |
| - uses: ./.github/actions/npm-ci-retry | |
| - run: npm run check:node-runtime | |
| - run: node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/8 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts" | |
| test-vitest: | |
| name: Vitest (MCP / autoCombo / UI components) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| needs: build | |
| env: | |
| JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation | |
| API_KEY_SECRET: ci-test-api-key-secret-long | |
| DISABLE_SQLITE_AUTO_BACKUP: "true" | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.CI_NODE_VERSION }} | |
| cache: npm | |
| - uses: ./.github/actions/npm-ci-retry | |
| # The second test runner (CLAUDE.md: "Both test runners must pass") β was never | |
| # wired into CI until the 2026-06-09 quality audit (Fase 6A.2). | |
| - run: npm run test:vitest | |
| # vitest:ui is RED today (14 fails β UI component drift accumulated while the | |
| # suite never ran in CI). Informational until the Fase 6A triage (2026-06-16+) | |
| # fixes the components/tests; then drop continue-on-error to make it blocking. | |
| - run: npm run test:vitest:ui | |
| continue-on-error: true | |
| node-24-compat: | |
| name: Node 24 Compatibility Tests (${{ matrix.shard }}/4) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| needs: build | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shard: [1, 2, 3, 4] | |
| env: | |
| JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation | |
| API_KEY_SECRET: ci-test-api-key-secret-long | |
| DISABLE_SQLITE_AUTO_BACKUP: "true" | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.CI_NODE_24_VERSION }} | |
| cache: npm | |
| - uses: ./.github/actions/npm-ci-retry | |
| - run: npm run check:node-runtime | |
| - run: node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/4 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts" | |
| node-26-compat-build: | |
| name: Node 26 Compatibility Build | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 25 | |
| needs: build | |
| env: | |
| JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation | |
| API_KEY_SECRET: ci-test-api-key-secret-long | |
| DISABLE_SQLITE_AUTO_BACKUP: "true" | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.CI_NODE_26_VERSION }} | |
| cache: npm | |
| - uses: ./.github/actions/npm-ci-retry | |
| - run: npm run check:node-runtime | |
| - name: Cache Next.js build cache | |
| uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 | |
| with: | |
| path: .build/next/cache | |
| key: nextjs-${{ runner.os }}-node-${{ env.CI_NODE_26_VERSION }}-${{ hashFiles('package-lock.json') }}-${{ hashFiles('src/**/*', 'open-sse/**/*', 'db/**/*', 'next.config.mjs', 'tsconfig*.json', 'postcss.config.*', 'tailwind.config.*') }} | |
| restore-keys: | | |
| nextjs-${{ runner.os }}-node-${{ env.CI_NODE_26_VERSION }}-${{ hashFiles('package-lock.json') }}- | |
| - run: npm run build | |
| node-26-compat: | |
| name: Node 26 Compatibility Tests (${{ matrix.shard }}/4) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| needs: node-26-compat-build | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shard: [1, 2, 3, 4] | |
| env: | |
| JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation | |
| API_KEY_SECRET: ci-test-api-key-secret-long | |
| DISABLE_SQLITE_AUTO_BACKUP: "true" | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.CI_NODE_26_VERSION }} | |
| cache: npm | |
| - uses: ./.github/actions/npm-ci-retry | |
| - run: npm run check:node-runtime | |
| - run: node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/4 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts" | |
| test-coverage-shard: | |
| name: Coverage Shard (${{ matrix.shard }}/8) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 25 | |
| needs: build | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shard: [1, 2, 3, 4, 5, 6, 7, 8] | |
| env: | |
| JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation | |
| API_KEY_SECRET: ci-test-api-key-secret-long | |
| DISABLE_SQLITE_AUTO_BACKUP: "true" | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.CI_NODE_VERSION }} | |
| cache: npm | |
| - uses: ./.github/actions/npm-ci-retry | |
| - run: npm run check:node-runtime | |
| - name: Run c8 over shard ${{ matrix.shard }}/8 | |
| run: | | |
| rm -rf coverage-shard coverage-shard-report | |
| # `--temp-directory` (writable via NODE_V8_COVERAGE) is what the merge | |
| # job reads with `c8 report --temp-directory ...`. Using `--output-dir` | |
| # only produces the final json *report* and leaves the raw v8 files in | |
| # `coverage/tmp`, so uploading `coverage-shard/` was empty. Pin the temp | |
| # dir so the raw coverage files live there and the artifact upload picks | |
| # them up regardless of `--test-force-exit` timing. | |
| npx c8 \ | |
| --temp-directory=coverage-shard \ | |
| --reports-dir=coverage-shard-report \ | |
| --reporter=json \ | |
| --exclude=tests/** \ | |
| --exclude=**/*.test.* \ | |
| node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 \ | |
| --test-shard=${{ matrix.shard }}/8 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts" | |
| - name: Upload raw shard coverage | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: coverage-shard-${{ matrix.shard }} | |
| path: coverage-shard/*.json | |
| if-no-files-found: error | |
| test-coverage: | |
| name: Coverage | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| needs: test-coverage-shard | |
| if: ${{ !cancelled() && needs.test-coverage-shard.result == 'success' }} | |
| env: | |
| JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation | |
| API_KEY_SECRET: ci-test-api-key-secret-long | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.CI_NODE_VERSION }} | |
| cache: npm | |
| - uses: ./.github/actions/npm-ci-retry | |
| - name: Download all shard coverage | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: coverage-shard-* | |
| path: coverage-shards/ | |
| merge-multiple: true | |
| - name: Merge + report + gate | |
| # Merging 8 shards of raw v8 coverage is memory-heavy. `--merge-async` | |
| # keeps the V8 coverage merge incremental instead of loading every raw | |
| # JSON blob into one in-memory merge, which avoids Node heap OOMs. | |
| env: | |
| NODE_OPTIONS: --max-old-space-size=8192 | |
| run: | | |
| mkdir -p coverage | |
| first_coverage_file="" | |
| if [ -d coverage-shards ]; then | |
| first_coverage_file="$(find coverage-shards -maxdepth 1 -type f -name '*.json' -print -quit)" | |
| fi | |
| if [ -z "$first_coverage_file" ]; then | |
| echo "::error::No raw coverage shard data was downloaded." | |
| find . -maxdepth 3 -type f | sort | |
| exit 1 | |
| fi | |
| # Gate aligned to the project's local coverage bar: `npm run test:coverage` | |
| # gates at 60/60/60/60, so CI must match it (the previous CI floor of 40 | |
| # silently undershot the local bar β a real drift). Real merged coverage is | |
| # ~79/79/82/75, so 60 is a conservative floor with headroom; the Fase-4 | |
| # coverage ratchet (quality-baseline.json) layers "must not drop vs baseline" | |
| # on top of this floor. | |
| npx c8 report \ | |
| --temp-directory coverage-shards \ | |
| --reports-dir coverage \ | |
| --merge-async \ | |
| --reporter=text-summary \ | |
| --reporter=json-summary \ | |
| --exclude=tests/** \ | |
| --exclude=**/*.test.* \ | |
| --check-coverage \ | |
| --statements 60 --lines 60 --functions 60 --branches 60 | |
| - name: Build coverage summary | |
| if: always() | |
| run: | | |
| mkdir -p coverage | |
| if [ -f coverage/coverage-summary.json ]; then | |
| node scripts/check/test-report-summary.mjs \ | |
| --input coverage/coverage-summary.json \ | |
| --output coverage/coverage-report.md \ | |
| --threshold 60 | |
| else | |
| printf '%s\n' \ | |
| '# Coverage Report' \ | |
| '' \ | |
| 'Coverage summary JSON was not generated. Inspect the Coverage job logs.' \ | |
| > coverage/coverage-report.md | |
| fi | |
| cat coverage/coverage-report.md >> "$GITHUB_STEP_SUMMARY" | |
| - name: Upload coverage artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: coverage-report | |
| path: | | |
| coverage/coverage-summary.json | |
| coverage/coverage-report.md | |
| coverage/lcov.info | |
| if-no-files-found: warn | |
| sonarqube: | |
| name: SonarQube | |
| runs-on: ubuntu-latest | |
| needs: test-coverage | |
| if: ${{ !cancelled() && needs.test-coverage.result == 'success' }} | |
| env: | |
| SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} | |
| SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| fetch-depth: 0 | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: coverage-report | |
| path: . | |
| - name: Explain SonarQube skip | |
| if: ${{ github.event_name != 'pull_request' || env.SONAR_TOKEN == '' || env.SONAR_HOST_URL == '' }} | |
| run: | | |
| if [ "${{ github.event_name }}" != "pull_request" ]; then | |
| echo "SonarQube scan skipped on non-PR events to keep main pushes governed by repository CI gates." >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "SonarQube scan skipped because SONAR_TOKEN or SONAR_HOST_URL is not configured." >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| - name: SonarQube Scan | |
| if: ${{ github.event_name == 'pull_request' && env.SONAR_TOKEN != '' && env.SONAR_HOST_URL != '' }} | |
| uses: SonarSource/sonarqube-scan-action@v8 | |
| env: | |
| SONAR_TOKEN: ${{ env.SONAR_TOKEN }} | |
| SONAR_HOST_URL: ${{ env.SONAR_HOST_URL }} | |
| coverage-pr-comment: | |
| name: PR Coverage Comment | |
| runs-on: ubuntu-latest | |
| if: ${{ !cancelled() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false && needs.changes.outputs.code == 'true' }} | |
| needs: | |
| - changes | |
| - pr-test-policy | |
| - test-coverage | |
| permissions: | |
| contents: read | |
| issues: write | |
| pull-requests: write | |
| steps: | |
| - name: Download coverage artifact | |
| if: ${{ needs.test-coverage.result != 'cancelled' }} | |
| continue-on-error: true | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: coverage-report | |
| path: . | |
| - name: Prepare PR coverage comment | |
| env: | |
| COVERAGE_RESULT: ${{ needs.test-coverage.result }} | |
| POLICY_RESULT: ${{ needs.pr-test-policy.result }} | |
| run: | | |
| mkdir -p .artifacts | |
| { | |
| echo "<!-- omniroute-coverage-report -->" | |
| echo "## CI Coverage Report" | |
| echo "" | |
| echo "- Coverage job: \`${COVERAGE_RESULT}\`" | |
| echo "- PR test policy: \`${POLICY_RESULT}\`" | |
| echo "" | |
| if [ -f coverage/coverage-report.md ]; then | |
| cat coverage/coverage-report.md | |
| else | |
| echo "Coverage artifact was not available for this run." | |
| fi | |
| if [ "${POLICY_RESULT}" = "failure" ]; then | |
| echo "" | |
| echo "## PR Test Policy" | |
| echo "" | |
| echo "This PR changes production code in \`src/\`, \`open-sse/\`, \`electron/\`, or \`bin/\` without accompanying automated tests." | |
| fi | |
| } > .artifacts/pr-coverage-comment.md | |
| - uses: actions/github-script@v9 | |
| with: | |
| script: | | |
| const fs = require("fs"); | |
| const marker = "<!-- omniroute-coverage-report -->"; | |
| const body = fs.readFileSync(".artifacts/pr-coverage-comment.md", "utf8"); | |
| const { owner, repo } = context.repo; | |
| const issue_number = context.issue.number; | |
| const comments = await github.paginate(github.rest.issues.listComments, { | |
| owner, | |
| repo, | |
| issue_number, | |
| per_page: 100, | |
| }); | |
| const existing = comments.find((comment) => comment.body?.includes(marker)); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner, | |
| repo, | |
| comment_id: existing.id, | |
| body, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number, | |
| body, | |
| }); | |
| } | |
| test-e2e: | |
| name: E2E Tests (${{ matrix.shard }}/9) | |
| runs-on: ubuntu-latest | |
| # Build artifact from the `build` job is downloaded instead of rebuilding | |
| # (~5min saved per shard). 9 shards (up from 6) reduces tests per shard by | |
| # ~33%. Playwright browser is cached across runs (~1.5min saved per shard). | |
| # Heavy shard target: β€20min (was ~40min). Timeout 45min to cover slow runners. | |
| timeout-minutes: 45 | |
| needs: build | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shard: [1, 2, 3, 4, 5, 6, 7, 8, 9] | |
| env: | |
| JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation | |
| API_KEY_SECRET: ci-test-api-key-secret-long | |
| DISABLE_SQLITE_AUTO_BACKUP: "true" | |
| OMNIROUTE_PLAYWRIGHT_SKIP_BUILD: "1" | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.CI_NODE_VERSION }} | |
| cache: npm | |
| - uses: ./.github/actions/npm-ci-retry | |
| - run: npm run check:node-runtime | |
| - name: Cache Playwright browsers | |
| uses: actions/cache@v6.0.0 | |
| with: | |
| path: ~/.cache/ms-playwright | |
| key: playwright-chromium-${{ runner.os }}-${{ hashFiles('package-lock.json') }} | |
| restore-keys: playwright-chromium-${{ runner.os }}- | |
| - run: npx playwright install --with-deps chromium | |
| - name: Download Next.js build artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: next-build | |
| path: /tmp/ | |
| - name: Extract Next.js build artifact | |
| run: | | |
| tar -xzf /tmp/e2e-build.tar.gz | |
| - run: npx playwright test tests/e2e/*.spec.ts --shard=${{ matrix.shard }}/9 | |
| test-integration: | |
| name: Integration Tests (${{ matrix.shard }}/2) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| needs: build | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shard: [1, 2] | |
| env: | |
| JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation | |
| API_KEY_SECRET: ci-test-api-key-secret-long | |
| INITIAL_PASSWORD: ci-test-password-for-integration | |
| DATA_DIR: /tmp/omniroute-ci-${{ matrix.shard }} | |
| DISABLE_SQLITE_AUTO_BACKUP: "true" | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.CI_NODE_VERSION }} | |
| cache: npm | |
| - uses: ./.github/actions/npm-ci-retry | |
| - run: npm run check:node-runtime | |
| - run: node --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 --test-shard=${{ matrix.shard }}/2 tests/integration/*.test.ts | |
| test-security: | |
| name: Security Tests | |
| runs-on: ubuntu-latest | |
| needs: build | |
| env: | |
| JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation | |
| API_KEY_SECRET: ci-test-api-key-secret-long | |
| DISABLE_SQLITE_AUTO_BACKUP: "true" | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.CI_NODE_VERSION }} | |
| cache: npm | |
| - uses: ./.github/actions/npm-ci-retry | |
| - run: npm run check:node-runtime | |
| - run: npm run test:security | |
| ci-summary: | |
| name: CI Dashboard | |
| runs-on: ubuntu-latest | |
| if: ${{ !cancelled() }} | |
| needs: | |
| - changes | |
| - lint | |
| - docs-sync-strict | |
| - i18n-ui-coverage | |
| - i18n | |
| - pr-test-policy | |
| - build | |
| - package-artifact | |
| - electron-package-smoke | |
| - test-unit | |
| - node-24-compat | |
| - node-26-compat-build | |
| - node-26-compat | |
| - test-coverage | |
| - sonarqube | |
| - coverage-pr-comment | |
| - test-e2e | |
| - test-integration | |
| - test-security | |
| steps: | |
| - name: Download i18n results | |
| continue-on-error: true | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: i18n-* | |
| path: results | |
| merge-multiple: true | |
| - name: Generate dashboard | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| run: | | |
| status() { | |
| case "$1" in | |
| success) echo "π’ PASS" ;; | |
| failure) echo "π΄ FAIL" ;; | |
| cancelled) echo "β« CANCELLED" ;; | |
| skipped) echo "βͺ SKIPPED" ;; | |
| *) echo "π‘ UNKNOWN" ;; | |
| esac | |
| } | |
| echo "# π CI Dashboard" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "## π§± Core Checks" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Job | Status |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "|-----|--------|" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Change Classification | $(status '${{ needs.changes.result }}') |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Lint | $(status '${{ needs.lint.result }}') |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Docs Sync (Strict) | $(status '${{ needs.docs-sync-strict.result }}') |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| i18n UI Coverage | $(status '${{ needs.i18n-ui-coverage.result }}') |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| PR Test Policy | $(status '${{ needs.pr-test-policy.result }}') |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| SonarQube | $(status '${{ needs.sonarqube.result }}') |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "## ποΈ Build" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Job | Status |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "|-----|--------|" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Build Matrix | $(status '${{ needs.build.result }}') |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Package Artifact | $(status '${{ needs.package-artifact.result }}') |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Electron Package Smoke | $(status '${{ needs.electron-package-smoke.result }}') |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Node 26 Compatibility Build | $(status '${{ needs.node-26-compat-build.result }}') |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "## π§ͺ Tests" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Suite | Status |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "|-------|--------|" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Unit | $(status '${{ needs.test-unit.result }}') |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Node 24 Compatibility Tests | $(status '${{ needs.node-24-compat.result }}') |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Node 26 Compatibility Tests | $(status '${{ needs.node-26-compat.result }}') |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Coverage | $(status '${{ needs.test-coverage.result }}') |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| PR Coverage Comment | $(status '${{ needs.coverage-pr-comment.result }}') |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| E2E | $(status '${{ needs.test-e2e.result }}') |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Integration | $(status '${{ needs.test-integration.result }}') |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Security Tests | $(status '${{ needs.test-security.result }}') |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "## π Translations" >> "$GITHUB_STEP_SUMMARY" | |
| total=0 | |
| langs=0 | |
| if [ -d results ]; then | |
| for file in results/*.txt; do | |
| [ -f "$file" ] || continue | |
| val=$(sed -r 's/\x1B\[[0-9;]*[mK]//g' "$file" | grep "Untranslated:" | awk '{print $2}') | |
| val=${val:-0} | |
| total=$((total + val)) | |
| langs=$((langs + 1)) | |
| done | |
| fi | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Metric | Value |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "|--------|------|" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Languages checked | $langs |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Total untranslated | $total |" >> "$GITHUB_STEP_SUMMARY" | |
| if [ "$total" -gt 0 ]; then | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "β οΈ **Translations need attention**" >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "β **All translations complete**" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |