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: 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@v6 - uses: actions/setup-node@v6 with: node-version: ${{ env.CI_NODE_VERSION }} cache: npm - run: npm ci - 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 - run: npm run check:docs-sync - 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 if: ${{ always() && needs.test-coverage.result == 'success' }} steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: ${{ env.CI_NODE_VERSION }} cache: npm - run: npm ci # Coverage mergeada (coverage-summary.json) p/ o ratchet de cobertura. - uses: actions/download-artifact@v8 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. - name: Ratchet check run: node scripts/quality/check-quality-ratchet.mjs --summary .artifacts/quality-ratchet.md # 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 - 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 extended quality gates — ADVISORY (continue-on-error). The 5 npm-based # ratchets (dead-code/cognitive-complexity/type-coverage/circular-deps/bundle-size) # run for real. The external scans (vuln/secrets/workflows) skip gracefully until the # owner adds install steps for osv-scanner/gitleaks/actionlint/zizmor; CodeQL ratchet # works via the runner's gh token. SonarQube needs SONAR_TOKEN/SONAR_HOST_URL secrets. quality-extended: name: Quality Gates (Extended, advisory) runs-on: ubuntu-latest continue-on-error: true steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: ${{ env.CI_NODE_VERSION }} cache: npm - run: npm ci - name: Dead-code (knip) run: npm run check:dead-code - name: Cognitive complexity (sonarjs) run: npm run check:cognitive-complexity - name: Type coverage run: npm run check:type-coverage - name: Circular deps (dpdm) run: npm run check:circular-deps - name: Bundle size run: npm run check:bundle-size - name: CodeQL alerts ratchet run: npm run check:codeql-ratchet env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Secret scan (gitleaks; skips if absent) run: npm run check:secrets - name: Vulnerability ratchet (osv-scanner; skips if absent) run: npm run check:vuln-ratchet - name: Workflow lint (actionlint+zizmor; skips if absent) run: npm run check:workflows docs-sync-strict: name: Docs Sync (Strict) runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: ${{ env.CI_NODE_VERSION }} cache: npm - run: npm ci - 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 i18n-ui-coverage: name: i18n UI Coverage runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: ${{ env.CI_NODE_VERSION }} cache: npm - run: npm ci - 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@v6 - 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@v6 - uses: actions/setup-python@v6 with: python-version: "3.12" - name: Validate ${{ 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@v6 with: 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 steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: ${{ env.CI_NODE_VERSION }} cache: npm - run: npm ci - run: npm run check:node-runtime - run: npm run build - name: Archive Next.js build for E2E shards # 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/...). run: | tar -czf /tmp/e2e-build.tar.gz \ --exclude='.build/next/standalone/node_modules' \ --exclude='.build/next/cache' \ .build/next - name: Upload Next.js build for E2E shards uses: actions/upload-artifact@v7 with: name: e2e-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@v6 - uses: actions/setup-node@v6 with: node-version: ${{ env.CI_NODE_VERSION }} cache: npm - run: npm ci - run: npm run check:node-runtime # build:cli runs a clean build into .build/next and assembles dist/ # For release builds prefer: npm run build:release (clean rebuild + HEAD sentinel) - 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@v6 - uses: actions/setup-node@v6 with: node-version: ${{ env.CI_NODE_VERSION }} cache: npm - run: npm ci - run: npm run check:node-runtime - run: npm run build - 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@v6 - uses: actions/setup-node@v6 with: node-version: ${{ env.CI_NODE_VERSION }} cache: npm - run: npm ci - run: npm run check:node-runtime - run: node --max-old-space-size=4096 --import tsx --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,compression,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.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@v6 - uses: actions/setup-node@v6 with: node-version: ${{ env.CI_NODE_VERSION }} cache: npm - run: npm ci # 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 (${{ 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 DISABLE_SQLITE_AUTO_BACKUP: "true" steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: ${{ env.CI_NODE_24_VERSION }} cache: npm - run: npm ci - run: npm run check:node-runtime - run: npm run build - run: node --import tsx --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/2 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,compression,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts" node-26-compat: name: Node 26 Compatibility (${{ 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 DISABLE_SQLITE_AUTO_BACKUP: "true" steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: ${{ env.CI_NODE_26_VERSION }} cache: npm - run: npm ci - run: npm run check:node-runtime - run: npm run build - run: node --import tsx --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/2 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,compression,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.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@v6 - uses: actions/setup-node@v6 with: node-version: ${{ env.CI_NODE_VERSION }} cache: npm - run: npm ci - 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 --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,compression,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.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: ${{ always() && 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@v6 - uses: actions/setup-node@v6 with: node-version: ${{ env.CI_NODE_VERSION }} cache: npm - run: npm ci - 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; the default Node # heap OOMs (exit 134). Raise it for the c8 merge/report step. env: NODE_OPTIONS: --max-old-space-size=6144 run: | mkdir -p coverage if [ ! -d coverage-shards ] || ! find coverage-shards -maxdepth 1 -type f -name '*.json' | grep -q .; 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 \ --reporter=text-summary \ --reporter=html \ --reporter=json-summary \ --reporter=lcov \ --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/lcov.info coverage/coverage-report.md if-no-files-found: warn sonarqube: name: SonarQube runs-on: ubuntu-latest needs: test-coverage if: ${{ always() && needs.test-coverage.result == 'success' }} env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} steps: - uses: actions/checkout@v6 with: 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: ${{ always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }} needs: - 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 "" 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 = ""; 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@v6 - uses: actions/setup-node@v6 with: node-version: ${{ env.CI_NODE_VERSION }} cache: npm - run: npm ci - run: npm run check:node-runtime - name: Cache Playwright browsers uses: actions/cache@v5 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: e2e-next-build path: /tmp/ - name: Extract Next.js build and restore standalone node_modules run: | tar -xzf /tmp/e2e-build.tar.gz cp -r node_modules .build/next/standalone/node_modules - 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@v6 - uses: actions/setup-node@v6 with: node-version: ${{ env.CI_NODE_VERSION }} cache: npm - run: npm ci - run: npm run check:node-runtime - run: node --import tsx --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@v6 - uses: actions/setup-node@v6 with: node-version: ${{ env.CI_NODE_VERSION }} cache: npm - run: npm ci - run: npm run check:node-runtime - run: npm run test:security ci-summary: name: CI Dashboard runs-on: ubuntu-latest if: always() needs: - 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 - 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 "| 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 "" >> "$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 | $(status '${{ needs.node-24-compat.result }}') |" >> "$GITHUB_STEP_SUMMARY" echo "| Node 26 Compatibility | $(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