diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8864cf9b..3d28c941 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,6 +1,8 @@ name: Rust -permissions: read-all +permissions: + contents: read + pull-requests: write on: push: @@ -81,6 +83,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 - name: Download coverage reports uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: @@ -91,6 +95,15 @@ jobs: run: | sed -i 's#SF:/Users/#SF:/home/#g' lcov/*apple-darwin*.info npx lcov-result-merger 'lcov/lcov-*.info' lcov.info - - name: Upload coverage - uses: coverallsapp/github-action@5cbfd81b66ca5d10c19b062c04de0199c215fb6e # v2.3.7 - continue-on-error: true + - name: Upload Coverage + run: | + PR_ARGS="" + if [ "${{ github.event_name }}" = "pull_request" ]; then + git fetch origin ${{ github.event.pull_request.base.sha }} + PR_ARGS="--pr ${{ github.event.pull_request.number }} --pr-base ${{ github.event.pull_request.base.sha }}" + fi + node tools/codecov/upload.mjs ./lcov.info $PR_ARGS + env: + CODECOV_URL: ${{ vars.CODECOV_URL }} + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/alioth/src/firmware/acpi/reg_test.rs b/alioth/src/firmware/acpi/reg_test.rs index 3fbac961..8b695ab5 100644 --- a/alioth/src/firmware/acpi/reg_test.rs +++ b/alioth/src/firmware/acpi/reg_test.rs @@ -20,5 +20,5 @@ fn test_pm_timer() { let timer = AcpiPmTimer::default(); let v1 = timer.read(0, 4).unwrap(); let v2 = timer.read(0, 4).unwrap(); - assert!(v2 > v1); + assert!(v2 >= v1); } diff --git a/tools/codecov/.gitignore b/tools/codecov/.gitignore new file mode 100644 index 00000000..a5e86995 --- /dev/null +++ b/tools/codecov/.gitignore @@ -0,0 +1,13 @@ +# Dependencies +node_modules/ + +# Build Output +dist/ +.wrangler/ + +# Environment Variables +.dev.vars + +# Operating System Files +.DS_Store +Thumbs.db diff --git a/tools/codecov/README.md b/tools/codecov/README.md new file mode 100644 index 00000000..47919572 --- /dev/null +++ b/tools/codecov/README.md @@ -0,0 +1,130 @@ +# Alioth Code Coverage Dashboard + +This project is a lightweight, Coveralls-style code coverage dashboard. It is built using the [Hono](https://hono.dev) framework and runs on **Cloudflare Workers**. It can be configured to serve any GitHub repository via `wrangler.toml`. + +To remain within edge execution limits and keep storage costs low, this app uses: +- **Cloudflare D1 (SQLite)**: To store lightweight metadata (commit SHA, branch, overall coverage %). +- **Cloudflare R2 (Blob Storage)**: To store parsed coverage mappings as JSON. +- **GitHub Raw API**: To dynamically fetch source code on demand, rather than storing massive source files in a database. + +## Prerequisites + +- Node.js (v18+) +- A Cloudflare account +- `wrangler` CLI installed globally or via `npx` + +## Setup & Configuration + +1. **Install dependencies:** + ```bash + npm install + ``` + +2. **Create the D1 Database:** + ```bash + npx wrangler d1 create alioth-codecov-db + ``` + *Take note of the `database_id` returned by this command.* + +3. **Update `wrangler.toml`:** + Open `wrangler.toml` and replace `REPLACE_WITH_YOUR_D1_DATABASE_ID` with the actual ID you just generated. + +4. **Create the R2 Bucket:** + ```bash + npx wrangler r2 bucket create alioth-codecov-reports + ``` + +5. **Initialize the Database Schema:** + Run the schema against your production D1 database: + ```bash + npx wrangler d1 execute alioth-codecov-db --remote --file=./schema.sql + ``` + +## Local Development + +To run the worker locally, you first need to initialize the local SQLite database simulation: + +```bash +# Initialize local DB +npx wrangler d1 execute alioth-codecov-db --local --file=./schema.sql + +# Start the development server +npm run dev +``` + +The app will be available at `http://localhost:8787`. + +## Securing the API (Authentication) + +To prevent unauthorized uploads to your dashboard, you should set a secret token. Both the worker and the upload script will use this to verify the payload. + +1. **Generate a random token** (e.g., `openssl rand -hex 32`). +2. **Set the token in Cloudflare Secrets:** + ```bash + npx wrangler secret put CODECOV_TOKEN + ``` + *(Paste your token when prompted)* +3. **Set the token for local development:** Create a `.dev.vars` file in the `codecov` directory: + ```env + CODECOV_TOKEN=your_local_secret_token + ``` + +## Deployment + +Deploy the worker to your Cloudflare account: + +```bash +npm run deploy +``` + +Once deployed, you will receive a `*.workers.dev` URL (or your custom domain) where the dashboard is hosted. + +## Uploading Coverage Reports + +A Node.js script (`upload.mjs`) is provided to parse `lcov.info` files and upload them to the worker. + +This script extracts git metadata (commit SHA, branch) directly from the local git repository and converts the LCOV file into an optimized JSON payload before sending it to the API. + +### Usage + +```bash +# Export the URL of your deployed worker (or use localhost for local testing) +export CODECOV_URL="https://your-worker-url.workers.dev/api/upload" + +# Export the authentication token you created +export CODECOV_TOKEN="your_secret_token" + +# Run the upload script from the root of your repository +node tools/codecov/upload.mjs ./lcov.info +``` + +### CI/CD Integration (GitHub Actions) +In your GitHub Actions workflow, after running tests and generating an `lcov` report, you can use a unified step to automatically push coverage data to your dashboard for both regular commits and pull requests: + +```yaml +- name: Upload Coverage + run: | + PR_ARGS="" + if [ "${{ github.event_name }}" = "pull_request" ]; then + PR_ARGS="--pr ${{ github.event.pull_request.number }} --pr-base ${{ github.event.pull_request.base.sha }}" + fi + node tools/codecov/upload.mjs ./lcov.info $PR_ARGS + env: + CODECOV_URL: ${{ vars.CODECOV_URL }} + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +``` + +## Shields.io Badge + +You can embed a dynamic code coverage badge in your repository's `README.md` using Shields.io. The badge will automatically turn green (>=80%), yellow (>=60%), or red depending on the coverage of your `main` branch. + +```md +![Coverage](https://img.shields.io/endpoint?url=https://your-worker-url.workers.dev/api/badge) +``` + +To fetch the badge for a specific branch, append `?branch=branch-name` to the endpoint URL: + +```md +![Coverage](https://img.shields.io/endpoint?url=https://your-worker-url.workers.dev/api/badge?branch=feature/test) +``` diff --git a/tools/codecov/package-lock.json b/tools/codecov/package-lock.json new file mode 100644 index 00000000..80eab1af --- /dev/null +++ b/tools/codecov/package-lock.json @@ -0,0 +1,1587 @@ +{ + "name": "codecov-worker", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "codecov-worker", + "version": "1.0.0", + "dependencies": { + "hono": "^4.0.0" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20240222.0", + "typescript": "^5.0.0", + "wrangler": "^4.73.0" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.2.tgz", + "integrity": "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==", + "dev": true, + "license": "MIT OR Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.15.0.tgz", + "integrity": "sha512-EGYmJaGZKWl+X8tXxcnx4v2bOZSjQeNI5dWFeXivgX9+YCT69AkzHHwlNbVpqtEUTbew8eQurpyOpeN8fg00nw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.24", + "workerd": "1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260312.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260312.1.tgz", + "integrity": "sha512-HUAtDWaqUduS6yasV6+NgsK7qBpP1qGU49ow/Wb117IHjYp+PZPUGReDYocpB4GOMRoQlvdd4L487iFxzdARpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260312.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260312.1.tgz", + "integrity": "sha512-DOn7TPTHSxJYfi4m4NYga/j32wOTqvJf/pY4Txz5SDKWIZHSTXFyGz2K4B+thoPWLop/KZxGoyTv7db0mk/qyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260312.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260312.1.tgz", + "integrity": "sha512-TdkIh3WzPXYHuvz7phAtFEEvAxvFd30tHrm4gsgpw0R0F5b8PtoM3hfL2uY7EcBBWVYUBtkY2ahDYFfufnXw/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260312.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260312.1.tgz", + "integrity": "sha512-kNauZhL569Iy94t844OMwa1zP6zKFiL3xiJ4tGLS+TFTEfZ3pZsRH6lWWOtkXkjTyCmBEOog0HSEKjIV4oAffw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260312.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260312.1.tgz", + "integrity": "sha512-5dBrlSK+nMsZy5bYQpj8t9iiQNvCRlkm9GGvswJa9vVU/1BNO4BhJMlqOLWT24EmFyApZ+kaBiPJMV8847NDTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workers-types": { + "version": "4.20260313.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260313.1.tgz", + "integrity": "sha512-jMEeX3RKfOSVqqXRKr/ulgglcTloeMzSH3FdzIfqJHtvc12/ELKd5Ldsg8ZHahKX/4eRxYdw3kbzb8jLXbq/jQ==", + "dev": true, + "license": "MIT OR Apache-2.0" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", + "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.14.tgz", + "integrity": "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/hono": { + "version": "4.12.8", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.8.tgz", + "integrity": "sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/miniflare": { + "version": "4.20260312.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260312.0.tgz", + "integrity": "sha512-pieP2rfXynPT6VRINYaiHe/tfMJ4c5OIhqRlIdLF6iZ9g5xgpEmvimvIgMpgAdDJuFlrLcwDUi8MfAo2R6dt/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "^0.34.5", + "undici": "7.18.2", + "workerd": "1.20260312.1", + "ws": "8.18.0", + "youch": "4.1.0-beta.10" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.2.tgz", + "integrity": "sha512-P9J1HWYV/ajFr8uCqk5QixwiRKmB1wOamgS0e+o2Z4A44Ej2+thFVRLG/eA7qprx88XXhnV5Bl8LHXTURpzB3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/unenv": { + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3" + } + }, + "node_modules/workerd": { + "version": "1.20260312.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260312.1.tgz", + "integrity": "sha512-nNpPkw9jaqo79B+iBCOiksx+N62xC+ETIfyzofUEdY3cSOHJg6oNnVSHm7vHevzVblfV76c8Gr0cXHEapYMBEg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260312.1", + "@cloudflare/workerd-darwin-arm64": "1.20260312.1", + "@cloudflare/workerd-linux-64": "1.20260312.1", + "@cloudflare/workerd-linux-arm64": "1.20260312.1", + "@cloudflare/workerd-windows-64": "1.20260312.1" + } + }, + "node_modules/wrangler": { + "version": "4.73.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.73.0.tgz", + "integrity": "sha512-VJXsqKDFCp6OtFEHXITSOR5kh95JOknwPY8m7RyQuWJQguSybJy43m4vhoCSt42prutTef7eeuw7L4V4xiynGw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.4.2", + "@cloudflare/unenv-preset": "2.15.0", + "blake3-wasm": "2.1.5", + "esbuild": "0.27.3", + "miniflare": "4.20260312.0", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20260312.1" + }, + "bin": { + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=20.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20260312.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/youch": { + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + } + } +} diff --git a/tools/codecov/package.json b/tools/codecov/package.json new file mode 100644 index 00000000..ab5c0a51 --- /dev/null +++ b/tools/codecov/package.json @@ -0,0 +1,21 @@ +{ + "name": "codecov-worker", + "version": "1.0.0", + "description": "A Cloudflare Worker for code coverage reporting", + "main": "src/index.tsx", + "scripts": { + "dev": "wrangler dev src/index.tsx", + "deploy": "wrangler deploy --minify src/index.tsx" + }, + "dependencies": { + "hono": "^4.0.0" + }, + "overrides": { + "undici": "^7.24.0" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20240222.0", + "typescript": "^5.0.0", + "wrangler": "^4.73.0" + } +} diff --git a/tools/codecov/schema.sql b/tools/codecov/schema.sql new file mode 100644 index 00000000..bade5489 --- /dev/null +++ b/tools/codecov/schema.sql @@ -0,0 +1,43 @@ +-- Copyright 2026 Google LLC +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +DROP TABLE IF EXISTS coverage_reports; + +CREATE TABLE coverage_reports ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + commit_sha TEXT NOT NULL UNIQUE, + branch TEXT NOT NULL, + coverage_pct REAL NOT NULL, + delta_coverage_pct REAL, + commit_timestamp DATETIME NOT NULL +); + +CREATE INDEX idx_coverage_reports_commit_sha ON coverage_reports(commit_sha); +CREATE INDEX idx_coverage_reports_branch ON coverage_reports(branch); +CREATE INDEX idx_coverage_reports_commit_timestamp ON coverage_reports(commit_timestamp DESC); + +DROP TABLE IF EXISTS pr_reports; + +CREATE TABLE pr_reports ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + pr_number INTEGER NOT NULL UNIQUE, + head_sha TEXT NOT NULL, + base_sha TEXT NOT NULL, + coverage_pct REAL NOT NULL, + delta_coverage_pct REAL, + commit_timestamp DATETIME NOT NULL +); + +CREATE INDEX idx_pr_reports_pr_number ON pr_reports(pr_number); +CREATE INDEX idx_pr_reports_commit_timestamp ON pr_reports(commit_timestamp DESC); diff --git a/tools/codecov/src/components/Layout.tsx b/tools/codecov/src/components/Layout.tsx new file mode 100644 index 00000000..3559545a --- /dev/null +++ b/tools/codecov/src/components/Layout.tsx @@ -0,0 +1,394 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { html } from "hono/html"; + +type LayoutProps = { + children: any; + title: string; + projectName: string; +}; + +export const Layout = (props: LayoutProps) => html` + + + + + + ${props.title} - ${props.projectName} Codecov + + + + + + + + + +
+
+

${props.projectName} Code Coverage

+
+
+
${props.children}
+ + + +`; diff --git a/tools/codecov/src/index.tsx b/tools/codecov/src/index.tsx new file mode 100644 index 00000000..8c6218b5 --- /dev/null +++ b/tools/codecov/src/index.tsx @@ -0,0 +1,53 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Hono } from "hono"; +import { Bindings } from "./types.ts"; +import api from "./routes/api.ts"; +import ui from "./routes/ui.tsx"; + +const app = new Hono<{ Bindings: Bindings }>(); + +app.route("/api", api); +app.route("/", ui); + +export default { + fetch: app.fetch, + async scheduled(event: any, env: Bindings, ctx: any) { + // Default retention is 90 days, configurable via env variables + const retentionDays = env.RETENTION_DAYS || 90; + + // 1. Find all reports older than the retention period + const { results } = await env.DB.prepare( + "SELECT commit_sha FROM coverage_reports WHERE commit_timestamp <= datetime('now', ?)", + ) + .bind(`-${retentionDays} days`) + .all(); + + if (results && results.length > 0) { + // 2. Delete the raw JSON coverage maps from R2 Blob Storage + const keysToDelete = results.map( + (r: any) => `coverage-${r.commit_sha}.json`, + ); + await env.COVERAGE_BUCKET.delete(keysToDelete); + + // 3. Delete the metadata records from D1 SQLite + await env.DB.prepare( + "DELETE FROM coverage_reports WHERE commit_timestamp <= datetime('now', ?)", + ) + .bind(`-${retentionDays} days`) + .run(); + } + }, +}; diff --git a/tools/codecov/src/routes/api.ts b/tools/codecov/src/routes/api.ts new file mode 100644 index 00000000..afecd390 --- /dev/null +++ b/tools/codecov/src/routes/api.ts @@ -0,0 +1,148 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Hono } from "hono"; +import { Bindings } from "../types.ts"; + +const api = new Hono<{ Bindings: Bindings }>(); + +// API: Upload coverage +// Expected payload: { commit_sha: "abc1234", branch: "main", coverage_pct: 85.5, report: { "src/main.rs": { "1": 1, "2": 0 } } } +api.post("/upload", async (c) => { + const expectedToken = c.env.CODECOV_TOKEN; + if (expectedToken) { + const authHeader = c.req.header("Authorization"); + if (!authHeader || authHeader !== `Bearer ${expectedToken}`) { + return c.json({ error: "Unauthorized" }, 401); + } + } + + const body = await c.req.json(); + const { + commit_sha, + branch, + commit_timestamp, + coverage_pct, + delta_coverage_pct, + report, + pr_number, + base_sha, + } = body; + + if ( + !commit_sha || + !branch || + !commit_timestamp || + coverage_pct === undefined || + !report + ) { + return c.json({ error: "Missing required fields" }, 400); + } + + // 1. Insert metadata into D1 + try { + if (pr_number) { + await c.env.DB.prepare( + "INSERT INTO pr_reports (pr_number, head_sha, base_sha, commit_timestamp, coverage_pct, delta_coverage_pct) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(pr_number) DO UPDATE SET head_sha = excluded.head_sha, base_sha = excluded.base_sha, commit_timestamp = excluded.commit_timestamp, coverage_pct = excluded.coverage_pct, delta_coverage_pct = excluded.delta_coverage_pct", + ) + .bind( + pr_number, + commit_sha, + base_sha || "", + commit_timestamp, + coverage_pct, + delta_coverage_pct ?? null, + ) + .run(); + } else { + await c.env.DB.prepare( + "INSERT INTO coverage_reports (commit_sha, branch, commit_timestamp, coverage_pct, delta_coverage_pct) VALUES (?, ?, ?, ?, ?) ON CONFLICT(commit_sha) DO UPDATE SET coverage_pct = excluded.coverage_pct, delta_coverage_pct = excluded.delta_coverage_pct, branch = excluded.branch, commit_timestamp = excluded.commit_timestamp", + ) + .bind( + commit_sha, + branch, + commit_timestamp, + coverage_pct, + delta_coverage_pct ?? null, + ) + .run(); + } + } catch (e: any) { + return c.json({ error: "Database error", details: e.message }, 500); + } + + // 2. Store JSON payload in R2 + if (pr_number) { + await c.env.COVERAGE_BUCKET.put( + `pr-${pr_number}.json`, + JSON.stringify(report), + ); + } else { + await c.env.COVERAGE_BUCKET.put( + `coverage-${commit_sha}.json`, + JSON.stringify(report), + ); + } + + return c.json({ success: true, commit_sha }); +}); + +// API: Get latest coverage percentage +api.get("/coverage", async (c) => { + const branch = c.req.query("branch") || "main"; + const result = await c.env.DB.prepare( + "SELECT coverage_pct FROM coverage_reports WHERE branch = ? ORDER BY commit_timestamp DESC LIMIT 1", + ) + .bind(branch) + .first(); + + if (!result) { + return c.json({ error: "No coverage data found for this branch" }, 404); + } + + return c.json({ branch, coverage_pct: result.coverage_pct }); +}); + +// API: Shields.io dynamic badge endpoint +api.get("/badge", async (c) => { + const branch = c.req.query("branch") || "main"; + const result = await c.env.DB.prepare( + "SELECT coverage_pct FROM coverage_reports WHERE branch = ? ORDER BY commit_timestamp DESC LIMIT 1", + ) + .bind(branch) + .first(); + + if (!result) { + return c.json({ + schemaVersion: 1, + label: "coverage", + message: "unknown", + color: "lightgrey", + }); + } + + const pct = result.coverage_pct as number; + let color = "red"; + if (pct >= 80) color = "success"; + else if (pct >= 60) color = "yellow"; + + return c.json({ + schemaVersion: 1, + label: "coverage", + message: `${pct.toFixed(2)}%`, + color, + }); +}); + +export default api; diff --git a/tools/codecov/src/routes/ui.tsx b/tools/codecov/src/routes/ui.tsx new file mode 100644 index 00000000..01e6584c --- /dev/null +++ b/tools/codecov/src/routes/ui.tsx @@ -0,0 +1,962 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Hono } from "hono"; +import { Bindings } from "../types.ts"; +import { Layout } from "../components/Layout.tsx"; +import { formatDate, buildTree, flattenTree } from "../utils.ts"; + +const ui = new Hono<{ Bindings: Bindings }>(); + +// UI: Home page (list recent coverage reports) +ui.get("/", async (c) => { + const { results: branches } = await c.env.DB.prepare( + "SELECT branch, commit_sha, coverage_pct, MAX(commit_timestamp) as commit_timestamp FROM coverage_reports GROUP BY branch ORDER BY commit_timestamp DESC", + ).all(); + + const { results: commits } = await c.env.DB.prepare( + "SELECT * FROM coverage_reports ORDER BY commit_timestamp DESC LIMIT 50", + ).all(); + + const { results: prs } = await c.env.DB.prepare( + "SELECT * FROM pr_reports ORDER BY commit_timestamp DESC LIMIT 50", + ).all(); + + return c.html( + +

Branches

+
+ + + + + + + + + + + {branches.map((b: any) => ( + + + + + + + ))} + {branches.length === 0 && ( + + + + )} + +
BranchLatest CommitOverall CoverageLast Updated
+ {b.branch} + + + {b.commit_sha.substring(0, 7)} + + + = 80 + ? "pct-high" + : b.coverage_pct >= 60 + ? "pct-medium" + : "pct-low" + } + > + {b.coverage_pct.toFixed(2)}% + + {formatDate(b.commit_timestamp)}
No branches found.
+
+ +

Recent Pull Requests

+
+ + + + + + + + + + + {prs.map((r: any) => ( + + + + + + + ))} + {prs.length === 0 && ( + + + + )} + +
PROverall CoveragePatch CoverageLast Updated
+ + #{r.pr_number} + + + = 80 + ? "pct-high" + : r.coverage_pct >= 60 + ? "pct-medium" + : "pct-low" + } + > + {r.coverage_pct.toFixed(2)}% + + + {r.delta_coverage_pct !== null ? ( + = 80 + ? "pct-high" + : r.delta_coverage_pct >= 60 + ? "pct-medium" + : "pct-low" + } + > + {r.delta_coverage_pct.toFixed(2)}% + + ) : ( + "-" + )} + {formatDate(r.commit_timestamp)}
No PR coverage reports uploaded yet.
+
+ +

Recent Commits

+
+ + + + + + + + + + + {commits.map((r: any) => ( + + + + + + + ))} + {commits.length === 0 && ( + + + + )} + +
CommitBranchOverall CoverageDate
+ + {r.commit_sha.substring(0, 7)} + + {r.branch} + = 80 + ? "pct-high" + : r.coverage_pct >= 60 + ? "pct-medium" + : "pct-low" + } + > + {r.coverage_pct.toFixed(2)}% + + {formatDate(r.commit_timestamp)}
No coverage reports uploaded yet.
+
+
, + ); +}); + +// UI: Commit overview (list files) +ui.get("/commit/:sha", async (c) => { + const sha = c.req.param("sha"); + + if (!/^[0-9a-f]{7,40}$/i.test(sha)) { + return c.text("Invalid commit SHA", 400); + } + + const report = await c.env.DB.prepare( + "SELECT * FROM coverage_reports WHERE commit_sha = ?", + ) + .bind(sha) + .first(); + + if (!report) return c.text("Commit not found", 404); + + const object = await c.env.COVERAGE_BUCKET.get(`coverage-${sha}.json`); + if (!object) return c.text("Coverage details not found in storage", 404); + + const coverageData: Record = await object.json(); + const tree = buildTree(coverageData); + const flatFiles = flattenTree(tree); + + const touchedFiles = Object.entries(coverageData) + .filter(([_, data]) => (data as any).status) + .map(([path, data]) => ({ path, ...(data as any) })) + .sort((a, b) => a.path.localeCompare(b.path)); + + const getStatusBadge = (status?: string) => { + if (!status) return ""; + if (status === "A") { + return ( + + A + + ) as any; + } else if (status === "M") { + return ( + + M + + ) as any; + } + return ( + + {status} + + ) as any; + }; + + c.header("Cache-Control", "public, max-age=31536000, immutable"); + return c.html( + + +

+ + Commit: {sha.substring(0, 12)} + + +

+

+ Overall Coverage:{" "} + = 80 + ? "pct-high" + : (report as any).coverage_pct >= 60 + ? "pct-medium" + : "pct-low" + } + > + {(report as any).coverage_pct.toFixed(2)}% + + {(report as any).delta_coverage_pct !== null && + (report as any).delta_coverage_pct !== undefined && ( + <> + {" "} +  |  Patch Coverage:{" "} + = 80 + ? "pct-high" + : (report as any).delta_coverage_pct >= 60 + ? "pct-medium" + : "pct-low" + } + > + {(report as any).delta_coverage_pct.toFixed(2)}% + + + )} +

+ + {touchedFiles.length > 0 && ( + <> +

Changed Files

+
+ + + + + + + + + + + {touchedFiles.map((f) => { + const pct = f.coverage_pct; + const pctDisplay = + pct !== undefined ? pct.toFixed(2) + "%" : "N/A"; + const pctClass = + pct !== undefined + ? pct >= 80 + ? "pct-high" + : pct >= 60 + ? "pct-medium" + : "pct-low" + : ""; + + const deltaPct = f.delta_coverage_pct; + const deltaDisplay = + deltaPct !== undefined ? deltaPct.toFixed(2) + "%" : "-"; + const deltaClass = + deltaPct !== undefined + ? deltaPct >= 80 + ? "pct-high" + : deltaPct >= 60 + ? "pct-medium" + : "pct-low" + : ""; + + return ( + + + + + + + ); + })} + +
FileCoveragePatch Coverage
+ {getStatusBadge(f.status)} + + {f.path} + + {pctDisplay} + + {deltaDisplay} +
+
+ + )} + +

Covered Files

+
+ + + + + + + + + + {flatFiles.map((node) => { + const pct = node.coverage_pct; + const pctDisplay = + pct !== undefined ? pct.toFixed(2) + "%" : "N/A"; + const pctClass = + pct !== undefined + ? pct >= 80 + ? "pct-high" + : pct >= 60 + ? "pct-medium" + : "pct-low" + : ""; + + return ( + + + + + + ); + })} + +
FileCoverage
+ {node.type === "file" ? getStatusBadge(node.status) : ""} + + {node.type === "dir" ? ( + {node.name}/ + ) : ( + + {node.name} + + )} + + {node.type === "file" && ( + {pctDisplay} + )} +
+
+
, + ); +}); + +// UI: View specific file source code with coverage highlighting +ui.get("/commit/:sha/file/:path{.+}", async (c) => { + const sha = c.req.param("sha"); + const filePath = c.req.param("path"); // Everything after /file/ + + if (!filePath) return c.text("File path is missing", 400); + + if (!/^[0-9a-f]{7,40}$/i.test(sha)) { + return c.text("Invalid commit SHA", 400); + } + + if (filePath.includes("..") || filePath.startsWith("/")) { + return c.text("Invalid file path", 400); + } + + // 1. Fetch coverage data from R2 + const object = await c.env.COVERAGE_BUCKET.get(`coverage-${sha}.json`); + if (!object) return c.text("Coverage details not found", 404); + + const coverageData: Record = await object.json(); + const fileCoverage = coverageData[filePath] || {}; + const filePct = fileCoverage.coverage_pct; + const deltaLines = fileCoverage.delta_lines || []; + + // 2. Fetch raw source code from GitHub + const githubRepo = c.env.GITHUB_REPO || "google/alioth"; + const githubUrl = `https://raw.githubusercontent.com/${githubRepo}/${sha}/${filePath}`; + const ghRes = await fetch(githubUrl); + + if (!ghRes.ok) { + return c.html( + +

File not found on GitHub

+

+ Attempted to fetch: {githubUrl} +

+

+ Status: {ghRes.status} {ghRes.statusText} +

+

+ ← Back to commit +

+
, + 404, + ); + } + + const sourceCode = await ghRes.text(); + const lines = sourceCode.split("\n"); + + c.header("Cache-Control", "public, max-age=31536000, immutable"); + return c.html( + + +

+ {filePath.split("/").pop()} + {filePct !== undefined && ( + = 80 + ? "pct-high" + : filePct >= 60 + ? "pct-medium" + : "pct-low" + } + > + {filePct.toFixed(2)}% + + )} +

+ +
+
+ {lines.map((line, index) => { + const lineNum = index + 1; + const hits = fileCoverage[lineNum.toString()]; + + let statusClass = ""; + if (hits !== undefined) { + statusClass = hits > 0 ? "covered" : "uncovered"; + } + + const isDelta = deltaLines.includes(lineNum); + + return ( +
+ {lineNum} + {isDelta ? ( + + + + + ) : ( + + )} + {line || " "} + {hits !== undefined && hits > 0 && ( + + {hits}x + + )} +
+ ); + })} +
+
+
, + ); +}); + +// UI: PR overview (list files) +ui.get("/pr/:pr_number", async (c) => { + const pr_number = parseInt(c.req.param("pr_number"), 10); + + if (isNaN(pr_number)) { + return c.text("Invalid PR number", 400); + } + + const report = await c.env.DB.prepare( + "SELECT * FROM pr_reports WHERE pr_number = ?", + ) + .bind(pr_number) + .first(); + + if (!report) return c.text("PR not found", 404); + + const object = await c.env.COVERAGE_BUCKET.get(`pr-${pr_number}.json`); + if (!object) return c.text("Coverage details not found in storage", 404); + + const coverageData: Record = await object.json(); + const tree = buildTree(coverageData); + const flatFiles = flattenTree(tree); + + const touchedFiles = Object.entries(coverageData) + .filter(([_, data]) => (data as any).status) + .map(([path, data]) => ({ path, ...(data as any) })) + .sort((a, b) => a.path.localeCompare(b.path)); + + const getStatusBadge = (status?: string) => { + if (!status) return ""; + if (status === "A") { + return ( + + A + + ) as any; + } else if (status === "M") { + return ( + + M + + ) as any; + } + return ( + + {status} + + ) as any; + }; + + const head_sha = report.head_sha as string; + const githubRepo = c.env.GITHUB_REPO || "google/alioth"; + + c.header("Cache-Control", "public, max-age=30"); // PRs can update, cache shortly + return c.html( + + +

+ + + Pull Request #{pr_number} + + +

+

+ Head:{" "} + + {head_sha.substring(0, 7)} + {" "} + | Base:{" "} + + {(report as any).base_sha.substring(0, 7)} + +

+

+ Overall Coverage:{" "} + = 80 + ? "pct-high" + : (report as any).coverage_pct >= 60 + ? "pct-medium" + : "pct-low" + } + > + {(report as any).coverage_pct.toFixed(2)}% + + {(report as any).delta_coverage_pct !== null && + (report as any).delta_coverage_pct !== undefined && ( + <> + {" "} +  |  Patch Coverage:{" "} + = 80 + ? "pct-high" + : (report as any).delta_coverage_pct >= 60 + ? "pct-medium" + : "pct-low" + } + > + {(report as any).delta_coverage_pct.toFixed(2)}% + + + )} +

+ + {touchedFiles.length > 0 && ( + <> +

Changed Files

+
+ + + + + + + + + + + {touchedFiles.map((f) => { + const pct = f.coverage_pct; + const pctDisplay = + pct !== undefined ? pct.toFixed(2) + "%" : "N/A"; + const pctClass = + pct !== undefined + ? pct >= 80 + ? "pct-high" + : pct >= 60 + ? "pct-medium" + : "pct-low" + : ""; + + const deltaPct = f.delta_coverage_pct; + const deltaDisplay = + deltaPct !== undefined ? deltaPct.toFixed(2) + "%" : "-"; + const deltaClass = + deltaPct !== undefined + ? deltaPct >= 80 + ? "pct-high" + : deltaPct >= 60 + ? "pct-medium" + : "pct-low" + : ""; + + return ( + + + + + + + ); + })} + +
FileCoveragePatch Coverage
+ {getStatusBadge(f.status)} + + {f.path} + + {pctDisplay} + + {deltaDisplay} +
+
+ + )} + +

Covered Files

+
+ + + + + + + + + + {flatFiles.map((node) => { + const pct = node.coverage_pct; + const pctDisplay = + pct !== undefined ? pct.toFixed(2) + "%" : "N/A"; + const pctClass = + pct !== undefined + ? pct >= 80 + ? "pct-high" + : pct >= 60 + ? "pct-medium" + : "pct-low" + : ""; + + return ( + + + + + + ); + })} + +
FileCoverage
+ {node.type === "file" ? getStatusBadge(node.status) : ""} + + {node.type === "dir" ? ( + {node.name}/ + ) : ( + + {node.name} + + )} + + {node.type === "file" && ( + {pctDisplay} + )} +
+
+
, + ); +}); + +// UI: View specific file source code for PR +ui.get("/pr/:pr_number/file/:path{.+}", async (c) => { + const pr_number = parseInt(c.req.param("pr_number"), 10); + const filePath = c.req.param("path"); + + if (isNaN(pr_number)) return c.text("Invalid PR number", 400); + if (!filePath) return c.text("File path is missing", 400); + + if (filePath.includes("..") || filePath.startsWith("/")) { + return c.text("Invalid file path", 400); + } + + const report = await c.env.DB.prepare( + "SELECT head_sha FROM pr_reports WHERE pr_number = ?", + ) + .bind(pr_number) + .first(); + + if (!report) return c.text("PR not found", 404); + const sha = report.head_sha as string; + + // 1. Fetch coverage data from R2 + const object = await c.env.COVERAGE_BUCKET.get(`pr-${pr_number}.json`); + if (!object) return c.text("Coverage details not found", 404); + + const coverageData: Record = await object.json(); + const fileCoverage = coverageData[filePath] || {}; + const filePct = fileCoverage.coverage_pct; + const deltaLines = fileCoverage.delta_lines || []; + + // 2. Fetch raw source code from GitHub + const githubRepo = c.env.GITHUB_REPO || "google/alioth"; + const githubUrl = `https://raw.githubusercontent.com/${githubRepo}/${sha}/${filePath}`; + const ghRes = await fetch(githubUrl); + + if (!ghRes.ok) { + return c.html( + +

File not found on GitHub

+

+ Attempted to fetch: {githubUrl} +

+

+ Status: {ghRes.status} {ghRes.statusText} +

+

+ ← Back to PR +

+
, + 404, + ); + } + + const sourceCode = await ghRes.text(); + const lines = sourceCode.split("\n"); + + c.header("Cache-Control", "public, max-age=30"); + return c.html( + + +

+ {filePath.split("/").pop()} + {filePct !== undefined && ( + = 80 + ? "pct-high" + : filePct >= 60 + ? "pct-medium" + : "pct-low" + } + > + {filePct.toFixed(2)}% + + )} +

+ +
+
+ {lines.map((line, index) => { + const lineNum = index + 1; + const hits = fileCoverage[lineNum.toString()]; + + let statusClass = ""; + if (hits !== undefined) { + statusClass = hits > 0 ? "covered" : "uncovered"; + } + + const isDelta = deltaLines.includes(lineNum); + + return ( +
+ {lineNum} + {isDelta ? ( + + + + + ) : ( + + )} + {line || " "} + {hits !== undefined && hits > 0 && ( + + {hits}x + + )} +
+ ); + })} +
+
+
, + ); +}); + +export default ui; diff --git a/tools/codecov/src/types.ts b/tools/codecov/src/types.ts new file mode 100644 index 00000000..87b65c01 --- /dev/null +++ b/tools/codecov/src/types.ts @@ -0,0 +1,44 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export type Bindings = { + DB: D1Database; + COVERAGE_BUCKET: R2Bucket; + CODECOV_TOKEN?: string; + GITHUB_REPO?: string; + PROJECT_NAME?: string; + RETENTION_DAYS?: number; +}; + +export type TreeNode = { + name: string; + type: "file" | "dir"; + path: string; + children: Record; + totalLines: number; + coveredLines: number; + coverage_pct?: number; + delta_coverage_pct?: number; + status?: string; +}; + +export type FlatNode = { + name: string; + type: "file" | "dir"; + path: string; + depth: number; + coverage_pct?: number; + delta_coverage_pct?: number; + status?: string; +}; diff --git a/tools/codecov/src/utils.ts b/tools/codecov/src/utils.ts new file mode 100644 index 00000000..9d9b8a81 --- /dev/null +++ b/tools/codecov/src/utils.ts @@ -0,0 +1,121 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { TreeNode, FlatNode } from "./types.ts"; + +export function formatDate(dateInput: string | number | Date) { + const d = new Date(dateInput); + const pad = (n: number) => String(n).padStart(2, "0"); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}, ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; +} + +export function buildTree(coverageData: Record): TreeNode { + const root: TreeNode = { + name: "root", + type: "dir", + path: "", + children: {}, + totalLines: 0, + coveredLines: 0, + }; + + for (const [filePath, fileData] of Object.entries(coverageData)) { + const parts = filePath.split("/"); + let current = root; + + let fileTotalLines = 0; + let fileCoveredLines = 0; + let status = fileData.status as string | undefined; + let delta_coverage_pct = fileData.delta_coverage_pct as number | undefined; + + for (const [key, hits] of Object.entries(fileData)) { + if ( + key !== "coverage_pct" && + key !== "status" && + key !== "delta_coverage_pct" && + key !== "delta_lines" + ) { + fileTotalLines++; + if ((hits as number) > 0) fileCoveredLines++; + } + } + + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + const isFile = i === parts.length - 1; + + if (!current.children[part]) { + current.children[part] = { + name: part, + type: isFile ? "file" : "dir", + path: isFile ? filePath : "", + children: {}, + totalLines: 0, + coveredLines: 0, + }; + } + + if (isFile && status) { + current.children[part].status = status; + } + if (isFile && delta_coverage_pct !== undefined) { + current.children[part].delta_coverage_pct = delta_coverage_pct; + } + + current.children[part].totalLines += fileTotalLines; + current.children[part].coveredLines += fileCoveredLines; + + if (current.children[part].totalLines > 0) { + current.children[part].coverage_pct = + (current.children[part].coveredLines / + current.children[part].totalLines) * + 100; + } + + current = current.children[part]; + } + + root.totalLines += fileTotalLines; + root.coveredLines += fileCoveredLines; + if (root.totalLines > 0) { + root.coverage_pct = (root.coveredLines / root.totalLines) * 100; + } + } + + return root; +} + +export function flattenTree(node: TreeNode, depth: number = 0): FlatNode[] { + let result: FlatNode[] = []; + const children = Object.values(node.children).sort((a, b) => { + if (a.type !== b.type) return a.type === "dir" ? -1 : 1; + return a.name.localeCompare(b.name); + }); + + for (const child of children) { + result.push({ + name: child.name, + type: child.type, + path: child.path, + depth, + coverage_pct: child.coverage_pct, + delta_coverage_pct: child.delta_coverage_pct, + status: child.status, + }); + if (child.type === "dir") { + result = result.concat(flattenTree(child, depth + 1)); + } + } + return result; +} diff --git a/tools/codecov/tsconfig.json b/tools/codecov/tsconfig.json new file mode 100644 index 00000000..9b46efcd --- /dev/null +++ b/tools/codecov/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "lib": ["ES2022"], + "types": ["@cloudflare/workers-types"], + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx", + "resolveJsonModule": true, + "allowImportingTsExtensions": true, + "noEmit": true, + "isolatedModules": true, + "allowJs": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/tools/codecov/upload.mjs b/tools/codecov/upload.mjs new file mode 100644 index 00000000..8b5aa3f6 --- /dev/null +++ b/tools/codecov/upload.mjs @@ -0,0 +1,523 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import fs from "fs/promises"; +import { exec } from "child_process"; +import { promisify } from "util"; +import path from "path"; + +const execAsync = promisify(exec); + +/** + * Executes a shell command and returns the trimmed standard output. + */ +async function runCmd(cmd, cwd) { + try { + const { stdout } = await execAsync(cmd, { cwd }); + return stdout.trim(); + } catch (err) { + console.error(`Failed to execute command: ${cmd}`); + throw err; + } +} + +/** + * Retrieves necessary Git information for the current repository. + */ +async function getGitInfo(customBranch, prBase) { + const cwd = process.cwd(); + const commit_sha = await runCmd("git rev-parse HEAD", cwd); + + let parent_sha = null; + try { + if (prBase) { + parent_sha = await runCmd(`git rev-parse ${prBase}`, cwd); + } else { + parent_sha = await runCmd("git rev-parse HEAD~1", cwd); + } + } catch (e) { + // If there is no parent (first commit), this is expected. + } + + let branch = + customBranch || process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME; + if (!branch) { + branch = await runCmd("git rev-parse --abbrev-ref HEAD", cwd); + if (branch === "HEAD") { + try { + const remoteBranches = await runCmd( + "git branch -r --contains HEAD", + cwd, + ); + const branches = remoteBranches + .split("\n") + .map((b) => b.trim()) + .filter((b) => b && !b.includes("->")); + if (branches.length > 0) { + branch = branches[0].replace(/^[^\/]+\//, ""); + } + } catch (err) {} + } + } + + const repoRoot = await runCmd("git rev-parse --show-toplevel", cwd); + const commit_timestamp = await runCmd("git show -s --format=%cI HEAD", cwd); + + let gitStatusMap = {}; + try { + const diffTreeCmd = + prBase && parent_sha + ? `git diff-tree --no-commit-id --name-status -r ${parent_sha} HEAD` + : "git diff-tree --no-commit-id --name-status -r HEAD"; + const nameStatus = await runCmd(diffTreeCmd, cwd); + const statusLines = nameStatus.split("\n"); + for (const statusLine of statusLines) { + if (!statusLine.trim()) continue; + const parts = statusLine.trim().split(/\s+/); + if (parts.length >= 2) { + const status = parts[0][0]; // 'M', 'A', 'D', 'R', etc. + const filePath = parts[parts.length - 1]; + gitStatusMap[filePath] = status; + } + } + } catch (err) { + // Ignore errors if diff-tree fails + } + + let gitLineDiffs = {}; + if (parent_sha) { + try { + const diffOutput = await runCmd(`git diff -U0 ${parent_sha} HEAD`, cwd); + + let currentFile = null; + for (const line of diffOutput.split("\n")) { + if (line.startsWith("+++ b/")) { + currentFile = line.substring(6).trim(); + gitLineDiffs[currentFile] = []; + } else if (line.startsWith("@@ ") && currentFile) { + // Format: @@ -old_line,old_count +new_line,new_count @@ + const match = line.match(/@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/); + if (match) { + const startLine = parseInt(match[1], 10); + const count = match[2] === undefined ? 1 : parseInt(match[2], 10); + + for (let i = 0; i < count; i++) { + gitLineDiffs[currentFile].push(startLine + i); + } + } + } + } + } catch (err) { + console.error(`Failed to execute git diff: ${err}`); + } + } + + return { + commit_sha, + parent_sha, + branch, + repoRoot, + commit_timestamp, + gitStatusMap, + gitLineDiffs, + }; +} + +/** + * Parses an LCOV coverage file into a JSON format expected by the worker API. + * Format: { "src/main.rs": { "1": 1, "2": 0 } } + */ +async function parseLcov( + filePath, + repoRoot, + lcovBase, + gitStatusMap, + gitLineDiffs, +) { + let content; + try { + content = await fs.readFile(filePath, "utf-8"); + } catch (err) { + console.error(`Error reading coverage file at ${filePath}`); + console.error("Make sure you have generated the coverage report first."); + process.exit(1); + } + + const lines = content.split("\n"); + const report = {}; + + let currentFile = null; + let totalLines = 0; + let coveredLines = 0; + let fileTotalLines = 0; + let fileCoveredLines = 0; + + let totalDeltaLines = 0; + let coveredDeltaLines = 0; + + for (const line of lines) { + if (line.startsWith("SF:")) { + // Extract the source file path + let sf = line.substring(3).trim(); + + if (lcovBase && sf.startsWith(lcovBase)) { + sf = path.relative(lcovBase, sf); + } else if (path.isAbsolute(sf) && sf.startsWith(repoRoot)) { + sf = path.relative(repoRoot, sf); + } + + try { + await fs.access(path.resolve(process.cwd(), sf)); + } catch (err) { + console.error( + `\n[!] Error: File path in lcov does not match any source file in working directory.`, + ); + console.error(` LCOV path: ${line.substring(3).trim()}`); + console.error(` Resolved relative path: ${sf}`); + console.error( + ` Expected full path: ${path.resolve(process.cwd(), sf)}`, + ); + console.error( + ` Try using --lcov-base to strip the prefix used in the lcov file.`, + ); + process.exit(1); + } + + currentFile = sf; + report[currentFile] = {}; + fileTotalLines = 0; + fileCoveredLines = 0; + } else if (line.startsWith("DA:") && currentFile) { + // Parse execution data: DA:,[,] + const parts = line.substring(3).split(","); + const lineNum = parts[0]; + const hits = parseInt(parts[1], 10); + + report[currentFile][lineNum] = hits; + + const isDelta = + gitLineDiffs && + gitLineDiffs[currentFile] && + gitLineDiffs[currentFile].includes(parseInt(lineNum, 10)); + + if (isDelta) { + if (!report[currentFile].delta_lines) { + report[currentFile].delta_lines = []; + } + report[currentFile].delta_lines.push(parseInt(lineNum, 10)); + totalDeltaLines++; + if (!report[currentFile]._fileTotalDelta) + report[currentFile]._fileTotalDelta = 0; + if (!report[currentFile]._fileCoveredDelta) + report[currentFile]._fileCoveredDelta = 0; + report[currentFile]._fileTotalDelta++; + } + + totalLines++; + fileTotalLines++; + if (hits > 0) { + coveredLines++; + fileCoveredLines++; + if (isDelta) { + coveredDeltaLines++; + report[currentFile]._fileCoveredDelta++; + } + } + } else if (line === "end_of_record") { + if (currentFile) { + report[currentFile].coverage_pct = + fileTotalLines === 0 ? 0 : (fileCoveredLines / fileTotalLines) * 100; + if (gitStatusMap && gitStatusMap[currentFile]) { + report[currentFile].status = gitStatusMap[currentFile]; + } + + if (report[currentFile]._fileTotalDelta !== undefined) { + report[currentFile].delta_coverage_pct = + report[currentFile]._fileTotalDelta === 0 + ? 0 + : (report[currentFile]._fileCoveredDelta / + report[currentFile]._fileTotalDelta) * + 100; + delete report[currentFile]._fileTotalDelta; + delete report[currentFile]._fileCoveredDelta; + } + + currentFile = null; + } + } + } + + const coverage_pct = totalLines === 0 ? 0 : (coveredLines / totalLines) * 100; + const delta_coverage_pct = + totalDeltaLines === 0 ? null : (coveredDeltaLines / totalDeltaLines) * 100; + + return { report, coverage_pct, delta_coverage_pct }; +} + +async function printAndPostComment( + prNumber, + commit_sha, + coverage_pct, + delta_coverage_pct, + report, + apiUrl, +) { + const ghToken = process.env.GITHUB_TOKEN; + const ghRepo = process.env.GITHUB_REPOSITORY; + + // Derive dashboard base URL from apiUrl + const dashboardUrl = apiUrl.replace(/\/api\/upload\/?$/, ""); + const isPr = prNumber !== null && prNumber !== undefined; + const linkBase = isPr + ? `${dashboardUrl}/pr/${prNumber}` + : `${dashboardUrl}/commit/${commit_sha}`; + + let body = `\n### Code Coverage Report\n\n`; + body += `**Overall Coverage:** ${coverage_pct.toFixed(2)}%\n`; + if (delta_coverage_pct !== null && delta_coverage_pct !== undefined) { + body += `**Patch Coverage:** ${delta_coverage_pct.toFixed(2)}%\n`; + } + body += `\n[View Detailed Report on Dashboard](${linkBase})\n\n`; + + // Get changed files + const touchedFiles = Object.entries(report) + .filter(([_, data]) => data.status) + .sort((a, b) => a[0].localeCompare(b[0])); + + if (touchedFiles.length > 0) { + body += `
\nChanged Files Coverage\n\n`; + body += `| File | Coverage | Patch Coverage |\n`; + body += `|:---|---:|---:|\n`; + + for (const [file, data] of touchedFiles) { + const covPct = + data.coverage_pct !== undefined + ? `${data.coverage_pct.toFixed(2)}%` + : "N/A"; + const deltaPct = + data.delta_coverage_pct !== undefined + ? `${data.delta_coverage_pct.toFixed(2)}%` + : "-"; + body += `| [${file}](${linkBase}/file/${file}) | ${covPct} | ${deltaPct} |\n`; + } + body += `\n
\n`; + } + + console.log( + `\n --- Markdown Comment ---\n${body} ------------------------\n`, + ); + + if (!isPr) { + return; + } + + if (!ghToken || !ghRepo) { + console.log( + " Skipping GitHub comment: GITHUB_TOKEN or GITHUB_REPOSITORY not set.", + ); + return; + } + + const commentsEndpoint = `https://api.github.com/repos/${ghRepo}/issues/${prNumber}/comments`; + + try { + const getRes = await fetch(commentsEndpoint, { + headers: { + Authorization: `Bearer ${ghToken}`, + Accept: "application/vnd.github.v3+json", + "User-Agent": "codecov-worker", + }, + }); + + let existingCommentId = null; + if (getRes.ok) { + const comments = await getRes.json(); + const existing = comments.find( + (c) => c.body && c.body.includes(""), + ); + if (existing) { + existingCommentId = existing.id; + } + } + + const endpoint = existingCommentId + ? `https://api.github.com/repos/${ghRepo}/issues/comments/${existingCommentId}` + : commentsEndpoint; + const method = existingCommentId ? "PATCH" : "POST"; + + const res = await fetch(endpoint, { + method, + headers: { + Authorization: `Bearer ${ghToken}`, + Accept: "application/vnd.github.v3+json", + "Content-Type": "application/json", + "User-Agent": "codecov-worker", + }, + body: JSON.stringify({ body }), + }); + + if (res.ok) { + console.log( + ` Successfully ${ + existingCommentId ? "updated" : "posted" + } comment on PR #${prNumber}`, + ); + } else { + console.error( + ` Failed to post PR comment: ${res.status} ${res.statusText}`, + ); + const errText = await res.text(); + console.error(` Details: ${errText}`); + } + } catch (err) { + console.error(` Error posting PR comment: ${err.message}`); + } +} + +async function main() { + let lcovPath = null; + let lcovBase = null; + let customBranch = null; + let prNumber = null; + let prBase = null; + const args = process.argv.slice(2); + + for (let i = 0; i < args.length; i++) { + if (args[i] === "--lcov-base") { + lcovBase = args[i + 1]; + i++; + } else if (args[i] === "--branch") { + customBranch = args[i + 1]; + i++; + } else if (args[i] === "--pr") { + prNumber = parseInt(args[i + 1], 10); + i++; + } else if (args[i] === "--pr-base") { + prBase = args[i + 1]; + i++; + } else { + lcovPath = args[i]; + } + } + if (!lcovPath) lcovPath = "../alioth/lcov.info"; + + const apiUrl = process.env.CODECOV_URL || "http://localhost:8787/api/upload"; + const apiToken = process.env.CODECOV_TOKEN; + + console.log(`[1/4] Reading Git information...`); + const { + commit_sha, + parent_sha, + branch, + repoRoot, + commit_timestamp, + gitStatusMap, + gitLineDiffs, + } = await getGitInfo(customBranch, prBase); + console.log(` Commit: ${commit_sha}`); + console.log(` Branch: ${branch}`); + console.log(` Root: ${repoRoot}`); + console.log(` Date: ${commit_timestamp}`); + if (lcovBase) { + console.log(` LCOV Base: ${lcovBase}`); + } + if (prNumber) { + console.log(` PR Number: ${prNumber}`); + console.log(` PR Base: ${parent_sha}`); + } + + console.log(`\n[2/4] Parsing coverage from ${lcovPath}...`); + const { report, coverage_pct, delta_coverage_pct } = await parseLcov( + lcovPath, + repoRoot, + lcovBase, + gitStatusMap, + gitLineDiffs, + ); + console.log(` Files tracked: ${Object.keys(report).length}`); + console.log(` Overall coverage: ${coverage_pct.toFixed(2)}%`); + if (delta_coverage_pct !== null) { + console.log(` Delta coverage: ${delta_coverage_pct.toFixed(2)}%`); + } + + const payload = { + commit_sha, + branch, + commit_timestamp, + coverage_pct, + delta_coverage_pct, + report, + }; + + if (prNumber) { + payload.pr_number = prNumber; + payload.base_sha = parent_sha; + } + + console.log(`\n[3/4] Uploading coverage data...`); + console.log(` Endpoint: ${apiUrl}`); + + const headers = { "Content-Type": "application/json" }; + if (apiToken) { + headers["Authorization"] = `Bearer ${apiToken}`; + } else { + console.warn( + ` Warning: CODECOV_TOKEN environment variable is not set. The upload may fail if the server requires authentication.`, + ); + } + + try { + const res = await fetch(apiUrl, { + method: "POST", + headers, + body: JSON.stringify(payload), + }); + + if (!res.ok) { + const errText = await res.text(); + console.error( + `\n[!] Failed to upload coverage: ${res.status} ${res.statusText}`, + ); + console.error(` Details: ${errText}`); + process.exit(1); + } + + const data = await res.json(); + console.log(`\n[4/4] Upload successful!`); + console.log(` Response:`, data); + + console.log(`\n[+] Generating Markdown Report...`); + await printAndPostComment( + prNumber, + commit_sha, + coverage_pct, + delta_coverage_pct, + report, + apiUrl, + ); + } catch (err) { + console.error(`\n[!] Network error while uploading coverage:`); + console.error(err.message); + console.error( + `\nMake sure your Cloudflare Worker is running locally (wrangler dev) or provide a valid CODECOV_URL.`, + ); + process.exit(1); + } +} + +main().catch((err) => { + console.error("\n[!] An unexpected error occurred:"); + console.error(err); + process.exit(1); +}); diff --git a/tools/codecov/wrangler.toml b/tools/codecov/wrangler.toml new file mode 100644 index 00000000..81e232cd --- /dev/null +++ b/tools/codecov/wrangler.toml @@ -0,0 +1,25 @@ +name = "alioth-codecov" +main = "src/index.tsx" +compatibility_date = "2024-02-27" +compatibility_flags = ["nodejs_compat"] + +# D1 Database for storing metadata (commit SHA, branch, overall coverage) +[[d1_databases]] +binding = "DB" +database_name = "alioth-codecov-db" +database_id = "721c795f-fda6-4110-a541-696a965c7966" + +# R2 Bucket for storing detailed coverage JSON payloads +[[r2_buckets]] +binding = "COVERAGE_BUCKET" +bucket_name = "alioth-codecov-reports" + +[triggers] +crons = ["0 0 * * *"] # Run daily at midnight UTC + +[vars] +PROJECT_NAME = "Alioth" +GITHUB_REPO = "google/alioth" +RETENTION_DAYS = 90 +# Add any environment variables here, e.g., GitHub API tokens if needed later +# GITHUB_TOKEN = "your_token_here"