Files
anki/.github/workflows/ci.yml
Fernando Lins a7f3ffb63a PoC: evaluate SonarCloud as a code quality and coverage tool (#4996)
## Linked issue

Closes #4995

## Summary / motivation

Bedges available:

[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=ankitects_anki&metric=coverage)](https://sonarcloud.io/summary/new_code?id=ankitects_anki)
[![Duplicated Lines
(%)](https://sonarcloud.io/api/project_badges/measure?project=ankitects_anki&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=ankitects_anki)
[![Code
Smells](https://sonarcloud.io/api/project_badges/measure?project=ankitects_anki&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=ankitects_anki)

[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=ankitects_anki&metric=bugs)](https://sonarcloud.io/summary/new_code?id=ankitects_anki)
[![Quality Gate
Status](https://sonarcloud.io/api/project_badges/measure?project=ankitects_anki&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=ankitects_anki)

[![Reliability](https://sonarcloud.io/api/project_badges/measure?project=ankitects_anki&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=ankitects_anki)

[![Security](https://sonarcloud.io/api/project_badges/measure?project=ankitects_anki&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=ankitects_anki)


**[SONAR
DASHBOARD](https://sonarcloud.io/project/overview?id=ankitects_anki)**

Integrates SonarCloud into the CI pipeline as a proof of concept to
evaluate whether
it surfaces actionable quality and security insights for this codebase.

Changes:
- Added `sonar-project.properties` configuring sources (`pylib`, `qt`,
`ts`, `rslib`)
  and coverage report paths
- Extended `tools/coverage/coverage-py` (and `.bat`) to emit
`coverage.xml` (Cobertura)
- Extended `tools/coverage/coverage-ts` (and `.bat`) to emit `lcov.info`
via the V8 provider
- Extended `tools/coverage/coverage-rust` (and `.bat`) to emit
`lcov.info` via
  `cargo-llvm-cov report`
- Added a `SonarCloud Scan` step to the `check-linux` CI job, running
after all
  checks pass and before the build cache is saved

## How to test

### Details

**1. Install sonar-scanner**
```bash
brew install sonar-scanner
```

**2. Generate and configure a token**

Go to [sonarcloud.io](https://sonarcloud.io/) → My Account → Security →
Generate Token
Copy the generated token and export it in your shell:
```
export SONAR_TOKEN=your_token_here
```

**3. Generate coverage reports**
```
just test --coverage
```
Expected output files:

 - out/coverage/python-pylib/coverage.xml
 - out/coverage/python-qt/coverage.xml
 - out/coverage/typescript/lcov.info
 - out/coverage/rust/lcov.info

**4. Run the scanner manually**
```
sonar-scanner
```
Results will appear in the SonarCloud dashboard

To test coverage generation locally:
```bash
just coverage
# verify files exist:
# out/coverage/python-pylib/coverage.xml
# out/coverage/python-qt/coverage.xml
# out/coverage/typescript/lcov.info
# out/coverage/rust/lcov.info
```
2026-08-26 15:48:45 -03:00

636 lines
20 KiB
YAML

name: CI
on:
workflow_dispatch:
push:
branches: [main, 'release/**']
pull_request:
branches: [main]
types: [opened, synchronize, reopened, labeled]
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
env:
N2_OUTPUT_PROGRESS: "1"
N2_OUTPUT_SUCCESS: "1"
jobs:
# Publish the PR number as a locator. The privileged SonarCloud workflow
# verifies it against the GitHub API before treating it as trusted metadata.
sonar-context:
if: github.event_name == 'pull_request'
runs-on: ubuntu-24.04
steps:
- name: Save PR number for SonarCloud
shell: bash
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
[[ "$PR_NUMBER" =~ ^[0-9]+$ ]]
mkdir -p out/sonar
printf '%s' "$PR_NUMBER" > out/sonar/pr-number.txt
- name: Upload PR context for SonarCloud
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
with:
name: sonar-pr-context
path: out/sonar/
retention-days: 1
minilints:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v4
with:
fetch-depth: 0
filter: blob:none
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 #v1
- name: Check for non-documentation changes
id: non-doc-changes
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 #v47.0.6
with:
files: |
**
files_ignore: |
**/*.md
**/*.mdx
docs/**/*.png
docs/**/*.svg
docs-site/**/*.png
docs-site/**/*.jpg
docs-site/**/*.svg
docs-site/**/*.mp4
- name: Run minilints
if: steps.non-doc-changes.outputs.any_changed == 'true'
run: cargo run -p minilints -- check /tmp/minilints.stamp
env:
CONTRIBUTORS_BYPASS_EMAILS: ${{ vars.CONTRIBUTORS_BYPASS_EMAILS }}
# Lightweight formatting checks (no build outputs needed).
# Uses individual tool installs instead of setup-anki to stay fast.
format:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v4
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 #v1
- name: Install uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 #v9.0.0
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 #v7.0.0
with:
node-version: '20'
- name: Install n2
run: tools/install-n2
env:
RUSTFLAGS: "--cap-lints warn"
- name: Install just
uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 #v4
- name: Run format checks
run: just fmt
# Linux runs on every PR and push to main.
check-linux:
runs-on: ubuntu-24.04
name: check (linux)
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v4
- name: Restore cargo cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 #v6.1.0
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
~/.cargo/bin
~/.cargo/.crates.toml
~/.cargo/.crates2.json
key: cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
restore-keys: cargo-${{ runner.os }}-
- name: Restore build output cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 #v6.1.0
with:
path: out
key: build-Linux-${{ github.event.pull_request.number || 'main' }}-${{ github.run_id }}
restore-keys: |
build-Linux-${{ github.event.pull_request.number || 'main' }}-
build-Linux-main-
build-Linux-
- name: Setup build environment
uses: ./.github/actions/setup-anki
- name: Install just
uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 #v4
- name: Install cargo-llvm-cov, cargo-nextest and cargo-deny
uses: taiki-e/install-action@67729d5c413db75907f0ad1e39bb04b9c868ff60 #v2.85.7
with:
tool: |
cargo-llvm-cov@0.8.4
cargo-nextest@0.9.99
cargo-deny@0.20.2
- name: Symlink node_modules
run: ln -sf out/node_modules .
- name: Restore coverage baseline
if: github.event_name == 'pull_request'
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 #v6.1.0
with:
path: out/coverage
key: coverage-baseline-linux-${{ github.sha }}
restore-keys: coverage-baseline-linux-
- name: Move restored baseline aside
if: github.event_name == 'pull_request'
run: test -d out/coverage && mv out/coverage out/coverage-baseline || true
- name: Build
run: just build
- name: Lint and test
env:
ONLINE_TESTS: "1"
run: |
just lint
just test --coverage
- name: Run complexipy diff
id: complexipy
continue-on-error: true
run: |
git fetch origin main:main 2>/dev/null || true
just complexipy-diff
- name: Upload SARIF results for complexipy
if: steps.complexipy.outcome == 'failure'
uses: github/codeql-action/upload-sarif@a6fd1787519fd23e68309fad43738e41a6ff2a9d #v4
with:
sarif_file: out/complexipy/complexipy-results.sarif
category: complexipy
- name: Cache coverage baseline
if: github.ref_name == 'main'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 #v6.1.0
with:
path: out/coverage
key: coverage-baseline-linux-${{ github.sha }}
- name: Check coverage regression
if: github.event_name == 'pull_request'
run: python3 tools/coverage/check-coverage-regression.py out/coverage-baseline out/coverage
- name: Run e2e tests
run: just test-e2e
- name: Upload Playwright report
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
with:
name: playwright-report
path: out/e2e-report/
retention-days: 7
- name: Ensure libs importable
env:
SKIP_RUN: "1"
run: ./run
- name: Check Rust dependencies
if: github.event_name == 'pull_request'
id: rust-deps
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 #v47.0.6
with:
files: |
Cargo.lock
**/Cargo.toml
.deny.toml
- name: Run cargo-deny check
if: github.event_name != 'pull_request' || steps.rust-deps.outputs.any_changed == 'true'
shell: bash
run: cargo deny check
- name: Upload coverage for SonarCloud
if: github.event_name == 'push' || github.event_name == 'pull_request'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7.0.1
with:
name: coverage-reports
path: |
out/coverage/python-pylib/coverage.xml
out/coverage/python-qt/coverage.xml
out/coverage/typescript/lcov.info
out/coverage/rust/lcov.info
retention-days: 1
# out/pyenv contains a venv with absolute Python paths that break
# across runs. out/build.ninja is regenerated by configure each time.
# Remove both before saving so the cache stays portable.
- name: Clean non-cacheable state
if: always()
shell: bash
run: |
rm -rf out/pyenv
rm -f out/build.ninja
- name: Save build output cache
if: always()
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 #v6.1.0
with:
path: out
key: build-Linux-${{ github.event.pull_request.number || 'main' }}-${{ github.run_id }}
# ARM Linux. Runs on pushes to release branches, manual dispatch, or on PRs
# with the check:linux label (not on every push to main).
check-linux-arm:
if: >-
(github.event_name == 'push' && startsWith(github.ref, 'refs/heads/release/'))
|| github.event_name == 'workflow_dispatch'
|| contains(github.event.pull_request.labels.*.name, 'check:linux')
runs-on: ubuntu-24.04-arm
name: check (linux-arm)
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v4
- name: Restore cargo cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 #v6.1.0
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
~/.cargo/bin
~/.cargo/.crates.toml
~/.cargo/.crates2.json
key: cargo-arm64-Linux-${{ hashFiles('Cargo.lock') }}
restore-keys: cargo-arm64-Linux-
- name: Restore build output cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 #v6.1.0
with:
path: out
key: build-arm64-Linux-${{ github.event.pull_request.number || 'main' }}-${{ github.run_id }}
restore-keys: |
build-arm64-Linux-${{ github.event.pull_request.number || 'main' }}-
build-arm64-Linux-main-
build-arm64-Linux-
- name: Setup build environment
uses: ./.github/actions/setup-anki
- name: Install just
uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 #v4
- name: Install cargo-llvm-cov and cargo-nextest
uses: taiki-e/install-action@67729d5c413db75907f0ad1e39bb04b9c868ff60 #v2.85.7
with:
tool: |
cargo-llvm-cov@0.8.4
cargo-nextest@0.9.99
- name: Symlink node_modules
run: ln -sf out/node_modules .
- name: Build
run: just build
- name: Lint and test
run: |
just lint
just test
- name: Clean non-cacheable state
if: always()
shell: bash
run: |
rm -rf out/pyenv
rm -f out/build.ninja
- name: Save build output cache
if: always()
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 #v6.1.0
with:
path: out
key: build-arm64-Linux-${{ github.event.pull_request.number || 'main' }}-${{ github.run_id }}
# Intel Mac. Runs on pushes to release branches, manual dispatch, or on PRs
# with the check:macos label (not on every push to main).
check-macos-intel:
if: >-
(github.event_name == 'push' && startsWith(github.ref, 'refs/heads/release/'))
|| github.event_name == 'workflow_dispatch'
|| contains(github.event.pull_request.labels.*.name, 'check:macos')
runs-on: macos-15-intel
name: check (macos-intel)
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v4
with:
submodules: true
- name: Restore cargo cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 #v6.1.0
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
~/.cargo/bin
~/.cargo/.crates.toml
~/.cargo/.crates2.json
key: cargo-intel-macOS-${{ hashFiles('Cargo.lock') }}
restore-keys: cargo-intel-macOS-
- name: Restore build output cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 #v6.1.0
with:
path: out
key: build-intel-macOS-${{ github.event.pull_request.number || 'main' }}-${{ github.run_id }}
restore-keys: |
build-intel-macOS-${{ github.event.pull_request.number || 'main' }}-
build-intel-macOS-main-
build-intel-macOS-
- name: Setup build environment
uses: ./.github/actions/setup-anki
- name: Install just
uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 #v4
- name: Install cargo-llvm-cov and cargo-nextest
uses: taiki-e/install-action@67729d5c413db75907f0ad1e39bb04b9c868ff60 #v2.85.7
with:
tool: |
cargo-llvm-cov@0.8.4
cargo-nextest@0.9.99
- name: Symlink node_modules
run: ln -sf out/node_modules .
- name: Build
run: just build
- name: Lint and test
run: |
just lint
just test
- name: Clean non-cacheable state
if: always()
shell: bash
run: |
rm -rf out/pyenv
rm -f out/build.ninja
- name: Save build output cache
if: always()
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 #v6.1.0
with:
path: out
key: build-intel-macOS-${{ github.event.pull_request.number || 'main' }}-${{ github.run_id }}
# Runs on pushes to main or on PRs with the check:macos label.
check-macos:
if: >-
github.event_name == 'push'
|| github.event_name == 'workflow_dispatch'
|| contains(github.event.pull_request.labels.*.name, 'check:macos')
runs-on: macos-latest
name: check (macos)
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v4
with:
submodules: true
- name: Restore cargo cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 #v6.1.0
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
~/.cargo/bin
~/.cargo/.crates.toml
~/.cargo/.crates2.json
key: cargo-macOS-${{ hashFiles('Cargo.lock') }}
restore-keys: cargo-macOS-
- name: Restore build output cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 #v6.1.0
with:
path: out
key: build-macOS-${{ github.event.pull_request.number || 'main' }}-${{ github.run_id }}
restore-keys: |
build-macOS-${{ github.event.pull_request.number || 'main' }}-
build-macOS-main-
build-macOS-
- name: Setup build environment
uses: ./.github/actions/setup-anki
- name: Install just
uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 #v4
- name: Install cargo-llvm-cov and cargo-nextest
uses: taiki-e/install-action@67729d5c413db75907f0ad1e39bb04b9c868ff60 #v2.85.7
with:
tool: |
cargo-llvm-cov@0.8.4
cargo-nextest@0.9.99
- name: Symlink node_modules
run: ln -sf out/node_modules .
- name: Build
run: just build
- name: Lint and test
run: |
just lint
just test
- name: Clean non-cacheable state
if: always()
shell: bash
run: |
rm -rf out/pyenv
rm -f out/build.ninja
- name: Save build output cache
if: always()
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 #v6.1.0
with:
path: out
key: build-macOS-${{ github.event.pull_request.number || 'main' }}-${{ github.run_id }}
# Runs on pushes to main or on PRs with the check:windows label.
check-windows:
if: >-
github.event_name == 'push'
|| github.event_name == 'workflow_dispatch'
|| contains(github.event.pull_request.labels.*.name, 'check:windows')
runs-on: windows-latest
name: check (windows)
# Colocate CARGO_HOME and TEMP on D: to keep all I/O on the same fast
# local disk.
env:
CARGO_HOME: D:\cargo-home
TEMP: D:\tmp
TMP: D:\tmp
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v4
with:
submodules: true
- name: Prepare D:\ directories
shell: bash
run: mkdir -p /d/cargo-home /d/tmp
- name: Restore cargo cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 #v6.1.0
with:
path: |
D:\cargo-home\registry\index
D:\cargo-home\registry\cache
D:\cargo-home\git\db
D:\cargo-home\bin
D:\cargo-home\.crates.toml
D:\cargo-home\.crates2.json
key: cargo-Windows-${{ hashFiles('Cargo.lock') }}
restore-keys: cargo-Windows-
- name: Restore build output cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 #v6.1.0
with:
path: out
key: build-Windows-${{ github.event.pull_request.number || 'main' }}-${{ github.run_id }}
restore-keys: |
build-Windows-${{ github.event.pull_request.number || 'main' }}-
build-Windows-main-
build-Windows-
- name: Setup build environment
uses: ./.github/actions/setup-anki
- name: Install just
uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 #v4
- name: Install cargo-llvm-cov and cargo-nextest
uses: taiki-e/install-action@67729d5c413db75907f0ad1e39bb04b9c868ff60 #v2.85.7
with:
tool: |
cargo-llvm-cov@0.8.4
cargo-nextest@0.9.99
- name: Build
run: just build
- name: Lint and test
run: |
just lint
just test
# Also remove node_modules on Windows — file-locking corrupts the cache.
- name: Clean non-cacheable state
if: always()
shell: bash
run: |
rm -rf out/pyenv out/node_modules
rm -f out/build.ninja
- name: Save build output cache
if: always()
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 #v6.1.0
with:
path: out
key: build-Windows-${{ github.event.pull_request.number || 'main' }}-${{ github.run_id }}
# ARM Windows. Runs on pushes to release branches, manual dispatch, or on
# PRs with the check:windows label (not on every push to main).
check-windows-arm:
if: >-
(github.event_name == 'push' && startsWith(github.ref, 'refs/heads/release/'))
|| github.event_name == 'workflow_dispatch'
|| contains(github.event.pull_request.labels.*.name, 'check:windows')
runs-on: windows-11-arm
name: check (windows-arm)
# Unlike the x64 Windows runner, windows-11-arm has no D: drive, so we use
# the default C:-based CARGO_HOME and TEMP.
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v4
with:
submodules: true
- name: Restore cargo cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 #v6.1.0
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
~/.cargo/bin
~/.cargo/.crates.toml
~/.cargo/.crates2.json
key: cargo-arm64-Windows-${{ hashFiles('Cargo.lock') }}
restore-keys: cargo-arm64-Windows-
- name: Restore build output cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 #v6.1.0
with:
path: out
key: build-arm64-Windows-${{ github.event.pull_request.number || 'main' }}-${{ github.run_id }}
restore-keys: |
build-arm64-Windows-${{ github.event.pull_request.number || 'main' }}-
build-arm64-Windows-main-
build-arm64-Windows-
- name: Setup build environment
uses: ./.github/actions/setup-anki
- name: Install just
uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 #v4
- name: Install cargo-llvm-cov and cargo-nextest
uses: taiki-e/install-action@67729d5c413db75907f0ad1e39bb04b9c868ff60 #v2.85.7
with:
tool: |
cargo-llvm-cov@0.8.4
cargo-nextest@0.9.99
- name: Build
run: just build
- name: Lint and test
run: |
just lint
just test
# Also remove node_modules on Windows — file-locking corrupts the cache.
- name: Clean non-cacheable state
if: always()
shell: bash
run: |
rm -rf out/pyenv out/node_modules
rm -f out/build.ninja
- name: Save build output cache
if: always()
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 #v6.1.0
with:
path: out
key: build-arm64-Windows-${{ github.event.pull_request.number || 'main' }}-${{ github.run_id }}