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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@
- any-glob-to-any-file:
- 'projects/pages/**'

'scope(plot)':
- changed-files:
- any-glob-to-any-file:
- 'projects/plot/**'

'scope(scene)':
- changed-files:
- any-glob-to-any-file:
- 'projects/scene/**'

'scope(starters)':
- changed-files:
- any-glob-to-any-file:
Expand Down
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@ permissions:
jobs:
ci:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
code-quality: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: 0
lfs: true
ref: ${{ github.event.pull_request.head.sha || github.sha }}
# The CLI build runs semantic-release dry-run to bake the next binary version.
# On main, checkout credentials must match the release token used by that dry-run.
token: ${{ github.ref == 'refs/heads/main' && secrets.RELEASE_TOKEN || github.token }}
Expand All @@ -32,6 +37,16 @@ jobs:
ELEMENTS_REPO_BASE_URL: ${{vars.ELEMENTS_REPO_BASE_URL}}
GITHUB_TOKEN: ${{ github.ref == 'refs/heads/main' && secrets.RELEASE_TOKEN || github.token }}
run: PAGES_BASE_URL="/elements/" pnpm run ci && PAGES_BASE_URL="/elements/" node ./projects/internals/ci/cache-validate.js ci
- name: Upload code coverage
if: >-
github.event_name == 'push' ||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)
uses: actions/upload-code-coverage@d8e329117199404bba6fc81efe8093dc7c015e34 # v1.4.2
with:
file: projects/internals/ci/coverage/cobertura.xml
language: TypeScript
label: code-coverage/vitest
fail-on-error: true
- name: Write CI job summary
if: always()
run: node ./projects/internals/ci/ci-summary.js >> "$GITHUB_STEP_SUMMARY"
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
"@semantic-release/github": "12.0.9",
"@semantic-release/npm": "13.1.5",
"@semantic-release/release-notes-generator": "14.1.1",
"@vitest/istanbul-lib-coverage": "catalog:",
"@vitest/istanbul-lib-report": "catalog:",
"@weiran.zsd/multi-eslint-rule-config": "0.0.6",
"commitlint": "21.2.2",
"glob": "catalog:",
Expand Down
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ catalog:
'@vitest/browser': 5.0.0
'@vitest/browser-playwright': 5.0.0
'@vitest/coverage-istanbul': 5.0.0
'@vitest/istanbul-lib-coverage': 1.0.1
'@vitest/istanbul-lib-report': 1.0.1
'adm-zip': 0.6.1
axe-core: 4.11.0
eslint: 10.10.0
Expand Down
4 changes: 3 additions & 1 deletion projects/code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,9 @@
"vite.config.ts",
"vitest.config.ts"
],
"output": [],
"output": [
"coverage/unit/**"
],
"dependencies": [
"../internals/testing:build",
"../internals/vite:ci",
Expand Down
4 changes: 3 additions & 1 deletion projects/forms/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,9 @@
"vite.config.ts",
"vitest.config.ts"
],
"output": [],
"output": [
"coverage/unit/**"
],
"dependencies": [
"../internals/testing:build",
"../internals/vite:ci",
Expand Down
8 changes: 6 additions & 2 deletions projects/internals/ci/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# GitLab
# Coverage reporting

This script is for reporting the code coverage outputs to [Gitlab](https://github.com/NVIDIA/elements/-/graphs/main/charts).
The `coverage` task merges the Istanbul output from each package that runs `test:coverage`. It writes one XML coverage report to `coverage/cobertura.xml` for GitHub Code Quality.

The GitHub Actions workflow uploads coverage for pushes to `main` and for pull requests from branches in this repository. GitHub does not support coverage uploads from fork pull requests.

Enable GitHub Code Quality before running the upload workflow. After the first successful upload to `main`, configure the default branch ruleset to require at least 95% line coverage and allow a maximum decrease of 0.25 percentage points.
16 changes: 2 additions & 14 deletions projects/internals/ci/ci-summary.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,12 @@
import { readFileSync } from 'fs';
import path from 'path';
import * as url from 'url';
import { COVERAGE_PROJECTS } from './coverage-projects.js';
// must not use node_modules dependencies, CI skips installation during this reporting call

const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
const PROJECTS_ROOT = path.resolve(__dirname, '../..');

const PROJECTS = [
{ name: '@nvidia-elements/core', dir: 'core' },
{ name: '@nvidia-elements/code', dir: 'code' },
{ name: '@nvidia-elements/cli', dir: 'cli' },
{ name: '@nvidia-elements/forms', dir: 'forms' },
{ name: '@nvidia-elements/lint', dir: 'lint' },
{ name: '@nvidia-elements/markdown', dir: 'markdown' },
{ name: '@nvidia-elements/media', dir: 'media' },
{ name: '@nvidia-elements/monaco', dir: 'monaco' },
{ name: '@internals/metadata', dir: 'internals/metadata' },
{ name: '@internals/tools', dir: 'internals/tools' }
];

function tryRead(filePath) {
try {
return JSON.parse(readFileSync(filePath, 'utf-8'));
Expand All @@ -35,7 +23,7 @@ function pctOf(covered, total) {
return total > 0 ? (covered / total) * 100 : NaN;
}

const rows = PROJECTS.map(({ name, dir }) => {
const rows = COVERAGE_PROJECTS.map(({ name, dir }) => {
const base = path.join(PROJECTS_ROOT, dir, 'coverage/unit');
const coverage = tryRead(path.join(base, 'coverage-summary.json'));
const tests = tryRead(path.join(base, 'summary.json'));
Expand Down
14 changes: 14 additions & 0 deletions projects/internals/ci/coverage-projects.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const COVERAGE_PROJECTS = [
{ name: '@nvidia-elements/core', dir: 'core' },
{ name: '@nvidia-elements/code', dir: 'code' },
{ name: '@nvidia-elements/cli', dir: 'cli' },
{ name: '@nvidia-elements/forms', dir: 'forms' },
{ name: '@nvidia-elements/lint', dir: 'lint' },
{ name: '@nvidia-elements/markdown', dir: 'markdown' },
{ name: '@nvidia-elements/media', dir: 'media' },
{ name: '@nvidia-elements/monaco', dir: 'monaco' },
{ name: '@nvidia-elements/plot', dir: 'plot' },
{ name: '@nvidia-elements/scene', dir: 'scene' },
{ name: '@internals/metadata', dir: 'internals/metadata' },
{ name: '@internals/tools', dir: 'internals/tools' }
];
145 changes: 58 additions & 87 deletions projects/internals/ci/coverage.js
Original file line number Diff line number Diff line change
@@ -1,92 +1,63 @@
import { readFileSync } from 'fs';
import path from 'path';
import * as url from 'url';
// must not use node_modules dependencies, CI skips installation during this reporting call
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createCoverageMap } from '@vitest/istanbul-lib-coverage';
import { create, createContext } from '@vitest/istanbul-lib-report';
import { COVERAGE_PROJECTS } from './coverage-projects.js';

const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
const resolve = rel => path.resolve(__dirname, rel);
const PROJECTS_ROOT = path.resolve(import.meta.dirname, '../..');
const REPOSITORY_ROOT = path.resolve(PROJECTS_ROOT, '..');
const OUTPUT_DIRECTORY = path.resolve(import.meta.dirname, 'coverage');

const elementsCoverage = JSON.parse(readFileSync(resolve('../../core/coverage/unit/coverage-summary.json')));
const labsCodeCoverage = JSON.parse(readFileSync(resolve('../../code/coverage/unit/coverage-summary.json')));
const monacoCoverage = JSON.parse(readFileSync(resolve('../../monaco/coverage/unit/coverage-summary.json')));
const labsFormsCoverage = JSON.parse(readFileSync(resolve('../../forms/coverage/unit/coverage-summary.json')));
const labsCliCoverage = JSON.parse(readFileSync(resolve('../../cli/coverage/unit/coverage-summary.json')));
const labsLintCoverage = JSON.parse(readFileSync(resolve('../../lint/coverage/unit/coverage-summary.json')));
const labsMarkdownCoverage = JSON.parse(readFileSync(resolve('../../markdown/coverage/unit/coverage-summary.json')));
const labsMediaCoverage = JSON.parse(readFileSync(resolve('../../media/coverage/unit/coverage-summary.json')));
const internalsMetadataCoverage = JSON.parse(readFileSync(resolve('../metadata/coverage/unit/coverage-summary.json')));
const internalsToolsCoverage = JSON.parse(readFileSync(resolve('../tools/coverage/unit/coverage-summary.json')));
function readCoverageReport(project, projectsRoot) {
const reportPath = path.join(projectsRoot, project.dir, 'coverage/unit/coverage-final.json');

const report = [
elementsCoverage,
labsCodeCoverage,
labsFormsCoverage,
labsCliCoverage,
labsLintCoverage,
labsMarkdownCoverage,
labsMediaCoverage,
monacoCoverage,
internalsMetadataCoverage,
internalsToolsCoverage
]
.map(r => r.total)
.reduce(
(all, report) => {
return {
lines: {
total: all.lines.total + report.lines.total,
covered: all.lines.covered + report.lines.covered,
pct: (all.lines.covered + report.lines.covered) / (all.lines.total + report.lines.total)
},
statements: {
total: all.statements.total + report.statements.total,
covered: all.statements.covered + report.statements.covered,
pct: (all.statements.covered + report.statements.covered) / (all.statements.total + report.statements.total)
},
functions: {
total: all.functions.total + report.functions.total,
covered: all.functions.covered + report.functions.covered,
pct: (all.functions.covered + report.functions.covered) / (all.functions.total + report.functions.total)
},
branches: {
total: all.branches.total + report.branches.total,
covered: all.branches.covered + report.branches.covered,
pct: (all.branches.covered + report.branches.covered) / (all.branches.total + report.branches.total)
}
};
},
{
lines: {
total: 0,
covered: 0,
pct: 0
},
statements: {
total: 0,
covered: 0,
pct: 0
},
functions: {
total: 0,
covered: 0,
pct: 0
},
branches: {
total: 0,
covered: 0,
pct: 0
}
try {
return JSON.parse(readFileSync(reportPath, 'utf-8'));
} catch (error) {
throw new Error(`Unable to read coverage for ${project.name} at ${reportPath}`, { cause: error });
}
}

function assertRepositoryPaths(coverageMap, repositoryRoot) {
for (const filename of coverageMap.files()) {
const relative = path.relative(repositoryRoot, filename);
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
throw new Error(`Coverage file is outside the repository: ${filename}`);
}
}
}

export function generateCoverageReport({
projects = COVERAGE_PROJECTS,
projectsRoot = PROJECTS_ROOT,
repositoryRoot = REPOSITORY_ROOT,
outputDirectory = OUTPUT_DIRECTORY,
outputFile = 'cobertura.xml'
} = {}) {
const coverageMap = createCoverageMap({});

for (const project of projects) {
try {
coverageMap.merge(readCoverageReport(project, projectsRoot));
} catch (error) {
if (error.message.startsWith('Unable to read coverage')) throw error;
throw new Error(`Unable to merge coverage for ${project.name}`, { cause: error });
}
);
}

assertRepositoryPaths(coverageMap, repositoryRoot);

const summary = coverageMap.getCoverageSummary();
if (summary.lines.total === 0) throw new Error('Coverage report does not contain any source lines');

const context = createContext({ dir: outputDirectory, coverageMap });
create('cobertura', { file: outputFile, projectRoot: repositoryRoot }).execute(context);

return summary.toJSON();
}

console.log(`Report:\n`, report, '\n');
console.log(`@nvidia-elements/core coverage: ${elementsCoverage.total.branches.pct}%`);
console.log(`@nvidia-elements/monaco coverage: ${monacoCoverage.total.branches.pct}%`);
console.log(`@nvidia-elements/cli coverage: ${labsCliCoverage.total.branches.pct}%`);
console.log(`@nvidia-elements/code coverage: ${labsCodeCoverage.total.branches.pct}%`);
console.log(`@nvidia-elements/forms coverage: ${labsFormsCoverage.total.branches.pct}%`);
console.log(`@nvidia-elements/lint coverage: ${labsLintCoverage.total.branches.pct}%`);
console.log(`@nvidia-elements/markdown coverage: ${labsMarkdownCoverage.total.branches.pct}%`);
console.log(`@internals/metadata coverage: ${internalsMetadataCoverage.total.branches.pct}%`);
console.log(`@internals/tools coverage: ${internalsToolsCoverage.total.branches.pct}%`);
console.log(`average coverage: ${((report.branches.covered / report.branches.total) * 100).toFixed(2)}%`);
if (path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const summary = generateCoverageReport();
console.log(`Generated ${path.join(OUTPUT_DIRECTORY, 'cobertura.xml')} (${summary.lines.pct}% line coverage)`);
}
30 changes: 26 additions & 4 deletions projects/internals/ci/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,48 @@
"wireit": {
"ci": {
"dependencies": [
"test"
"test",
"coverage"
]
},
"coverage": {
"command": "node ./coverage.js",
"packageLocks": []
"dependencies": [
"../../cli:test:coverage",
"../../code:test:coverage",
"../../core:test:coverage",
"../../forms:test:coverage",
"../../lint:test:coverage",
"../../markdown:test:coverage",
"../../media:test:coverage",
"../../monaco:test:coverage",
"../../plot:test:coverage",
"../../scene:test:coverage",
"../metadata:test:coverage",
"../tools:test:coverage"
],
"files": [
"coverage.js",
"coverage-projects.js"
],
"output": [
"coverage/cobertura.xml"
]
},
"test": {
"command": "node --test './*.test.js'",
"files": [
"ci-profile.js",
"ci-profile.test.js",
"coverage-projects.js",
"coverage.js",
"lighthouse-projects.js",
"lighthouse-summary.js",
"metrics.lighthouse.js",
"release-notes.test.js",
"package.json"
],
"output": [],
"packageLocks": []
"output": []
}
}
}
Loading
Loading