From 1a30cafb12daf09f598443d4ab33ab1bcd6b459f Mon Sep 17 00:00:00 2001 From: Daniel Osorio Date: Tue, 29 Sep 2026 12:56:50 -0500 Subject: [PATCH 1/4] Add GitHub Actions CI (R CMD check) and release workflows --- .Rbuildignore | 1 + .github/workflows/R-CMD-check.yaml | 65 ++++++++++++++++++++++++++++++ .github/workflows/release.yaml | 48 ++++++++++++++++++++++ README.md | 2 + 4 files changed, 116 insertions(+) create mode 100644 .github/workflows/R-CMD-check.yaml create mode 100644 .github/workflows/release.yaml diff --git a/.Rbuildignore b/.Rbuildignore index b8482c2..19fa90c 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -6,3 +6,4 @@ README.md tests/* ^\.positai$ ^\.claude$ +^\.github$ diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml new file mode 100644 index 0000000..5c4c399 --- /dev/null +++ b/.github/workflows/R-CMD-check.yaml @@ -0,0 +1,65 @@ +# Continuous integration: R CMD check on Linux, macOS and Windows. +on: + push: + branches: [main, master] + pull_request: + workflow_dispatch: + +name: R-CMD-check + +permissions: read-all + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + R-CMD-check: + runs-on: ${{ matrix.config.os }} + + name: ${{ matrix.config.os }} (${{ matrix.config.r }}) + + strategy: + fail-fast: false + matrix: + config: + - {os: macos-latest, r: 'release'} + - {os: windows-latest, r: 'release'} + - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} + - {os: ubuntu-latest, r: 'release'} + - {os: ubuntu-latest, r: 'oldrel-1'} + + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + R_KEEP_PKG_SOURCE: yes + + steps: + - uses: actions/checkout@v4 + + - uses: r-lib/actions/setup-pandoc@v2 + + - uses: r-lib/actions/setup-r@v2 + with: + r-version: ${{ matrix.config.r }} + http-user-agent: ${{ matrix.config.http-user-agent }} + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::rcmdcheck, any::pkgload + needs: check + + - uses: r-lib/actions/check-r-package@v2 + with: + upload-snapshots: true + build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")' + + # tests/ is excluded from the source tarball via .Rbuildignore, so + # R CMD check skips them; run the testthat suite from the repo instead. + - name: Run testthat suite + shell: Rscript {0} + run: | + options(testthat.progress.max_fails = Inf) + pkgload::load_all() + testthat::test_dir("tests/testthat", stop_on_failure = TRUE, + reporter = testthat::ProgressReporter$new(show_praise = FALSE)) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..9ceefa1 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,48 @@ +# Continuous delivery: on a version tag, build and check the source +# tarball and attach it to a GitHub release. +on: + push: + tags: ['v*', '[0-9]*'] + workflow_dispatch: + +name: release + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v4 + + - uses: r-lib/actions/setup-pandoc@v2 + + - uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::rcmdcheck, any::pkgload + needs: check + + - name: Build source tarball + run: R CMD build --no-manual . + + - name: Check source tarball + shell: Rscript {0} + run: | + tarball <- Sys.glob("*.tar.gz") + rcmdcheck::rcmdcheck(tarball, args = c("--no-manual", "--as-cran"), + error_on = "error", check_dir = "check") + + - name: Create GitHub release + if: startsWith(github.ref, 'refs/tags/') + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create "$GITHUB_REF_NAME" *.tar.gz \ + --title "$GITHUB_REF_NAME" --generate-notes diff --git a/README.md b/README.md index 390dad4..0dbec64 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # SCORPION +[![R-CMD-check](https://github.com/kuijjerlab/SCORPION/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/kuijjerlab/SCORPION/actions/workflows/R-CMD-check.yaml) + **SCORPION** (**S**ingle-**C**ell **O**riented **R**econstruction of **P**ANDA **I**ndividually **O**ptimized Gene Regulatory **N**etworks) is an R package for constructing gene regulatory networks from single-cell and single-nucleus RNA sequencing data. The package addresses the sparsity inherent in single-cell expression data through coarse-graining, which aggregates similar cells to improve correlation structure detection. Network reconstruction is performed using the PANDA (Passing Attributes between Networks for Data Assimilation) message-passing algorithm, integrating transcription factor motifs, protein-protein interactions, and gene expression data. By using consistent baseline priors across samples, SCORPION produces comparable, fully-connected, weighted regulatory networks suitable for population-level analyses. From 7f66d168b8b014f748252647b3616b1740e013fa Mon Sep 17 00:00:00 2001 From: Daniel Osorio Date: Tue, 29 Sep 2026 13:05:09 -0500 Subject: [PATCH 2/4] Harden release workflow: split read-only build from publish, verify tag version, fail on warnings --- .github/workflows/R-CMD-check.yaml | 2 ++ .github/workflows/release.yaml | 54 ++++++++++++++++++++++++++---- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index 5c4c399..af84ca9 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -58,6 +58,8 @@ jobs: # R CMD check skips them; run the testthat suite from the repo instead. - name: Run testthat suite shell: Rscript {0} + env: + NOT_CRAN: 'true' run: | options(testthat.progress.max_fails = Inf) pkgload::load_all() diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 9ceefa1..53f5fbb 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,5 +1,5 @@ # Continuous delivery: on a version tag, build and check the source -# tarball and attach it to a GitHub release. +# tarball, then attach it to a GitHub release. on: push: tags: ['v*', '[0-9]*'] @@ -7,12 +7,13 @@ on: name: release -permissions: - contents: write +permissions: read-all jobs: - release: + build: runs-on: ubuntu-latest + permissions: + contents: read env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} steps: @@ -29,6 +30,29 @@ jobs: extra-packages: any::rcmdcheck, any::pkgload needs: check + # Tags may be written as 1.2.3, v1.2.3 or v.1.2.3. + - name: Verify tag matches DESCRIPTION version + if: startsWith(github.ref, 'refs/tags/') + run: | + pkg_version=$(grep '^Version:' DESCRIPTION | sed 's/Version:[[:space:]]*//') + tag_version=${GITHUB_REF_NAME#v} + tag_version=${tag_version#.} + if [ "$tag_version" != "$pkg_version" ]; then + echo "::error::Tag $GITHUB_REF_NAME does not match DESCRIPTION version $pkg_version" + exit 1 + fi + + # tests/ is excluded from the source tarball via .Rbuildignore, so + # run the testthat suite from the checkout before releasing. + - name: Run testthat suite + shell: Rscript {0} + env: + NOT_CRAN: 'true' + run: | + pkgload::load_all() + testthat::test_dir("tests/testthat", stop_on_failure = TRUE, + reporter = testthat::ProgressReporter$new(show_praise = FALSE)) + - name: Build source tarball run: R CMD build --no-manual . @@ -36,13 +60,31 @@ jobs: shell: Rscript {0} run: | tarball <- Sys.glob("*.tar.gz") + stopifnot(length(tarball) == 1) rcmdcheck::rcmdcheck(tarball, args = c("--no-manual", "--as-cran"), - error_on = "error", check_dir = "check") + error_on = "warning", check_dir = "check") + + - uses: actions/upload-artifact@v4 + with: + name: source-tarball + path: '*.tar.gz' + if-no-files-found: error + + publish: + needs: build + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@v4 + with: + name: source-tarball - name: Create GitHub release - if: startsWith(github.ref, 'refs/tags/') env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh release create "$GITHUB_REF_NAME" *.tar.gz \ + --repo "$GITHUB_REPOSITORY" \ --title "$GITHUB_REF_NAME" --generate-notes From 5bcc2ee85783487d285f3a802d4300f4bb39c312 Mon Sep 17 00:00:00 2001 From: Daniel Osorio Date: Tue, 29 Sep 2026 13:20:24 -0500 Subject: [PATCH 3/4] CI: run testthat against the installed package so parallel future workers can attach it --- .github/workflows/R-CMD-check.yaml | 9 ++++++--- .github/workflows/release.yaml | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index af84ca9..75516d6 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -46,7 +46,7 @@ jobs: - uses: r-lib/actions/setup-r-dependencies@v2 with: - extra-packages: any::rcmdcheck, any::pkgload + extra-packages: any::rcmdcheck needs: check - uses: r-lib/actions/check-r-package@v2 @@ -56,12 +56,15 @@ jobs: # tests/ is excluded from the source tarball via .Rbuildignore, so # R CMD check skips them; run the testthat suite from the repo instead. + # Install first: the parallel tests start future workers that attach the + # installed package, so pkgload::load_all() is not enough. - name: Run testthat suite shell: Rscript {0} env: NOT_CRAN: 'true' run: | options(testthat.progress.max_fails = Inf) - pkgload::load_all() - testthat::test_dir("tests/testthat", stop_on_failure = TRUE, + install.packages(".", repos = NULL, type = "source") + testthat::test_dir("tests/testthat", package = "SCORPION", load_package = "installed", + stop_on_failure = TRUE, reporter = testthat::ProgressReporter$new(show_praise = FALSE)) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 53f5fbb..8f04e28 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -27,7 +27,7 @@ jobs: - uses: r-lib/actions/setup-r-dependencies@v2 with: - extra-packages: any::rcmdcheck, any::pkgload + extra-packages: any::rcmdcheck needs: check # Tags may be written as 1.2.3, v1.2.3 or v.1.2.3. @@ -44,13 +44,16 @@ jobs: # tests/ is excluded from the source tarball via .Rbuildignore, so # run the testthat suite from the checkout before releasing. + # Install first: the parallel tests start future workers that attach the + # installed package, so pkgload::load_all() is not enough. - name: Run testthat suite shell: Rscript {0} env: NOT_CRAN: 'true' run: | - pkgload::load_all() - testthat::test_dir("tests/testthat", stop_on_failure = TRUE, + install.packages(".", repos = NULL, type = "source") + testthat::test_dir("tests/testthat", package = "SCORPION", load_package = "installed", + stop_on_failure = TRUE, reporter = testthat::ProgressReporter$new(show_praise = FALSE)) - name: Build source tarball From 1191af93ad98dc8a71c0eab259f2da55524d43f9 Mon Sep 17 00:00:00 2001 From: dosorio Date: Tue, 29 Sep 2026 14:30:29 -0500 Subject: [PATCH 4/4] runSCORPION: skip batch correction when batch has a single level Previously the per-gene median was added to uncorrected data when batch had fewer than two levels. Now correction is skipped with a warning. --- R/runSCORPION.R | 18 ++++++++++------ tests/testthat/test_data_preprocessing.R | 26 ++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/R/runSCORPION.R b/R/runSCORPION.R index acec84f..e87529e 100644 --- a/R/runSCORPION.R +++ b/R/runSCORPION.R @@ -262,15 +262,21 @@ runSCORPION <- function(gexMatrix, # Removing batch effect if (removeBatchEffect) { - if (showProgress) { - cli::cli_alert_success("Correcting for batch effects") - } if (is.null(batch)) { cli::cli_abort('batch is needed for batch effect correction') } - mean_expr <- apply(gexMatrix, 1, median) - gexMatrix <- remove_batch(X = gexMatrix, batch = batch) - gexMatrix <- gexMatrix + mean_expr + if (length(unique(batch[!is.na(batch)])) < 2) { + # A single batch leaves nothing to correct; skip so the per-gene median + # is not added to uncorrected data. + cli::cli_alert_warning("batch has fewer than two levels; skipping batch effect correction") + } else { + if (showProgress) { + cli::cli_alert_success("Correcting for batch effects") + } + mean_expr <- apply(gexMatrix, 1, median) + gexMatrix <- remove_batch(X = gexMatrix, batch = batch) + gexMatrix <- gexMatrix + mean_expr + } } rm(batch) diff --git a/tests/testthat/test_data_preprocessing.R b/tests/testthat/test_data_preprocessing.R index 8886c1c..96e5de7 100644 --- a/tests/testthat/test_data_preprocessing.R +++ b/tests/testthat/test_data_preprocessing.R @@ -77,3 +77,29 @@ test_that("runSCORPION() batch effect correction works with valid batch", { expect_true(is.data.frame(result)) expect_true(all(c("tf", "target") %in% colnames(result))) }) + +test_that("runSCORPION() skips batch correction when batch has a single level", { + data(scorpionTest) + + args <- list( + gexMatrix = scorpionTest$gex, + tfMotifs = scorpionTest$tf, + ppiNet = scorpionTest$ppi, + cellsMetadata = scorpionTest$metadata, + groupBy = "region", + showProgress = FALSE + ) + + set.seed(1) + uncorrected <- do.call(runSCORPION, args) + set.seed(1) + expect_message( + single_batch <- do.call(runSCORPION, c(args, list( + removeBatchEffect = TRUE, + batch = rep("b1", ncol(scorpionTest$gex)) + ))), + "fewer than two levels" + ) + + expect_equal(single_batch, uncorrected) +})