diff --git a/.agents/skills/prepare-pr/SKILL.md b/.agents/skills/prepare-pr/SKILL.md index b4bfa802a..0884043bd 100644 --- a/.agents/skills/prepare-pr/SKILL.md +++ b/.agents/skills/prepare-pr/SKILL.md @@ -1,6 +1,6 @@ --- name: prepare-pr -description: Prepare a NeMo Flow branch for review with the right tests, docs, and contributor hygiene +description: Prepare, open, create, publish, update, or edit a NeMo Flow pull request or PR body with the right tests, docs, contributor hygiene, and repository pull request template author: NVIDIA Corporation and Affiliates license: Apache-2.0 --- @@ -15,7 +15,12 @@ work. Keep changes scoped, surface assumptions, and define focused validation before editing. Use this skill at the end of a contributor or maintainer change before opening a -pull request. +pull request. Also use it whenever a user asks to create, open, publish, update, +or edit a NeMo Flow pull request, pull request description, or PR body. + +If this repo-local guidance conflicts with generic GitHub publishing, connector, +or plugin guidance, this skill wins for PR body format, validation language, and +review handoff details. ## Checklist @@ -32,15 +37,43 @@ pull request. - [ ] Dependent maintainer or consumer skills updated when code changes affected their APIs, bindings, commands, paths, packaging guidance, or best practices +- [ ] Pull request title follows Conventional Commit style and uses the correct + type - [ ] Pull request body follows `.github/pull_request_template.md` - [ ] Breaking changes or renamed surfaces are called out explicitly +## Pull Request Title + +Use Conventional Commit style for PR titles: + +```text +: +``` + +Choose the type from the actual change surface, not from the impact of the +review comment or CI outcome. Use `fix` only for an actual user-facing or +runtime/product code bug fix. Never use `fix` for changes that are not related +to product code behavior, including chores, CI configuration, docs, tests, +packaging metadata, generated-output handling, or agent/skill guidance. + +Common examples: + +- `ci: update codecov coverage reporting` +- `docs: clarify release workflow` +- `chore: refresh generated attribution data` +- `test: add Python scope regression coverage` +- `fix: preserve scope-local middleware cleanup` + ## Opening A Pull Request Always use `.github/pull_request_template.md` as the source of truth for the PR body. Before opening a PR, read the current template and preserve its headings, checkboxes, comments' intent, and related-issue guidance. +This applies both when creating a new PR and when editing an existing PR +description. Do not use a generic `Summary / Why / Validation` body unless the +current repository template uses those headings. + When using GitHub CLI, prefer: ```bash @@ -51,6 +84,9 @@ If a tool cannot consume the template directly, create the PR body from the template content and then fill in every visible section before opening the PR. Do not replace the template with a freeform summary. +After creating or editing a PR, fetch the rendered PR body and verify that the +template's visible headings and checklist items are still present. + The PR body must include: - `#### Overview` with a concise summary and both contribution confirmation diff --git a/.coderabbit.yaml b/.coderabbit.yaml index f76cf646c..1fc2b1b9a 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -90,6 +90,10 @@ reviews: instructions: | Review documentation for technical accuracy against the current API, command correctness, and consistency across language bindings. Flag stale examples, missing SPDX headers where required, and instructions that no longer match CI or pre-commit behavior. + - path: "**/SKILL.md" + instructions: | + Do not flag SKILL.md files for missing SPDX headers. Skill entrypoints intentionally start with YAML frontmatter instead. + Verify that every SKILL.md keeps valid YAML frontmatter with at least name and description fields before the Markdown body. - path: "{.github/**,.gitlab-ci.yml,.pre-commit-config.yaml,justfile,scripts/**}" instructions: | Review automation changes for reproducibility, pinned versions where appropriate, secret handling, and consistency with the documented validation matrix. diff --git a/.github/ci-path-filters.yml b/.github/ci-path-filters.yml index 33474454a..9e0312f70 100644 --- a/.github/ci-path-filters.yml +++ b/.github/ci-path-filters.yml @@ -5,6 +5,7 @@ ci: - '.github/actionlint.yaml' - '.github/ci-path-filters.yml' - '.github/workflows/**' + - 'codecov.yml' shared: - 'crates/adaptive/Cargo.toml' diff --git a/AGENTS.md b/AGENTS.md index 54074ca79..e5c712b72 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -167,6 +167,7 @@ These conventions keep source, documentation, and binding behavior consistent ac repository. - Keep SPDX headers on source, docs, scripts, and configuration files. The project is Apache-2.0. +- `SKILL.md` files are skill entrypoints and do not need SPDX headers, but they must always start with YAML frontmatter containing at least `name` and `description`. - Follow binding naming conventions: Rust and Python `snake_case`, C FFI exports prefixed `nemo_flow_`, Go `PascalCase` for public APIs, Node.js `camelCase`. - Preserve the shared runtime model across bindings. Do not add behavior to one primary binding without considering Rust, Python, and Node.js parity. - Prefer documented public APIs and stable wrapper commands. Do not rely on internal helpers in examples or user-facing docs. @@ -236,4 +237,6 @@ with repository expectations. - Keep stable public wrappers at the `scripts/` root in docs and examples. Reference namespaced helper paths only when documenting internal maintenance work. - Use branch prefixes from the contributor docs: `feat/`, `fix/`, `docs/`, `test/`, or `refactor/`. - Use signed-off commits for PR work: `git commit -s`. -- PR descriptions should include what changed, why, how it was tested, and any breaking changes. +- Before creating, opening, publishing, or editing a pull request, read `.github/pull_request_template.md` and use it as the PR body skeleton. Preserve its visible headings, checklist items, and related-issue guidance; fill the sections instead of replacing them with a generic summary. +- If repo-local PR guidance such as the `prepare-pr` skill conflicts with generic GitHub connector or plugin guidance, follow the repo-local PR guidance for PR body format and review handoff details. +- PR descriptions should include what changed, why, how it was tested, and any breaking changes within the repository template format. diff --git a/codecov.yml b/codecov.yml index 4fab8a83f..d16905404 100644 --- a/codecov.yml +++ b/codecov.yml @@ -4,8 +4,16 @@ codecov: require_ci_to_pass: true notify: - # CI uploads coverage from 5 test jobs, each on a 3-way platform matrix. - after_n_builds: 15 + # Codecov counts one build per upload step in a full CI run: + # | Binding | Platforms that upload coverage | Uploads | + # |-------------|----------------------------------------------------------------------|---------| + # | Rust | linux-amd64, linux-arm64, macos-arm64, windows-amd64 | 4 | + # | Python | linux-amd64, linux-arm64, macos-arm64, windows-amd64, windows-arm64 | 5 | + # | Go | linux-amd64, linux-arm64, macos-arm64, windows-amd64, windows-arm64 | 5 | + # | Node.js | linux-amd64, linux-arm64, macos-arm64 | 3 | + # | WebAssembly | linux-amd64, linux-arm64, macos-arm64, windows-amd64, windows-arm64 | 5 | + # | Total | | 22 | + after_n_builds: 22 wait_for_ci: true coverage: @@ -81,7 +89,7 @@ component_management: - component_id: wasm_binding name: WebAssembly Binding paths: - - "NeMo-Flow/crates/wasm/pkg/*.js" + - "NeMo-Flow/crates/wasm/wrappers/nodejs/*.js" statuses: - type: project target: 84% # cannot be any higher due to internals @@ -90,7 +98,7 @@ component_management: if_ci_failed: error comment: - after_n_builds: 15 + after_n_builds: 22 layout: "header, diff, components, files, footer" behavior: default require_changes: false @@ -113,5 +121,6 @@ ignore: - "**/*.d.ts" - "**/*.pyi" - "**/python/nemo_flow/lib_native*.dylib.dSYM/**" - # impossible to cover due to wasm-bindgen internals - - "**/crates/wasm/src/api/mod.rs" + # WebAssembly Rust wrappers are covered through wasm-pack execution and + # reported through generated package JavaScript coverage. + - "**/crates/wasm/src/**/*.rs" diff --git a/crates/wasm/package.json b/crates/wasm/package.json index 3cc1a7701..afef61201 100644 --- a/crates/wasm/package.json +++ b/crates/wasm/package.json @@ -9,7 +9,7 @@ "pretest:pkg": "npm run build:pkg", "test:pkg": "node --test --test-force-exit tests-js/*.mjs", "precoverage:pkg": "npm run build:pkg", - "coverage:pkg": "c8 --include=\"pkg/**/*.js\" --exclude=\"tests-js/**\" --reporter=text --reporter=json-summary --reporter=cobertura --reports-dir coverage node --test --test-force-exit --test-reporter=spec --test-reporter-destination=stdout --test-reporter=junit --test-reporter-destination=junit.xml tests-js/*.mjs" + "coverage:pkg": "c8 --include=\"pkg/**/*.js\" --exclude=\"tests-js/**\" --reporter=text --reporter=json-summary --reporter=cobertura --reports-dir coverage node --test --test-force-exit --test-reporter=spec --test-reporter-destination=stdout --test-reporter=junit --test-reporter-destination=junit.xml tests-js/*.mjs && node scripts/normalize_coverage.mjs coverage/cobertura-coverage.xml" }, "devDependencies": { "c8": "^11.0.0" diff --git a/crates/wasm/scripts/normalize_coverage.mjs b/crates/wasm/scripts/normalize_coverage.mjs new file mode 100644 index 000000000..ecd4e959a --- /dev/null +++ b/crates/wasm/scripts/normalize_coverage.mjs @@ -0,0 +1,130 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import fs from 'node:fs'; + +const [inputPath, outputPath = inputPath] = process.argv.slice(2); + +if (!inputPath) { + console.error('Usage: node scripts/normalize_coverage.mjs [output-cobertura.xml]'); + process.exit(1); +} + +const filenameMap = new Map([ + ['pkg/adaptive.js', 'wrappers/nodejs/adaptive.js'], + ['pkg/index.js', 'wrappers/nodejs/index.js'], + ['pkg/plugin.js', 'wrappers/nodejs/plugin.js'], + ['pkg/typed.js', 'wrappers/nodejs/typed.js'], + ['wrappers/nodejs/adaptive.js', 'wrappers/nodejs/adaptive.js'], + ['wrappers/nodejs/index.js', 'wrappers/nodejs/index.js'], + ['wrappers/nodejs/plugin.js', 'wrappers/nodejs/plugin.js'], + ['wrappers/nodejs/typed.js', 'wrappers/nodejs/typed.js'], +]); + +function getAttribute(xml, name) { + const match = xml.match(new RegExp(`${name}="([^"]*)"`)); + return match?.[1] ?? ''; +} + +function setAttribute(xml, name, value) { + const escaped = String(value).replaceAll('&', '&').replaceAll('"', '"'); + const pattern = new RegExp(`${name}="[^"]*"`); + return xml.replace(pattern, `${name}="${escaped}"`); +} + +function rate(covered, valid) { + return valid === 0 ? '1' : String(covered / valid); +} + +function normalizeCoverageFilename(filename) { + return filename.replaceAll('\\', '/').replace(/^(?:\.\/)+/, ''); +} + +function summarizeClass(xml) { + let linesValid = 0; + let linesCovered = 0; + let branchesValid = 0; + let branchesCovered = 0; + + for (const lineMatch of xml.matchAll(/]*>/g)) { + const line = lineMatch[0]; + linesValid += 1; + if (Number(getAttribute(line, 'hits')) > 0) { + linesCovered += 1; + } + + const branchMatch = getAttribute(line, 'condition-coverage').match(/\((\d+)\/(\d+)\)/); + if (branchMatch) { + branchesCovered += Number(branchMatch[1]); + branchesValid += Number(branchMatch[2]); + } + } + + return { + linesValid, + linesCovered, + branchesValid, + branchesCovered, + }; +} + +function sumCoverage(items) { + return items.reduce( + (total, item) => ({ + linesValid: total.linesValid + item.linesValid, + linesCovered: total.linesCovered + item.linesCovered, + branchesValid: total.branchesValid + item.branchesValid, + branchesCovered: total.branchesCovered + item.branchesCovered, + }), + { linesValid: 0, linesCovered: 0, branchesValid: 0, branchesCovered: 0 }, + ); +} + +const input = fs.readFileSync(inputPath, 'utf8'); +const classes = []; + +for (const classMatch of input.matchAll(//g)) { + let classXml = classMatch[0]; + const filename = normalizeCoverageFilename(getAttribute(classXml, 'filename')); + const normalizedFilename = filenameMap.get(filename); + + if (!normalizedFilename) { + continue; + } + + const summary = summarizeClass(classXml); + classXml = setAttribute(classXml, 'filename', normalizedFilename); + classXml = setAttribute(classXml, 'line-rate', rate(summary.linesCovered, summary.linesValid)); + classXml = setAttribute(classXml, 'branch-rate', rate(summary.branchesCovered, summary.branchesValid)); + classes.push({ xml: classXml, ...summary }); +} + +if (classes.length === 0) { + throw new Error(`no checked-in WebAssembly wrapper coverage classes found in ${inputPath}`); +} + +const total = sumCoverage(classes); +let output = input; +output = output.replace(/]*>/, (match) => { + let updated = match; + updated = setAttribute(updated, 'lines-valid', total.linesValid); + updated = setAttribute(updated, 'lines-covered', total.linesCovered); + updated = setAttribute(updated, 'line-rate', rate(total.linesCovered, total.linesValid)); + updated = setAttribute(updated, 'branches-valid', total.branchesValid); + updated = setAttribute(updated, 'branches-covered', total.branchesCovered); + updated = setAttribute(updated, 'branch-rate', rate(total.branchesCovered, total.branchesValid)); + return updated; +}); +output = output.replace(/]*>/, (match) => { + let updated = match; + updated = setAttribute(updated, 'name', 'wasm_wrappers'); + updated = setAttribute(updated, 'line-rate', rate(total.linesCovered, total.linesValid)); + updated = setAttribute(updated, 'branch-rate', rate(total.branchesCovered, total.branchesValid)); + return updated; +}); +output = output.replace( + /[\s\S]*?<\/classes>/, + `\n${classes.map((item) => item.xml).join('\n')}\n `, +); + +fs.writeFileSync(outputPath, output);