diff --git a/.github/workflows/revdeps.yml b/.github/workflows/revdeps.yml new file mode 100644 index 0000000..df860f5 --- /dev/null +++ b/.github/workflows/revdeps.yml @@ -0,0 +1,424 @@ +--- + +name: Reverse Dependencies +on: + workflow_call: + inputs: + package: + description: 'Package to check (default is rstan)' + required: false + default: 'rstan' + type: string + batch-size: + description: 'Number of packages to check in each batch (default is 15)' + required: false + default: 20 + type: number + rstan-source: + description: 'rstan source (either a git ref or "CRAN", default is CRAN)' + required: false + default: 'CRAN' + type: string + stanheaders-source: + description: 'StanHeaders source (either a git ref or "CRAN", default is CRAN)' + required: false + default: 'CRAN' + type: string + rstantools-source: + description: rstantools source (either a git ref or "CRAN", default is CRAN)' + required: false + default: 'CRAN' + type: string + loo-source: + description: loo source (either a git ref or "CRAN", default is CRAN)' + required: false + default: 'CRAN' + type: string + posterior-source: + description: posterior source (either a git ref or "CRAN", default is CRAN)' + required: false + default: 'CRAN' + type: string + bayesplot-source: + description: bayesplot source (either a git ref or "CRAN", default is CRAN)' + required: false + default: 'CRAN' + type: string + run-donttest: + description: 'Run `donttest` examples for each package (increasing check time), default is false' + required: false + default: false + type: boolean + +jobs: + prepare-library: + runs-on: ubuntu-latest + name: Build & Cache Package Dependencies + outputs: + deps_matrix: ${{ steps.dep-batches.outputs.deps_matrix }} + cache_key: ${{ steps.calc-hash.outputs.cache_key }} + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + + steps: + - name: Clone rstan + if: inputs.rstan-source != 'CRAN' + uses: actions/checkout@v6 + with: + repository: stan-dev/rstan + ref: ${{ inputs.rstan-source }} + path: rstan-source + + - name: Clone StanHeaders + if: inputs.stanheaders-source != 'CRAN' + uses: actions/checkout@v6 + with: + submodules: recursive + repository: stan-dev/rstan + ref: ${{ inputs.stanheaders-source }} + path: StanHeaders-source + + - name: Clone rstantools + if: inputs.rstantools-source != 'CRAN' + uses: actions/checkout@v6 + with: + repository: stan-dev/rstantools + ref: ${{ inputs.rstantools-source }} + path: rstantools-source + + - name: Clone loo + if: inputs.loo-source != 'CRAN' + uses: actions/checkout@v6 + with: + repository: stan-dev/loo + ref: ${{ inputs.loo-source }} + path: loo-source + + - name: Clone posterior + if: inputs.posterior-source != 'CRAN' + uses: actions/checkout@v6 + with: + repository: stan-dev/posterior + ref: ${{ inputs.posterior-source }} + path: posterior-source + + - name: Clone bayesplot + if: inputs.bayesplot-source != 'CRAN' + uses: actions/checkout@v6 + with: + repository: stan-dev/bayesplot + ref: ${{ inputs.bayesplot-source }} + path: bayesplot-source + + - name: Restore cached INLA source + id: cache-inla + uses: actions/cache@v6 + with: + path: /tmp/INLA_25.10.19.tar.gz + key: inla-25.10.19 + + - name: Download INLA source if not cached + if: steps.cache-inla.outputs.cache-hit != 'true' + run: | + wget https://inla.r-inla-download.org/R/stable/src/contrib/INLA_25.10.19.tar.gz -O /tmp/INLA_25.10.19.tar.gz + + - uses: r-lib/actions/setup-r@v2 + with: + r-version: 'release' + + - name: Install system dependencies + run: | + sudo apt-get install -y pandoc libudunits2-dev libglpk-dev libgsl0-dev \ + libproj-dev libglu1-mesa-dev libgdal-dev libgl1-mesa-dev libmecab-dev \ + libcurl4-openssl-dev + + - name: Download recursive dependencies, batch dependencies to check + id: dep-batches + run: | + batch_size <- ${{ inputs.batch-size }} + pkg <- "${{ inputs.package }}" + + # Parallelise building as much as possible + options(Ncpus = parallel::detectCores()) + Sys.setenv(MAKEFLAGS = paste0("-j", parallel::detectCores())) + + # Initialise a standalone library + dir.create("/tmp/PakLibrary") + .libPaths(c("/tmp/PakLibrary", .libPaths())) + + # Lookup and (attempt to) install all dependencies for reverse-dependencies + pkg_deps <- tools::package_dependencies(pkg, reverse = TRUE, which = "all")[[pkg]] + rec_deps <- unique(do.call(c, tools::package_dependencies(pkg_deps, which = 'all'))) + + # Undeclared dependency for vignettes + if ("shinymrp" %in% pkg_deps) { + rec_deps <- c(rec_deps, "cmdstanr") + } + + install.packages(c(rec_deps, "pak")) + + if ("cmdstanr" %in% rec_deps) { + install.packages("cmdstanr", repos = c("https://stan-dev.r-universe.dev", getOption("repos"))) + } + + # Remaining packages will be bioconductor or other non-CRAN packages, install manually + rec_deps <- rec_deps[!(rec_deps %in% installed.packages()[,"Package"])] + + # Ignore archived packages which are still listed as 'Suggests' + rec_deps <- rec_deps[!(rec_deps %in% c("BRugs", "qs", "rkeops", "mcmcplots", "pryr", "glmmADMB", "survHEhmc"))] + + if (length(rec_deps) > 0) { + # Update install locations for Github sources + rec_deps[rec_deps == "blavsam"] <- "github::ecmerkle/blavsam" + rec_deps[rec_deps == "eggCountsExtra"] <- "github::CraigWangStat/eggCountsExtra" + rec_deps[rec_deps == "INLA"] <- "local::/tmp/INLA_25.10.19.tar.gz" + rec_deps[rec_deps == "iwmm"] <- "github::topipa/iwmm" + rec_deps[rec_deps == "bridgestan"] <- "github::roualdes/bridgestan/R" + rec_deps[rec_deps == "infinitefactor"] <- "github::poworoznek/infinitefactor" + rec_deps[rec_deps == "survHEinla"] <- "github::giabaio/survHEinla" + + pak::pak(rec_deps) + } + + install_stan_pkg <- function(pkg, pkg_src) { + if (tolower(pkg_src) == "cran") { + install.packages(pkg) + } else { + if (pkg == "rstan") { + pkg <- "rstan-source/rstan/rstan" + } else if (pkg == "StanHeaders") { + pkg <- "StanHeaders-source/StanHeaders" + } else { + pkg <- paste0(pkg, "-source") + } + pak::pak(paste0("local::./", pkg)) + } + } + + # Install dev versions last so that they're not used for any dependencies + install_stan_pkg("StanHeaders", "${{ inputs.stanheaders-source }}") + install_stan_pkg("rstan", "${{ inputs.rstan-source }}") + install_stan_pkg("rstantools", "${{ inputs.rstantools-source }}") + install_stan_pkg("loo", "${{ inputs.loo-source }}") + install_stan_pkg("posterior", "${{ inputs.posterior-source }}") + install_stan_pkg("bayesplot", "${{ inputs.bayesplot-source }}") + + # Split dependencies into requested batch size and + # restructure into format for actions 'matrix' argument + curr_deps <- split(pkg_deps, ceiling(seq_along(pkg_deps) / batch_size)) + deps_matrix <- list() + for (i in seq_along(curr_deps)) { + deps_matrix[[i]] <- list(group = i, deps = jsonlite::toJSON(curr_deps[[i]])) + } + cat(paste0("deps_matrix=", jsonlite::toJSON(list(include = deps_matrix), auto_unbox = TRUE)), + file=Sys.getenv("GITHUB_OUTPUT"), append = TRUE) + shell: Rscript {0} + + - name: Generate unique hash key + id: calc-hash + run: | + # Ensure that the key is unique for each combination of inputs within each workflow run + # in case of multiple job runs within the same workflow + echo '${{ toJSON(inputs) }}' > /tmp/inputs.json + echo "cache_key=$(sha256sum /tmp/inputs.json | awk '{print "${{ github.run_id }}-" $1}')" >> "$GITHUB_OUTPUT" + + - name: Cache built packages + uses: actions/cache/save@v6 + with: + path: /tmp/PakLibrary/ + key: ${{ steps.calc-hash.outputs.cache_key }} + + reverse-dependencies: + needs: prepare-library + runs-on: ubuntu-latest + name: Check Reverse Dependencies (Batch ${{ matrix.group }}) + strategy: + fail-fast: false + max-parallel: 5 + matrix: ${{ fromJson(needs.prepare-library.outputs.deps_matrix) }} + env: + R_REMOTES_NO_ERRORS_FROM_WARNINGS: true + _R_CHECK_FORCE_SUGGESTS_: false + R_LIBS_USER: /tmp/PakLibrary + NOT_CRAN: + _R_CHECK_CRAN_INCOMING_: false + _R_CHECK_CRAN_INCOMING_REMOTE_: false + _R_CHECK_DONTTEST_EXAMPLES_: ${{ inputs.run-donttest }} + _R_CHECK_ELAPSED_TIMEOUT_: 1h + _R_CHECK_SYSTEM_CLOCK_: 0 + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + + steps: + - name: Restore built library + uses: actions/cache/restore@v6 + with: + path: /tmp/PakLibrary/ + key: ${{ needs.prepare-library.outputs.cache_key }} + + - uses: r-lib/actions/setup-r@v2 + with: + r-version: 'release' + + - uses: r-lib/actions/setup-pandoc@v2 + - uses: quarto-dev/quarto-actions/setup@v2 + + - name: Configure Makevars + run: | + mkdir -p ~/.R + cat < ~/.R/Makevars + # Eigen warnings significantly bloat log size, so ignore + CXXFLAGS += -w + CXX17FLAGS += -w + EOF + + - name: Install system dependencies + run: | + sudo apt-get install -y pandoc libudunits2-dev libglpk-dev libgsl0-dev \ + texlive-latex-base texlive-latex-recommended texlive-fonts-extra \ + texlive-fonts-recommended texlive-luatex texlive-latex-extra \ + texlive-bibtex-extra libproj-dev libglu1-mesa-dev libgdal-dev \ + libgl1-mesa-dev libmecab-dev + + - name: Print Session Info + env: + R_LIBS_USER: /tmp/PakLibrary + run: installed.packages()[,c("Package", "Version")] + shell: Rscript {0} + + - name: Run reverse-dependency checks + env: + R_LIBS_USER: /tmp/PakLibrary + NOT_CRAN: + _R_CHECK_CRAN_INCOMING_: false + _R_CHECK_CRAN_INCOMING_REMOTE_: false + _R_CHECK_DONTTEST_EXAMPLES_: ${{ inputs.run-donttest }} + _R_CHECK_ELAPSED_TIMEOUT_: 1h + _R_CHECK_SYSTEM_CLOCK_: 0 + run: | + curr_deps <- jsonlite::fromJSON('${{ matrix.deps }}') + rec_deps <- unique(do.call(c, tools::package_dependencies(curr_deps, which = 'all'))) + if ("shinymrp" %in% curr_deps) { + rec_deps <- c(rec_deps, "cmdstanr") + } + + # Install any system dependencies needed for packages + pak::sysreqs_fix_installed(c(curr_deps, rec_deps)) + + if ("cmdstanr" %in% c(curr_deps, rec_deps)) { + cmdstanr::install_cmdstan(cores = parallel::detectCores()) + } + + dir.create("testdir") + download.packages(curr_deps, "testdir", repos = "https://cran.rstudio.com") + Sys.unsetenv("NOT_CRAN") + Ncpus <- ifelse(any(c("ctsem", "blavaan", "rstanarm") %in% curr_deps), 2, 3) + tools::check_packages_in_dir("testdir", check_args = "--as-cran --no-manual", Ncpus = Ncpus) + shell: Rscript {0} + + - uses: actions/upload-artifact@v7 + id: upload-batch-results + with: + name: ${{ inputs.package }}-checkresults-${{ needs.prepare-library.outputs.cache_key }}-${{ matrix.group }} + path: | + testdir/**/00check.log + testdir/**/00install.out + testdir/**/*.Rout + compression-level: 9 + + summarise-results: + name: Summarise Results + needs: [prepare-library, reverse-dependencies] + runs-on: ubuntu-latest + + steps: + - name: Download all results + uses: actions/download-artifact@v7 + with: + path: checkresults + pattern: ${{ inputs.package }}-checkresults-${{ needs.prepare-library.outputs.cache_key }}-* + merge-multiple: true + + - uses: r-lib/actions/setup-r@v2 + with: + r-version: 'release' + + - name: Generate results summary output + run: | + install.packages("knitr") + + setwd("checkresults") + all_pkgs <- gsub("^\\./(.*)\\.Rcheck", "\\1", list.dirs(".", recursive = FALSE)) + res <- tools::check_packages_in_dir_details(".") + + install_failures <- unique(res$Package[res$Check == "whether package can be installed" & res$Status == "ERROR"]) + install_failures <- install_failures[order(install_failures)] + check_statuses <- c("tests", "re-building of vignette outputs", "examples", "examples with --run-donttest") + check_failures <- unique(res$Package[res$Check %in% check_statuses]) + check_failures <- check_failures[order(check_failures)] + + summary_tbl <- knitr::kable( + data.frame( + "OK" = length(setdiff(all_pkgs, c(install_failures, check_failures))), + "Install Failed" = length(install_failures), + "Tests/Vignettes/Examples Failed" = length(check_failures), + check.names = FALSE + ) + ) + + install_logs <- paste0( + sapply(install_failures, \(pkg) { + paste0(c( + paste("###", pkg), + "
", + "Install Log", + "", + "```", + paste0(readLines(paste0(pkg, ".Rcheck/00install.out")), collapse = "\n"), + "```", + "", + "
\n"), + collapse = "\n" + ) + }), + collapse = "\n" + ) + + check_logs <- paste0( + sapply(check_failures, \(pkg) { + paste0(c( + "", + paste("###", pkg), + "", + "
", + "Check Log", + "", + "```", + paste0(readLines(paste0(pkg, ".Rcheck/00check.log")), collapse = "\n"), + "```", + "", + "
\n"), + collapse = "\n" + ) + }), + collapse = "\n" + ) + + result_md <- paste0(c( + "# Reverse-Dependency Status (${{ inputs.package }})\n", + "## Summary\n", + summary_tbl, + "## Install Failures\n", + install_logs, + "## Vignette/Test Failures\n", + check_logs), + collapse = "\n" + ) + cat(result_md, file = Sys.getenv("GITHUB_STEP_SUMMARY"), append = TRUE) + shell: Rscript {0} + + - name: Upload combined results + uses: actions/upload-artifact@v7 + with: + name: ${{ inputs.package }}-all-logs-${{ needs.prepare-library.outputs.cache_key }} + path: checkresults + compression-level: 9