diff --git a/.github/workflows/bioccheck.yml b/.github/workflows/bioccheck.yml index ec65fca..6afe905 100644 --- a/.github/workflows/bioccheck.yml +++ b/.github/workflows/bioccheck.yml @@ -13,6 +13,11 @@ on: required: false type: boolean default: false + enable_coverage_report: + description: 'Compute test coverage with covr and save an HTML report as an artifact, without Codecov' + required: false + type: boolean + default: false error_on: description: 'Error policy for rcmdcheck (never, note, warning, error)' required: false @@ -122,13 +127,18 @@ jobs: - name: Install Package Dependencies env: ENABLE_CYCLOCOMP: ${{ inputs.enable_cyclocomp }} + ENABLE_COVERAGE_REPORT: ${{ inputs.enable_coverage_report }} shell: Rscript {0} run: | options(repos = c(CRAN = "https://packagemanager.posit.co/cran/__linux__/noble/latest", BiocManager::repositories())) remotes::install_local(".", dependencies = TRUE, Ncpus = parallel::detectCores()) # Only install the tools for features that are used in this run. pkgs <- c("rcmdcheck", "BiocCheck") - if (nzchar(Sys.getenv("CODECOV_TOKEN"))) pkgs <- c(pkgs, "covr") + upload_coverage <- nzchar(Sys.getenv("CODECOV_TOKEN")) + report_coverage <- Sys.getenv("ENABLE_COVERAGE_REPORT") == "true" + if (upload_coverage || report_coverage) pkgs <- c(pkgs, "covr") + if (upload_coverage) pkgs <- c(pkgs, "xml2") + if (report_coverage) pkgs <- c(pkgs, "DT", "htmltools") if (Sys.getenv("INSTALL_PKGDOWN") == "true") pkgs <- c(pkgs, "pkgdown") if (Sys.getenv("ENABLE_CYCLOCOMP") == "true") pkgs <- c(pkgs, "cyclocomp") BiocManager::install(pkgs, Ncpus = parallel::detectCores()) @@ -154,12 +164,84 @@ jobs: `quit-with-status` = TRUE, `no-check-bioc-help` = TRUE ) + # Coverage is computed once and used by whichever of the Codecov upload + # (CODECOV_TOKEN set) and the HTML report (enable_coverage_report) is on. + # Coverage problems never fail the check (ADR 0011). - name: Test Coverage - if: success() && needs.setup.outputs.is_release_branch == 'false' && env.CODECOV_TOKEN != '' + id: coverage + if: >- + success() + && needs.setup.outputs.is_release_branch == 'false' + && (env.CODECOV_TOKEN != '' || inputs.enable_coverage_report == true) + continue-on-error: true + env: + ENABLE_COVERAGE_REPORT: ${{ inputs.enable_coverage_report }} shell: Rscript {0} run: | cov <- covr::package_coverage() - covr::codecov(coverage = cov, token = Sys.getenv("CODECOV_TOKEN")) + print(cov) + if (nzchar(Sys.getenv("CODECOV_TOKEN"))) { + covr::to_cobertura(cov, filename = "coverage.xml") + } + if (Sys.getenv("ENABLE_COVERAGE_REPORT") == "true") { + covr::report(cov, file = "coverage-report/index.html", browse = FALSE) + # Per-file percentages on the run's summary page. + per_line <- covr::tally_coverage(cov, by = "line") + files <- split(per_line$value > 0, per_line$filename) + # Lowest coverage first, so the files that need tests are at the top. + pct <- sort(vapply(files, function(x) 100 * mean(x), numeric(1))) + total <- covr::percent_coverage(cov) + if (length(pct) == 0L || is.nan(total)) { + summary_md <- "### Test coverage: no lines of R code were measured" + } else { + summary_md <- c( + sprintf("### Test coverage: %.1f%%", total), + "", + "| File | Coverage |", + "|---|---|", + sprintf("| `%s` | %.1f%% |", names(pct), pct), + "", + "The line-by-line report is in the `coverage-report` artifact." + ) + } + cat(summary_md, file = Sys.getenv("GITHUB_STEP_SUMMARY"), sep = "\n", append = TRUE) + } + + - name: Upload Coverage Report + if: steps.coverage.outcome == 'success' && inputs.enable_coverage_report == true + continue-on-error: true + uses: actions/upload-artifact@v7 + with: + name: coverage-report + path: coverage-report + if-no-files-found: warn + + # The Codecov action verifies its uploader with gpg, which the + # Bioconductor container doesn't include. + - name: Install gpg for Codecov + id: gpg + if: steps.coverage.outcome == 'success' && env.CODECOV_TOKEN != '' + continue-on-error: true + shell: bash + env: + DEBIAN_FRONTEND: noninteractive + run: | + command -v gpg >/dev/null 2>&1 || { + apt-get update -qq && apt-get install -y -qq --no-install-recommends gnupg + } + + # The Codecov action, unlike covr::codecov(), accepts an organization's + # global upload token as well as a repository token (ADR 0011). + - name: Upload Coverage to Codecov + if: steps.gpg.outcome == 'success' + continue-on-error: true + uses: codecov/codecov-action@v7 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: coverage.xml + disable_search: true + disable_telem: true + fail_ci_if_error: false - name: Run Cyclocomp if: inputs.enable_cyclocomp == true diff --git a/bioccheck_usage.md b/bioccheck_usage.md index 7b97032..6784683 100644 --- a/bioccheck_usage.md +++ b/bioccheck_usage.md @@ -26,13 +26,22 @@ To add additional features like `COVR` and `pkgdown`, or change error sensitivit Even if you use the Workflow Generator, you must perform these one-time repository settings to use optional features: **For Codecov** (coverage is uploaded on pushes and PRs to non-release branches only): -1. **Obtain Codecov Token**: Log in to [Codecov](https://about.codecov.io/), link your repository, and copy the repository upload token. +1. **Obtain Codecov Token**: Log in to [Codecov](https://about.codecov.io/), link your repository, and copy the repository upload token. Alternatively, an organization admin can copy the organization's global upload token (organization settings in Codecov), which works for every repository in the organization; store it once as a GitHub organization secret named `CODECOV_TOKEN`. Paste only the token itself, not `CODECOV_TOKEN=...`. 2. **Set Repository Secret**: In your GitHub repository, navigate to **Settings** → **Secrets and variables** → **Actions** → **New repository secret**. Create a secret named `CODECOV_TOKEN` containing your token. **For `pkgdown`:** 1. **Configure GitHub Pages Source**: In your package repository on GitHub, navigate to **Settings** → **Pages**. Under **Build and deployment** → **Source**, select **GitHub Actions** (do NOT select "Deploy from a branch"). 2. **Allow Deployment Branches**: Navigate to **Settings** → **Environments** → **github-pages**. Under **Deployment branches**, change the rule to **All branches** (or add `RELEASE_*` to the allowed branch pattern) so that GitHub allows deployments from Bioconductor release branches. +### Test Coverage + +Two independent options, which can be used together. Both skip release branches, and coverage problems never fail the check. + +- **Codecov upload**: pass a `CODECOV_TOKEN` secret (see above). Coverage is uploaded to Codecov, which provides a badge, history and pull request comments. +- **Coverage report without Codecov**: set `enable_coverage_report: true`. No account or token is needed, and it works on pull requests from forks. The run's summary page shows the total and per-file coverage, and the line-by-line HTML report is saved as the `coverage-report` artifact: download it from the **Artifacts** section at the bottom of the run page and open `index.html`. + +Coverage runs the test suite a second time with instrumentation, so either option adds roughly the time of one more test run. + ### Using the Cyclocomp Artifact When `enable_cyclocomp` is true, the workflow uploads a `cyclocomp-results` CSV artifact. @@ -94,9 +103,10 @@ Here's a full set of badges for the top of your package's `README.md`. Replace ` | :--- | :--- | :--- | :--- | | `enable_pkgdown` | `boolean` | `false` | Enable building and deploying `pkgdown` site on release branch pushes. | | `enable_cyclocomp` | `boolean` | `false` | Enable cyclomatic complexity analysis via cyclocomp. | +| `enable_coverage_report` | `boolean` | `false` | Compute test coverage with covr and save an HTML report as the `coverage-report` artifact, without Codecov. | | `error_on` | `string` | `"warning"` | Error policy for `rcmdcheck` (`"never"`, `"note"`, `"warning"`, `"error"`). | | `bioc_version` | `string` | `""` | Optional override for container tag (e.g. `devel`, `RELEASE_3_20`). | -| `secrets.CODECOV_TOKEN` | secret | `""` | Optional Codecov token for coverage uploads. | +| `secrets.CODECOV_TOKEN` | secret | `""` | Optional Codecov token (repository or organization global upload token). Uploads coverage to Codecov when set. | ## Which Version to Use diff --git a/docs/adr/0011-codecov-action-upload.md b/docs/adr/0011-codecov-action-upload.md new file mode 100644 index 0000000..f0df2a1 --- /dev/null +++ b/docs/adr/0011-codecov-action-upload.md @@ -0,0 +1,32 @@ +# 0011. Upload Coverage with the Codecov Action + +- **Status:** Accepted +- **Date:** 2026-10-30 +- **Deciders:** Levi Waldron + +## Context and Problem Statement + +`bioccheck.yml` uploaded coverage with `covr::codecov()`, which posts to Codecov's legacy upload endpoint. That endpoint finds the repository from the upload token, so it accepts only a repository's own token. An organization's global upload token, which Codecov provides so that one secret works for every repository in the organization, is rejected with "Could not find a repository associated with upload token". An organization that stores `CODECOV_TOKEN` once, as an organization secret, therefore gets no coverage for any repository, and because the upload error does not fail the step, the run still looks successful. + +## Decision + +Compute coverage with `covr` as before, write it as a Cobertura report with `covr::to_cobertura()`, and upload it with the official `codecov/codecov-action`, which uses Codecov's current uploader and accepts either a repository token or a global upload token. The action is pinned to its major version (`@v7`), like the other actions in the workflow. + +The action verifies the uploader it downloads with `gpg`, which the Bioconductor container does not include. A step installs `gnupg` with `apt-get` when `gpg` is missing, and only in runs that upload coverage. Skipping the verification (`skip_validation`) was not chosen, because it would run an unverified binary with the token. + +`xml2`, needed by `covr::to_cobertura()`, is installed with `covr` under the same condition (ADR 0010). + +Coverage problems never fail the package check, as with `covr::codecov()`, whose errors were only printed: the coverage, `gpg` and upload steps use `continue-on-error`, and the upload sets `fail_ci_if_error: false`. The upload also sets `disable_telem: true`, so Codecov's uploader does not send its own error reports from callers' runs. + +## Alternatives Considered + +- **Keep `covr::codecov()` and require repository tokens**: rejected; every repository needs its own secret, and an organization secret silently uploads nothing. +- **Call the Codecov CLI directly**: equivalent, but the action already handles downloading and verifying the CLI and passing CI metadata. +- **OIDC authentication (`use_oidc`)**: needs no token, but requires callers to grant `id-token: write`, which changes every caller's `ci.yml`. It could be added later as an option. + +## Consequences + +- Repository tokens keep working, and a global upload token stored as an organization secret now works too. +- Runs with coverage spend about 20 seconds installing `gnupg`. +- The workflow depends on a third-party action and on Codecov's CLI release process, in addition to `covr`. +- Upload failures are still not reported as check failures, so a broken token remains easy to miss; the Codecov badge or dashboard is the place to notice it. diff --git a/docs/adr/0012-opt-in-coverage-report.md b/docs/adr/0012-opt-in-coverage-report.md new file mode 100644 index 0000000..30d2276 --- /dev/null +++ b/docs/adr/0012-opt-in-coverage-report.md @@ -0,0 +1,28 @@ +# 0012. Opt-in Coverage Report Without Codecov + +- **Status:** Accepted +- **Date:** 2026-10-30 +- **Deciders:** Levi Waldron + +## Context and Problem Statement + +Test coverage was available only by uploading to Codecov, which needs a Codecov account, a token stored as a secret, and a third-party service. Some maintainers want to see coverage without any of these, and secrets are not available to pull requests from forks. + +## Decision + +Add an opt-in input, `enable_coverage_report` (default `false`). When it is true, `bioccheck.yml` computes coverage with `covr`, saves the line-by-line HTML report from `covr::report()` as the `coverage-report` artifact, and writes the total and per-file percentages to the run's summary page. + +Coverage is computed once per run if either this input or the Codecov upload (`CODECOV_TOKEN` set, ADR 0011) is on, and each feature uses the same result. The Codecov upload stays controlled by the token alone, so existing callers are unaffected. `DT` and `htmltools`, which `covr::report()` needs, are installed only when the report is on (ADR 0010). + +Like the Codecov upload, the report is skipped on release branches, and its steps use `continue-on-error`, so coverage never fails the package check. + +## Alternatives Considered + +- **Coverage report by default**: rejected. `covr::package_coverage()` runs the test suite a second time with instrumentation, which would roughly double test time in the default path, and instrumented runs occasionally fail where ordinary runs pass. This also follows ADR 0005, which makes non-core features opt-in. +- **A coverage badge without Codecov**, by committing a badge from CI: rejected for now. It needs `contents: write` in every caller and produces bot commits, and it cannot run on pull requests. + +## Consequences + +- Coverage is available with no account, token or third-party service, including on fork pull requests. +- The report must be downloaded from the run page; there is no badge, history or pull request comment, which Codecov provides. +- Runs with the report on spend the time of a second, instrumented test run. diff --git a/docs/adr/README.md b/docs/adr/README.md index 4452f45..bc0093b 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -13,4 +13,6 @@ This directory contains Architecture Decision Records (ADRs) for the `bioconduct - [0007. Web App Workflow Generator](0007-web-app-workflow-generator.md) - [0008. Opt-in Cyclomatic Complexity Analysis (`cyclocomp`)](0008-cyclocomp-complexity-analysis.md) - [0009. Separation of Package Checking from Container Building](0009-separation-of-container-building-concerns.md) -- [0010. Install Optional Tools Only When Used](0010-conditional-tool-installation.md) (Proposed) +- [0010. Install Optional Tools Only When Used](0010-conditional-tool-installation.md) +- [0011. Upload Coverage with the Codecov Action](0011-codecov-action-upload.md) +- [0012. Opt-in Coverage Report Without Codecov](0012-opt-in-coverage-report.md) diff --git a/docs/index.html b/docs/index.html index c505f47..4020266 100644 --- a/docs/index.html +++ b/docs/index.html @@ -304,7 +304,17 @@

Configuration

Enable Codecov - Uploads test coverage to Codecov using covr. Requires a CODECOV_TOKEN repository secret. + Uploads test coverage to Codecov using covr. Requires a CODECOV_TOKEN repository or organization secret. +
+ + + +
+
@@ -356,12 +366,13 @@

Generated YAML

const elPkgdown = document.getElementById('opt-pkgdown'); const elCodecov = document.getElementById('opt-codecov'); const elCyclocomp = document.getElementById('opt-cyclocomp'); + const elCoverageReport = document.getElementById('opt-coverage-report'); const elErrorOn = document.getElementById('opt-error-on'); const elOutput = document.getElementById('yaml-output'); const btnCopy = document.getElementById('copy-btn'); // Listeners - [elPkgdown, elCodecov, elCyclocomp, elErrorOn].forEach(el => { + [elPkgdown, elCodecov, elCyclocomp, elCoverageReport, elErrorOn].forEach(el => { el.addEventListener('change', generateYaml); }); @@ -369,6 +380,7 @@

Generated YAML

const hasPkgdown = elPkgdown.checked; const hasCodecov = elCodecov.checked; const hasCyclocomp = elCyclocomp.checked; + const hasCoverageReport = elCoverageReport.checked; const errorOn = elErrorOn.value; // Build the JavaScript Object Model @@ -403,6 +415,11 @@

Generated YAML

workflowObj.jobs.bioccheck.with.enable_cyclocomp = true; } + // Add coverage report parameter if enabled + if (hasCoverageReport) { + workflowObj.jobs.bioccheck.with.enable_coverage_report = true; + } + // If 'with' is empty, remove it to keep yaml clean if (Object.keys(workflowObj.jobs.bioccheck.with).length === 0) { delete workflowObj.jobs.bioccheck.with;