Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .babelrc
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
]
],
"env": {
"test": {
"cypress-coverage": {
"plugins": ["istanbul"]
}
}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ jobs:

- name: Run gitleaks scan
if: always()
uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2
uses: gitleaks/gitleaks-action@e0c47f4f8be36e29cdc102c57e68cb5cbf0e8d1e # v3.0.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
Expand Down
194 changes: 194 additions & 0 deletions .github/workflows/sonar.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
name: SonarQube

on:
push:
branches: [main]
pull_request:
branches: [main]
types: [opened, synchronize, reopened]

permissions:
contents: read

concurrency:
group: sonar-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
sonarqube-pr:
name: SonarQube PR analysis
if: >-
github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository &&
github.actor != 'dependabot[bot]'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 0
Comment thread
coderabbitai[bot] marked this conversation as resolved.
persist-credentials: false

# Keep PR feedback close to Automatic Analysis: scan new code on every
# update, but do not install dependencies or run coverage suites.
- name: SonarQube Scan
uses: SonarSource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28f # v8.2.1
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

unit-coverage:
name: Jest coverage
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: "24"
cache: npm
cache-dependency-path: package-lock.json

- name: Install dependencies
run: npm ci --ignore-scripts

- name: Run Jest coverage
run: npm run test:unit:coverage

- name: Verify Jest LCOV
run: test -s coverage/jest/lcov.info

- name: Upload Jest coverage report
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: jest-coverage-${{ github.run_id }}
path: coverage/jest
if-no-files-found: error
retention-days: 14

component-coverage:
name: Cypress component coverage
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: "24"
cache: npm
cache-dependency-path: package-lock.json

- name: Install dependencies
run: npm ci --ignore-scripts

- name: Install Cypress binary
run: npx --no-install cypress install

- name: Run Cypress component coverage
run: npm run test:component:coverage

- name: Render Cypress component report
run: npm run coverage:component

- name: Verify Cypress LCOV
run: test -s coverage/component/lcov.info

- name: Upload Cypress component coverage report
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: component-coverage-${{ github.run_id }}
path: coverage/component
if-no-files-found: error
retention-days: 14

mobile-coverage:
name: Mobile Jest coverage
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: "24"
cache: npm
cache-dependency-path: |
package-lock.json
ui/mobile/package-lock.json

- name: Install root dependencies
run: npm ci --ignore-scripts

- name: Install mobile dependencies
run: npm --prefix ui/mobile ci --ignore-scripts

- name: Run mobile Jest coverage
run: npm --prefix ui/mobile run test:coverage -- --ci

- name: Verify mobile Jest LCOV
run: test -s ui/mobile/coverage/lcov.info

- name: Upload mobile Jest coverage report
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: mobile-coverage-${{ github.run_id }}
path: ui/mobile/coverage
if-no-files-found: error
retention-days: 14

sonarqube-main:
name: SonarQube main analysis
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: [unit-coverage, component-coverage, mobile-coverage]
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 0
persist-credentials: false

- name: Download Jest coverage report
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: jest-coverage-${{ github.run_id }}
path: coverage/jest

- name: Download Cypress component coverage report
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: component-coverage-${{ github.run_id }}
path: coverage/component

- name: Download mobile Jest coverage report
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: mobile-coverage-${{ github.run_id }}
path: ui/mobile/coverage

- name: Verify imported LCOV reports
run: |
test -s coverage/jest/lcov.info
test -s coverage/component/lcov.info
test -s ui/mobile/coverage/lcov.info

- name: SonarQube Scan with coverage
uses: SonarSource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28f # v8.2.1
with:
args: >-
-Dsonar.javascript.lcov.reportPaths=coverage/jest/lcov.info,coverage/component/lcov.info,ui/mobile/coverage/lcov.info
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
8 changes: 7 additions & 1 deletion cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ const componentCoverageOptions = {
'coverage/**/*',
],
include: [
'app/**/*.{js,jsx,ts,tsx}',
'core/**/*.{js,jsx,ts,tsx}',
'ui/**/*.{js,jsx,ts,tsx}',
'ui/web/**/*.{js,jsx,ts,tsx}',
],
}

Expand Down Expand Up @@ -55,9 +56,14 @@ export default defineConfig({

const coverageEnabled = config.env.codeCoverage === true || config.env.codeCoverage === 'true'
if (coverageEnabled) {
// Cypress is the owner of browser-side instrumentation. Jest adds its
// own Istanbul plugin when --coverage is enabled, so do not use the
// generic test Babel environment for this process.
process.env.BABEL_ENV = 'cypress-coverage'
config.env.codeCoverage = componentCoverageOptions
require('@cypress/code-coverage/task')(on, config)
} else {
process.env.BABEL_ENV = 'test'
delete config.env.codeCoverage
}

Expand Down
70 changes: 70 additions & 0 deletions docs/ai/deployment/2026-07-21-feature-sonar-test-coverage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
phase: deployment
title: Sonar Test Coverage Deployment
description: One-time migration from Automatic Analysis to GitHub Actions
---

# Sonar Test Coverage Deployment

## Infrastructure

- GitHub Actions runs the scanner and coverage producers.
- SonarQube Cloud stores static-analysis and main coverage measures.
- GitHub Actions artifacts store independent root Jest, Cypress, and mobile Jest
reports for 14 days.

## Deployment Pipeline

### Build Process

- PR: checkout full Git history and run only the Sonar scanner.
- Main: run root Jest, Cypress, and mobile Jest coverage in parallel, transfer
all reports to a final job, then run the scanner.

### CI/CD Pipeline

The dedicated `.github/workflows/sonar.yml` does not replace the existing
application `.github/workflows/build.yml`.

## Environment Configuration

### Development

Local SonarQube keeps project key `EverFreeNote` through a scanner command-line
override. Local reports must be generated before a local scan.

### Production

- SonarQube Cloud project: `koreyba_EverFreeNote`.
- Organization: `koreyba`.
- GitHub secret: `SONAR_TOKEN`.

## Deployment Steps

1. Ensure `SONAR_TOKEN` exists in GitHub repository secrets.
2. Immediately before pushing/enabling this workflow, open SonarQube Cloud
`Administration > Analysis Method` and disable Automatic Analysis. Do not
leave both analysis methods active for the same commit.
3. Push the workflow branch or update its PR and confirm the scanner-only PR
check behaves like the previous automatic check.
4. Merge to `main` and confirm the resulting workflow imports all three LCOV
files before publishing the main analysis.
5. Confirm the analyzed revision in SonarQube Cloud matches the merged commit.
6. Update branch protection if the required-check identity changed from the
SonarQube Cloud GitHub App check to the GitHub Actions job.

## Database Migrations

None.

## Secrets Management

`SONAR_TOKEN` is stored only in GitHub Secrets. Rotate it in SonarQube Cloud and
replace the GitHub secret if it is exposed or its owner changes.

## Rollback Plan

1. Disable the GitHub Actions Sonar workflow.
2. Re-enable Automatic Analysis in SonarQube Cloud.
3. Accept that coverage will no longer be imported while Automatic Analysis is
active.
Loading