Skip to content

Commit 9994c8f

Browse files
authored
chore(ci): Combine code coverage in CI before uploading to codecov (#24)
* chore(ci): Combine code coverage in CI before uploading to codecov * fix(ci): Account for parallel mode creating multiple coverage files * test: Debug coverage files * fix: Debug coverage artifacts * test: Simplify code coverage artifact paths * test: Inspect generated coverage files as they are missing before upload * fix: Ensure upload-artifact action includes gitignored `.coverage` file * test: Debug coverage combining step * fix: Coverage artifact ID incrementing * fix: Fail fast and use correct directory when combining coverage files * fix: Only upload the provided combined coverage file to codecov * chore(ci): Simplify the logic and error handling for combining coverage * fix: Debug coverage using nox target * fix: Lint * chore: Incorporate PR review suggestions
1 parent 7c193ca commit 9994c8f

File tree

2 files changed

+114
-40
lines changed

2 files changed

+114
-40
lines changed

.github/workflows/ci.yml

Lines changed: 67 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -334,62 +334,99 @@ jobs:
334334
run: poetry install --no-interaction --only-root
335335

336336
- name: Run unit tests
337-
id: test-unit
338337
env:
339338
TOOLKIT_VERSION: ${{ steps.version.outputs.VERSION }}
340-
PYTHON_VERSION: ${{ matrix.python-version }}
341-
COVERAGE_FILE: coverage/.coverage.${{ matrix.python-version }}
342339
run: |
343-
set -euo pipefail
344-
345-
# Create coverage directory
346-
mkdir -p coverage
347-
348-
# Run tests with coverage
349340
poetry run pytest tests/unit \
350341
--cov=deepnote_toolkit \
351342
--cov=installer \
352343
--cov=deepnote_core \
353344
--cov-branch \
354345
--cov-report=term-missing:skip-covered \
355-
--cov-report=xml:coverage/coverage-${PYTHON_VERSION}.xml \
356-
--cov-report=json:coverage/coverage-${PYTHON_VERSION}.json \
357346
--junitxml=junit.xml \
358347
-o junit_family=legacy
359348
360-
# Check if coverage file was generated
361-
if [ -f "coverage/.coverage.${PYTHON_VERSION}" ]; then
362-
echo "coverage_generated=true" >> $GITHUB_OUTPUT
363-
echo "Coverage files found:"
364-
ls -la coverage/
365-
else
366-
echo "coverage_generated=false" >> $GITHUB_OUTPUT
367-
echo "Warning: No coverage file generated"
368-
fi
369-
370349
- name: Per-version coverage summary
371-
if: steps.test-unit.outputs.coverage_generated == 'true'
372-
env:
373-
PYTHON_VERSION: ${{ matrix.python-version }}
374350
run: |
375-
echo "## Python ${PYTHON_VERSION} Coverage" >> $GITHUB_STEP_SUMMARY
376-
poetry run coverage report --data-file=coverage/.coverage.${PYTHON_VERSION} --format=markdown >> $GITHUB_STEP_SUMMARY
351+
echo "## Python ${{ matrix.python-version }} Coverage" >> $GITHUB_STEP_SUMMARY
352+
poetry run coverage report --format=markdown >> $GITHUB_STEP_SUMMARY
377353
378-
- name: Upload test results to Codecov (these are results not coverage reports)
354+
- name: Upload test results to Codecov
379355
if: ${{ !cancelled() }}
380356
uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1
381357
with:
382358
token: ${{ secrets.CODECOV_TOKEN }}
383359
flags: python-${{ matrix.python-version }}
384360

385-
- name: Upload coverage to Codecov
361+
- name: Upload coverage artifacts
362+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
363+
with:
364+
name: coverage-${{ matrix.python-version }}
365+
path: .coverage
366+
retention-days: 1
367+
include-hidden-files: true
368+
if-no-files-found: error
369+
370+
coverage-combine:
371+
name: Combine and Upload Coverage
372+
runs-on: ubuntu-latest
373+
needs: tests-unit
374+
if: always()
375+
steps:
376+
- name: Checkout code
377+
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
378+
with:
379+
persist-credentials: false
380+
381+
- name: Set up Python
382+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
383+
with:
384+
python-version: '3.11'
385+
386+
- name: Install coverage
387+
run: pip install coverage[toml]
388+
389+
- name: Download all coverage artifacts
390+
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4
391+
with:
392+
pattern: coverage-*
393+
path: coverage-artifacts/
394+
395+
- name: Combine coverage files
396+
run: |
397+
shopt -s nullglob
398+
mkdir -p coverage-data
399+
400+
i=0
401+
for file in coverage-artifacts/*/.coverage; do
402+
cp "$file" "coverage-data/.coverage.$i"
403+
i=$((i + 1))
404+
done
405+
406+
coverage combine coverage-data/
407+
coverage xml -o coverage-data/coverage.xml
408+
coverage report
409+
410+
echo "## Combined Coverage Report" >> $GITHUB_STEP_SUMMARY
411+
coverage report --format=markdown >> $GITHUB_STEP_SUMMARY
412+
413+
- name: Upload combined coverage to Codecov
386414
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5
387415
with:
388416
token: ${{ secrets.CODECOV_TOKEN }}
389-
slug: deepnote/deepnote-toolkit
390-
files: ./coverage/coverage-${{ matrix.python-version }}.xml
417+
slug: ${{ github.repository }}
418+
files: ./coverage-data/coverage.xml
419+
flags: combined
420+
disable_search: true
391421
fail_ci_if_error: ${{ github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push' }}
392422

423+
- name: Upload combined coverage report
424+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
425+
with:
426+
name: coverage-combined-report
427+
path: coverage-data/coverage.xml
428+
retention-days: 30
429+
393430
audit-prod:
394431
name: Audit - Production
395432
runs-on: ubuntu-latest

noxfile.py

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -146,17 +146,54 @@ def coverage_report(session):
146146

147147
import pathlib
148148

149-
coverage_dir = pathlib.Path("coverage")
149+
# Use absolute path relative to session.invoked_from
150+
project_root = pathlib.Path(session.invoked_from)
151+
coverage_dir = project_root / "coverage"
152+
150153
if not coverage_dir.exists():
151-
session.error("No coverage directory found. Run tests with --coverage first.")
154+
session.error("No coverage directory found. Run tests with nox -s unit first.")
155+
156+
# Check if we have a combined coverage file or individual files
157+
combined_file = coverage_dir / ".coverage"
158+
coverage_files = sorted(coverage_dir.glob(".coverage.*"))
159+
160+
if not combined_file.exists() and not coverage_files:
161+
session.error("No coverage files found. Run tests with nox -s unit first.")
162+
163+
if coverage_files:
164+
session.log(f"Combining {len(coverage_files)} coverage files")
165+
session.run(
166+
"coverage",
167+
"combine",
168+
f"--data-file={combined_file}",
169+
*[str(f) for f in coverage_files],
170+
)
171+
else:
172+
session.log("Using existing combined coverage file")
152173

153-
# Combine all coverage files from coverage directory
174+
# Generate reports in coverage directory
154175
session.run(
155-
"coverage", "combine", "--data-file=coverage/.coverage", "coverage/.coverage.*"
176+
"coverage", "report", f"--data-file={combined_file}", "--format=markdown"
177+
)
178+
session.run(
179+
"coverage",
180+
"html",
181+
f"--data-file={combined_file}",
182+
"-d",
183+
str(coverage_dir / "htmlcov"),
184+
)
185+
session.run(
186+
"coverage",
187+
"xml",
188+
f"--data-file={combined_file}",
189+
"-o",
190+
str(coverage_dir / "coverage.xml"),
191+
"-i",
192+
)
193+
session.run(
194+
"coverage",
195+
"json",
196+
f"--data-file={combined_file}",
197+
"-o",
198+
str(coverage_dir / "coverage.json"),
156199
)
157-
158-
# Generate reports in coverage directory
159-
session.run("coverage", "report", "--format=markdown")
160-
session.run("coverage", "html", "-d", "coverage/htmlcov")
161-
session.run("coverage", "xml", "-o", "coverage/coverage.xml", "-i")
162-
session.run("coverage", "json", "-o", "coverage/coverage.json")

0 commit comments

Comments
 (0)