name: CI on: push: branches: [main] pull_request: branches: [main] types: [opened, synchronize, reopened, ready_for_review] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: contents: read env: CI_NODE_VERSION: "22.22.2" CI_NODE_24_VERSION: "24" jobs: lint: name: Lint 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 lint - run: npm run check:cycles - run: npm run check:route-validation:t06 - run: npm run check:any-budget:t11 - run: npm run check:docs-sync - run: npm run typecheck:core - run: npm run typecheck:noimplicit:core 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.2.0 with: python-version: "3.12" - name: Validate ${{ matrix.lang }} run: | python3 scripts/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-pr-test-policy.mjs --summary-file .artifacts/pr-test-policy.md - 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 advanced-security: name: Advanced Security Scans runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: TruffleHog Secret Scan uses: trufflesecurity/trufflehog@main with: path: ./ base: ${{ github.event.repository.default_branch }} head: HEAD extra_args: --only-verified - uses: actions/setup-node@v6 with: node-version: ${{ env.CI_NODE_VERSION }} cache: npm - run: npm ci - run: npm run check:node-runtime - name: Dependency audit run: npm audit --audit-level=high --omit=dev - name: Check for known vulnerabilities run: npx is-my-node-vulnerable - name: Run Snyk Vulnerability checks if: github.actor != 'dependabot[bot]' uses: snyk/actions/node@master env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} with: args: --severity-threshold=high 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 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 - run: npm run build:cli - run: npm run check:pack-artifact test-unit: name: Unit Tests 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 - run: npm run check:node-runtime - run: npm run test:unit node-24-compat: name: Node 24 Compatibility 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_24_VERSION }} cache: npm - run: npm ci - run: npm run check:node-runtime - run: npm run build - run: npm run test:unit test-coverage: name: Coverage 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 - run: npm run check:node-runtime - name: Run coverage gate run: npm run test:coverage - name: Build coverage summary if: always() run: | mkdir -p coverage if [ -f coverage/coverage-summary.json ]; then node scripts/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: ${{ env.SONAR_TOKEN == '' || env.SONAR_HOST_URL == '' }} run: | echo "SonarQube scan skipped because SONAR_TOKEN or SONAR_HOST_URL is not configured." >> "$GITHUB_STEP_SUMMARY" - name: SonarQube Scan if: ${{ env.SONAR_TOKEN != '' && env.SONAR_HOST_URL != '' }} uses: SonarSource/sonarqube-scan-action@v7 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 }}/4) runs-on: ubuntu-latest timeout-minutes: 15 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@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: npx playwright install --with-deps chromium - run: npm run build - run: npx playwright test tests/e2e/*.spec.ts --shard=${{ matrix.shard }}/4 test-integration: name: Integration Tests runs-on: ubuntu-latest timeout-minutes: 10 needs: build 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 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:integration 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 - i18n - pr-test-policy - advanced-security - build - package-artifact - test-unit - node-24-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 "| PR Test Policy | $(status '${{ needs.pr-test-policy.result }}') |" >> "$GITHUB_STEP_SUMMARY" echo "| Advanced Security | $(status '${{ needs.advanced-security.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 "" >> "$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 "| 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