From 366c44f224916258b518b6df2f7d08faf1879ab2 Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Tue, 19 May 2026 16:56:48 +0200 Subject: [PATCH] ci: configure Codecov coverage uploads (#150) --- .github/workflows/ci.yml | 27 +++++-- README.md | 3 +- codecov.yml | 109 ++++++++++++++++++++++++++ package.json | 2 +- scripts/normalize-lcov-paths.mjs | 59 ++++++++++++++ scripts/normalize-lcov-paths.test.mjs | 31 ++++++++ 6 files changed, 222 insertions(+), 9 deletions(-) create mode 100644 codecov.yml create mode 100644 scripts/normalize-lcov-paths.mjs create mode 100644 scripts/normalize-lcov-paths.test.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de9d056d..fbfe41d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -191,9 +191,8 @@ jobs: coverage: name: Coverage runs-on: ubuntu-latest - permissions: - contents: read - id-token: write + env: + CODECOV_TOKEN_CONFIGURED: ${{ secrets.CODECOV_TOKEN != '' }} steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -232,25 +231,39 @@ jobs: run: pnpm run test:coverage:ts - name: Upload TypeScript coverage + if: env.CODECOV_TOKEN_CONFIGURED == 'true' uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5.5.4 with: - use_oidc: true + token: ${{ secrets.CODECOV_TOKEN }} + slug: Kaelio/ktx files: ./packages/cli/coverage/lcov.info,./packages/connector-bigquery/coverage/lcov.info,./packages/connector-clickhouse/coverage/lcov.info,./packages/connector-mysql/coverage/lcov.info,./packages/connector-postgres/coverage/lcov.info,./packages/connector-snowflake/coverage/lcov.info,./packages/connector-sqlite/coverage/lcov.info,./packages/connector-sqlserver/coverage/lcov.info,./packages/context/coverage/lcov.info,./packages/llm/coverage/lcov.info flags: typescript name: typescript - fail_ci_if_error: false + disable_search: true + fail_ci_if_error: true + + - name: Warn when Codecov token is missing for TypeScript + if: env.CODECOV_TOKEN_CONFIGURED != 'true' + run: echo "::warning::CODECOV_TOKEN is not configured; skipping TypeScript coverage upload" - name: Generate Python coverage run: pnpm run test:coverage:py - name: Upload Python coverage + if: env.CODECOV_TOKEN_CONFIGURED == 'true' uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5.5.4 with: - use_oidc: true + token: ${{ secrets.CODECOV_TOKEN }} + slug: Kaelio/ktx files: ./coverage/python.xml flags: python name: python - fail_ci_if_error: false + disable_search: true + fail_ci_if_error: true + + - name: Warn when Codecov token is missing for Python + if: env.CODECOV_TOKEN_CONFIGURED != 'true' + run: echo "::warning::CODECOV_TOKEN is not configured; skipping Python coverage upload" artifact-checks: name: Artifact checks diff --git a/README.md b/README.md index 517d3923..3fc4f69e 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,8 @@

npm version - Codecov + Codecov + Documentation License GitHub stars

diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..0ff83e2f --- /dev/null +++ b/codecov.yml @@ -0,0 +1,109 @@ +codecov: + branch: main + require_ci_to_pass: true + notify: + after_n_builds: 2 + +coverage: + precision: 2 + round: down + range: "70...100" + status: + project: + default: + target: auto + threshold: 1% + if_ci_failed: error + typescript: + target: auto + threshold: 1% + flags: + - typescript + if_ci_failed: error + python: + target: auto + threshold: 1% + flags: + - python + if_ci_failed: error + patch: + default: + target: 75% + threshold: 5% + if_ci_failed: error + informational: true + +comment: + layout: "header, diff, flags, components, files" + behavior: default + require_changes: false + require_base: false + require_head: true + +flags: + typescript: + paths: + - packages/ + carryforward: false + python: + paths: + - python/ + carryforward: false + +component_management: + individual_components: + - component_id: pkg_cli + name: CLI + paths: + - packages/cli/src/** + - component_id: pkg_context + name: Context engine + paths: + - packages/context/src/** + - component_id: pkg_llm + name: LLM + paths: + - packages/llm/src/** + - component_id: connector_bigquery + name: BigQuery connector + paths: + - packages/connector-bigquery/src/** + - component_id: connector_clickhouse + name: ClickHouse connector + paths: + - packages/connector-clickhouse/src/** + - component_id: connector_mysql + name: MySQL connector + paths: + - packages/connector-mysql/src/** + - component_id: connector_postgres + name: Postgres connector + paths: + - packages/connector-postgres/src/** + - component_id: connector_snowflake + name: Snowflake connector + paths: + - packages/connector-snowflake/src/** + - component_id: connector_sqlite + name: SQLite connector + paths: + - packages/connector-sqlite/src/** + - component_id: connector_sqlserver + name: SQL Server connector + paths: + - packages/connector-sqlserver/src/** + - component_id: py_semantic_layer + name: Python semantic layer + paths: + - python/ktx-sl/semantic_layer/** + - component_id: py_daemon + name: Python daemon + paths: + - python/ktx-daemon/src/ktx_daemon/** + +ignore: + - docs-site/** + - examples/** + - packages/*/coverage/** + - packages/*/dist/** + - python/ktx-sl/demos/** diff --git a/package.json b/package.json index ae1fb151..71970b58 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "test": "node --test scripts/*.test.mjs && pnpm --filter './packages/*' run test", "test:coverage": "pnpm run test:coverage:ts && pnpm run test:coverage:py", "test:coverage:py": "uv run pytest --cov=python/ktx-sl/semantic_layer --cov=python/ktx-daemon/src/ktx_daemon --cov-report=xml:coverage/python.xml --cov-report=term", - "test:coverage:ts": "pnpm --filter './packages/*' run build && pnpm --filter './packages/*' run test --coverage --coverage.reporter=lcov --coverage.exclude='dist/**'", + "test:coverage:ts": "pnpm --filter './packages/*' run build && pnpm --filter './packages/*' run test --coverage --coverage.reporter=lcov --coverage.exclude='dist/**' && node scripts/normalize-lcov-paths.mjs packages/*/coverage/lcov.info", "test:slow": "pnpm --filter @ktx/context run test:slow && pnpm --filter @ktx/cli run test:slow", "type-check": "pnpm --filter './packages/*' run type-check" }, diff --git a/scripts/normalize-lcov-paths.mjs b/scripts/normalize-lcov-paths.mjs new file mode 100644 index 00000000..62df4f1a --- /dev/null +++ b/scripts/normalize-lcov-paths.mjs @@ -0,0 +1,59 @@ +#!/usr/bin/env node + +import { readFile, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +function toPosix(filePath) { + return filePath.replace(/\\/g, '/'); +} + +export function normalizeLcovContent(content, packagePath) { + const normalizedPackagePath = toPosix(packagePath).replace(/\/$/, ''); + + return content.replace(/^SF:(.+)$/gm, (line, sourcePath) => { + const normalizedSourcePath = toPosix(sourcePath); + + if ( + path.isAbsolute(sourcePath) || + normalizedSourcePath.startsWith(`${normalizedPackagePath}/`) || + normalizedSourcePath.startsWith('../') + ) { + return line; + } + + return `SF:${normalizedPackagePath}/${normalizedSourcePath}`; + }); +} + +export async function normalizeLcovFile(rootDir, reportPath) { + const absoluteReportPath = path.resolve(rootDir, reportPath); + const packagePath = toPosix(path.relative(rootDir, path.dirname(path.dirname(absoluteReportPath)))); + const content = await readFile(absoluteReportPath, 'utf8'); + const normalizedContent = normalizeLcovContent(content, packagePath); + + if (normalizedContent !== content) { + await writeFile(absoluteReportPath, normalizedContent); + } +} + +async function main() { + const scriptDir = path.dirname(fileURLToPath(import.meta.url)); + const rootDir = path.resolve(scriptDir, '..'); + const reportPaths = process.argv.slice(2); + + if (reportPaths.length === 0) { + throw new Error('Usage: normalize-lcov-paths.mjs [...]'); + } + + for (const reportPath of reportPaths) { + await normalizeLcovFile(rootDir, reportPath); + } +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + main().catch((error) => { + console.error(error instanceof Error ? error.message : error); + process.exitCode = 1; + }); +} diff --git a/scripts/normalize-lcov-paths.test.mjs b/scripts/normalize-lcov-paths.test.mjs new file mode 100644 index 00000000..44ab9fe6 --- /dev/null +++ b/scripts/normalize-lcov-paths.test.mjs @@ -0,0 +1,31 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import { normalizeLcovContent } from './normalize-lcov-paths.mjs'; + +describe('normalizeLcovContent', () => { + it('prefixes relative LCOV source paths with the package path', () => { + const input = ['TN:', 'SF:src/index.ts', 'SF:src\\windows.ts', 'DA:1,1', 'end_of_record'].join('\n'); + + assert.equal( + normalizeLcovContent(input, 'packages/context'), + [ + 'TN:', + 'SF:packages/context/src/index.ts', + 'SF:packages/context/src/windows.ts', + 'DA:1,1', + 'end_of_record', + ].join('\n'), + ); + }); + + it('leaves already-normalized and absolute paths unchanged', () => { + const input = [ + 'SF:packages/cli/src/index.ts', + 'SF:/tmp/repo/packages/cli/src/index.ts', + 'SF:../shared/source.ts', + ].join('\n'); + + assert.equal(normalizeLcovContent(input, 'packages/cli'), input); + }); +});