From 87a99d75d6a4c2c3b89e5f14e1c5a3d923178670 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Tue, 25 Aug 2026 11:03:57 +0200 Subject: [PATCH 01/56] try: ai attempt at updating github actions flow --- .../workflows/dsBaseClient_test_suite.yaml | 1048 +++++++++++++++-- 1 file changed, 943 insertions(+), 105 deletions(-) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index b8a6f3ccf..573d73d1b 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -1,13 +1,35 @@ ################################################################################ # DataSHIELD GHA test suite - dsBaseClient -# Adapted from `armadillo_azure-pipelines.yml` by Roberto Villegas-Diaz +# Replaces azure-pipelines.yml / opal_azure-pipelines.yml / armadillo_azure-pipelines.yml. # -# Inside the root directory $(Pipeline.Workspace) will be a file tree like: -# /dsBaseClient <- Checked out version of datashield/dsBaseClient -# /dsBaseClient/logs <- Where results of tests and logs are collated -# /testStatus <- Checked out version of datashield/testStatus +# Structure (all jobs below run in parallel except where "needs" says otherwise): +# r-checks - devtools::document()/check() sync checks. No backend needed. +# opal-dsbase (matrix x4 shards) - dsBase suite against Opal, one shard each. +# opal-dsdanger - dsDanger suite against Opal (small, not sharded). +# opal-report - needs the two Opal jobs above; merges + publishes report. +# armadillo-dsbase (matrix x4 shards) - dsBase suite against Armadillo. +# armadillo-dsdanger - dsDanger suite against Armadillo. +# armadillo-report - needs the two Armadillo jobs above; merges + publishes report. # -# As of Sept. 2025 this takes ~ 95 mins to run. +# Each dsbase/dsdanger job spins up its OWN backend instance (isolated - no +# shared server state / concurrency risk between shards). This is what makes +# splitting the ~260-file dsBase suite into shards safe: shard 4x lets that +# phase run in roughly a quarter of the wall-clock instead of one long +# sequential testthat::test_package() call. +# +# Opal runs via docker-compose (docker-compose_opal.yml). +# Armadillo runs as a plain `java -jar` process (not docker-compose): Armadillo +# self-manages its Rock container over the host Docker socket +# (docker-management-enabled: true / docker-run-in-container: false), which skips +# building/pulling the old custom armadillo_citest image. See +# molgenis-service-armadillo's application.template.yml for that flag pairing. +# +# Every job starts its backend as the very first step so it boots in the +# background while R dependencies install, instead of paying for both serially. +# +# As of Sept. 2025 the single-backend, dsBase-only, unsharded version of this +# took ~ 95 mins; the full (Opal+Armadillo, dsBase+dsDanger) unsharded run is +# well over an hour. ################################################################################ name: dsBaseClient tests' suite @@ -17,27 +39,94 @@ on: - cron: '0 0 * * 0' # Weekly - cron: '0 1 * * *' # Nightly +permissions: + contents: read + +env: + _r_check_system_clock_: 0 + WORKFLOW_ID: ${{ github.run_id }}-${{ github.run_attempt }} + PROJECT_NAME: dsBaseClient + BRANCH_NAME: ${{ github.head_ref || github.ref_name }} + REPO_OWNER: ${{ github.repository_owner }} + R_KEEP_PKG_SOURCE: yes + GITHUB_TOKEN: ${{ github.token || 'placeholder-token' }} + # These should all be constant, except the two TEST_FILTER_* values. These can + # be used to test subsets of test files in the testthat directory. Options are + # like: '*' <- run all tests, '*_smk_*' <- run all the smoke tests. + TEST_FILTER_DSBASE: '_-|datachk-|smk-|arg-|disc-|perf-|smk_expt-|expt-|math-' + TEST_FILTER_DSDANGER: '__dgr-|datachk_dgr-|smk_dgr-|arg_dgr-|disc_dgr-|smk_expt_dgr-|expt_dgr-|math_dgr-' + jobs: - dsBaseClient_test_suite: + + ################################################################################ + # Doc-sync / R CMD check - no backend needed, so this runs once, not per + # backend or per shard. + ################################################################################ + r-checks: + name: Package checks (doc sync, R CMD check) + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + + - uses: r-lib/actions/setup-pandoc@v2 + + - uses: r-lib/actions/setup-r@v2 + with: + r-version: release + http-user-agent: release + use-public-rspm: true + + - name: Install system libraries + run: | + sudo apt-get update -qq + sudo apt-get install -qq libxml2-dev libcurl4-openssl-dev libssl-dev libgsl-dev libgit2-dev -y + sudo apt-get install -qq libharfbuzz-dev libfribidi-dev libmagick++-dev libudunits2-dev libuv1-dev -y + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + dependencies: 'c("Imports")' + extra-packages: | + any::rcmdcheck + cran::devtools + needs: check + + - name: Check manual updated + run: | + orig_sum=$(find man -type f | sort -u | xargs cat | md5sum) + R -q -e "devtools::document()" + new_sum=$(find man -type f | sort -u | xargs cat | md5sum) + if [ "$orig_sum" != "$new_sum" ]; then + echo "Your committed man/*.Rd files are out of sync with the R headers." + exit 1 + fi + continue-on-error: true + + - name: Devtools checks + run: | + R -q -e "devtools::check(args = c('--no-examples', '--no-tests'))" | tee azure-pipelines_check.Rout + grep --quiet "^0 errors" azure-pipelines_check.Rout && grep --quiet " 0 warnings" azure-pipelines_check.Rout && grep --quiet " 0 notes" azure-pipelines_check.Rout + continue-on-error: true + + + ################################################################################ + # Opal - dsBase suite, sharded 4 ways. Each shard is a fully isolated job + # with its own Opal instance. + ################################################################################ + opal-dsbase: + name: Opal dsBase tests (shard ${{ matrix.shard }}) runs-on: ubuntu-latest - timeout-minutes: 120 - permissions: - contents: read - - # These should all be constant, except TEST_FILTER. This can be used to test - # subsets of test files in the testthat directory. Options are like: - # '*' <- Run all tests. - # 'asNumericDS*' <- Run all asNumericDS tests, i.e. all the arg, etc. tests. - # '*_smk_*' <- Run all the smoke tests for all functions. + timeout-minutes: 60 + strategy: + fail-fast: false + # Shard count is hardcoded here and in "Compute shard filter" below (both + # must match) - there's no clean way to share a single value between a + # matrix definition and a step's env in plain workflow YAML. + matrix: + shard: [0, 1, 2, 3] env: - TEST_FILTER: '_-|datachk-|smk-|arg-|disc-|perf-|smk_expt-|expt-|math-' - _r_check_system_clock_: 0 - WORKFLOW_ID: ${{ github.run_id }}-${{ github.run_attempt }} - PROJECT_NAME: dsBaseClient - BRANCH_NAME: ${{ github.head_ref || github.ref_name }} - REPO_OWNER: ${{ github.repository_owner }} - R_KEEP_PKG_SOURCE: yes - GITHUB_TOKEN: ${{ github.token || 'placeholder-token' }} + DS_DRIVER: OpalDriver + DSBASE_REF: v7.0-dev steps: - name: Checkout dsBaseClient @@ -45,15 +134,148 @@ jobs: with: path: dsBaseClient - - name: Checkout testStatus - if: ${{ github.actor != 'nektos/act' }} # for local deployment only + - name: Start Opal docker-compose + run: docker compose -f docker-compose_opal.yml up -d --build + working-directory: dsBaseClient + + - name: Uninstall default MySQL + run: | + curl https://bazel.build/bazel-release.pub.gpg | sudo apt-key add - + sudo service mysql stop || true + sudo apt-get update + sudo apt-get remove --purge mysql-client mysql-server mysql-common -y + sudo apt-get autoremove -y + sudo apt-get autoclean -y + sudo rm -rf /var/lib/mysql/ + + - uses: r-lib/actions/setup-pandoc@v2 + + - uses: r-lib/actions/setup-r@v2 + with: + r-version: release + http-user-agent: release + use-public-rspm: true + + - name: Install system libraries + run: | + sudo apt-get update -qq + sudo apt-get install -qq libxml2-dev libcurl4-openssl-dev libssl-dev libgsl-dev libgit2-dev -y + sudo apt-get install -qq libharfbuzz-dev libfribidi-dev libmagick++-dev libudunits2-dev libuv1-dev -y + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + dependencies: 'c("Imports")' + extra-packages: | + cran::devtools + cran::covr + cran::fields + cran::meta + cran::metafor + cran::ggplot2 + cran::gridExtra + cran::data.table + cran::DSI + cran::DSOpal + cran::DSLite + cran::MolgenisAuth + cran::MolgenisArmadillo + cran::DSMolgenisArmadillo + cran::DescTools + cran::e1071 + needs: check + + - name: Install test datasets to Opal + run: | + sleep 60 + R -q -f "obiba_opal-upload_testing_datasets.R" + working-directory: dsBaseClient/tests/testthat/data_files + + - name: Install dsBase to Opal, set disclosure test options + run: | + R -q -e "library(opalr); opal <- opal.login(username = 'administrator', password = 'datashield_test&', url = 'http://localhost:8080/'); opal.put(opal, 'system', 'conf', 'general', '_rPackage'); opal.logout(opal)" + R -q -e "library(opalr); opal <- opal.login('administrator','datashield_test&', url='http://localhost:8080/'); dsadmin.install_github_package(opal, 'dsBase', username = 'datashield', ref = '${{ env.DSBASE_REF }}'); opal.logout(opal)" + + sleep 60 + + expected_version=$(curl -sf "https://raw.githubusercontent.com/datashield/dsBase/${{ env.DSBASE_REF }}/DESCRIPTION" | sed -n 's/^Version: //p' | tr -d '\r') + if [ -z "$expected_version" ]; then + echo "Could not determine expected dsBase version from GitHub" + exit 1 + fi + echo "Expected dsBase version: $expected_version" + + R -q -e "library(opalr); opal <- opal.login('administrator', 'datashield_test&', url='http://localhost:8080/'); desc <- dsadmin.package_description(opal, 'dsBase'); opal.logout(opal); installed_version <- desc[['Version']]; cat('Installed dsBase version:', installed_version, '\n'); if (is.null(installed_version) || installed_version != '$expected_version') stop('dsBase version mismatch: expected $expected_version, found ', installed_version)" + R -q -e "library(opalr); opal <- opal.login('administrator', 'datashield_test&', url='http://localhost:8080/'); dsadmin.profile_init(opal, name = 'default', packages = c('dsBase', 'dsTidyverse', 'resourcer')); opal.logout(opal)" + R -q -e "library(opalr); opal <- opal.login('administrator', 'datashield_test&', url='http://localhost:8080/'); dsadmin.set_option(opal, 'default.datashield.privacyControlLevel', 'permissive'); opal.logout(opal)" + working-directory: dsBaseClient/tests/testthat/data_files + + - name: Compute shard filter + run: | + all_files=$(cd tests/testthat && ls test-*.R | grep -E "${{ env.TEST_FILTER_DSBASE }}" | sort) + shard_files=$(echo "$all_files" | awk -v n=4 -v s=${{ matrix.shard }} 'NR % n == s') + echo "Shard ${{ matrix.shard }}/4 running $(echo "$shard_files" | grep -c .) files:" + echo "$shard_files" + shard_filter=$(echo "$shard_files" | sed -E 's/^test-//; s/\.R$//' | sed 's/\./\\./g' | paste -sd'|' -) + echo "SHARD_FILTER=$shard_filter" >> "$GITHUB_ENV" + working-directory: dsBaseClient + + - name: Run dsBase tests with coverage & JUnit report + run: | + R -q -e "devtools::reload();" + R -q -e ' + write.csv( + covr::coverage_to_list( + covr::package_coverage( + type = c("none"), + code = c('"'"' + output_file <- file("test_console_output_dsbase.txt"); + sink(output_file); + sink(output_file, type = "message"); + junit_rep <- testthat::JunitReporter$new(file = file.path(getwd(), "test_results_dsbase.xml")); + progress_rep <- testthat::ProgressReporter$new(max_failures = 999999); + multi_rep <- testthat::MultiReporter$new(reporters = list(progress_rep, junit_rep)); + options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); + testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ env.SHARD_FILTER }}", reporter = multi_rep, stop_on_failure = FALSE)'"'"' + ) + ) + ), + "coveragelist.csv" + )' + cat test_console_output_dsbase.txt + working-directory: dsBaseClient + + - name: Upload shard results + uses: actions/upload-artifact@v4 + with: + name: opal-dsbase-shard-${{ matrix.shard }} + path: | + dsBaseClient/test_results_dsbase.xml + dsBaseClient/test_console_output_dsbase.txt + dsBaseClient/coveragelist.csv + + + ################################################################################ + # Opal - dsDanger suite. Small (~15 files), so it stays a single job rather + # than sharding further. + ################################################################################ + opal-dsdanger: + name: Opal dsDanger tests + runs-on: ubuntu-latest + timeout-minutes: 60 + env: + DS_DRIVER: OpalDriver + DSBASE_REF: v7.0-dev + DSDANGER_REF: '6.3.4' + + steps: + - name: Checkout dsBaseClient uses: actions/checkout@v4 with: - repository: ${{ env.REPO_OWNER }}/testStatus - ref: master - path: testStatus - persist-credentials: false - token: ${{ env.GITHUB_TOKEN }} + path: dsBaseClient + + - name: Start Opal docker-compose + run: docker compose -f docker-compose_opal.yml up -d --build + working-directory: dsBaseClient - name: Uninstall default MySQL run: | @@ -73,82 +295,414 @@ jobs: http-user-agent: release use-public-rspm: true - - name: Install R and dependencies + - name: Install system libraries run: | - sudo apt-get install --no-install-recommends software-properties-common dirmngr -y - wget -qO- https://cloud.r-project.org/bin/linux/ubuntu/marutter_pubkey.asc | sudo tee -a /etc/apt/trusted.gpg.d/cran_ubuntu_key.asc - sudo add-apt-repository "deb https://cloud.r-project.org/bin/linux/ubuntu $(lsb_release -cs)-cran40/" sudo apt-get update -qq - sudo apt-get upgrade -y - sudo apt-get install -qq libxml2-dev libcurl4-openssl-dev libssl-dev libgsl-dev libgit2-dev r-base -y - sudo apt-get install -qq libharfbuzz-dev libfribidi-dev libmagick++-dev xml-twig-tools -y - sudo R -q -e "install.packages(c('devtools','covr','fields','meta','metafor','ggplot2','gridExtra','data.table','DSI','DSOpal','DSLite','MolgenisAuth','MolgenisArmadillo','DSMolgenisArmadillo','DescTools','e1071'), repos='https://cloud.r-project.org')" - sudo R -q -e "devtools::install_github(repo='datashield/dsDangerClient', ref=Sys.getenv('BRANCH_NAME'))" + sudo apt-get install -qq libxml2-dev libcurl4-openssl-dev libssl-dev libgsl-dev libgit2-dev -y + sudo apt-get install -qq libharfbuzz-dev libfribidi-dev libmagick++-dev libudunits2-dev libuv1-dev -y - uses: r-lib/actions/setup-r-dependencies@v2 with: dependencies: 'c("Imports")' extra-packages: | - any::rcmdcheck cran::devtools cran::git2r - cran::RCurl - cran::readr - cran::magrittr - cran::xml2 - cran::purrr - cran::dplyr - cran::stringr - cran::tidyr + cran::covr + cran::fields + cran::meta + cran::metafor + cran::ggplot2 + cran::gridExtra + cran::data.table + cran::DSI + cran::DSOpal + cran::DSLite + cran::MolgenisAuth + cran::MolgenisArmadillo + cran::DSMolgenisArmadillo + cran::DescTools + cran::e1071 + needs: check + + - name: Install dsDangerClient + run: R -q -e "devtools::install_github(repo='datashield/dsDangerClient', ref=Sys.getenv('BRANCH_NAME'))" + + - name: Install test datasets to Opal + run: | + sleep 60 + R -q -f "obiba_opal-upload_testing_datasets.R" + working-directory: dsBaseClient/tests/testthat/data_files + + - name: Install dsBase to Opal, set disclosure test options + run: | + R -q -e "library(opalr); opal <- opal.login(username = 'administrator', password = 'datashield_test&', url = 'http://localhost:8080/'); opal.put(opal, 'system', 'conf', 'general', '_rPackage'); opal.logout(opal)" + R -q -e "library(opalr); opal <- opal.login('administrator','datashield_test&', url='http://localhost:8080/'); dsadmin.install_github_package(opal, 'dsBase', username = 'datashield', ref = '${{ env.DSBASE_REF }}'); opal.logout(opal)" + + sleep 60 + + expected_version=$(curl -sf "https://raw.githubusercontent.com/datashield/dsBase/${{ env.DSBASE_REF }}/DESCRIPTION" | sed -n 's/^Version: //p' | tr -d '\r') + if [ -z "$expected_version" ]; then + echo "Could not determine expected dsBase version from GitHub" + exit 1 + fi + echo "Expected dsBase version: $expected_version" + + R -q -e "library(opalr); opal <- opal.login('administrator', 'datashield_test&', url='http://localhost:8080/'); desc <- dsadmin.package_description(opal, 'dsBase'); opal.logout(opal); installed_version <- desc[['Version']]; cat('Installed dsBase version:', installed_version, '\n'); if (is.null(installed_version) || installed_version != '$expected_version') stop('dsBase version mismatch: expected $expected_version, found ', installed_version)" + R -q -e "library(opalr); opal <- opal.login('administrator', 'datashield_test&', url='http://localhost:8080/'); dsadmin.profile_init(opal, name = 'default', packages = c('dsBase', 'dsTidyverse', 'resourcer')); opal.logout(opal)" + R -q -e "library(opalr); opal <- opal.login('administrator', 'datashield_test&', url='http://localhost:8080/'); dsadmin.set_option(opal, 'default.datashield.privacyControlLevel', 'permissive'); opal.logout(opal)" + working-directory: dsBaseClient/tests/testthat/data_files + + - name: Install dsDanger package on Opal server + run: | + R -q -e "library(opalr); opal <- opal.login(username = 'administrator', password = 'datashield_test&', url = 'http://localhost:8080'); opal.put(opal, 'system', 'conf', 'general', '_rPackage'); opal.logout(opal)" + R -q -e "library(opalr); opal <- opal.login('administrator','datashield_test&', url='http://localhost:8080/'); dsadmin.install_github_package(opal, 'dsDanger', username = 'datashield', ref = '${{ env.DSDANGER_REF }}'); opal.logout(opal)" + working-directory: dsBaseClient + + - name: Run dsDanger tests with JUnit report + run: | + R -q -e ' + library(testthat); + output_file <- file("test_console_output_dsdanger.txt"); + sink(output_file); + sink(output_file, type = "message"); + junit_rep <- JunitReporter$new(file = "test_results_dsdanger.xml"); + progress_rep <- ProgressReporter$new(max_failures = 999999); + multi_rep <- MultiReporter$new(reporters = list(progress_rep, junit_rep)); + options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); + testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ env.TEST_FILTER_DSDANGER }}", reporter = multi_rep, stop_on_failure = FALSE)' + cat test_console_output_dsdanger.txt + working-directory: dsBaseClient + + - name: Upload dsDanger results + uses: actions/upload-artifact@v4 + with: + name: opal-dsdanger + path: | + dsBaseClient/test_results_dsdanger.xml + dsBaseClient/test_console_output_dsdanger.txt + + + ################################################################################ + # Opal - merge all shard + dsDanger results and publish the report. + ################################################################################ + opal-report: + name: Opal report + needs: [opal-dsbase, opal-dsdanger] + if: always() + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout dsBaseClient + uses: actions/checkout@v4 + with: + path: dsBaseClient + + - name: Checkout testStatus + if: ${{ github.actor != 'nektos/act' }} # for local deployment only + uses: actions/checkout@v4 + with: + repository: ${{ env.REPO_OWNER }}/testStatus + ref: master + path: testStatus + persist-credentials: false + token: ${{ env.GITHUB_TOKEN }} + + - uses: r-lib/actions/setup-pandoc@v2 + + - uses: r-lib/actions/setup-r@v2 + with: + r-version: release + use-public-rspm: true + + - name: Install xml-twig-tools + run: | + sudo apt-get update -qq + sudo apt-get install -qq xml-twig-tools -y + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + dependencies: 'NA' + extra-packages: | cran::quarto cran::knitr cran::kableExtra cran::rmarkdown cran::downlit - needs: check + cran::xml2 + cran::purrr + cran::dplyr + cran::stringr + cran::tidyr + cran::readr + cran::magrittr - - name: Check manual updated + - name: Download shard/dsdanger results + uses: actions/download-artifact@v4 + with: + pattern: 'opal-*' + path: dsBaseClient/artifacts + + - name: Merge results run: | - orig_sum=$(find man -type f | sort -u | xargs cat | md5sum) - R -q -e "devtools::document()" - new_sum=$(find man -type f | sort -u | xargs cat | md5sum) - if [ "$orig_sum" != "$new_sum" ]; then - echo "Your committed man/*.Rd files are out of sync with the R headers." - exit 1 - fi + mkdir -p logs + xml_grep --pretty_print indented --wrap "testsuites" --descr "" --cond "testsuite" artifacts/*/test_results_*.xml > logs/test_results.xml + cat artifacts/*/test_console_output_*.txt > logs/test_console_output.txt + + first=1 + for f in artifacts/opal-dsbase-shard-*/coveragelist.csv; do + if [ $first -eq 1 ]; then + cat "$f" > logs/coveragelist.csv + first=0 + else + tail -n +2 "$f" >> logs/coveragelist.csv + fi + done working-directory: dsBaseClient - continue-on-error: true - - name: Devtools checks + - name: Check for JUnit errors run: | - R -q -e "devtools::check(args = c('--no-examples', '--no-tests'))" | tee azure-pipelines_check.Rout - grep --quiet "^0 errors" azure-pipelines_check.Rout && grep --quiet " 0 warnings" azure-pipelines_check.Rout && grep --quiet " 0 notes" azure-pipelines_check.Rout + issue_count=$(sed 's/failures="0" errors="0"//' test_results.xml | grep -c errors= || true) + echo "Number of testsuites with issues: $issue_count" + sed 's/failures="0" errors="0"//' test_results.xml | grep errors= > issues.log || true + cat issues.log || true + # continue with workflow even when some tests fail + exit 0 + working-directory: dsBaseClient/logs + + - name: Write versions to file + run: | + echo "branch:${{ env.BRANCH_NAME }}" > ${{ env.WORKFLOW_ID }}.txt + echo "os:$(lsb_release -ds)" >> ${{ env.WORKFLOW_ID }}.txt + echo "R:$(R --version | head -n1)" >> ${{ env.WORKFLOW_ID }}.txt + working-directory: dsBaseClient/logs + + - name: Parse results from testthat and covr + run: Rscript --verbose --vanilla ../testStatus/source/parse_test_report.R logs/ working-directory: dsBaseClient - continue-on-error: true - - name: Start Armadillo docker-compose - run: docker compose -f docker-compose_armadillo.yml up -d --build + - name: Render report + run: | + cd testStatus + + mkdir -p new/logs/${{ env.PROJECT_NAME }}/${{ env.BRANCH_NAME }}/${{ env.WORKFLOW_ID }}/ + mkdir -p new/docs/${{ env.PROJECT_NAME }}/${{ env.BRANCH_NAME }}/${{ env.WORKFLOW_ID }}/ + mkdir -p new/docs/${{ env.PROJECT_NAME }}/${{ env.BRANCH_NAME }}/latest/ + + cp -rv ../dsBaseClient/logs/* new/logs/${{ env.PROJECT_NAME }}/${{ env.BRANCH_NAME }}/${{ env.WORKFLOW_ID }}/ + cp -rv ../dsBaseClient/logs/${{ env.WORKFLOW_ID }}.txt new/logs/${{ env.PROJECT_NAME }}/${{ env.BRANCH_NAME }}/${{ env.WORKFLOW_ID }}/ + + R -e 'input_dir <- file.path("../new/logs", Sys.getenv("PROJECT_NAME"), Sys.getenv("BRANCH_NAME"), Sys.getenv("WORKFLOW_ID")); quarto::quarto_render("source/test_report.qmd", execute_params = list(input_dir = input_dir))' + mv source/test_report.html new/docs/${{ env.PROJECT_NAME }}/${{ env.BRANCH_NAME }}/${{ env.WORKFLOW_ID }}/index.html + cp -r new/docs/${{ env.PROJECT_NAME }}/${{ env.BRANCH_NAME }}/${{ env.WORKFLOW_ID }}/* new/docs/${{ env.PROJECT_NAME }}/${{ env.BRANCH_NAME }}/latest + env: + PROJECT_NAME: ${{ env.PROJECT_NAME }} + BRANCH_NAME: ${{ env.BRANCH_NAME }} + WORKFLOW_ID: ${{ env.WORKFLOW_ID }} + + - name: Upload test logs + uses: actions/upload-artifact@v4 + with: + name: dsbaseclient-logs-opal + path: testStatus/new + + + ################################################################################ + # Armadillo - dsBase suite, sharded 4 ways. Each shard downloads and runs its + # own Armadillo jar instance. + # + # Runs as a plain `java -jar` process instead of docker-compose: Armadillo + # self-manages its own Rock container over the host Docker socket + # (docker-management-enabled: true / docker-run-in-container: false), which + # avoids building/pulling the old custom armadillo_citest image and its + # dockerised Armadillo layer. The latest GitHub release jar is downloaded at + # run time. No process is restarted after installing dsBase - install then + # whitelist directly. + ################################################################################ + armadillo-dsbase: + name: Armadillo dsBase tests (shard ${{ matrix.shard }}) + runs-on: ubuntu-latest + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + shard: [0, 1, 2, 3] + env: + DS_DRIVER: ArmadilloDriver + DSBASE_TARBALL: dsBase_7.0.0-permissive.tar.gz + + steps: + - name: Checkout dsBaseClient + uses: actions/checkout@v4 + with: + path: dsBaseClient + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + + - name: Download and start Armadillo (jar) + run: | + mkdir -p armadillo_home/config armadillo_home/storage armadillo_home/logs + + cat > armadillo_home/config/application.yml < armadillo_home/logs/stdout.log 2>&1 & + echo $! > armadillo_home/armadillo.pid working-directory: dsBaseClient - - name: Install test datasets + - name: Uninstall default MySQL run: | - sleep 60 - R -q -f "molgenis_armadillo-upload_testing_datasets.R" + curl https://bazel.build/bazel-release.pub.gpg | sudo apt-key add - + sudo service mysql stop || true + sudo apt-get update + sudo apt-get remove --purge mysql-client mysql-server mysql-common -y + sudo apt-get autoremove -y + sudo apt-get autoclean -y + sudo rm -rf /var/lib/mysql/ + + - uses: r-lib/actions/setup-pandoc@v2 + + - uses: r-lib/actions/setup-r@v2 + with: + r-version: release + http-user-agent: release + use-public-rspm: true + + - name: Install system libraries + run: | + sudo apt-get update -qq + sudo apt-get install -qq libxml2-dev libcurl4-openssl-dev libssl-dev libgsl-dev libgit2-dev -y + sudo apt-get install -qq libharfbuzz-dev libfribidi-dev libmagick++-dev libudunits2-dev libuv1-dev -y + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + dependencies: 'c("Imports")' + extra-packages: | + cran::devtools + cran::covr + cran::fields + cran::meta + cran::metafor + cran::ggplot2 + cran::gridExtra + cran::data.table + cran::DSI + cran::DSOpal + cran::DSLite + cran::MolgenisAuth + cran::MolgenisArmadillo + cran::DSMolgenisArmadillo + cran::DescTools + cran::e1071 + needs: check + + - name: Wait for Armadillo to be ready + run: | + for i in $(seq 1 60); do + if curl -fsS http://localhost:8080/actuator/health 2>/dev/null | grep -q '"status":"UP"'; then + echo "Armadillo is ready" + exit 0 + fi + sleep 5 + done + echo "Armadillo did not become ready in time" + cat armadillo_home/logs/stdout.log || true + exit 1 + working-directory: dsBaseClient + + - name: Install test datasets to Armadillo + run: R -q -f "molgenis_armadillo-upload_testing_datasets.R" working-directory: dsBaseClient/tests/testthat/data_files - name: Install dsBase to Armadillo run: | curl -u admin:admin -X GET http://localhost:8080/packages - curl -u admin:admin -H 'Content-Type: multipart/form-data' -F "file=@dsBase_7.0.0-permissive.tar.gz" -X POST http://localhost:8080/install-package - sleep 60 - docker restart dsbaseclient-armadillo-1 - sleep 30 + install_status=$(curl -u admin:admin -H 'Content-Type: multipart/form-data' -F "file=@${{ env.DSBASE_TARBALL }}" -o /dev/null -w '%{http_code}' -X POST http://localhost:8080/install-package) + if [ "$install_status" != "200" ]; then + echo "dsBase install request failed with HTTP status $install_status" + exit 1 + fi + + expected_version=$(tar -xOzf ${{ env.DSBASE_TARBALL }} dsBase/DESCRIPTION | sed -n 's/^Version: //p' | tr -d '\r') + if [ -z "$expected_version" ]; then + echo "Could not determine expected dsBase version from ${{ env.DSBASE_TARBALL }}" + exit 1 + fi + echo "Expected dsBase version: $expected_version" + + actual_version="" + for i in $(seq 1 30); do + packages_json=$(curl -sf -u admin:admin -X GET http://localhost:8080/packages || true) + actual_version=$(echo "$packages_json" | jq -r '.[] | select(.name == "dsBase") | .version' 2>/dev/null || true) + if [ "$actual_version" == "$expected_version" ]; then + break + fi + sleep 10 + done + echo "Installed dsBase version: $actual_version" + if [ "$actual_version" != "$expected_version" ]; then + echo "dsBase version mismatch: expected $expected_version, found '$actual_version'" + exit 1 + fi + curl -u admin:admin -X POST http://localhost:8080/whitelist/dsBase working-directory: dsBaseClient - - name: Run tests with coverage & JUnit report + - name: Compute shard filter + run: | + all_files=$(cd tests/testthat && ls test-*.R | grep -E "${{ env.TEST_FILTER_DSBASE }}" | sort) + shard_files=$(echo "$all_files" | awk -v n=4 -v s=${{ matrix.shard }} 'NR % n == s') + echo "Shard ${{ matrix.shard }}/4 running $(echo "$shard_files" | grep -c .) files:" + echo "$shard_files" + shard_filter=$(echo "$shard_files" | sed -E 's/^test-//; s/\.R$//' | sed 's/\./\\./g' | paste -sd'|' -) + echo "SHARD_FILTER=$shard_filter" >> "$GITHUB_ENV" + working-directory: dsBaseClient + + - name: Run dsBase tests with coverage & JUnit report run: | - mkdir -p logs R -q -e "devtools::reload();" R -q -e ' write.csv( @@ -156,22 +710,329 @@ jobs: covr::package_coverage( type = c("none"), code = c('"'"' - output_file <- file("test_console_output.txt"); + output_file <- file("test_console_output_dsbase.txt"); sink(output_file); sink(output_file, type = "message"); - junit_rep <- testthat::JunitReporter$new(file = file.path(getwd(), "test_results.xml")); + junit_rep <- testthat::JunitReporter$new(file = file.path(getwd(), "test_results_dsbase.xml")); progress_rep <- testthat::ProgressReporter$new(max_failures = 999999); multi_rep <- testthat::MultiReporter$new(reporters = list(progress_rep, junit_rep)); - options("datashield.return_errors" = FALSE, "default_driver" = "ArmadilloDriver"); - testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ env.TEST_FILTER }}", reporter = multi_rep, stop_on_failure = FALSE)'"'"' + options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); + testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ env.SHARD_FILTER }}", reporter = multi_rep, stop_on_failure = FALSE)'"'"' ) ) ), "coveragelist.csv" )' + cat test_console_output_dsbase.txt + working-directory: dsBaseClient + + - name: Upload shard results + uses: actions/upload-artifact@v4 + with: + name: armadillo-dsbase-shard-${{ matrix.shard }} + path: | + dsBaseClient/test_results_dsbase.xml + dsBaseClient/test_console_output_dsbase.txt + dsBaseClient/coveragelist.csv + + + ################################################################################ + # Armadillo - dsDanger suite. Small, so it stays a single job. + ################################################################################ + armadillo-dsdanger: + name: Armadillo dsDanger tests + runs-on: ubuntu-latest + timeout-minutes: 60 + env: + DS_DRIVER: ArmadilloDriver + DSBASE_TARBALL: dsBase_7.0.0-permissive.tar.gz + DSDANGER_TARBALL: dsDanger_6.3.4.tar.gz + + steps: + - name: Checkout dsBaseClient + uses: actions/checkout@v4 + with: + path: dsBaseClient + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + + - name: Download and start Armadillo (jar) + run: | + mkdir -p armadillo_home/config armadillo_home/storage armadillo_home/logs + + cat > armadillo_home/config/application.yml < armadillo_home/logs/stdout.log 2>&1 & + echo $! > armadillo_home/armadillo.pid + working-directory: dsBaseClient + + - name: Uninstall default MySQL + run: | + curl https://bazel.build/bazel-release.pub.gpg | sudo apt-key add - + sudo service mysql stop || true + sudo apt-get update + sudo apt-get remove --purge mysql-client mysql-server mysql-common -y + sudo apt-get autoremove -y + sudo apt-get autoclean -y + sudo rm -rf /var/lib/mysql/ + + - uses: r-lib/actions/setup-pandoc@v2 + + - uses: r-lib/actions/setup-r@v2 + with: + r-version: release + http-user-agent: release + use-public-rspm: true + + - name: Install system libraries + run: | + sudo apt-get update -qq + sudo apt-get install -qq libxml2-dev libcurl4-openssl-dev libssl-dev libgsl-dev libgit2-dev -y + sudo apt-get install -qq libharfbuzz-dev libfribidi-dev libmagick++-dev libudunits2-dev libuv1-dev -y + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + dependencies: 'c("Imports")' + extra-packages: | + cran::devtools + cran::git2r + cran::covr + cran::fields + cran::meta + cran::metafor + cran::ggplot2 + cran::gridExtra + cran::data.table + cran::DSI + cran::DSOpal + cran::DSLite + cran::MolgenisAuth + cran::MolgenisArmadillo + cran::DSMolgenisArmadillo + cran::DescTools + cran::e1071 + needs: check + + - name: Install dsDangerClient + run: R -q -e "devtools::install_github(repo='datashield/dsDangerClient', ref=Sys.getenv('BRANCH_NAME'))" - mv coveragelist.csv logs/ - mv test_* logs/ + - name: Wait for Armadillo to be ready + run: | + for i in $(seq 1 60); do + if curl -fsS http://localhost:8080/actuator/health 2>/dev/null | grep -q '"status":"UP"'; then + echo "Armadillo is ready" + exit 0 + fi + sleep 5 + done + echo "Armadillo did not become ready in time" + cat armadillo_home/logs/stdout.log || true + exit 1 + working-directory: dsBaseClient + + - name: Install test datasets to Armadillo + run: R -q -f "molgenis_armadillo-upload_testing_datasets.R" + working-directory: dsBaseClient/tests/testthat/data_files + + - name: Install dsBase to Armadillo + run: | + curl -u admin:admin -X GET http://localhost:8080/packages + install_status=$(curl -u admin:admin -H 'Content-Type: multipart/form-data' -F "file=@${{ env.DSBASE_TARBALL }}" -o /dev/null -w '%{http_code}' -X POST http://localhost:8080/install-package) + if [ "$install_status" != "200" ]; then + echo "dsBase install request failed with HTTP status $install_status" + exit 1 + fi + + expected_version=$(tar -xOzf ${{ env.DSBASE_TARBALL }} dsBase/DESCRIPTION | sed -n 's/^Version: //p' | tr -d '\r') + if [ -z "$expected_version" ]; then + echo "Could not determine expected dsBase version from ${{ env.DSBASE_TARBALL }}" + exit 1 + fi + echo "Expected dsBase version: $expected_version" + + actual_version="" + for i in $(seq 1 30); do + packages_json=$(curl -sf -u admin:admin -X GET http://localhost:8080/packages || true) + actual_version=$(echo "$packages_json" | jq -r '.[] | select(.name == "dsBase") | .version' 2>/dev/null || true) + if [ "$actual_version" == "$expected_version" ]; then + break + fi + sleep 10 + done + echo "Installed dsBase version: $actual_version" + if [ "$actual_version" != "$expected_version" ]; then + echo "dsBase version mismatch: expected $expected_version, found '$actual_version'" + exit 1 + fi + + curl -u admin:admin -X POST http://localhost:8080/whitelist/dsBase + working-directory: dsBaseClient + + - name: Install dsDanger package on Armadillo server + run: | + curl -u admin:admin http://localhost:8080/whitelist + install_status=$(curl -u admin:admin -H 'Content-Type: multipart/form-data' -F "file=@${{ env.DSDANGER_TARBALL }}" -o /dev/null -w '%{http_code}' -X POST http://localhost:8080/install-package) + if [ "$install_status" != "200" ]; then + echo "dsDanger install request failed with HTTP status $install_status" + exit 1 + fi + + for i in $(seq 1 30); do + packages_json=$(curl -sf -u admin:admin -X GET http://localhost:8080/packages || true) + if echo "$packages_json" | jq -e '.[] | select(.name == "dsDanger")' >/dev/null 2>&1; then + break + fi + sleep 10 + done + + curl -u admin:admin -X POST http://localhost:8080/whitelist/dsDanger + curl -u admin:admin http://localhost:8080/whitelist + working-directory: dsBaseClient + + - name: Run dsDanger tests with JUnit report + run: | + R -q -e ' + library(testthat); + output_file <- file("test_console_output_dsdanger.txt"); + sink(output_file); + sink(output_file, type = "message"); + junit_rep <- JunitReporter$new(file = "test_results_dsdanger.xml"); + progress_rep <- ProgressReporter$new(max_failures = 999999); + multi_rep <- MultiReporter$new(reporters = list(progress_rep, junit_rep)); + options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); + testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ env.TEST_FILTER_DSDANGER }}", reporter = multi_rep, stop_on_failure = FALSE)' + cat test_console_output_dsdanger.txt + working-directory: dsBaseClient + + - name: Upload dsDanger results + uses: actions/upload-artifact@v4 + with: + name: armadillo-dsdanger + path: | + dsBaseClient/test_results_dsdanger.xml + dsBaseClient/test_console_output_dsdanger.txt + + + ################################################################################ + # Armadillo - merge all shard + dsDanger results and publish the report. + ################################################################################ + armadillo-report: + name: Armadillo report + needs: [armadillo-dsbase, armadillo-dsdanger] + if: always() + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout dsBaseClient + uses: actions/checkout@v4 + with: + path: dsBaseClient + + - name: Checkout testStatus + if: ${{ github.actor != 'nektos/act' }} # for local deployment only + uses: actions/checkout@v4 + with: + repository: ${{ env.REPO_OWNER }}/testStatus + ref: master + path: testStatus + persist-credentials: false + token: ${{ env.GITHUB_TOKEN }} + + - uses: r-lib/actions/setup-pandoc@v2 + + - uses: r-lib/actions/setup-r@v2 + with: + r-version: release + use-public-rspm: true + + - name: Install xml-twig-tools + run: | + sudo apt-get update -qq + sudo apt-get install -qq xml-twig-tools -y + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + dependencies: 'NA' + extra-packages: | + cran::quarto + cran::knitr + cran::kableExtra + cran::rmarkdown + cran::downlit + cran::xml2 + cran::purrr + cran::dplyr + cran::stringr + cran::tidyr + cran::readr + cran::magrittr + + - name: Download shard/dsdanger results + uses: actions/download-artifact@v4 + with: + pattern: 'armadillo-*' + path: dsBaseClient/artifacts + + - name: Merge results + run: | + mkdir -p logs + xml_grep --pretty_print indented --wrap "testsuites" --descr "" --cond "testsuite" artifacts/*/test_results_*.xml > logs/test_results.xml + cat artifacts/*/test_console_output_*.txt > logs/test_console_output.txt + + first=1 + for f in artifacts/armadillo-dsbase-shard-*/coveragelist.csv; do + if [ $first -eq 1 ]; then + cat "$f" > logs/coveragelist.csv + first=0 + else + tail -n +2 "$f" >> logs/coveragelist.csv + fi + done working-directory: dsBaseClient - name: Check for JUnit errors @@ -192,8 +1053,7 @@ jobs: working-directory: dsBaseClient/logs - name: Parse results from testthat and covr - run: | - Rscript --verbose --vanilla ../testStatus/source/parse_test_report.R logs/ + run: Rscript --verbose --vanilla ../testStatus/source/parse_test_report.R logs/ working-directory: dsBaseClient - name: Render report @@ -204,14 +1064,12 @@ jobs: mkdir -p new/docs/${{ env.PROJECT_NAME }}/${{ env.BRANCH_NAME }}/${{ env.WORKFLOW_ID }}/ mkdir -p new/docs/${{ env.PROJECT_NAME }}/${{ env.BRANCH_NAME }}/latest/ - # Copy logs to new logs directory location cp -rv ../dsBaseClient/logs/* new/logs/${{ env.PROJECT_NAME }}/${{ env.BRANCH_NAME }}/${{ env.WORKFLOW_ID }}/ cp -rv ../dsBaseClient/logs/${{ env.WORKFLOW_ID }}.txt new/logs/${{ env.PROJECT_NAME }}/${{ env.BRANCH_NAME }}/${{ env.WORKFLOW_ID }}/ R -e 'input_dir <- file.path("../new/logs", Sys.getenv("PROJECT_NAME"), Sys.getenv("BRANCH_NAME"), Sys.getenv("WORKFLOW_ID")); quarto::quarto_render("source/test_report.qmd", execute_params = list(input_dir = input_dir))' mv source/test_report.html new/docs/${{ env.PROJECT_NAME }}/${{ env.BRANCH_NAME }}/${{ env.WORKFLOW_ID }}/index.html cp -r new/docs/${{ env.PROJECT_NAME }}/${{ env.BRANCH_NAME }}/${{ env.WORKFLOW_ID }}/* new/docs/${{ env.PROJECT_NAME }}/${{ env.BRANCH_NAME }}/latest - env: PROJECT_NAME: ${{ env.PROJECT_NAME }} BRANCH_NAME: ${{ env.BRANCH_NAME }} @@ -220,25 +1078,5 @@ jobs: - name: Upload test logs uses: actions/upload-artifact@v4 with: - name: dsbaseclient-logs + name: dsbaseclient-logs-armadillo path: testStatus/new - - - name: Dump environment info - run: | - echo -e "\n#############################" - echo -e "ls /: ######################" - ls -al . - echo -e "\n#############################" - echo -e "lscpu: ######################" - lscpu - echo -e "\n#############################" - echo -e "memory: #####################" - free -m - echo -e "\n#############################" - echo -e "env: ########################" - env - echo -e "\n#############################" - echo -e "R sessionInfo(): ############" - R -e 'sessionInfo()' - sudo apt install tree -y - tree . From 3901de8769869f638cfb55c70307a2a45b11045e Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Wed, 26 Aug 2026 10:31:24 +0200 Subject: [PATCH 02/56] update test batch split --- .../workflows/dsBaseClient_test_suite.yaml | 87 ++++++++++--------- 1 file changed, 48 insertions(+), 39 deletions(-) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index 573d73d1b..2e909ce32 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -4,18 +4,24 @@ # # Structure (all jobs below run in parallel except where "needs" says otherwise): # r-checks - devtools::document()/check() sync checks. No backend needed. -# opal-dsbase (matrix x4 shards) - dsBase suite against Opal, one shard each. +# opal-dsbase (matrix: smk/arg/perf/misc) - dsBase suite against Opal, +# one category per job. # opal-dsdanger - dsDanger suite against Opal (small, not sharded). # opal-report - needs the two Opal jobs above; merges + publishes report. -# armadillo-dsbase (matrix x4 shards) - dsBase suite against Armadillo. +# armadillo-dsbase (matrix: smk/arg/perf/misc) - dsBase suite against Armadillo. # armadillo-dsdanger - dsDanger suite against Armadillo. # armadillo-report - needs the two Armadillo jobs above; merges + publishes report. # +# The dsBase suite (matching TEST_FILTER_DSBASE - same 292 files the Azure +# pipelines run) is split by matrix into 4 fixed categories - smk (116 files), +# arg (96), perf (51), and misc (the smaller categories: datachk, disc, expt, +# smk_expt, math, and the "_-" catch-all - 29 combined) - each with its own +# testthat filter substring, no file-list partitioning logic. Categories +# aren't evenly sized, so job durations differ, but a category can be added, +# removed, or temporarily skipped just by editing the matrix list. +# # Each dsbase/dsdanger job spins up its OWN backend instance (isolated - no -# shared server state / concurrency risk between shards). This is what makes -# splitting the ~260-file dsBase suite into shards safe: shard 4x lets that -# phase run in roughly a quarter of the wall-clock instead of one long -# sequential testthat::test_package() call. +# shared server state / concurrency risk between categories running at once). # # Opal runs via docker-compose (docker-compose_opal.yml). # Armadillo runs as a plain `java -jar` process (not docker-compose): Armadillo @@ -114,16 +120,26 @@ jobs: # with its own Opal instance. ################################################################################ opal-dsbase: - name: Opal dsBase tests (shard ${{ matrix.shard }}) + name: Opal dsBase tests (${{ matrix.category }}) runs-on: ubuntu-latest timeout-minutes: 60 strategy: fail-fast: false - # Shard count is hardcoded here and in "Compute shard filter" below (both - # must match) - there's no clean way to share a single value between a - # matrix definition and a step's env in plain workflow YAML. + # One matrix entry per test-file category, each with its own testthat + # filter substring - not evenly sized (smk/arg are much bigger than + # perf/misc), so job durations differ. Traded for the ability to add, + # remove, or temporarily skip a whole category by editing this list + # alone, with no shell-side partitioning logic to keep in sync. matrix: - shard: [0, 1, 2, 3] + include: + - category: smk + filter: 'smk-' + - category: arg + filter: 'arg-' + - category: perf + filter: 'perf-' + - category: misc + filter: 'datachk-|disc-|expt-|smk_expt-|math-|_-' env: DS_DRIVER: OpalDriver DSBASE_REF: v7.0-dev @@ -209,16 +225,6 @@ jobs: R -q -e "library(opalr); opal <- opal.login('administrator', 'datashield_test&', url='http://localhost:8080/'); dsadmin.set_option(opal, 'default.datashield.privacyControlLevel', 'permissive'); opal.logout(opal)" working-directory: dsBaseClient/tests/testthat/data_files - - name: Compute shard filter - run: | - all_files=$(cd tests/testthat && ls test-*.R | grep -E "${{ env.TEST_FILTER_DSBASE }}" | sort) - shard_files=$(echo "$all_files" | awk -v n=4 -v s=${{ matrix.shard }} 'NR % n == s') - echo "Shard ${{ matrix.shard }}/4 running $(echo "$shard_files" | grep -c .) files:" - echo "$shard_files" - shard_filter=$(echo "$shard_files" | sed -E 's/^test-//; s/\.R$//' | sed 's/\./\\./g' | paste -sd'|' -) - echo "SHARD_FILTER=$shard_filter" >> "$GITHUB_ENV" - working-directory: dsBaseClient - - name: Run dsBase tests with coverage & JUnit report run: | R -q -e "devtools::reload();" @@ -235,7 +241,7 @@ jobs: progress_rep <- testthat::ProgressReporter$new(max_failures = 999999); multi_rep <- testthat::MultiReporter$new(reporters = list(progress_rep, junit_rep)); options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); - testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ env.SHARD_FILTER }}", reporter = multi_rep, stop_on_failure = FALSE)'"'"' + testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ matrix.filter }}", reporter = multi_rep, stop_on_failure = FALSE)'"'"' ) ) ), @@ -247,7 +253,7 @@ jobs: - name: Upload shard results uses: actions/upload-artifact@v4 with: - name: opal-dsbase-shard-${{ matrix.shard }} + name: opal-dsbase-${{ matrix.category }} path: | dsBaseClient/test_results_dsbase.xml dsBaseClient/test_console_output_dsbase.txt @@ -449,7 +455,7 @@ jobs: cat artifacts/*/test_console_output_*.txt > logs/test_console_output.txt first=1 - for f in artifacts/opal-dsbase-shard-*/coveragelist.csv; do + for f in artifacts/opal-dsbase-*/coveragelist.csv; do if [ $first -eq 1 ]; then cat "$f" > logs/coveragelist.csv first=0 @@ -519,13 +525,26 @@ jobs: # whitelist directly. ################################################################################ armadillo-dsbase: - name: Armadillo dsBase tests (shard ${{ matrix.shard }}) + name: Armadillo dsBase tests (${{ matrix.category }}) runs-on: ubuntu-latest timeout-minutes: 60 strategy: fail-fast: false + # One matrix entry per test-file category, each with its own testthat + # filter substring - not evenly sized (smk/arg are much bigger than + # perf/misc), so job durations differ. Traded for the ability to add, + # remove, or temporarily skip a whole category by editing this list + # alone, with no shell-side partitioning logic to keep in sync. matrix: - shard: [0, 1, 2, 3] + include: + - category: smk + filter: 'smk-' + - category: arg + filter: 'arg-' + - category: perf + filter: 'perf-' + - category: misc + filter: 'datachk-|disc-|expt-|smk_expt-|math-|_-' env: DS_DRIVER: ArmadilloDriver DSBASE_TARBALL: dsBase_7.0.0-permissive.tar.gz @@ -691,16 +710,6 @@ jobs: curl -u admin:admin -X POST http://localhost:8080/whitelist/dsBase working-directory: dsBaseClient - - name: Compute shard filter - run: | - all_files=$(cd tests/testthat && ls test-*.R | grep -E "${{ env.TEST_FILTER_DSBASE }}" | sort) - shard_files=$(echo "$all_files" | awk -v n=4 -v s=${{ matrix.shard }} 'NR % n == s') - echo "Shard ${{ matrix.shard }}/4 running $(echo "$shard_files" | grep -c .) files:" - echo "$shard_files" - shard_filter=$(echo "$shard_files" | sed -E 's/^test-//; s/\.R$//' | sed 's/\./\\./g' | paste -sd'|' -) - echo "SHARD_FILTER=$shard_filter" >> "$GITHUB_ENV" - working-directory: dsBaseClient - - name: Run dsBase tests with coverage & JUnit report run: | R -q -e "devtools::reload();" @@ -717,7 +726,7 @@ jobs: progress_rep <- testthat::ProgressReporter$new(max_failures = 999999); multi_rep <- testthat::MultiReporter$new(reporters = list(progress_rep, junit_rep)); options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); - testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ env.SHARD_FILTER }}", reporter = multi_rep, stop_on_failure = FALSE)'"'"' + testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ matrix.filter }}", reporter = multi_rep, stop_on_failure = FALSE)'"'"' ) ) ), @@ -729,7 +738,7 @@ jobs: - name: Upload shard results uses: actions/upload-artifact@v4 with: - name: armadillo-dsbase-shard-${{ matrix.shard }} + name: armadillo-dsbase-${{ matrix.category }} path: | dsBaseClient/test_results_dsbase.xml dsBaseClient/test_console_output_dsbase.txt @@ -1025,7 +1034,7 @@ jobs: cat artifacts/*/test_console_output_*.txt > logs/test_console_output.txt first=1 - for f in artifacts/armadillo-dsbase-shard-*/coveragelist.csv; do + for f in artifacts/armadillo-dsbase-*/coveragelist.csv; do if [ $first -eq 1 ]; then cat "$f" > logs/coveragelist.csv first=0 From c102a50d21e0ae9cb2e467e760b186e70548b5f1 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Wed, 26 Aug 2026 10:36:04 +0200 Subject: [PATCH 03/56] enable workflow dispatch --- .github/workflows/dsBaseClient_test_suite.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index 2e909ce32..38be359e4 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -41,6 +41,7 @@ name: dsBaseClient tests' suite on: push: + workflow_dispatch: schedule: - cron: '0 0 * * 0' # Weekly - cron: '0 1 * * *' # Nightly From de2d40464e50795bfd772970b9ed4e4903f887b7 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Wed, 26 Aug 2026 10:49:15 +0200 Subject: [PATCH 04/56] try: trigger github actions ci --- .github/workflows/dsBaseClient_test_suite.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index 38be359e4..169f50be3 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -1,4 +1,5 @@ ################################################################################ +# trigger CI # DataSHIELD GHA test suite - dsBaseClient # Replaces azure-pipelines.yml / opal_azure-pipelines.yml / armadillo_azure-pipelines.yml. # From cd6400579c4ecfdac2b4b97bda0a6c834a32de6e Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Wed, 26 Aug 2026 10:59:23 +0200 Subject: [PATCH 05/56] fix workflow error --- .github/workflows/dsBaseClient_test_suite.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index 169f50be3..015e2577c 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -158,7 +158,6 @@ jobs: - name: Uninstall default MySQL run: | - curl https://bazel.build/bazel-release.pub.gpg | sudo apt-key add - sudo service mysql stop || true sudo apt-get update sudo apt-get remove --purge mysql-client mysql-server mysql-common -y @@ -287,7 +286,6 @@ jobs: - name: Uninstall default MySQL run: | - curl https://bazel.build/bazel-release.pub.gpg | sudo apt-key add - sudo service mysql stop || true sudo apt-get update sudo apt-get remove --purge mysql-client mysql-server mysql-common -y @@ -616,7 +614,6 @@ jobs: - name: Uninstall default MySQL run: | - curl https://bazel.build/bazel-release.pub.gpg | sudo apt-key add - sudo service mysql stop || true sudo apt-get update sudo apt-get remove --purge mysql-client mysql-server mysql-common -y @@ -824,7 +821,6 @@ jobs: - name: Uninstall default MySQL run: | - curl https://bazel.build/bazel-release.pub.gpg | sudo apt-key add - sudo service mysql stop || true sudo apt-get update sudo apt-get remove --purge mysql-client mysql-server mysql-common -y From 06ded578d6a52720e7703f35006969586ac2f203 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Wed, 26 Aug 2026 11:18:53 +0200 Subject: [PATCH 06/56] added opalr dependency --- .github/workflows/dsBaseClient_test_suite.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index 015e2577c..83afc697e 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -192,6 +192,7 @@ jobs: cran::gridExtra cran::data.table cran::DSI + cran::opalr cran::DSOpal cran::DSLite cran::MolgenisAuth @@ -321,6 +322,7 @@ jobs: cran::gridExtra cran::data.table cran::DSI + cran::opalr cran::DSOpal cran::DSLite cran::MolgenisAuth @@ -584,6 +586,7 @@ jobs: spring: security: user: + name: admin password: admin servlet: multipart: @@ -648,6 +651,7 @@ jobs: cran::gridExtra cran::data.table cran::DSI + cran::opalr cran::DSOpal cran::DSLite cran::MolgenisAuth @@ -791,6 +795,7 @@ jobs: spring: security: user: + name: admin password: admin servlet: multipart: @@ -856,6 +861,7 @@ jobs: cran::gridExtra cran::data.table cran::DSI + cran::opalr cran::DSOpal cran::DSLite cran::MolgenisAuth From e6c9b561a9c416ee720bb615c73dedd7e774e8df Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Thu, 27 Aug 2026 10:10:24 +0200 Subject: [PATCH 07/56] Add missing dependency --- .github/workflows/dsBaseClient_test_suite.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index 83afc697e..45e603323 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -97,6 +97,7 @@ jobs: extra-packages: | any::rcmdcheck cran::devtools + cran::usethis needs: check - name: Check manual updated @@ -184,6 +185,7 @@ jobs: dependencies: 'c("Imports")' extra-packages: | cran::devtools + cran::usethis cran::covr cran::fields cran::meta @@ -313,6 +315,7 @@ jobs: dependencies: 'c("Imports")' extra-packages: | cran::devtools + cran::usethis cran::git2r cran::covr cran::fields @@ -643,6 +646,7 @@ jobs: dependencies: 'c("Imports")' extra-packages: | cran::devtools + cran::usethis cran::covr cran::fields cran::meta @@ -852,6 +856,7 @@ jobs: dependencies: 'c("Imports")' extra-packages: | cran::devtools + cran::usethis cran::git2r cran::covr cran::fields From 150c6f5dd210e83019182ab47c9a29205db0204e Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Thu, 27 Aug 2026 11:22:22 +0200 Subject: [PATCH 08/56] docker pull fix --- .github/workflows/dsBaseClient_test_suite.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index 45e603323..cd9eee7f2 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -616,6 +616,7 @@ jobs: --spring.config.additional-location=file:$(pwd)/armadillo_home/config/application.yml \ > armadillo_home/logs/stdout.log 2>&1 & echo $! > armadillo_home/armadillo.pid + docker pull datashield/rock_citest-permissive:latest working-directory: dsBaseClient - name: Uninstall default MySQL @@ -826,6 +827,7 @@ jobs: --spring.config.additional-location=file:$(pwd)/armadillo_home/config/application.yml \ > armadillo_home/logs/stdout.log 2>&1 & echo $! > armadillo_home/armadillo.pid + docker pull datashield/rock_citest-permissive:latest working-directory: dsBaseClient - name: Uninstall default MySQL From bd81862a0664de4d10bc32e99754795db19fc293 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Thu, 27 Aug 2026 11:33:54 +0200 Subject: [PATCH 09/56] debugging install failure --- .github/workflows/dsBaseClient_test_suite.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index cd9eee7f2..7eef559c0 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -577,7 +577,6 @@ jobs: - name: default image: datashield/rock_citest-permissive:latest port: 8085 - host: default package-whitelist: - dsBase - dsTidyverse @@ -788,7 +787,6 @@ jobs: - name: default image: datashield/rock_citest-permissive:latest port: 8085 - host: default package-whitelist: - dsBase - dsTidyverse From c983a930e923bed04c548f5a1a1c3e31721ce2d0 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:06:26 +0200 Subject: [PATCH 10/56] add logging to debug --- .github/workflows/dsBaseClient_test_suite.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index 7eef559c0..391f52063 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -717,6 +717,11 @@ jobs: curl -u admin:admin -X POST http://localhost:8080/whitelist/dsBase working-directory: dsBaseClient + - name: Dump Armadillo server log + if: failure() + run: tail -c 20000 armadillo_home/logs/stdout.log + working-directory: dsBaseClient + - name: Run dsBase tests with coverage & JUnit report run: | R -q -e "devtools::reload();" @@ -931,6 +936,11 @@ jobs: curl -u admin:admin -X POST http://localhost:8080/whitelist/dsBase working-directory: dsBaseClient + - name: Dump Armadillo server log + if: failure() + run: tail -c 20000 armadillo_home/logs/stdout.log + working-directory: dsBaseClient + - name: Install dsDanger package on Armadillo server run: | curl -u admin:admin http://localhost:8080/whitelist From 4dea902b4f1a9cd898a4cfbcb05d61614f41c771 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:22:32 +0200 Subject: [PATCH 11/56] add more logging to debug --- .github/workflows/dsBaseClient_test_suite.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index 391f52063..3f8345ab8 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -719,7 +719,7 @@ jobs: - name: Dump Armadillo server log if: failure() - run: tail -c 20000 armadillo_home/logs/stdout.log + run: grep "Caused by:" armadillo_home/logs/stdout.log | sort -u working-directory: dsBaseClient - name: Run dsBase tests with coverage & JUnit report @@ -938,7 +938,7 @@ jobs: - name: Dump Armadillo server log if: failure() - run: tail -c 20000 armadillo_home/logs/stdout.log + run: grep "Caused by:" armadillo_home/logs/stdout.log | sort -u working-directory: dsBaseClient - name: Install dsDanger package on Armadillo server From 07c8749f9f4f9c78306915fc3d424fd4feb8e762 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Sat, 29 Aug 2026 08:37:32 +0200 Subject: [PATCH 12/56] try: add auth --- .github/workflows/dsBaseClient_test_suite.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index 3f8345ab8..6a0e1ce2b 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -604,7 +604,7 @@ jobs: org.molgenis: "warn" EOF - release_json=$(curl -sf https://api.github.com/repos/molgenis/molgenis-service-armadillo/releases/latest) + release_json=$(curl -sf -H "Authorization: Bearer ${{ env.GITHUB_TOKEN }}" https://api.github.com/repos/molgenis/molgenis-service-armadillo/releases/latest) || { curl -s -H "Authorization: Bearer ${{ env.GITHUB_TOKEN }}" https://api.github.com/repos/molgenis/molgenis-service-armadillo/releases/latest; exit 1; } jar_url=$(echo "$release_json" | jq -r '.assets[] | select(.name | endswith(".jar")) | .browser_download_url') echo "Installing Armadillo $(echo "$release_json" | jq -r '.tag_name')" curl -fSL -o armadillo_home/armadillo.jar "$jar_url" @@ -819,7 +819,7 @@ jobs: org.molgenis: "warn" EOF - release_json=$(curl -sf https://api.github.com/repos/molgenis/molgenis-service-armadillo/releases/latest) + release_json=$(curl -sf -H "Authorization: Bearer ${{ env.GITHUB_TOKEN }}" https://api.github.com/repos/molgenis/molgenis-service-armadillo/releases/latest) || { curl -s -H "Authorization: Bearer ${{ env.GITHUB_TOKEN }}" https://api.github.com/repos/molgenis/molgenis-service-armadillo/releases/latest; exit 1; } jar_url=$(echo "$release_json" | jq -r '.assets[] | select(.name | endswith(".jar")) | .browser_download_url') echo "Installing Armadillo $(echo "$release_json" | jq -r '.tag_name')" curl -fSL -o armadillo_home/armadillo.jar "$jar_url" From bf756382e8ebfdce303749ddd59257f19ad8a04a Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Sat, 29 Aug 2026 09:09:53 +0200 Subject: [PATCH 13/56] poll profile status --- .../workflows/dsBaseClient_test_suite.yaml | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index 6a0e1ce2b..ff16007a0 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -685,7 +685,17 @@ jobs: - name: Install dsBase to Armadillo run: | - curl -u admin:admin -X GET http://localhost:8080/packages + profile_status="" + for i in $(seq 1 30); do + profile_status=$(curl -sf -u admin:admin http://localhost:8080/ds-profiles/status | jq -r '.[] | select(.name=="default") | .status') + [ "$profile_status" == "RUNNING" ] && break + sleep 10 + done + if [ "$profile_status" != "RUNNING" ]; then + echo "default profile container did not become RUNNING in time (status: $profile_status)" + exit 1 + fi + install_status=$(curl -u admin:admin -H 'Content-Type: multipart/form-data' -F "file=@${{ env.DSBASE_TARBALL }}" -o /dev/null -w '%{http_code}' -X POST http://localhost:8080/install-package) if [ "$install_status" != "200" ]; then echo "dsBase install request failed with HTTP status $install_status" @@ -904,7 +914,17 @@ jobs: - name: Install dsBase to Armadillo run: | - curl -u admin:admin -X GET http://localhost:8080/packages + profile_status="" + for i in $(seq 1 30); do + profile_status=$(curl -sf -u admin:admin http://localhost:8080/ds-profiles/status | jq -r '.[] | select(.name=="default") | .status') + [ "$profile_status" == "RUNNING" ] && break + sleep 10 + done + if [ "$profile_status" != "RUNNING" ]; then + echo "default profile container did not become RUNNING in time (status: $profile_status)" + exit 1 + fi + install_status=$(curl -u admin:admin -H 'Content-Type: multipart/form-data' -F "file=@${{ env.DSBASE_TARBALL }}" -o /dev/null -w '%{http_code}' -X POST http://localhost:8080/install-package) if [ "$install_status" != "200" ]; then echo "dsBase install request failed with HTTP status $install_status" From 3387f0e97aaccb87ac02cb51384a2b1874e01e6d Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Sat, 29 Aug 2026 09:20:34 +0200 Subject: [PATCH 14/56] start profiles --- .github/workflows/dsBaseClient_test_suite.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index ff16007a0..07a206e4d 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -685,6 +685,8 @@ jobs: - name: Install dsBase to Armadillo run: | + curl -sf -u admin:admin -X POST http://localhost:8080/ds-profiles/default/start + profile_status="" for i in $(seq 1 30); do profile_status=$(curl -sf -u admin:admin http://localhost:8080/ds-profiles/status | jq -r '.[] | select(.name=="default") | .status') @@ -914,6 +916,8 @@ jobs: - name: Install dsBase to Armadillo run: | + curl -sf -u admin:admin -X POST http://localhost:8080/ds-profiles/default/start + profile_status="" for i in $(seq 1 30); do profile_status=$(curl -sf -u admin:admin http://localhost:8080/ds-profiles/status | jq -r '.[] | select(.name=="default") | .status') From d32ebbab76938c85822841028a678ed0ae60af08 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Sat, 29 Aug 2026 09:29:57 +0200 Subject: [PATCH 15/56] add logging to debug failure --- .github/workflows/dsBaseClient_test_suite.yaml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index 07a206e4d..d18a2ae9e 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -250,8 +250,9 @@ jobs: ) ), "coveragelist.csv" - )' + )' || R_EXIT=$? cat test_console_output_dsbase.txt + exit "${R_EXIT:-0}" working-directory: dsBaseClient - name: Upload shard results @@ -380,8 +381,9 @@ jobs: progress_rep <- ProgressReporter$new(max_failures = 999999); multi_rep <- MultiReporter$new(reporters = list(progress_rep, junit_rep)); options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); - testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ env.TEST_FILTER_DSDANGER }}", reporter = multi_rep, stop_on_failure = FALSE)' + testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ env.TEST_FILTER_DSDANGER }}", reporter = multi_rep, stop_on_failure = FALSE)' || R_EXIT=$? cat test_console_output_dsdanger.txt + exit "${R_EXIT:-0}" working-directory: dsBaseClient - name: Upload dsDanger results @@ -755,8 +757,9 @@ jobs: ) ), "coveragelist.csv" - )' + )' || R_EXIT=$? cat test_console_output_dsbase.txt + exit "${R_EXIT:-0}" working-directory: dsBaseClient - name: Upload shard results @@ -997,8 +1000,9 @@ jobs: progress_rep <- ProgressReporter$new(max_failures = 999999); multi_rep <- MultiReporter$new(reporters = list(progress_rep, junit_rep)); options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); - testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ env.TEST_FILTER_DSDANGER }}", reporter = multi_rep, stop_on_failure = FALSE)' + testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ env.TEST_FILTER_DSDANGER }}", reporter = multi_rep, stop_on_failure = FALSE)' || R_EXIT=$? cat test_console_output_dsdanger.txt + exit "${R_EXIT:-0}" working-directory: dsBaseClient - name: Upload dsDanger results From ff67398af183dbb0021dfb0092a0dacc6fd03573 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Sat, 29 Aug 2026 09:41:18 +0200 Subject: [PATCH 16/56] install dependencies --- .github/workflows/dsBaseClient_test_suite.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index d18a2ae9e..bbf9f81c7 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -93,7 +93,7 @@ jobs: - uses: r-lib/actions/setup-r-dependencies@v2 with: - dependencies: 'c("Imports")' + dependencies: 'NA' extra-packages: | any::rcmdcheck cran::devtools @@ -182,7 +182,7 @@ jobs: - uses: r-lib/actions/setup-r-dependencies@v2 with: - dependencies: 'c("Imports")' + dependencies: 'NA' extra-packages: | cran::devtools cran::usethis @@ -313,7 +313,7 @@ jobs: - uses: r-lib/actions/setup-r-dependencies@v2 with: - dependencies: 'c("Imports")' + dependencies: 'NA' extra-packages: | cran::devtools cran::usethis @@ -645,7 +645,7 @@ jobs: - uses: r-lib/actions/setup-r-dependencies@v2 with: - dependencies: 'c("Imports")' + dependencies: 'NA' extra-packages: | cran::devtools cran::usethis @@ -873,7 +873,7 @@ jobs: - uses: r-lib/actions/setup-r-dependencies@v2 with: - dependencies: 'c("Imports")' + dependencies: 'NA' extra-packages: | cran::devtools cran::usethis From 380fd97b35cf0d9df42e828b1cddf930f0f07268 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Sat, 29 Aug 2026 09:51:00 +0200 Subject: [PATCH 17/56] fix dep install again --- .github/workflows/dsBaseClient_test_suite.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index bbf9f81c7..e0c8bbd14 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -93,7 +93,7 @@ jobs: - uses: r-lib/actions/setup-r-dependencies@v2 with: - dependencies: 'NA' + dependencies: 'c("Depends", "Imports", "LinkingTo")' extra-packages: | any::rcmdcheck cran::devtools @@ -182,7 +182,7 @@ jobs: - uses: r-lib/actions/setup-r-dependencies@v2 with: - dependencies: 'NA' + dependencies: 'c("Depends", "Imports", "LinkingTo")' extra-packages: | cran::devtools cran::usethis @@ -313,7 +313,7 @@ jobs: - uses: r-lib/actions/setup-r-dependencies@v2 with: - dependencies: 'NA' + dependencies: 'c("Depends", "Imports", "LinkingTo")' extra-packages: | cran::devtools cran::usethis @@ -434,7 +434,7 @@ jobs: - uses: r-lib/actions/setup-r-dependencies@v2 with: - dependencies: 'NA' + dependencies: 'c("Depends", "Imports", "LinkingTo")' extra-packages: | cran::quarto cran::knitr @@ -645,7 +645,7 @@ jobs: - uses: r-lib/actions/setup-r-dependencies@v2 with: - dependencies: 'NA' + dependencies: 'c("Depends", "Imports", "LinkingTo")' extra-packages: | cran::devtools cran::usethis @@ -873,7 +873,7 @@ jobs: - uses: r-lib/actions/setup-r-dependencies@v2 with: - dependencies: 'NA' + dependencies: 'c("Depends", "Imports", "LinkingTo")' extra-packages: | cran::devtools cran::usethis @@ -1053,7 +1053,7 @@ jobs: - uses: r-lib/actions/setup-r-dependencies@v2 with: - dependencies: 'NA' + dependencies: 'c("Depends", "Imports", "LinkingTo")' extra-packages: | cran::quarto cran::knitr From 42b24d986726ef95a336a1af5ea40b43a9124ee3 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Sat, 29 Aug 2026 10:17:06 +0200 Subject: [PATCH 18/56] add danger to suggests so installed as dep --- DESCRIPTION | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 6e41033db..45f28fd70 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -87,7 +87,10 @@ Suggests: DescTools, DSOpal, DSMolgenisArmadillo, - DSLite + DSLite, + dsDangerClient +Remotes: + datashield/dsDangerClient RoxygenNote: 8.0.0 Encoding: UTF-8 Language: en-GB From cf7bdab4e16feec6177a99f7332a05e5b419d0ed Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Sat, 29 Aug 2026 11:19:16 +0200 Subject: [PATCH 19/56] revert description --- DESCRIPTION | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 45f28fd70..6e41033db 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -87,10 +87,7 @@ Suggests: DescTools, DSOpal, DSMolgenisArmadillo, - DSLite, - dsDangerClient -Remotes: - datashield/dsDangerClient + DSLite RoxygenNote: 8.0.0 Encoding: UTF-8 Language: en-GB From 2b144b02ba94cf68aafaecbdeec3496418901810 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Sat, 29 Aug 2026 11:19:27 +0200 Subject: [PATCH 20/56] intsall danger separately --- .github/workflows/dsBaseClient_test_suite.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index e0c8bbd14..1f9c40b64 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -98,6 +98,7 @@ jobs: any::rcmdcheck cran::devtools cran::usethis + github::datashield/dsDangerClient needs: check - name: Check manual updated @@ -202,6 +203,7 @@ jobs: cran::DSMolgenisArmadillo cran::DescTools cran::e1071 + github::datashield/dsDangerClient needs: check - name: Install test datasets to Opal @@ -334,6 +336,7 @@ jobs: cran::DSMolgenisArmadillo cran::DescTools cran::e1071 + github::datashield/dsDangerClient needs: check - name: Install dsDangerClient @@ -448,6 +451,7 @@ jobs: cran::tidyr cran::readr cran::magrittr + github::datashield/dsDangerClient - name: Download shard/dsdanger results uses: actions/download-artifact@v4 @@ -665,6 +669,7 @@ jobs: cran::DSMolgenisArmadillo cran::DescTools cran::e1071 + github::datashield/dsDangerClient needs: check - name: Wait for Armadillo to be ready @@ -894,6 +899,7 @@ jobs: cran::DSMolgenisArmadillo cran::DescTools cran::e1071 + github::datashield/dsDangerClient needs: check - name: Install dsDangerClient @@ -1067,6 +1073,7 @@ jobs: cran::tidyr cran::readr cran::magrittr + github::datashield/dsDangerClient - name: Download shard/dsdanger results uses: actions/download-artifact@v4 From 4d55a92b8085cc0f3a5839de0fa2f5ff7a79a499 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Sat, 29 Aug 2026 11:34:29 +0200 Subject: [PATCH 21/56] separate off code coverage, stream test output --- .../workflows/dsBaseClient_test_suite.yaml | 70 +++++++++++-------- 1 file changed, 42 insertions(+), 28 deletions(-) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index 1f9c40b64..6db175a0c 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -231,30 +231,37 @@ jobs: R -q -e "library(opalr); opal <- opal.login('administrator', 'datashield_test&', url='http://localhost:8080/'); dsadmin.set_option(opal, 'default.datashield.privacyControlLevel', 'permissive'); opal.logout(opal)" working-directory: dsBaseClient/tests/testthat/data_files - - name: Run dsBase tests with coverage & JUnit report + - name: Run dsBase tests with JUnit report run: | R -q -e "devtools::reload();" + R -q -e ' + output_file <- file("test_console_output_dsbase.txt"); + sink(output_file, split = TRUE); + sink(output_file, type = "message", split = TRUE); + junit_rep <- testthat::JunitReporter$new(file = file.path(getwd(), "test_results_dsbase.xml")); + progress_rep <- testthat::ProgressReporter$new(max_failures = 999999); + multi_rep <- testthat::MultiReporter$new(reporters = list(progress_rep, junit_rep)); + options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); + testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ matrix.filter }}", reporter = multi_rep, stop_on_failure = FALSE)' || R_EXIT=$? + cat test_console_output_dsbase.txt + exit "${R_EXIT:-0}" + working-directory: dsBaseClient + + - name: Generate dsBase coverage report + run: | R -q -e ' write.csv( covr::coverage_to_list( covr::package_coverage( - type = c("none"), + type = "none", code = c('"'"' - output_file <- file("test_console_output_dsbase.txt"); - sink(output_file); - sink(output_file, type = "message"); - junit_rep <- testthat::JunitReporter$new(file = file.path(getwd(), "test_results_dsbase.xml")); - progress_rep <- testthat::ProgressReporter$new(max_failures = 999999); - multi_rep <- testthat::MultiReporter$new(reporters = list(progress_rep, junit_rep)); options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); - testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ matrix.filter }}", reporter = multi_rep, stop_on_failure = FALSE)'"'"' + testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ matrix.filter }}", reporter = "silent", stop_on_failure = FALSE)'"'"' ) ) ), "coveragelist.csv" - )' || R_EXIT=$? - cat test_console_output_dsbase.txt - exit "${R_EXIT:-0}" + )' working-directory: dsBaseClient - name: Upload shard results @@ -378,8 +385,8 @@ jobs: R -q -e ' library(testthat); output_file <- file("test_console_output_dsdanger.txt"); - sink(output_file); - sink(output_file, type = "message"); + sink(output_file, split = TRUE); + sink(output_file, type = "message", split = TRUE); junit_rep <- JunitReporter$new(file = "test_results_dsdanger.xml"); progress_rep <- ProgressReporter$new(max_failures = 999999); multi_rep <- MultiReporter$new(reporters = list(progress_rep, junit_rep)); @@ -741,30 +748,37 @@ jobs: run: grep "Caused by:" armadillo_home/logs/stdout.log | sort -u working-directory: dsBaseClient - - name: Run dsBase tests with coverage & JUnit report + - name: Run dsBase tests with JUnit report run: | R -q -e "devtools::reload();" + R -q -e ' + output_file <- file("test_console_output_dsbase.txt"); + sink(output_file, split = TRUE); + sink(output_file, type = "message", split = TRUE); + junit_rep <- testthat::JunitReporter$new(file = file.path(getwd(), "test_results_dsbase.xml")); + progress_rep <- testthat::ProgressReporter$new(max_failures = 999999); + multi_rep <- testthat::MultiReporter$new(reporters = list(progress_rep, junit_rep)); + options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); + testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ matrix.filter }}", reporter = multi_rep, stop_on_failure = FALSE)' || R_EXIT=$? + cat test_console_output_dsbase.txt + exit "${R_EXIT:-0}" + working-directory: dsBaseClient + + - name: Generate dsBase coverage report + run: | R -q -e ' write.csv( covr::coverage_to_list( covr::package_coverage( - type = c("none"), + type = "none", code = c('"'"' - output_file <- file("test_console_output_dsbase.txt"); - sink(output_file); - sink(output_file, type = "message"); - junit_rep <- testthat::JunitReporter$new(file = file.path(getwd(), "test_results_dsbase.xml")); - progress_rep <- testthat::ProgressReporter$new(max_failures = 999999); - multi_rep <- testthat::MultiReporter$new(reporters = list(progress_rep, junit_rep)); options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); - testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ matrix.filter }}", reporter = multi_rep, stop_on_failure = FALSE)'"'"' + testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ matrix.filter }}", reporter = "silent", stop_on_failure = FALSE)'"'"' ) ) ), "coveragelist.csv" - )' || R_EXIT=$? - cat test_console_output_dsbase.txt - exit "${R_EXIT:-0}" + )' working-directory: dsBaseClient - name: Upload shard results @@ -1000,8 +1014,8 @@ jobs: R -q -e ' library(testthat); output_file <- file("test_console_output_dsdanger.txt"); - sink(output_file); - sink(output_file, type = "message"); + sink(output_file, split = TRUE); + sink(output_file, type = "message", split = TRUE); junit_rep <- JunitReporter$new(file = "test_results_dsdanger.xml"); progress_rep <- ProgressReporter$new(max_failures = 999999); multi_rep <- MultiReporter$new(reporters = list(progress_rep, junit_rep)); From 14982bd025cc3899235362b551aa8813c985b826 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Sat, 29 Aug 2026 11:42:50 +0200 Subject: [PATCH 22/56] fix pipeline --- .github/workflows/dsBaseClient_test_suite.yaml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index 6db175a0c..c29759559 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -237,7 +237,6 @@ jobs: R -q -e ' output_file <- file("test_console_output_dsbase.txt"); sink(output_file, split = TRUE); - sink(output_file, type = "message", split = TRUE); junit_rep <- testthat::JunitReporter$new(file = file.path(getwd(), "test_results_dsbase.xml")); progress_rep <- testthat::ProgressReporter$new(max_failures = 999999); multi_rep <- testthat::MultiReporter$new(reporters = list(progress_rep, junit_rep)); @@ -347,7 +346,7 @@ jobs: needs: check - name: Install dsDangerClient - run: R -q -e "devtools::install_github(repo='datashield/dsDangerClient', ref=Sys.getenv('BRANCH_NAME'))" + run: R -q -e "pak::pkg_install(sprintf('github::datashield/dsDangerClient@%s', Sys.getenv('BRANCH_NAME')))" - name: Install test datasets to Opal run: | @@ -386,7 +385,6 @@ jobs: library(testthat); output_file <- file("test_console_output_dsdanger.txt"); sink(output_file, split = TRUE); - sink(output_file, type = "message", split = TRUE); junit_rep <- JunitReporter$new(file = "test_results_dsdanger.xml"); progress_rep <- ProgressReporter$new(max_failures = 999999); multi_rep <- MultiReporter$new(reporters = list(progress_rep, junit_rep)); @@ -754,7 +752,6 @@ jobs: R -q -e ' output_file <- file("test_console_output_dsbase.txt"); sink(output_file, split = TRUE); - sink(output_file, type = "message", split = TRUE); junit_rep <- testthat::JunitReporter$new(file = file.path(getwd(), "test_results_dsbase.xml")); progress_rep <- testthat::ProgressReporter$new(max_failures = 999999); multi_rep <- testthat::MultiReporter$new(reporters = list(progress_rep, junit_rep)); @@ -917,7 +914,7 @@ jobs: needs: check - name: Install dsDangerClient - run: R -q -e "devtools::install_github(repo='datashield/dsDangerClient', ref=Sys.getenv('BRANCH_NAME'))" + run: R -q -e "pak::pkg_install(sprintf('github::datashield/dsDangerClient@%s', Sys.getenv('BRANCH_NAME')))" - name: Wait for Armadillo to be ready run: | @@ -1015,7 +1012,6 @@ jobs: library(testthat); output_file <- file("test_console_output_dsdanger.txt"); sink(output_file, split = TRUE); - sink(output_file, type = "message", split = TRUE); junit_rep <- JunitReporter$new(file = "test_results_dsdanger.xml"); progress_rep <- ProgressReporter$new(max_failures = 999999); multi_rep <- MultiReporter$new(reporters = list(progress_rep, junit_rep)); From 79bef776766d967b5a997c41258659d451eb9eef Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:12:18 +0200 Subject: [PATCH 23/56] fix codecov --- .../workflows/dsBaseClient_test_suite.yaml | 58 +++++++------------ 1 file changed, 22 insertions(+), 36 deletions(-) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index c29759559..075ef785f 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -231,36 +231,29 @@ jobs: R -q -e "library(opalr); opal <- opal.login('administrator', 'datashield_test&', url='http://localhost:8080/'); dsadmin.set_option(opal, 'default.datashield.privacyControlLevel', 'permissive'); opal.logout(opal)" working-directory: dsBaseClient/tests/testthat/data_files - - name: Run dsBase tests with JUnit report + - name: Run dsBase tests with coverage & JUnit report run: | R -q -e "devtools::reload();" - R -q -e ' - output_file <- file("test_console_output_dsbase.txt"); - sink(output_file, split = TRUE); - junit_rep <- testthat::JunitReporter$new(file = file.path(getwd(), "test_results_dsbase.xml")); - progress_rep <- testthat::ProgressReporter$new(max_failures = 999999); - multi_rep <- testthat::MultiReporter$new(reporters = list(progress_rep, junit_rep)); - options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); - testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ matrix.filter }}", reporter = multi_rep, stop_on_failure = FALSE)' || R_EXIT=$? - cat test_console_output_dsbase.txt - exit "${R_EXIT:-0}" - working-directory: dsBaseClient - - - name: Generate dsBase coverage report - run: | R -q -e ' write.csv( covr::coverage_to_list( covr::package_coverage( - type = "none", + type = c("none"), code = c('"'"' + output_file <- file("test_console_output_dsbase.txt"); + sink(output_file, split = TRUE); + junit_rep <- testthat::JunitReporter$new(file = file.path(getwd(), "test_results_dsbase.xml")); + progress_rep <- testthat::ProgressReporter$new(max_failures = 999999); + multi_rep <- testthat::MultiReporter$new(reporters = list(progress_rep, junit_rep)); options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); - testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ matrix.filter }}", reporter = "silent", stop_on_failure = FALSE)'"'"' + testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ matrix.filter }}", reporter = multi_rep, stop_on_failure = FALSE)'"'"' ) ) ), "coveragelist.csv" - )' + )' || R_EXIT=$? + cat test_console_output_dsbase.txt + exit "${R_EXIT:-0}" working-directory: dsBaseClient - name: Upload shard results @@ -746,36 +739,29 @@ jobs: run: grep "Caused by:" armadillo_home/logs/stdout.log | sort -u working-directory: dsBaseClient - - name: Run dsBase tests with JUnit report + - name: Run dsBase tests with coverage & JUnit report run: | R -q -e "devtools::reload();" - R -q -e ' - output_file <- file("test_console_output_dsbase.txt"); - sink(output_file, split = TRUE); - junit_rep <- testthat::JunitReporter$new(file = file.path(getwd(), "test_results_dsbase.xml")); - progress_rep <- testthat::ProgressReporter$new(max_failures = 999999); - multi_rep <- testthat::MultiReporter$new(reporters = list(progress_rep, junit_rep)); - options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); - testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ matrix.filter }}", reporter = multi_rep, stop_on_failure = FALSE)' || R_EXIT=$? - cat test_console_output_dsbase.txt - exit "${R_EXIT:-0}" - working-directory: dsBaseClient - - - name: Generate dsBase coverage report - run: | R -q -e ' write.csv( covr::coverage_to_list( covr::package_coverage( - type = "none", + type = c("none"), code = c('"'"' + output_file <- file("test_console_output_dsbase.txt"); + sink(output_file, split = TRUE); + junit_rep <- testthat::JunitReporter$new(file = file.path(getwd(), "test_results_dsbase.xml")); + progress_rep <- testthat::ProgressReporter$new(max_failures = 999999); + multi_rep <- testthat::MultiReporter$new(reporters = list(progress_rep, junit_rep)); options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); - testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ matrix.filter }}", reporter = "silent", stop_on_failure = FALSE)'"'"' + testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ matrix.filter }}", reporter = multi_rep, stop_on_failure = FALSE)'"'"' ) ) ), "coveragelist.csv" - )' + )' || R_EXIT=$? + cat test_console_output_dsbase.txt + exit "${R_EXIT:-0}" working-directory: dsBaseClient - name: Upload shard results From 80baec9ca761348560023988ff4cfcb508deaa0c Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:52:37 +0200 Subject: [PATCH 24/56] split long tasks into separate strands --- .../workflows/dsBaseClient_test_suite.yaml | 64 +++++++++++++------ 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index 075ef785f..03e57dd54 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -130,18 +130,26 @@ jobs: strategy: fail-fast: false # One matrix entry per test-file category, each with its own testthat - # filter substring - not evenly sized (smk/arg are much bigger than - # perf/misc), so job durations differ. Traded for the ability to add, - # remove, or temporarily skip a whole category by editing this list - # alone, with no shell-side partitioning logic to keep in sync. + # filter substring. smk and perf are split further by the first letter + # of the function name (after any "ds." prefix) rather than an + # enumerated file list, so newly added test files fall into a bucket + # automatically without editing this matrix. perf (50 fixed 30s-loop + # benchmarks) and smk (116 files) dominated wall-clock before this + # split - see armadillo-dsbase timings in job history. matrix: include: - - category: smk - filter: 'smk-' + - category: smk-1 + filter: 'smk-ds.[a-lA-L]|smk-(checkClass|isDefined)' + - category: smk-2 + filter: 'smk-ds.[m-vM-V]' - category: arg filter: 'arg-' - - category: perf - filter: 'perf-' + - category: perf-1 + filter: 'perf-ds.[a-cA-C]' + - category: perf-2 + filter: 'perf-ds.[d-mD-M]' + - category: perf-3 + filter: 'perf-ds.[n-vN-V]|perf-(conndisconn|void)' - category: misc filter: 'datachk-|disc-|expt-|smk_expt-|math-|_-' env: @@ -233,7 +241,7 @@ jobs: - name: Run dsBase tests with coverage & JUnit report run: | - R -q -e "devtools::reload();" + R -q -e "devtools::load_all();" R -q -e ' write.csv( covr::coverage_to_list( @@ -339,7 +347,11 @@ jobs: needs: check - name: Install dsDangerClient - run: R -q -e "pak::pkg_install(sprintf('github::datashield/dsDangerClient@%s', Sys.getenv('BRANCH_NAME')))" + run: | + R -q -e " + ref <- Sys.getenv('BRANCH_NAME') + ok <- tryCatch({ pak::pkg_install(sprintf('github::datashield/dsDangerClient@%s', ref)); TRUE }, error = function(e) FALSE) + if (!ok) pak::pkg_install('github::datashield/dsDangerClient')" - name: Install test datasets to Opal run: | @@ -540,18 +552,26 @@ jobs: strategy: fail-fast: false # One matrix entry per test-file category, each with its own testthat - # filter substring - not evenly sized (smk/arg are much bigger than - # perf/misc), so job durations differ. Traded for the ability to add, - # remove, or temporarily skip a whole category by editing this list - # alone, with no shell-side partitioning logic to keep in sync. + # filter substring. smk and perf are split further by the first letter + # of the function name (after any "ds." prefix) rather than an + # enumerated file list, so newly added test files fall into a bucket + # automatically without editing this matrix. perf (50 fixed 30s-loop + # benchmarks) and smk (116 files) dominated wall-clock before this + # split - see armadillo-dsbase timings in job history. matrix: include: - - category: smk - filter: 'smk-' + - category: smk-1 + filter: 'smk-ds.[a-lA-L]|smk-(checkClass|isDefined)' + - category: smk-2 + filter: 'smk-ds.[m-vM-V]' - category: arg filter: 'arg-' - - category: perf - filter: 'perf-' + - category: perf-1 + filter: 'perf-ds.[a-cA-C]' + - category: perf-2 + filter: 'perf-ds.[d-mD-M]' + - category: perf-3 + filter: 'perf-ds.[n-vN-V]|perf-(conndisconn|void)' - category: misc filter: 'datachk-|disc-|expt-|smk_expt-|math-|_-' env: @@ -741,7 +761,7 @@ jobs: - name: Run dsBase tests with coverage & JUnit report run: | - R -q -e "devtools::reload();" + R -q -e "devtools::load_all();" R -q -e ' write.csv( covr::coverage_to_list( @@ -900,7 +920,11 @@ jobs: needs: check - name: Install dsDangerClient - run: R -q -e "pak::pkg_install(sprintf('github::datashield/dsDangerClient@%s', Sys.getenv('BRANCH_NAME')))" + run: | + R -q -e " + ref <- Sys.getenv('BRANCH_NAME') + ok <- tryCatch({ pak::pkg_install(sprintf('github::datashield/dsDangerClient@%s', ref)); TRUE }, error = function(e) FALSE) + if (!ok) pak::pkg_install('github::datashield/dsDangerClient')" - name: Wait for Armadillo to be ready run: | From 579cd7140627db3f1b38c3462cf51125175a1af6 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Sun, 30 Aug 2026 07:38:06 +0200 Subject: [PATCH 25/56] collate results in report --- .../workflows/dsBaseClient_test_suite.yaml | 186 +++++++++++++----- 1 file changed, 134 insertions(+), 52 deletions(-) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index 03e57dd54..fba12595a 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -243,23 +243,20 @@ jobs: run: | R -q -e "devtools::load_all();" R -q -e ' - write.csv( - covr::coverage_to_list( - covr::package_coverage( - type = c("none"), - code = c('"'"' - output_file <- file("test_console_output_dsbase.txt"); - sink(output_file, split = TRUE); - junit_rep <- testthat::JunitReporter$new(file = file.path(getwd(), "test_results_dsbase.xml")); - progress_rep <- testthat::ProgressReporter$new(max_failures = 999999); - multi_rep <- testthat::MultiReporter$new(reporters = list(progress_rep, junit_rep)); - options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); - testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ matrix.filter }}", reporter = multi_rep, stop_on_failure = FALSE)'"'"' - ) - ) - ), - "coveragelist.csv" - )' || R_EXIT=$? + cov <- covr::package_coverage( + type = c("none"), + code = c('"'"' + output_file <- file("test_console_output_dsbase.txt"); + sink(output_file, split = TRUE); + junit_rep <- testthat::JunitReporter$new(file = file.path(getwd(), "test_results_dsbase.xml")); + progress_rep <- testthat::ProgressReporter$new(max_failures = 999999); + multi_rep <- testthat::MultiReporter$new(reporters = list(progress_rep, junit_rep)); + options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); + testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ matrix.filter }}", reporter = multi_rep, stop_on_failure = FALSE)'"'"' + ) + ) + saveRDS(cov, "coverage.rds") + write.csv(covr::coverage_to_list(cov), "coveragelist.csv")' || R_EXIT=$? cat test_console_output_dsbase.txt exit "${R_EXIT:-0}" working-directory: dsBaseClient @@ -272,6 +269,7 @@ jobs: dsBaseClient/test_results_dsbase.xml dsBaseClient/test_console_output_dsbase.txt dsBaseClient/coveragelist.csv + dsBaseClient/coverage.rds ################################################################################ @@ -461,6 +459,7 @@ jobs: cran::tidyr cran::readr cran::magrittr + cran::covr github::datashield/dsDangerClient - name: Download shard/dsdanger results @@ -474,16 +473,58 @@ jobs: mkdir -p logs xml_grep --pretty_print indented --wrap "testsuites" --descr "" --cond "testsuite" artifacts/*/test_results_*.xml > logs/test_results.xml cat artifacts/*/test_console_output_*.txt > logs/test_console_output.txt + working-directory: dsBaseClient - first=1 - for f in artifacts/opal-dsbase-*/coveragelist.csv; do - if [ $first -eq 1 ]; then - cat "$f" > logs/coveragelist.csv - first=0 - else - tail -n +2 "$f" >> logs/coveragelist.csv - fi - done + - name: Merge coverage and write job summary + run: | + Rscript -e ' + rds_files <- list.files("artifacts", pattern = "coverage\\.rds$", recursive = TRUE, full.names = TRUE) + if (length(rds_files) > 0) { + merged <- covr::merge_coverage(rds_files) + cl <- covr::coverage_to_list(merged) + write.csv(cl, "logs/coveragelist.csv") + low <- head(sort(cl$filecoverage), 5) + cov_lines <- c( + sprintf("**Combined coverage: %.1f%%**", cl$totalcoverage), + "", + "Lowest-covered files:", + paste0("- `", names(low), "`: ", low, "%") + ) + } else { + cov_lines <- "No coverage data found." + } + + doc <- xml2::read_xml("logs/test_results.xml") + suites <- xml2::xml_find_all(doc, ".//testsuite") + n_tests <- sum(as.integer(xml2::xml_attr(suites, "tests")), na.rm = TRUE) + n_failures <- sum(as.integer(xml2::xml_attr(suites, "failures")), na.rm = TRUE) + n_errors <- sum(as.integer(xml2::xml_attr(suites, "errors")), na.rm = TRUE) + n_skipped <- sum(as.integer(xml2::xml_attr(suites, "skipped")), na.rm = TRUE) + + failed <- xml2::xml_find_all(doc, ".//testcase[failure or error]") + fail_lines <- if (length(failed) > 0) { + msgs <- vapply(failed, function(tc) { + node <- xml2::xml_find_first(tc, "failure|error") + m <- xml2::xml_attr(node, "message") + if (is.na(m) || !nzchar(m)) m <- trimws(xml2::xml_text(node)) + m + }, character(1)) + c("", "### Failed tests", + paste0("- `", xml2::xml_attr(failed, "classname"), "::", xml2::xml_attr(failed, "name"), "` - ", msgs)) + } else { + character(0) + } + + summary <- c( + "## Opal dsBase/dsDanger test results", + "", + sprintf("**%d tests** - %d failures, %d errors, %d skipped", n_tests, n_failures, n_errors, n_skipped), + "", + cov_lines, + fail_lines + ) + writeLines(summary, Sys.getenv("GITHUB_STEP_SUMMARY")) + ' working-directory: dsBaseClient - name: Check for JUnit errors @@ -763,23 +804,20 @@ jobs: run: | R -q -e "devtools::load_all();" R -q -e ' - write.csv( - covr::coverage_to_list( - covr::package_coverage( - type = c("none"), - code = c('"'"' - output_file <- file("test_console_output_dsbase.txt"); - sink(output_file, split = TRUE); - junit_rep <- testthat::JunitReporter$new(file = file.path(getwd(), "test_results_dsbase.xml")); - progress_rep <- testthat::ProgressReporter$new(max_failures = 999999); - multi_rep <- testthat::MultiReporter$new(reporters = list(progress_rep, junit_rep)); - options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); - testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ matrix.filter }}", reporter = multi_rep, stop_on_failure = FALSE)'"'"' - ) - ) - ), - "coveragelist.csv" - )' || R_EXIT=$? + cov <- covr::package_coverage( + type = c("none"), + code = c('"'"' + output_file <- file("test_console_output_dsbase.txt"); + sink(output_file, split = TRUE); + junit_rep <- testthat::JunitReporter$new(file = file.path(getwd(), "test_results_dsbase.xml")); + progress_rep <- testthat::ProgressReporter$new(max_failures = 999999); + multi_rep <- testthat::MultiReporter$new(reporters = list(progress_rep, junit_rep)); + options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); + testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ matrix.filter }}", reporter = multi_rep, stop_on_failure = FALSE)'"'"' + ) + ) + saveRDS(cov, "coverage.rds") + write.csv(covr::coverage_to_list(cov), "coveragelist.csv")' || R_EXIT=$? cat test_console_output_dsbase.txt exit "${R_EXIT:-0}" working-directory: dsBaseClient @@ -792,6 +830,7 @@ jobs: dsBaseClient/test_results_dsbase.xml dsBaseClient/test_console_output_dsbase.txt dsBaseClient/coveragelist.csv + dsBaseClient/coverage.rds ################################################################################ @@ -1093,6 +1132,7 @@ jobs: cran::tidyr cran::readr cran::magrittr + cran::covr github::datashield/dsDangerClient - name: Download shard/dsdanger results @@ -1106,16 +1146,58 @@ jobs: mkdir -p logs xml_grep --pretty_print indented --wrap "testsuites" --descr "" --cond "testsuite" artifacts/*/test_results_*.xml > logs/test_results.xml cat artifacts/*/test_console_output_*.txt > logs/test_console_output.txt + working-directory: dsBaseClient - first=1 - for f in artifacts/armadillo-dsbase-*/coveragelist.csv; do - if [ $first -eq 1 ]; then - cat "$f" > logs/coveragelist.csv - first=0 - else - tail -n +2 "$f" >> logs/coveragelist.csv - fi - done + - name: Merge coverage and write job summary + run: | + Rscript -e ' + rds_files <- list.files("artifacts", pattern = "coverage\\.rds$", recursive = TRUE, full.names = TRUE) + if (length(rds_files) > 0) { + merged <- covr::merge_coverage(rds_files) + cl <- covr::coverage_to_list(merged) + write.csv(cl, "logs/coveragelist.csv") + low <- head(sort(cl$filecoverage), 5) + cov_lines <- c( + sprintf("**Combined coverage: %.1f%%**", cl$totalcoverage), + "", + "Lowest-covered files:", + paste0("- `", names(low), "`: ", low, "%") + ) + } else { + cov_lines <- "No coverage data found." + } + + doc <- xml2::read_xml("logs/test_results.xml") + suites <- xml2::xml_find_all(doc, ".//testsuite") + n_tests <- sum(as.integer(xml2::xml_attr(suites, "tests")), na.rm = TRUE) + n_failures <- sum(as.integer(xml2::xml_attr(suites, "failures")), na.rm = TRUE) + n_errors <- sum(as.integer(xml2::xml_attr(suites, "errors")), na.rm = TRUE) + n_skipped <- sum(as.integer(xml2::xml_attr(suites, "skipped")), na.rm = TRUE) + + failed <- xml2::xml_find_all(doc, ".//testcase[failure or error]") + fail_lines <- if (length(failed) > 0) { + msgs <- vapply(failed, function(tc) { + node <- xml2::xml_find_first(tc, "failure|error") + m <- xml2::xml_attr(node, "message") + if (is.na(m) || !nzchar(m)) m <- trimws(xml2::xml_text(node)) + m + }, character(1)) + c("", "### Failed tests", + paste0("- `", xml2::xml_attr(failed, "classname"), "::", xml2::xml_attr(failed, "name"), "` - ", msgs)) + } else { + character(0) + } + + summary <- c( + "## Armadillo dsBase/dsDanger test results", + "", + sprintf("**%d tests** - %d failures, %d errors, %d skipped", n_tests, n_failures, n_errors, n_skipped), + "", + cov_lines, + fail_lines + ) + writeLines(summary, Sys.getenv("GITHUB_STEP_SUMMARY")) + ' working-directory: dsBaseClient - name: Check for JUnit errors From f56467a6e0d636df691bae1b46b1180390a462c2 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Sun, 30 Aug 2026 08:02:34 +0200 Subject: [PATCH 26/56] collate codecov properly --- .github/workflows/dsBaseClient_test_suite.yaml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index fba12595a..c1e8c2824 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -480,8 +480,11 @@ jobs: Rscript -e ' rds_files <- list.files("artifacts", pattern = "coverage\\.rds$", recursive = TRUE, full.names = TRUE) if (length(rds_files) > 0) { - merged <- covr::merge_coverage(rds_files) - cl <- covr::coverage_to_list(merged) + tallies <- lapply(rds_files, function(f) covr::tally_coverage(readRDS(f))) + agg <- aggregate(value ~ filename + line, data = do.call(rbind, tallies), FUN = sum) + filecoverage <- tapply(agg$value, agg$filename, function(x) round(sum(x > 0) / length(x) * 100, 2)) + totalcoverage <- round(sum(agg$value > 0) / nrow(agg) * 100, 2) + cl <- list(filecoverage = filecoverage, totalcoverage = totalcoverage) write.csv(cl, "logs/coveragelist.csv") low <- head(sort(cl$filecoverage), 5) cov_lines <- c( @@ -1153,8 +1156,11 @@ jobs: Rscript -e ' rds_files <- list.files("artifacts", pattern = "coverage\\.rds$", recursive = TRUE, full.names = TRUE) if (length(rds_files) > 0) { - merged <- covr::merge_coverage(rds_files) - cl <- covr::coverage_to_list(merged) + tallies <- lapply(rds_files, function(f) covr::tally_coverage(readRDS(f))) + agg <- aggregate(value ~ filename + line, data = do.call(rbind, tallies), FUN = sum) + filecoverage <- tapply(agg$value, agg$filename, function(x) round(sum(x > 0) / length(x) * 100, 2)) + totalcoverage <- round(sum(agg$value > 0) / nrow(agg) * 100, 2) + cl <- list(filecoverage = filecoverage, totalcoverage = totalcoverage) write.csv(cl, "logs/coveragelist.csv") low <- head(sort(cl$filecoverage), 5) cov_lines <- c( From f38c533de7ebbbb0749bcbb22ab9886e60fccf30 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Sun, 30 Aug 2026 09:27:01 +0200 Subject: [PATCH 27/56] streamline report generation --- .../workflows/dsBaseClient_test_suite.yaml | 182 +++--------------- 1 file changed, 30 insertions(+), 152 deletions(-) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index c1e8c2824..8d31ecceb 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -421,44 +421,16 @@ jobs: with: path: dsBaseClient - - name: Checkout testStatus - if: ${{ github.actor != 'nektos/act' }} # for local deployment only - uses: actions/checkout@v4 - with: - repository: ${{ env.REPO_OWNER }}/testStatus - ref: master - path: testStatus - persist-credentials: false - token: ${{ env.GITHUB_TOKEN }} - - - uses: r-lib/actions/setup-pandoc@v2 - - uses: r-lib/actions/setup-r@v2 with: r-version: release use-public-rspm: true - - name: Install xml-twig-tools - run: | - sudo apt-get update -qq - sudo apt-get install -qq xml-twig-tools -y - - uses: r-lib/actions/setup-r-dependencies@v2 with: dependencies: 'c("Depends", "Imports", "LinkingTo")' extra-packages: | - cran::quarto - cran::knitr - cran::kableExtra - cran::rmarkdown - cran::downlit cran::xml2 - cran::purrr - cran::dplyr - cran::stringr - cran::tidyr - cran::readr - cran::magrittr cran::covr github::datashield/dsDangerClient @@ -468,16 +440,20 @@ jobs: pattern: 'opal-*' path: dsBaseClient/artifacts - - name: Merge results + - name: Merge results and write job summary run: | mkdir -p logs - xml_grep --pretty_print indented --wrap "testsuites" --descr "" --cond "testsuite" artifacts/*/test_results_*.xml > logs/test_results.xml cat artifacts/*/test_console_output_*.txt > logs/test_console_output.txt - working-directory: dsBaseClient - - name: Merge coverage and write job summary - run: | Rscript -e ' + xml_files <- list.files("artifacts", pattern = "^test_results_.*\\.xml$", recursive = TRUE, full.names = TRUE) + docs <- lapply(xml_files, xml2::read_xml) + root <- xml2::xml_new_root("testsuites") + for (doc in docs) { + for (s in xml2::xml_find_all(doc, ".//testsuite")) xml2::xml_add_child(root, s) + } + xml2::write_xml(root, "logs/test_results.xml") + rds_files <- list.files("artifacts", pattern = "coverage\\.rds$", recursive = TRUE, full.names = TRUE) if (length(rds_files) > 0) { tallies <- lapply(rds_files, function(f) covr::tally_coverage(readRDS(f))) @@ -530,51 +506,14 @@ jobs: ' working-directory: dsBaseClient - - name: Check for JUnit errors - run: | - issue_count=$(sed 's/failures="0" errors="0"//' test_results.xml | grep -c errors= || true) - echo "Number of testsuites with issues: $issue_count" - sed 's/failures="0" errors="0"//' test_results.xml | grep errors= > issues.log || true - cat issues.log || true - # continue with workflow even when some tests fail - exit 0 - working-directory: dsBaseClient/logs - - - name: Write versions to file - run: | - echo "branch:${{ env.BRANCH_NAME }}" > ${{ env.WORKFLOW_ID }}.txt - echo "os:$(lsb_release -ds)" >> ${{ env.WORKFLOW_ID }}.txt - echo "R:$(R --version | head -n1)" >> ${{ env.WORKFLOW_ID }}.txt - working-directory: dsBaseClient/logs - - - name: Parse results from testthat and covr - run: Rscript --verbose --vanilla ../testStatus/source/parse_test_report.R logs/ - working-directory: dsBaseClient - - - name: Render report - run: | - cd testStatus - - mkdir -p new/logs/${{ env.PROJECT_NAME }}/${{ env.BRANCH_NAME }}/${{ env.WORKFLOW_ID }}/ - mkdir -p new/docs/${{ env.PROJECT_NAME }}/${{ env.BRANCH_NAME }}/${{ env.WORKFLOW_ID }}/ - mkdir -p new/docs/${{ env.PROJECT_NAME }}/${{ env.BRANCH_NAME }}/latest/ - - cp -rv ../dsBaseClient/logs/* new/logs/${{ env.PROJECT_NAME }}/${{ env.BRANCH_NAME }}/${{ env.WORKFLOW_ID }}/ - cp -rv ../dsBaseClient/logs/${{ env.WORKFLOW_ID }}.txt new/logs/${{ env.PROJECT_NAME }}/${{ env.BRANCH_NAME }}/${{ env.WORKFLOW_ID }}/ - - R -e 'input_dir <- file.path("../new/logs", Sys.getenv("PROJECT_NAME"), Sys.getenv("BRANCH_NAME"), Sys.getenv("WORKFLOW_ID")); quarto::quarto_render("source/test_report.qmd", execute_params = list(input_dir = input_dir))' - mv source/test_report.html new/docs/${{ env.PROJECT_NAME }}/${{ env.BRANCH_NAME }}/${{ env.WORKFLOW_ID }}/index.html - cp -r new/docs/${{ env.PROJECT_NAME }}/${{ env.BRANCH_NAME }}/${{ env.WORKFLOW_ID }}/* new/docs/${{ env.PROJECT_NAME }}/${{ env.BRANCH_NAME }}/latest - env: - PROJECT_NAME: ${{ env.PROJECT_NAME }} - BRANCH_NAME: ${{ env.BRANCH_NAME }} - WORKFLOW_ID: ${{ env.WORKFLOW_ID }} - - - name: Upload test logs + - name: Upload merged results uses: actions/upload-artifact@v4 with: - name: dsbaseclient-logs-opal - path: testStatus/new + name: opal-report-results + path: | + dsBaseClient/logs/test_results.xml + dsBaseClient/logs/test_console_output.txt + dsBaseClient/logs/coveragelist.csv ################################################################################ @@ -1097,44 +1036,16 @@ jobs: with: path: dsBaseClient - - name: Checkout testStatus - if: ${{ github.actor != 'nektos/act' }} # for local deployment only - uses: actions/checkout@v4 - with: - repository: ${{ env.REPO_OWNER }}/testStatus - ref: master - path: testStatus - persist-credentials: false - token: ${{ env.GITHUB_TOKEN }} - - - uses: r-lib/actions/setup-pandoc@v2 - - uses: r-lib/actions/setup-r@v2 with: r-version: release use-public-rspm: true - - name: Install xml-twig-tools - run: | - sudo apt-get update -qq - sudo apt-get install -qq xml-twig-tools -y - - uses: r-lib/actions/setup-r-dependencies@v2 with: dependencies: 'c("Depends", "Imports", "LinkingTo")' extra-packages: | - cran::quarto - cran::knitr - cran::kableExtra - cran::rmarkdown - cran::downlit cran::xml2 - cran::purrr - cran::dplyr - cran::stringr - cran::tidyr - cran::readr - cran::magrittr cran::covr github::datashield/dsDangerClient @@ -1144,16 +1055,20 @@ jobs: pattern: 'armadillo-*' path: dsBaseClient/artifacts - - name: Merge results + - name: Merge results and write job summary run: | mkdir -p logs - xml_grep --pretty_print indented --wrap "testsuites" --descr "" --cond "testsuite" artifacts/*/test_results_*.xml > logs/test_results.xml cat artifacts/*/test_console_output_*.txt > logs/test_console_output.txt - working-directory: dsBaseClient - - name: Merge coverage and write job summary - run: | Rscript -e ' + xml_files <- list.files("artifacts", pattern = "^test_results_.*\\.xml$", recursive = TRUE, full.names = TRUE) + docs <- lapply(xml_files, xml2::read_xml) + root <- xml2::xml_new_root("testsuites") + for (doc in docs) { + for (s in xml2::xml_find_all(doc, ".//testsuite")) xml2::xml_add_child(root, s) + } + xml2::write_xml(root, "logs/test_results.xml") + rds_files <- list.files("artifacts", pattern = "coverage\\.rds$", recursive = TRUE, full.names = TRUE) if (length(rds_files) > 0) { tallies <- lapply(rds_files, function(f) covr::tally_coverage(readRDS(f))) @@ -1206,48 +1121,11 @@ jobs: ' working-directory: dsBaseClient - - name: Check for JUnit errors - run: | - issue_count=$(sed 's/failures="0" errors="0"//' test_results.xml | grep -c errors= || true) - echo "Number of testsuites with issues: $issue_count" - sed 's/failures="0" errors="0"//' test_results.xml | grep errors= > issues.log || true - cat issues.log || true - # continue with workflow even when some tests fail - exit 0 - working-directory: dsBaseClient/logs - - - name: Write versions to file - run: | - echo "branch:${{ env.BRANCH_NAME }}" > ${{ env.WORKFLOW_ID }}.txt - echo "os:$(lsb_release -ds)" >> ${{ env.WORKFLOW_ID }}.txt - echo "R:$(R --version | head -n1)" >> ${{ env.WORKFLOW_ID }}.txt - working-directory: dsBaseClient/logs - - - name: Parse results from testthat and covr - run: Rscript --verbose --vanilla ../testStatus/source/parse_test_report.R logs/ - working-directory: dsBaseClient - - - name: Render report - run: | - cd testStatus - - mkdir -p new/logs/${{ env.PROJECT_NAME }}/${{ env.BRANCH_NAME }}/${{ env.WORKFLOW_ID }}/ - mkdir -p new/docs/${{ env.PROJECT_NAME }}/${{ env.BRANCH_NAME }}/${{ env.WORKFLOW_ID }}/ - mkdir -p new/docs/${{ env.PROJECT_NAME }}/${{ env.BRANCH_NAME }}/latest/ - - cp -rv ../dsBaseClient/logs/* new/logs/${{ env.PROJECT_NAME }}/${{ env.BRANCH_NAME }}/${{ env.WORKFLOW_ID }}/ - cp -rv ../dsBaseClient/logs/${{ env.WORKFLOW_ID }}.txt new/logs/${{ env.PROJECT_NAME }}/${{ env.BRANCH_NAME }}/${{ env.WORKFLOW_ID }}/ - - R -e 'input_dir <- file.path("../new/logs", Sys.getenv("PROJECT_NAME"), Sys.getenv("BRANCH_NAME"), Sys.getenv("WORKFLOW_ID")); quarto::quarto_render("source/test_report.qmd", execute_params = list(input_dir = input_dir))' - mv source/test_report.html new/docs/${{ env.PROJECT_NAME }}/${{ env.BRANCH_NAME }}/${{ env.WORKFLOW_ID }}/index.html - cp -r new/docs/${{ env.PROJECT_NAME }}/${{ env.BRANCH_NAME }}/${{ env.WORKFLOW_ID }}/* new/docs/${{ env.PROJECT_NAME }}/${{ env.BRANCH_NAME }}/latest - env: - PROJECT_NAME: ${{ env.PROJECT_NAME }} - BRANCH_NAME: ${{ env.BRANCH_NAME }} - WORKFLOW_ID: ${{ env.WORKFLOW_ID }} - - - name: Upload test logs + - name: Upload merged results uses: actions/upload-artifact@v4 with: - name: dsbaseclient-logs-armadillo - path: testStatus/new + name: armadillo-report-results + path: | + dsBaseClient/logs/test_results.xml + dsBaseClient/logs/test_console_output.txt + dsBaseClient/logs/coveragelist.csv From c3833b95e37662272eaab8e422aba98967b95317 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Mon, 31 Aug 2026 10:00:47 +0200 Subject: [PATCH 28/56] add linting --- .../workflows/dsBaseClient_test_suite.yaml | 222 ++++++++++-------- 1 file changed, 119 insertions(+), 103 deletions(-) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index 8d31ecceb..1531e5129 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -233,6 +233,7 @@ jobs: exit 1 fi echo "Expected dsBase version: $expected_version" + echo "$expected_version" > dsbase_version.txt R -q -e "library(opalr); opal <- opal.login('administrator', 'datashield_test&', url='http://localhost:8080/'); desc <- dsadmin.package_description(opal, 'dsBase'); opal.logout(opal); installed_version <- desc[['Version']]; cat('Installed dsBase version:', installed_version, '\n'); if (is.null(installed_version) || installed_version != '$expected_version') stop('dsBase version mismatch: expected $expected_version, found ', installed_version)" R -q -e "library(opalr); opal <- opal.login('administrator', 'datashield_test&', url='http://localhost:8080/'); dsadmin.profile_init(opal, name = 'default', packages = c('dsBase', 'dsTidyverse', 'resourcer')); opal.logout(opal)" @@ -270,6 +271,7 @@ jobs: dsBaseClient/test_console_output_dsbase.txt dsBaseClient/coveragelist.csv dsBaseClient/coverage.rds + dsBaseClient/tests/testthat/data_files/dsbase_version.txt ################################################################################ @@ -370,6 +372,7 @@ jobs: exit 1 fi echo "Expected dsBase version: $expected_version" + echo "$expected_version" > dsbase_version.txt R -q -e "library(opalr); opal <- opal.login('administrator', 'datashield_test&', url='http://localhost:8080/'); desc <- dsadmin.package_description(opal, 'dsBase'); opal.logout(opal); installed_version <- desc[['Version']]; cat('Installed dsBase version:', installed_version, '\n'); if (is.null(installed_version) || installed_version != '$expected_version') stop('dsBase version mismatch: expected $expected_version, found ', installed_version)" R -q -e "library(opalr); opal <- opal.login('administrator', 'datashield_test&', url='http://localhost:8080/'); dsadmin.profile_init(opal, name = 'default', packages = c('dsBase', 'dsTidyverse', 'resourcer')); opal.logout(opal)" @@ -431,7 +434,6 @@ jobs: dependencies: 'c("Depends", "Imports", "LinkingTo")' extra-packages: | cran::xml2 - cran::covr github::datashield/dsDangerClient - name: Download shard/dsdanger results @@ -440,7 +442,7 @@ jobs: pattern: 'opal-*' path: dsBaseClient/artifacts - - name: Merge results and write job summary + - name: Merge JUnit results run: | mkdir -p logs cat artifacts/*/test_console_output_*.txt > logs/test_console_output.txt @@ -453,56 +455,6 @@ jobs: for (s in xml2::xml_find_all(doc, ".//testsuite")) xml2::xml_add_child(root, s) } xml2::write_xml(root, "logs/test_results.xml") - - rds_files <- list.files("artifacts", pattern = "coverage\\.rds$", recursive = TRUE, full.names = TRUE) - if (length(rds_files) > 0) { - tallies <- lapply(rds_files, function(f) covr::tally_coverage(readRDS(f))) - agg <- aggregate(value ~ filename + line, data = do.call(rbind, tallies), FUN = sum) - filecoverage <- tapply(agg$value, agg$filename, function(x) round(sum(x > 0) / length(x) * 100, 2)) - totalcoverage <- round(sum(agg$value > 0) / nrow(agg) * 100, 2) - cl <- list(filecoverage = filecoverage, totalcoverage = totalcoverage) - write.csv(cl, "logs/coveragelist.csv") - low <- head(sort(cl$filecoverage), 5) - cov_lines <- c( - sprintf("**Combined coverage: %.1f%%**", cl$totalcoverage), - "", - "Lowest-covered files:", - paste0("- `", names(low), "`: ", low, "%") - ) - } else { - cov_lines <- "No coverage data found." - } - - doc <- xml2::read_xml("logs/test_results.xml") - suites <- xml2::xml_find_all(doc, ".//testsuite") - n_tests <- sum(as.integer(xml2::xml_attr(suites, "tests")), na.rm = TRUE) - n_failures <- sum(as.integer(xml2::xml_attr(suites, "failures")), na.rm = TRUE) - n_errors <- sum(as.integer(xml2::xml_attr(suites, "errors")), na.rm = TRUE) - n_skipped <- sum(as.integer(xml2::xml_attr(suites, "skipped")), na.rm = TRUE) - - failed <- xml2::xml_find_all(doc, ".//testcase[failure or error]") - fail_lines <- if (length(failed) > 0) { - msgs <- vapply(failed, function(tc) { - node <- xml2::xml_find_first(tc, "failure|error") - m <- xml2::xml_attr(node, "message") - if (is.na(m) || !nzchar(m)) m <- trimws(xml2::xml_text(node)) - m - }, character(1)) - c("", "### Failed tests", - paste0("- `", xml2::xml_attr(failed, "classname"), "::", xml2::xml_attr(failed, "name"), "` - ", msgs)) - } else { - character(0) - } - - summary <- c( - "## Opal dsBase/dsDanger test results", - "", - sprintf("**%d tests** - %d failures, %d errors, %d skipped", n_tests, n_failures, n_errors, n_skipped), - "", - cov_lines, - fail_lines - ) - writeLines(summary, Sys.getenv("GITHUB_STEP_SUMMARY")) ' working-directory: dsBaseClient @@ -513,7 +465,6 @@ jobs: path: | dsBaseClient/logs/test_results.xml dsBaseClient/logs/test_console_output.txt - dsBaseClient/logs/coveragelist.csv ################################################################################ @@ -733,6 +684,7 @@ jobs: echo "dsBase version mismatch: expected $expected_version, found '$actual_version'" exit 1 fi + echo "$actual_version" > dsbase_version.txt curl -u admin:admin -X POST http://localhost:8080/whitelist/dsBase working-directory: dsBaseClient @@ -773,6 +725,7 @@ jobs: dsBaseClient/test_console_output_dsbase.txt dsBaseClient/coveragelist.csv dsBaseClient/coverage.rds + dsBaseClient/dsbase_version.txt ################################################################################ @@ -967,6 +920,7 @@ jobs: echo "dsBase version mismatch: expected $expected_version, found '$actual_version'" exit 1 fi + echo "$actual_version" > dsbase_version.txt curl -u admin:admin -X POST http://localhost:8080/whitelist/dsBase working-directory: dsBaseClient @@ -1046,7 +1000,6 @@ jobs: dependencies: 'c("Depends", "Imports", "LinkingTo")' extra-packages: | cran::xml2 - cran::covr github::datashield/dsDangerClient - name: Download shard/dsdanger results @@ -1055,7 +1008,7 @@ jobs: pattern: 'armadillo-*' path: dsBaseClient/artifacts - - name: Merge results and write job summary + - name: Merge JUnit results run: | mkdir -p logs cat artifacts/*/test_console_output_*.txt > logs/test_console_output.txt @@ -1068,64 +1021,127 @@ jobs: for (s in xml2::xml_find_all(doc, ".//testsuite")) xml2::xml_add_child(root, s) } xml2::write_xml(root, "logs/test_results.xml") + ' + working-directory: dsBaseClient - rds_files <- list.files("artifacts", pattern = "coverage\\.rds$", recursive = TRUE, full.names = TRUE) - if (length(rds_files) > 0) { - tallies <- lapply(rds_files, function(f) covr::tally_coverage(readRDS(f))) - agg <- aggregate(value ~ filename + line, data = do.call(rbind, tallies), FUN = sum) - filecoverage <- tapply(agg$value, agg$filename, function(x) round(sum(x > 0) / length(x) * 100, 2)) - totalcoverage <- round(sum(agg$value > 0) / nrow(agg) * 100, 2) - cl <- list(filecoverage = filecoverage, totalcoverage = totalcoverage) - write.csv(cl, "logs/coveragelist.csv") - low <- head(sort(cl$filecoverage), 5) - cov_lines <- c( - sprintf("**Combined coverage: %.1f%%**", cl$totalcoverage), - "", - "Lowest-covered files:", - paste0("- `", names(low), "`: ", low, "%") + - name: Upload merged results + uses: actions/upload-artifact@v4 + with: + name: armadillo-report-results + path: | + dsBaseClient/logs/test_results.xml + dsBaseClient/logs/test_console_output.txt + + + ################################################################################ + # Single consolidated summary across both backends: one combined coverage + # figure (merged from every shard's raw coverage.rds, both backends + # together - coverage measures client-side code paths, which are largely + # backend-agnostic, so one figure is more meaningful than two near-duplicate + # ones), a pass/fail table per backend, and failures tucked into collapsible + #
blocks rather than cluttering the main summary. + ################################################################################ + test-summary: + name: Test summary + needs: [opal-report, armadillo-report] + if: always() + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: r-lib/actions/setup-r@v2 + with: + r-version: release + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + dependencies: 'c("Depends", "Imports", "LinkingTo")' + packages: 'any::sessioninfo' + extra-packages: | + cran::xml2 + cran::covr + + - name: Download merged report results + uses: actions/download-artifact@v4 + with: + pattern: '*-report-results' + path: artifacts/reports + + - name: Download shard coverage and versions + uses: actions/download-artifact@v4 + with: + pattern: '*-dsbase-*' + path: artifacts/shards + + - name: Write combined job summary + run: | + Rscript -e ' + find_version <- function(prefix) { + files <- list.files("artifacts/shards", pattern = "dsbase_version\\.txt$", recursive = TRUE, full.names = TRUE) + files <- files[grepl(prefix, files)] + if (length(files) == 0) return("unknown") + trimws(readLines(files[1], warn = FALSE)[1]) + } + + summarise_backend <- function(name, xml_path, version) { + doc <- xml2::read_xml(xml_path) + suites <- xml2::xml_find_all(doc, ".//testsuite") + n_tests <- sum(as.integer(xml2::xml_attr(suites, "tests")), na.rm = TRUE) + n_failures <- sum(as.integer(xml2::xml_attr(suites, "failures")), na.rm = TRUE) + n_errors <- sum(as.integer(xml2::xml_attr(suites, "errors")), na.rm = TRUE) + n_skipped <- sum(as.integer(xml2::xml_attr(suites, "skipped")), na.rm = TRUE) + + failed <- xml2::xml_find_all(doc, ".//testcase[failure or error]") + fail_lines <- if (length(failed) > 0) { + msgs <- vapply(failed, function(tc) { + node <- xml2::xml_find_first(tc, "failure|error") + m <- xml2::xml_attr(node, "message") + if (is.na(m) || !nzchar(m)) m <- trimws(xml2::xml_text(node)) + m + }, character(1)) + paste0("- `", xml2::xml_attr(failed, "classname"), "::", xml2::xml_attr(failed, "name"), "` - ", msgs) + } else { + "All tests passed." + } + + list( + row = sprintf("| %s | %s | %d | %d | %d | %d |", name, version, n_tests, n_failures, n_errors, n_skipped), + details = c( + sprintf("
Failed tests (%s)", name), + "", + fail_lines, + "", + "
" + ) ) - } else { - cov_lines <- "No coverage data found." } - doc <- xml2::read_xml("logs/test_results.xml") - suites <- xml2::xml_find_all(doc, ".//testsuite") - n_tests <- sum(as.integer(xml2::xml_attr(suites, "tests")), na.rm = TRUE) - n_failures <- sum(as.integer(xml2::xml_attr(suites, "failures")), na.rm = TRUE) - n_errors <- sum(as.integer(xml2::xml_attr(suites, "errors")), na.rm = TRUE) - n_skipped <- sum(as.integer(xml2::xml_attr(suites, "skipped")), na.rm = TRUE) - - failed <- xml2::xml_find_all(doc, ".//testcase[failure or error]") - fail_lines <- if (length(failed) > 0) { - msgs <- vapply(failed, function(tc) { - node <- xml2::xml_find_first(tc, "failure|error") - m <- xml2::xml_attr(node, "message") - if (is.na(m) || !nzchar(m)) m <- trimws(xml2::xml_text(node)) - m - }, character(1)) - c("", "### Failed tests", - paste0("- `", xml2::xml_attr(failed, "classname"), "::", xml2::xml_attr(failed, "name"), "` - ", msgs)) + armadillo <- summarise_backend("Armadillo", "artifacts/reports/armadillo-report-results/logs/test_results.xml", find_version("armadillo-dsbase")) + opal <- summarise_backend("Opal", "artifacts/reports/opal-report-results/logs/test_results.xml", find_version("opal-dsbase")) + + rds_files <- list.files("artifacts/shards", pattern = "coverage\\.rds$", recursive = TRUE, full.names = TRUE) + if (length(rds_files) > 0) { + tallies <- lapply(rds_files, function(f) covr::tally_coverage(readRDS(f))) + agg <- aggregate(value ~ filename + line, data = do.call(rbind, tallies), FUN = sum) + totalcoverage <- round(sum(agg$value > 0) / nrow(agg) * 100, 1) + cov_line <- sprintf("**Combined coverage: %.1f%%**", totalcoverage) } else { - character(0) + cov_line <- "No coverage data found." } summary <- c( - "## Armadillo dsBase/dsDanger test results", + "## dsBaseClient test suite results", + "", + cov_line, + "", + "| Backend | dsBase version | Tests | Failures | Errors | Skipped |", + "|---|---|---|---|---|---|", + armadillo$row, + opal$row, "", - sprintf("**%d tests** - %d failures, %d errors, %d skipped", n_tests, n_failures, n_errors, n_skipped), + armadillo$details, "", - cov_lines, - fail_lines + opal$details ) writeLines(summary, Sys.getenv("GITHUB_STEP_SUMMARY")) ' - working-directory: dsBaseClient - - - name: Upload merged results - uses: actions/upload-artifact@v4 - with: - name: armadillo-report-results - path: | - dsBaseClient/logs/test_results.xml - dsBaseClient/logs/test_console_output.txt - dsBaseClient/logs/coveragelist.csv From 8939bf47260d15b683dd466e55cc0dbb1b0debee Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Mon, 31 Aug 2026 11:16:14 +0200 Subject: [PATCH 29/56] refactored --- .../setup-armadillo-with-dsbase/action.yaml | 200 +++++ .../setup-opal-with-dsbase/action.yaml | 100 +++ .../workflows/_armadillo-dsbase-shard.yaml | 69 ++ .github/workflows/_opal-dsbase-shard.yaml | 69 ++ .../workflows/dsBaseClient_test_suite.yaml | 749 +++--------------- .github/workflows/lint.yaml | 50 ++ 6 files changed, 595 insertions(+), 642 deletions(-) create mode 100644 .github/actions/setup-armadillo-with-dsbase/action.yaml create mode 100644 .github/actions/setup-opal-with-dsbase/action.yaml create mode 100644 .github/workflows/_armadillo-dsbase-shard.yaml create mode 100644 .github/workflows/_opal-dsbase-shard.yaml create mode 100644 .github/workflows/lint.yaml diff --git a/.github/actions/setup-armadillo-with-dsbase/action.yaml b/.github/actions/setup-armadillo-with-dsbase/action.yaml new file mode 100644 index 000000000..77c49ca3d --- /dev/null +++ b/.github/actions/setup-armadillo-with-dsbase/action.yaml @@ -0,0 +1,200 @@ +name: Setup Armadillo with dsBase +description: > + Starts a fresh Armadillo instance (self-managed Rock container), installs R + and system dependencies, waits for readiness, uploads test datasets, and + installs dsBase. Assumes the caller has already checked out dsBaseClient at + ./dsBaseClient. Shared by the Armadillo dsbase shard workflow and the + Armadillo dsdanger job, so this setup only needs fixing in one place. + +inputs: + dsbase-tarball: + description: dsBase tarball filename to install + required: false + default: dsBase_7.0.0-permissive.tar.gz + +runs: + using: composite + steps: + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + + - name: Download and start Armadillo (jar) + shell: bash + run: | + mkdir -p armadillo_home/config armadillo_home/storage armadillo_home/logs + + cat > armadillo_home/config/application.yml < armadillo_home/logs/stdout.log 2>&1 & + echo $! > armadillo_home/armadillo.pid + docker pull datashield/rock_citest-permissive:latest + working-directory: dsBaseClient + + - name: Uninstall default MySQL + shell: bash + run: | + sudo service mysql stop || true + sudo apt-get update + sudo apt-get remove --purge mysql-client mysql-server mysql-common -y + sudo apt-get autoremove -y + sudo apt-get autoclean -y + sudo rm -rf /var/lib/mysql/ + + - uses: r-lib/actions/setup-pandoc@v2 + + - uses: r-lib/actions/setup-r@v2 + with: + r-version: release + http-user-agent: release + use-public-rspm: true + + - name: Install system libraries + shell: bash + run: | + sudo apt-get update -qq + sudo apt-get install -qq libxml2-dev libcurl4-openssl-dev libssl-dev libgsl-dev libgit2-dev -y + sudo apt-get install -qq libharfbuzz-dev libfribidi-dev libmagick++-dev libudunits2-dev libuv1-dev -y + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + dependencies: 'c("Depends", "Imports", "LinkingTo")' + extra-packages: | + cran::devtools + cran::usethis + cran::git2r + cran::covr + cran::fields + cran::meta + cran::metafor + cran::ggplot2 + cran::gridExtra + cran::data.table + cran::DSI + cran::opalr + cran::DSOpal + cran::DSLite + cran::MolgenisAuth + cran::MolgenisArmadillo + cran::DSMolgenisArmadillo + cran::DescTools + cran::e1071 + github::datashield/dsDangerClient + needs: check + + - name: Wait for Armadillo to be ready + shell: bash + run: | + for i in $(seq 1 60); do + if curl -fsS http://localhost:8080/actuator/health 2>/dev/null | grep -q '"status":"UP"'; then + echo "Armadillo is ready" + exit 0 + fi + sleep 5 + done + echo "Armadillo did not become ready in time" + cat armadillo_home/logs/stdout.log || true + exit 1 + working-directory: dsBaseClient + + - name: Install test datasets to Armadillo + shell: bash + run: R -q -f "molgenis_armadillo-upload_testing_datasets.R" + working-directory: dsBaseClient/tests/testthat/data_files + + - name: Install dsBase to Armadillo + shell: bash + run: | + curl -sf -u admin:admin -X POST http://localhost:8080/ds-profiles/default/start + + profile_status="" + for i in $(seq 1 30); do + profile_status=$(curl -sf -u admin:admin http://localhost:8080/ds-profiles/status | jq -r '.[] | select(.name=="default") | .status') + [ "$profile_status" == "RUNNING" ] && break + sleep 10 + done + if [ "$profile_status" != "RUNNING" ]; then + echo "default profile container did not become RUNNING in time (status: $profile_status)" + exit 1 + fi + + install_status=$(curl -u admin:admin -H 'Content-Type: multipart/form-data' -F "file=@${{ inputs.dsbase-tarball }}" -o /dev/null -w '%{http_code}' -X POST http://localhost:8080/install-package) + if [ "$install_status" != "200" ]; then + echo "dsBase install request failed with HTTP status $install_status" + exit 1 + fi + + expected_version=$(tar -xOzf ${{ inputs.dsbase-tarball }} dsBase/DESCRIPTION | sed -n 's/^Version: //p' | tr -d '\r') + if [ -z "$expected_version" ]; then + echo "Could not determine expected dsBase version from ${{ inputs.dsbase-tarball }}" + exit 1 + fi + echo "Expected dsBase version: $expected_version" + + actual_version="" + for i in $(seq 1 30); do + packages_json=$(curl -sf -u admin:admin -X GET http://localhost:8080/packages || true) + actual_version=$(echo "$packages_json" | jq -r '.[] | select(.name == "dsBase") | .version' 2>/dev/null || true) + if [ "$actual_version" == "$expected_version" ]; then + break + fi + sleep 10 + done + echo "Installed dsBase version: $actual_version" + if [ "$actual_version" != "$expected_version" ]; then + echo "dsBase version mismatch: expected $expected_version, found '$actual_version'" + exit 1 + fi + echo "$actual_version" > dsbase_version.txt + + curl -u admin:admin -X POST http://localhost:8080/whitelist/dsBase + working-directory: dsBaseClient + + - name: Dump Armadillo server log + if: failure() + shell: bash + run: grep "Caused by:" armadillo_home/logs/stdout.log | sort -u + working-directory: dsBaseClient diff --git a/.github/actions/setup-opal-with-dsbase/action.yaml b/.github/actions/setup-opal-with-dsbase/action.yaml new file mode 100644 index 000000000..bf9c45cbd --- /dev/null +++ b/.github/actions/setup-opal-with-dsbase/action.yaml @@ -0,0 +1,100 @@ +name: Setup Opal with dsBase +description: > + Starts a fresh Opal instance via docker-compose, installs R and system + dependencies, uploads test datasets, and installs dsBase with disclosure + test options. Assumes the caller has already checked out dsBaseClient at + ./dsBaseClient. Shared by the Opal dsbase shard workflow and the Opal + dsdanger job, so this setup only needs fixing in one place. + +inputs: + dsbase-ref: + description: dsBase GitHub ref to install + required: false + default: v7.0-dev + +runs: + using: composite + steps: + - name: Start Opal docker-compose + shell: bash + run: docker compose -f docker-compose_opal.yml up -d --build + working-directory: dsBaseClient + + - name: Uninstall default MySQL + shell: bash + run: | + sudo service mysql stop || true + sudo apt-get update + sudo apt-get remove --purge mysql-client mysql-server mysql-common -y + sudo apt-get autoremove -y + sudo apt-get autoclean -y + sudo rm -rf /var/lib/mysql/ + + - uses: r-lib/actions/setup-pandoc@v2 + + - uses: r-lib/actions/setup-r@v2 + with: + r-version: release + http-user-agent: release + use-public-rspm: true + + - name: Install system libraries + shell: bash + run: | + sudo apt-get update -qq + sudo apt-get install -qq libxml2-dev libcurl4-openssl-dev libssl-dev libgsl-dev libgit2-dev -y + sudo apt-get install -qq libharfbuzz-dev libfribidi-dev libmagick++-dev libudunits2-dev libuv1-dev -y + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + dependencies: 'c("Depends", "Imports", "LinkingTo")' + extra-packages: | + cran::devtools + cran::usethis + cran::git2r + cran::covr + cran::fields + cran::meta + cran::metafor + cran::ggplot2 + cran::gridExtra + cran::data.table + cran::DSI + cran::opalr + cran::DSOpal + cran::DSLite + cran::MolgenisAuth + cran::MolgenisArmadillo + cran::DSMolgenisArmadillo + cran::DescTools + cran::e1071 + github::datashield/dsDangerClient + needs: check + + - name: Install test datasets to Opal + shell: bash + run: | + sleep 60 + R -q -f "obiba_opal-upload_testing_datasets.R" + working-directory: dsBaseClient/tests/testthat/data_files + + - name: Install dsBase to Opal, set disclosure test options + shell: bash + run: | + R -q -e "library(opalr); opal <- opal.login(username = 'administrator', password = 'datashield_test&', url = 'http://localhost:8080/'); opal.put(opal, 'system', 'conf', 'general', '_rPackage'); opal.logout(opal)" + R -q -e "library(opalr); opal <- opal.login('administrator','datashield_test&', url='http://localhost:8080/'); dsadmin.install_github_package(opal, 'dsBase', username = 'datashield', ref = '${{ inputs.dsbase-ref }}'); opal.logout(opal)" + + sleep 60 + + expected_version=$(curl -sf "https://raw.githubusercontent.com/datashield/dsBase/${{ inputs.dsbase-ref }}/DESCRIPTION" | sed -n 's/^Version: //p' | tr -d '\r') + if [ -z "$expected_version" ]; then + echo "Could not determine expected dsBase version from GitHub" + exit 1 + fi + echo "Expected dsBase version: $expected_version" + echo "$expected_version" > dsbase_version.txt + + R -q -e "library(opalr); opal <- opal.login('administrator', 'datashield_test&', url='http://localhost:8080/'); desc <- dsadmin.package_description(opal, 'dsBase'); opal.logout(opal); installed_version <- desc[['Version']]; cat('Installed dsBase version:', installed_version, '\n'); if (is.null(installed_version) || installed_version != '$expected_version') stop('dsBase version mismatch: expected $expected_version, found ', installed_version)" + R -q -e "library(opalr); opal <- opal.login('administrator', 'datashield_test&', url='http://localhost:8080/'); dsadmin.profile_init(opal, name = 'default', packages = c('dsBase', 'dsTidyverse', 'resourcer')); opal.logout(opal)" + R -q -e "library(opalr); opal <- opal.login('administrator', 'datashield_test&', url='http://localhost:8080/'); dsadmin.set_option(opal, 'default.datashield.privacyControlLevel', 'permissive'); opal.logout(opal)" + working-directory: dsBaseClient/tests/testthat/data_files diff --git a/.github/workflows/_armadillo-dsbase-shard.yaml b/.github/workflows/_armadillo-dsbase-shard.yaml new file mode 100644 index 000000000..3fa989d41 --- /dev/null +++ b/.github/workflows/_armadillo-dsbase-shard.yaml @@ -0,0 +1,69 @@ +name: Armadillo dsBase shard + +# Reusable workflow: runs one dsBase testthat filter against a fresh Armadillo +# instance. Called (with a different category/filter per matrix entry) from +# dsBaseClient_test_suite.yaml's armadillo-dsbase-smk/-perf/-other jobs, so +# each category group shows as its own box in the run graph instead of one +# flat 7-shard matrix, without duplicating this setup 3x. The setup itself +# (checkout through installing dsBase) lives in the setup-armadillo-with- +# dsbase composite action, shared with armadillo-dsdanger too. + +on: + workflow_call: + inputs: + category: + required: true + type: string + filter: + required: true + type: string + +env: + PROJECT_NAME: dsBaseClient + DS_DRIVER: ArmadilloDriver + +jobs: + run-shard: + name: Armadillo dsBase tests (${{ inputs.category }}) + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Checkout dsBaseClient + uses: actions/checkout@v4 + with: + path: dsBaseClient + + - uses: ./.github/actions/setup-armadillo-with-dsbase + + - name: Run dsBase tests with coverage & JUnit report + run: | + R -q -e "devtools::load_all();" + R -q -e ' + cov <- covr::package_coverage( + type = c("none"), + code = c('"'"' + output_file <- file("test_console_output_dsbase.txt"); + sink(output_file, split = TRUE); + junit_rep <- testthat::JunitReporter$new(file = file.path(getwd(), "test_results_dsbase.xml")); + progress_rep <- testthat::ProgressReporter$new(max_failures = 999999); + multi_rep <- testthat::MultiReporter$new(reporters = list(progress_rep, junit_rep)); + options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); + testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ inputs.filter }}", reporter = multi_rep, stop_on_failure = FALSE)'"'"' + ) + ) + saveRDS(cov, "coverage.rds") + write.csv(covr::coverage_to_list(cov), "coveragelist.csv")' || R_EXIT=$? + cat test_console_output_dsbase.txt + exit "${R_EXIT:-0}" + working-directory: dsBaseClient + + - name: Upload shard results + uses: actions/upload-artifact@v4 + with: + name: armadillo-dsbase-${{ inputs.category }} + path: | + dsBaseClient/test_results_dsbase.xml + dsBaseClient/test_console_output_dsbase.txt + dsBaseClient/coveragelist.csv + dsBaseClient/coverage.rds + dsBaseClient/dsbase_version.txt diff --git a/.github/workflows/_opal-dsbase-shard.yaml b/.github/workflows/_opal-dsbase-shard.yaml new file mode 100644 index 000000000..55f7dc38d --- /dev/null +++ b/.github/workflows/_opal-dsbase-shard.yaml @@ -0,0 +1,69 @@ +name: Opal dsBase shard + +# Reusable workflow: runs one dsBase testthat filter against a fresh Opal +# instance. Called (with a different category/filter per matrix entry) from +# dsBaseClient_test_suite.yaml's opal-dsbase-smk/-perf/-other jobs, so each +# category group shows as its own box in the run graph instead of one flat +# 7-shard matrix, without duplicating this setup 3x. The setup itself +# (checkout through installing dsBase) lives in the setup-opal-with-dsbase +# composite action, shared with opal-dsdanger too. + +on: + workflow_call: + inputs: + category: + required: true + type: string + filter: + required: true + type: string + +env: + PROJECT_NAME: dsBaseClient + DS_DRIVER: OpalDriver + +jobs: + run-shard: + name: Opal dsBase tests (${{ inputs.category }}) + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Checkout dsBaseClient + uses: actions/checkout@v4 + with: + path: dsBaseClient + + - uses: ./.github/actions/setup-opal-with-dsbase + + - name: Run dsBase tests with coverage & JUnit report + run: | + R -q -e "devtools::load_all();" + R -q -e ' + cov <- covr::package_coverage( + type = c("none"), + code = c('"'"' + output_file <- file("test_console_output_dsbase.txt"); + sink(output_file, split = TRUE); + junit_rep <- testthat::JunitReporter$new(file = file.path(getwd(), "test_results_dsbase.xml")); + progress_rep <- testthat::ProgressReporter$new(max_failures = 999999); + multi_rep <- testthat::MultiReporter$new(reporters = list(progress_rep, junit_rep)); + options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); + testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ inputs.filter }}", reporter = multi_rep, stop_on_failure = FALSE)'"'"' + ) + ) + saveRDS(cov, "coverage.rds") + write.csv(covr::coverage_to_list(cov), "coveragelist.csv")' || R_EXIT=$? + cat test_console_output_dsbase.txt + exit "${R_EXIT:-0}" + working-directory: dsBaseClient + + - name: Upload shard results + uses: actions/upload-artifact@v4 + with: + name: opal-dsbase-${{ inputs.category }} + path: | + dsBaseClient/test_results_dsbase.xml + dsBaseClient/test_console_output_dsbase.txt + dsBaseClient/coveragelist.csv + dsBaseClient/coverage.rds + dsBaseClient/tests/testthat/data_files/dsbase_version.txt diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index 1531e5129..99e669031 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -4,22 +4,32 @@ # Replaces azure-pipelines.yml / opal_azure-pipelines.yml / armadillo_azure-pipelines.yml. # # Structure (all jobs below run in parallel except where "needs" says otherwise): -# r-checks - devtools::document()/check() sync checks. No backend needed. -# opal-dsbase (matrix: smk/arg/perf/misc) - dsBase suite against Opal, -# one category per job. -# opal-dsdanger - dsDanger suite against Opal (small, not sharded). -# opal-report - needs the two Opal jobs above; merges + publishes report. -# armadillo-dsbase (matrix: smk/arg/perf/misc) - dsBase suite against Armadillo. -# armadillo-dsdanger - dsDanger suite against Armadillo. -# armadillo-report - needs the two Armadillo jobs above; merges + publishes report. +# r-checks - devtools::document()/check() sync checks. No backend needed. +# opal-dsbase-smk (matrix x2) \ +# opal-dsbase-perf (matrix x3) } dsBase suite against Opal, one category +# opal-dsbase-other (matrix x2) / group per job - see below. +# opal-dsdanger - dsDanger suite against Opal (small, not sharded). +# opal-report - needs the three opal-dsbase-* jobs + opal-dsdanger; merges results. +# armadillo-dsbase-smk (matrix x2) \ +# armadillo-dsbase-perf (matrix x3) } dsBase suite against Armadillo, same split. +# armadillo-dsbase-other(matrix x2) / +# armadillo-dsdanger - dsDanger suite against Armadillo. +# armadillo-report - needs the three armadillo-dsbase-* jobs + armadillo-dsdanger; merges results. +# test-summary - needs opal-report + armadillo-report; one combined +# coverage figure + pass/fail table + collapsible +# failure details, written to the job summary. # # The dsBase suite (matching TEST_FILTER_DSBASE - same 292 files the Azure -# pipelines run) is split by matrix into 4 fixed categories - smk (116 files), -# arg (96), perf (51), and misc (the smaller categories: datachk, disc, expt, -# smk_expt, math, and the "_-" catch-all - 29 combined) - each with its own -# testthat filter substring, no file-list partitioning logic. Categories -# aren't evenly sized, so job durations differ, but a category can be added, -# removed, or temporarily skipped just by editing the matrix list. +# pipelines run) is split into 7 shards across 3 category groups - smk (116 +# files, 2 shards), perf (51 fixed 30s-loop benchmarks, 3 shards - by far the +# slowest per-file), and other (arg + misc, 2 shards) - each shard with its +# own testthat filter substring. smk/perf are additionally split by the first +# letter of the function name (after any "ds." prefix) rather than an +# enumerated file list, so newly added test files fall into a bucket +# automatically. The 3 category groups are separate job definitions (not one +# 7-entry matrix) purely so they show as separate boxes in the run graph; +# each calls the shared _armadillo-dsbase-shard.yaml / _opal-dsbase-shard.yaml +# reusable workflow to avoid tripling the setup steps. # # Each dsbase/dsdanger job spins up its OWN backend instance (isolated - no # shared server state / concurrency risk between categories running at once). @@ -52,12 +62,9 @@ permissions: env: _r_check_system_clock_: 0 - WORKFLOW_ID: ${{ github.run_id }}-${{ github.run_attempt }} PROJECT_NAME: dsBaseClient BRANCH_NAME: ${{ github.head_ref || github.ref_name }} - REPO_OWNER: ${{ github.repository_owner }} R_KEEP_PKG_SOURCE: yes - GITHUB_TOKEN: ${{ github.token || 'placeholder-token' }} # These should all be constant, except the two TEST_FILTER_* values. These can # be used to test subsets of test files in the testthat directory. Options are # like: '*' <- run all tests, '*_smk_*' <- run all the smoke tests. @@ -123,155 +130,56 @@ jobs: # Opal - dsBase suite, sharded 4 ways. Each shard is a fully isolated job # with its own Opal instance. ################################################################################ - opal-dsbase: - name: Opal dsBase tests (${{ matrix.category }}) - runs-on: ubuntu-latest - timeout-minutes: 60 + # Split into 3 caller jobs (smk / perf / other) rather than one 7-entry + # matrix, so each category group shows as its own box in the run graph. + # All three call the same reusable workflow (_opal-dsbase-shard.yaml) to + # avoid tripling the setup boilerplate. + opal-dsbase-smk: + name: Opal dsBase tests (smk) strategy: fail-fast: false - # One matrix entry per test-file category, each with its own testthat - # filter substring. smk and perf are split further by the first letter - # of the function name (after any "ds." prefix) rather than an - # enumerated file list, so newly added test files fall into a bucket - # automatically without editing this matrix. perf (50 fixed 30s-loop - # benchmarks) and smk (116 files) dominated wall-clock before this - # split - see armadillo-dsbase timings in job history. matrix: include: - category: smk-1 filter: 'smk-ds.[a-lA-L]|smk-(checkClass|isDefined)' - category: smk-2 filter: 'smk-ds.[m-vM-V]' - - category: arg - filter: 'arg-' + uses: ./.github/workflows/_opal-dsbase-shard.yaml + with: + category: ${{ matrix.category }} + filter: ${{ matrix.filter }} + + opal-dsbase-perf: + name: Opal dsBase tests (perf) + strategy: + fail-fast: false + matrix: + include: - category: perf-1 filter: 'perf-ds.[a-cA-C]' - category: perf-2 filter: 'perf-ds.[d-mD-M]' - category: perf-3 filter: 'perf-ds.[n-vN-V]|perf-(conndisconn|void)' + uses: ./.github/workflows/_opal-dsbase-shard.yaml + with: + category: ${{ matrix.category }} + filter: ${{ matrix.filter }} + + opal-dsbase-other: + name: Opal dsBase tests (other) + strategy: + fail-fast: false + matrix: + include: + - category: arg + filter: 'arg-' - category: misc filter: 'datachk-|disc-|expt-|smk_expt-|math-|_-' - env: - DS_DRIVER: OpalDriver - DSBASE_REF: v7.0-dev - - steps: - - name: Checkout dsBaseClient - uses: actions/checkout@v4 - with: - path: dsBaseClient - - - name: Start Opal docker-compose - run: docker compose -f docker-compose_opal.yml up -d --build - working-directory: dsBaseClient - - - name: Uninstall default MySQL - run: | - sudo service mysql stop || true - sudo apt-get update - sudo apt-get remove --purge mysql-client mysql-server mysql-common -y - sudo apt-get autoremove -y - sudo apt-get autoclean -y - sudo rm -rf /var/lib/mysql/ - - - uses: r-lib/actions/setup-pandoc@v2 - - - uses: r-lib/actions/setup-r@v2 - with: - r-version: release - http-user-agent: release - use-public-rspm: true - - - name: Install system libraries - run: | - sudo apt-get update -qq - sudo apt-get install -qq libxml2-dev libcurl4-openssl-dev libssl-dev libgsl-dev libgit2-dev -y - sudo apt-get install -qq libharfbuzz-dev libfribidi-dev libmagick++-dev libudunits2-dev libuv1-dev -y - - - uses: r-lib/actions/setup-r-dependencies@v2 - with: - dependencies: 'c("Depends", "Imports", "LinkingTo")' - extra-packages: | - cran::devtools - cran::usethis - cran::covr - cran::fields - cran::meta - cran::metafor - cran::ggplot2 - cran::gridExtra - cran::data.table - cran::DSI - cran::opalr - cran::DSOpal - cran::DSLite - cran::MolgenisAuth - cran::MolgenisArmadillo - cran::DSMolgenisArmadillo - cran::DescTools - cran::e1071 - github::datashield/dsDangerClient - needs: check - - - name: Install test datasets to Opal - run: | - sleep 60 - R -q -f "obiba_opal-upload_testing_datasets.R" - working-directory: dsBaseClient/tests/testthat/data_files - - - name: Install dsBase to Opal, set disclosure test options - run: | - R -q -e "library(opalr); opal <- opal.login(username = 'administrator', password = 'datashield_test&', url = 'http://localhost:8080/'); opal.put(opal, 'system', 'conf', 'general', '_rPackage'); opal.logout(opal)" - R -q -e "library(opalr); opal <- opal.login('administrator','datashield_test&', url='http://localhost:8080/'); dsadmin.install_github_package(opal, 'dsBase', username = 'datashield', ref = '${{ env.DSBASE_REF }}'); opal.logout(opal)" - - sleep 60 - - expected_version=$(curl -sf "https://raw.githubusercontent.com/datashield/dsBase/${{ env.DSBASE_REF }}/DESCRIPTION" | sed -n 's/^Version: //p' | tr -d '\r') - if [ -z "$expected_version" ]; then - echo "Could not determine expected dsBase version from GitHub" - exit 1 - fi - echo "Expected dsBase version: $expected_version" - echo "$expected_version" > dsbase_version.txt - - R -q -e "library(opalr); opal <- opal.login('administrator', 'datashield_test&', url='http://localhost:8080/'); desc <- dsadmin.package_description(opal, 'dsBase'); opal.logout(opal); installed_version <- desc[['Version']]; cat('Installed dsBase version:', installed_version, '\n'); if (is.null(installed_version) || installed_version != '$expected_version') stop('dsBase version mismatch: expected $expected_version, found ', installed_version)" - R -q -e "library(opalr); opal <- opal.login('administrator', 'datashield_test&', url='http://localhost:8080/'); dsadmin.profile_init(opal, name = 'default', packages = c('dsBase', 'dsTidyverse', 'resourcer')); opal.logout(opal)" - R -q -e "library(opalr); opal <- opal.login('administrator', 'datashield_test&', url='http://localhost:8080/'); dsadmin.set_option(opal, 'default.datashield.privacyControlLevel', 'permissive'); opal.logout(opal)" - working-directory: dsBaseClient/tests/testthat/data_files - - - name: Run dsBase tests with coverage & JUnit report - run: | - R -q -e "devtools::load_all();" - R -q -e ' - cov <- covr::package_coverage( - type = c("none"), - code = c('"'"' - output_file <- file("test_console_output_dsbase.txt"); - sink(output_file, split = TRUE); - junit_rep <- testthat::JunitReporter$new(file = file.path(getwd(), "test_results_dsbase.xml")); - progress_rep <- testthat::ProgressReporter$new(max_failures = 999999); - multi_rep <- testthat::MultiReporter$new(reporters = list(progress_rep, junit_rep)); - options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); - testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ matrix.filter }}", reporter = multi_rep, stop_on_failure = FALSE)'"'"' - ) - ) - saveRDS(cov, "coverage.rds") - write.csv(covr::coverage_to_list(cov), "coveragelist.csv")' || R_EXIT=$? - cat test_console_output_dsbase.txt - exit "${R_EXIT:-0}" - working-directory: dsBaseClient - - - name: Upload shard results - uses: actions/upload-artifact@v4 - with: - name: opal-dsbase-${{ matrix.category }} - path: | - dsBaseClient/test_results_dsbase.xml - dsBaseClient/test_console_output_dsbase.txt - dsBaseClient/coveragelist.csv - dsBaseClient/coverage.rds - dsBaseClient/tests/testthat/data_files/dsbase_version.txt + uses: ./.github/workflows/_opal-dsbase-shard.yaml + with: + category: ${{ matrix.category }} + filter: ${{ matrix.filter }} ################################################################################ @@ -284,7 +192,6 @@ jobs: timeout-minutes: 60 env: DS_DRIVER: OpalDriver - DSBASE_REF: v7.0-dev DSDANGER_REF: '6.3.4' steps: @@ -293,58 +200,9 @@ jobs: with: path: dsBaseClient - - name: Start Opal docker-compose - run: docker compose -f docker-compose_opal.yml up -d --build - working-directory: dsBaseClient - - - name: Uninstall default MySQL - run: | - sudo service mysql stop || true - sudo apt-get update - sudo apt-get remove --purge mysql-client mysql-server mysql-common -y - sudo apt-get autoremove -y - sudo apt-get autoclean -y - sudo rm -rf /var/lib/mysql/ - - - uses: r-lib/actions/setup-pandoc@v2 - - - uses: r-lib/actions/setup-r@v2 - with: - r-version: release - http-user-agent: release - use-public-rspm: true - - - name: Install system libraries - run: | - sudo apt-get update -qq - sudo apt-get install -qq libxml2-dev libcurl4-openssl-dev libssl-dev libgsl-dev libgit2-dev -y - sudo apt-get install -qq libharfbuzz-dev libfribidi-dev libmagick++-dev libudunits2-dev libuv1-dev -y - - - uses: r-lib/actions/setup-r-dependencies@v2 + - uses: ./.github/actions/setup-opal-with-dsbase with: - dependencies: 'c("Depends", "Imports", "LinkingTo")' - extra-packages: | - cran::devtools - cran::usethis - cran::git2r - cran::covr - cran::fields - cran::meta - cran::metafor - cran::ggplot2 - cran::gridExtra - cran::data.table - cran::DSI - cran::opalr - cran::DSOpal - cran::DSLite - cran::MolgenisAuth - cran::MolgenisArmadillo - cran::DSMolgenisArmadillo - cran::DescTools - cran::e1071 - github::datashield/dsDangerClient - needs: check + dsbase-ref: v7.0-dev - name: Install dsDangerClient run: | @@ -353,32 +211,6 @@ jobs: ok <- tryCatch({ pak::pkg_install(sprintf('github::datashield/dsDangerClient@%s', ref)); TRUE }, error = function(e) FALSE) if (!ok) pak::pkg_install('github::datashield/dsDangerClient')" - - name: Install test datasets to Opal - run: | - sleep 60 - R -q -f "obiba_opal-upload_testing_datasets.R" - working-directory: dsBaseClient/tests/testthat/data_files - - - name: Install dsBase to Opal, set disclosure test options - run: | - R -q -e "library(opalr); opal <- opal.login(username = 'administrator', password = 'datashield_test&', url = 'http://localhost:8080/'); opal.put(opal, 'system', 'conf', 'general', '_rPackage'); opal.logout(opal)" - R -q -e "library(opalr); opal <- opal.login('administrator','datashield_test&', url='http://localhost:8080/'); dsadmin.install_github_package(opal, 'dsBase', username = 'datashield', ref = '${{ env.DSBASE_REF }}'); opal.logout(opal)" - - sleep 60 - - expected_version=$(curl -sf "https://raw.githubusercontent.com/datashield/dsBase/${{ env.DSBASE_REF }}/DESCRIPTION" | sed -n 's/^Version: //p' | tr -d '\r') - if [ -z "$expected_version" ]; then - echo "Could not determine expected dsBase version from GitHub" - exit 1 - fi - echo "Expected dsBase version: $expected_version" - echo "$expected_version" > dsbase_version.txt - - R -q -e "library(opalr); opal <- opal.login('administrator', 'datashield_test&', url='http://localhost:8080/'); desc <- dsadmin.package_description(opal, 'dsBase'); opal.logout(opal); installed_version <- desc[['Version']]; cat('Installed dsBase version:', installed_version, '\n'); if (is.null(installed_version) || installed_version != '$expected_version') stop('dsBase version mismatch: expected $expected_version, found ', installed_version)" - R -q -e "library(opalr); opal <- opal.login('administrator', 'datashield_test&', url='http://localhost:8080/'); dsadmin.profile_init(opal, name = 'default', packages = c('dsBase', 'dsTidyverse', 'resourcer')); opal.logout(opal)" - R -q -e "library(opalr); opal <- opal.login('administrator', 'datashield_test&', url='http://localhost:8080/'); dsadmin.set_option(opal, 'default.datashield.privacyControlLevel', 'permissive'); opal.logout(opal)" - working-directory: dsBaseClient/tests/testthat/data_files - - name: Install dsDanger package on Opal server run: | R -q -e "library(opalr); opal <- opal.login(username = 'administrator', password = 'datashield_test&', url = 'http://localhost:8080'); opal.put(opal, 'system', 'conf', 'general', '_rPackage'); opal.logout(opal)" @@ -414,7 +246,7 @@ jobs: ################################################################################ opal-report: name: Opal report - needs: [opal-dsbase, opal-dsdanger] + needs: [opal-dsbase-smk, opal-dsbase-perf, opal-dsbase-other, opal-dsdanger] if: always() runs-on: ubuntu-latest timeout-minutes: 30 @@ -479,253 +311,56 @@ jobs: # run time. No process is restarted after installing dsBase - install then # whitelist directly. ################################################################################ - armadillo-dsbase: - name: Armadillo dsBase tests (${{ matrix.category }}) - runs-on: ubuntu-latest - timeout-minutes: 60 + # Split into 3 caller jobs (smk / perf / other) rather than one 7-entry + # matrix, so each category group shows as its own box in the run graph. + # All three call the same reusable workflow (_armadillo-dsbase-shard.yaml) + # to avoid tripling the setup boilerplate. + armadillo-dsbase-smk: + name: Armadillo dsBase tests (smk) strategy: fail-fast: false - # One matrix entry per test-file category, each with its own testthat - # filter substring. smk and perf are split further by the first letter - # of the function name (after any "ds." prefix) rather than an - # enumerated file list, so newly added test files fall into a bucket - # automatically without editing this matrix. perf (50 fixed 30s-loop - # benchmarks) and smk (116 files) dominated wall-clock before this - # split - see armadillo-dsbase timings in job history. matrix: include: - category: smk-1 filter: 'smk-ds.[a-lA-L]|smk-(checkClass|isDefined)' - category: smk-2 filter: 'smk-ds.[m-vM-V]' - - category: arg - filter: 'arg-' + uses: ./.github/workflows/_armadillo-dsbase-shard.yaml + with: + category: ${{ matrix.category }} + filter: ${{ matrix.filter }} + + armadillo-dsbase-perf: + name: Armadillo dsBase tests (perf) + strategy: + fail-fast: false + matrix: + include: - category: perf-1 filter: 'perf-ds.[a-cA-C]' - category: perf-2 filter: 'perf-ds.[d-mD-M]' - category: perf-3 filter: 'perf-ds.[n-vN-V]|perf-(conndisconn|void)' + uses: ./.github/workflows/_armadillo-dsbase-shard.yaml + with: + category: ${{ matrix.category }} + filter: ${{ matrix.filter }} + + armadillo-dsbase-other: + name: Armadillo dsBase tests (other) + strategy: + fail-fast: false + matrix: + include: + - category: arg + filter: 'arg-' - category: misc filter: 'datachk-|disc-|expt-|smk_expt-|math-|_-' - env: - DS_DRIVER: ArmadilloDriver - DSBASE_TARBALL: dsBase_7.0.0-permissive.tar.gz - - steps: - - name: Checkout dsBaseClient - uses: actions/checkout@v4 - with: - path: dsBaseClient - - - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: '21' - - - name: Download and start Armadillo (jar) - run: | - mkdir -p armadillo_home/config armadillo_home/storage armadillo_home/logs - - cat > armadillo_home/config/application.yml < armadillo_home/logs/stdout.log 2>&1 & - echo $! > armadillo_home/armadillo.pid - docker pull datashield/rock_citest-permissive:latest - working-directory: dsBaseClient - - - name: Uninstall default MySQL - run: | - sudo service mysql stop || true - sudo apt-get update - sudo apt-get remove --purge mysql-client mysql-server mysql-common -y - sudo apt-get autoremove -y - sudo apt-get autoclean -y - sudo rm -rf /var/lib/mysql/ - - - uses: r-lib/actions/setup-pandoc@v2 - - - uses: r-lib/actions/setup-r@v2 - with: - r-version: release - http-user-agent: release - use-public-rspm: true - - - name: Install system libraries - run: | - sudo apt-get update -qq - sudo apt-get install -qq libxml2-dev libcurl4-openssl-dev libssl-dev libgsl-dev libgit2-dev -y - sudo apt-get install -qq libharfbuzz-dev libfribidi-dev libmagick++-dev libudunits2-dev libuv1-dev -y - - - uses: r-lib/actions/setup-r-dependencies@v2 - with: - dependencies: 'c("Depends", "Imports", "LinkingTo")' - extra-packages: | - cran::devtools - cran::usethis - cran::covr - cran::fields - cran::meta - cran::metafor - cran::ggplot2 - cran::gridExtra - cran::data.table - cran::DSI - cran::opalr - cran::DSOpal - cran::DSLite - cran::MolgenisAuth - cran::MolgenisArmadillo - cran::DSMolgenisArmadillo - cran::DescTools - cran::e1071 - github::datashield/dsDangerClient - needs: check - - - name: Wait for Armadillo to be ready - run: | - for i in $(seq 1 60); do - if curl -fsS http://localhost:8080/actuator/health 2>/dev/null | grep -q '"status":"UP"'; then - echo "Armadillo is ready" - exit 0 - fi - sleep 5 - done - echo "Armadillo did not become ready in time" - cat armadillo_home/logs/stdout.log || true - exit 1 - working-directory: dsBaseClient - - - name: Install test datasets to Armadillo - run: R -q -f "molgenis_armadillo-upload_testing_datasets.R" - working-directory: dsBaseClient/tests/testthat/data_files - - - name: Install dsBase to Armadillo - run: | - curl -sf -u admin:admin -X POST http://localhost:8080/ds-profiles/default/start - - profile_status="" - for i in $(seq 1 30); do - profile_status=$(curl -sf -u admin:admin http://localhost:8080/ds-profiles/status | jq -r '.[] | select(.name=="default") | .status') - [ "$profile_status" == "RUNNING" ] && break - sleep 10 - done - if [ "$profile_status" != "RUNNING" ]; then - echo "default profile container did not become RUNNING in time (status: $profile_status)" - exit 1 - fi - - install_status=$(curl -u admin:admin -H 'Content-Type: multipart/form-data' -F "file=@${{ env.DSBASE_TARBALL }}" -o /dev/null -w '%{http_code}' -X POST http://localhost:8080/install-package) - if [ "$install_status" != "200" ]; then - echo "dsBase install request failed with HTTP status $install_status" - exit 1 - fi - - expected_version=$(tar -xOzf ${{ env.DSBASE_TARBALL }} dsBase/DESCRIPTION | sed -n 's/^Version: //p' | tr -d '\r') - if [ -z "$expected_version" ]; then - echo "Could not determine expected dsBase version from ${{ env.DSBASE_TARBALL }}" - exit 1 - fi - echo "Expected dsBase version: $expected_version" - - actual_version="" - for i in $(seq 1 30); do - packages_json=$(curl -sf -u admin:admin -X GET http://localhost:8080/packages || true) - actual_version=$(echo "$packages_json" | jq -r '.[] | select(.name == "dsBase") | .version' 2>/dev/null || true) - if [ "$actual_version" == "$expected_version" ]; then - break - fi - sleep 10 - done - echo "Installed dsBase version: $actual_version" - if [ "$actual_version" != "$expected_version" ]; then - echo "dsBase version mismatch: expected $expected_version, found '$actual_version'" - exit 1 - fi - echo "$actual_version" > dsbase_version.txt - - curl -u admin:admin -X POST http://localhost:8080/whitelist/dsBase - working-directory: dsBaseClient - - - name: Dump Armadillo server log - if: failure() - run: grep "Caused by:" armadillo_home/logs/stdout.log | sort -u - working-directory: dsBaseClient - - - name: Run dsBase tests with coverage & JUnit report - run: | - R -q -e "devtools::load_all();" - R -q -e ' - cov <- covr::package_coverage( - type = c("none"), - code = c('"'"' - output_file <- file("test_console_output_dsbase.txt"); - sink(output_file, split = TRUE); - junit_rep <- testthat::JunitReporter$new(file = file.path(getwd(), "test_results_dsbase.xml")); - progress_rep <- testthat::ProgressReporter$new(max_failures = 999999); - multi_rep <- testthat::MultiReporter$new(reporters = list(progress_rep, junit_rep)); - options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); - testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ matrix.filter }}", reporter = multi_rep, stop_on_failure = FALSE)'"'"' - ) - ) - saveRDS(cov, "coverage.rds") - write.csv(covr::coverage_to_list(cov), "coveragelist.csv")' || R_EXIT=$? - cat test_console_output_dsbase.txt - exit "${R_EXIT:-0}" - working-directory: dsBaseClient - - - name: Upload shard results - uses: actions/upload-artifact@v4 - with: - name: armadillo-dsbase-${{ matrix.category }} - path: | - dsBaseClient/test_results_dsbase.xml - dsBaseClient/test_console_output_dsbase.txt - dsBaseClient/coveragelist.csv - dsBaseClient/coverage.rds - dsBaseClient/dsbase_version.txt + uses: ./.github/workflows/_armadillo-dsbase-shard.yaml + with: + category: ${{ matrix.category }} + filter: ${{ matrix.filter }} ################################################################################ @@ -737,7 +372,6 @@ jobs: timeout-minutes: 60 env: DS_DRIVER: ArmadilloDriver - DSBASE_TARBALL: dsBase_7.0.0-permissive.tar.gz DSDANGER_TARBALL: dsDanger_6.3.4.tar.gz steps: @@ -746,112 +380,7 @@ jobs: with: path: dsBaseClient - - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: '21' - - - name: Download and start Armadillo (jar) - run: | - mkdir -p armadillo_home/config armadillo_home/storage armadillo_home/logs - - cat > armadillo_home/config/application.yml < armadillo_home/logs/stdout.log 2>&1 & - echo $! > armadillo_home/armadillo.pid - docker pull datashield/rock_citest-permissive:latest - working-directory: dsBaseClient - - - name: Uninstall default MySQL - run: | - sudo service mysql stop || true - sudo apt-get update - sudo apt-get remove --purge mysql-client mysql-server mysql-common -y - sudo apt-get autoremove -y - sudo apt-get autoclean -y - sudo rm -rf /var/lib/mysql/ - - - uses: r-lib/actions/setup-pandoc@v2 - - - uses: r-lib/actions/setup-r@v2 - with: - r-version: release - http-user-agent: release - use-public-rspm: true - - - name: Install system libraries - run: | - sudo apt-get update -qq - sudo apt-get install -qq libxml2-dev libcurl4-openssl-dev libssl-dev libgsl-dev libgit2-dev -y - sudo apt-get install -qq libharfbuzz-dev libfribidi-dev libmagick++-dev libudunits2-dev libuv1-dev -y - - - uses: r-lib/actions/setup-r-dependencies@v2 - with: - dependencies: 'c("Depends", "Imports", "LinkingTo")' - extra-packages: | - cran::devtools - cran::usethis - cran::git2r - cran::covr - cran::fields - cran::meta - cran::metafor - cran::ggplot2 - cran::gridExtra - cran::data.table - cran::DSI - cran::opalr - cran::DSOpal - cran::DSLite - cran::MolgenisAuth - cran::MolgenisArmadillo - cran::DSMolgenisArmadillo - cran::DescTools - cran::e1071 - github::datashield/dsDangerClient - needs: check + - uses: ./.github/actions/setup-armadillo-with-dsbase - name: Install dsDangerClient run: | @@ -860,76 +389,6 @@ jobs: ok <- tryCatch({ pak::pkg_install(sprintf('github::datashield/dsDangerClient@%s', ref)); TRUE }, error = function(e) FALSE) if (!ok) pak::pkg_install('github::datashield/dsDangerClient')" - - name: Wait for Armadillo to be ready - run: | - for i in $(seq 1 60); do - if curl -fsS http://localhost:8080/actuator/health 2>/dev/null | grep -q '"status":"UP"'; then - echo "Armadillo is ready" - exit 0 - fi - sleep 5 - done - echo "Armadillo did not become ready in time" - cat armadillo_home/logs/stdout.log || true - exit 1 - working-directory: dsBaseClient - - - name: Install test datasets to Armadillo - run: R -q -f "molgenis_armadillo-upload_testing_datasets.R" - working-directory: dsBaseClient/tests/testthat/data_files - - - name: Install dsBase to Armadillo - run: | - curl -sf -u admin:admin -X POST http://localhost:8080/ds-profiles/default/start - - profile_status="" - for i in $(seq 1 30); do - profile_status=$(curl -sf -u admin:admin http://localhost:8080/ds-profiles/status | jq -r '.[] | select(.name=="default") | .status') - [ "$profile_status" == "RUNNING" ] && break - sleep 10 - done - if [ "$profile_status" != "RUNNING" ]; then - echo "default profile container did not become RUNNING in time (status: $profile_status)" - exit 1 - fi - - install_status=$(curl -u admin:admin -H 'Content-Type: multipart/form-data' -F "file=@${{ env.DSBASE_TARBALL }}" -o /dev/null -w '%{http_code}' -X POST http://localhost:8080/install-package) - if [ "$install_status" != "200" ]; then - echo "dsBase install request failed with HTTP status $install_status" - exit 1 - fi - - expected_version=$(tar -xOzf ${{ env.DSBASE_TARBALL }} dsBase/DESCRIPTION | sed -n 's/^Version: //p' | tr -d '\r') - if [ -z "$expected_version" ]; then - echo "Could not determine expected dsBase version from ${{ env.DSBASE_TARBALL }}" - exit 1 - fi - echo "Expected dsBase version: $expected_version" - - actual_version="" - for i in $(seq 1 30); do - packages_json=$(curl -sf -u admin:admin -X GET http://localhost:8080/packages || true) - actual_version=$(echo "$packages_json" | jq -r '.[] | select(.name == "dsBase") | .version' 2>/dev/null || true) - if [ "$actual_version" == "$expected_version" ]; then - break - fi - sleep 10 - done - echo "Installed dsBase version: $actual_version" - if [ "$actual_version" != "$expected_version" ]; then - echo "dsBase version mismatch: expected $expected_version, found '$actual_version'" - exit 1 - fi - echo "$actual_version" > dsbase_version.txt - - curl -u admin:admin -X POST http://localhost:8080/whitelist/dsBase - working-directory: dsBaseClient - - - name: Dump Armadillo server log - if: failure() - run: grep "Caused by:" armadillo_home/logs/stdout.log | sort -u - working-directory: dsBaseClient - - name: Install dsDanger package on Armadillo server run: | curl -u admin:admin http://localhost:8080/whitelist @@ -980,7 +439,7 @@ jobs: ################################################################################ armadillo-report: name: Armadillo report - needs: [armadillo-dsbase, armadillo-dsdanger] + needs: [armadillo-dsbase-smk, armadillo-dsbase-perf, armadillo-dsbase-other, armadillo-dsdanger] if: always() runs-on: ubuntu-latest timeout-minutes: 30 @@ -1083,8 +542,14 @@ jobs: trimws(readLines(files[1], warn = FALSE)[1]) } - summarise_backend <- function(name, xml_path, version) { - doc <- xml2::read_xml(xml_path) + find_results_xml <- function(artifact_dir) { + files <- list.files(artifact_dir, pattern = "^test_results\\.xml$", recursive = TRUE, full.names = TRUE) + if (length(files) == 0) stop(sprintf("No test_results.xml found under %s", artifact_dir)) + files[1] + } + + summarise_backend <- function(name, artifact_dir, version) { + doc <- xml2::read_xml(find_results_xml(artifact_dir)) suites <- xml2::xml_find_all(doc, ".//testsuite") n_tests <- sum(as.integer(xml2::xml_attr(suites, "tests")), na.rm = TRUE) n_failures <- sum(as.integer(xml2::xml_attr(suites, "failures")), na.rm = TRUE) @@ -1116,8 +581,8 @@ jobs: ) } - armadillo <- summarise_backend("Armadillo", "artifacts/reports/armadillo-report-results/logs/test_results.xml", find_version("armadillo-dsbase")) - opal <- summarise_backend("Opal", "artifacts/reports/opal-report-results/logs/test_results.xml", find_version("opal-dsbase")) + armadillo <- summarise_backend("Armadillo", "artifacts/reports/armadillo-report-results", find_version("armadillo-dsbase")) + opal <- summarise_backend("Opal", "artifacts/reports/opal-report-results", find_version("opal-dsbase")) rds_files <- list.files("artifacts/shards", pattern = "coverage\\.rds$", recursive = TRUE, full.names = TRUE) if (length(rds_files) > 0) { diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 000000000..fd467b0f6 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,50 @@ +name: Lint + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + lint: + name: R lint (lintr) + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + security-events: write + steps: + - uses: actions/checkout@v4 + + - uses: r-lib/actions/setup-r@v2 + with: + r-version: release + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + dependencies: 'c("Depends", "Imports", "LinkingTo")' + extra-packages: | + cran::lintr + github::datashield/dsDangerClient + + - name: Lint + run: | + Rscript -e ' + al <- lintr::available_linters() + keep <- vapply(al$tags, function(t) any(t %in% c("correctness", "common_mistakes", "robustness")), logical(1)) + sel <- al$linter[keep] + linter_funs <- lapply(sel, function(n) get(n, envir = asNamespace("lintr"))()) + names(linter_funs) <- sel + lints <- lintr::lint_package(linters = linter_funs) + cat("n lints:", length(lints), "\n") + lintr::sarif_output(lints, "lintr_results.sarif") + ' + continue-on-error: true + + - name: Upload lint results + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: lintr_results.sarif From dc1e2ab8ed766328616ae12e8223f279ee2fa2c4 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Mon, 31 Aug 2026 12:11:06 +0200 Subject: [PATCH 30/56] regroup test flows --- .../workflows/_armadillo-dsbase-shard.yaml | 2 +- .github/workflows/_opal-dsbase-shard.yaml | 2 +- .github/workflows/check.yaml | 60 ++++++++++++++++++ .../workflows/dsBaseClient_test_suite.yaml | 63 +++---------------- 4 files changed, 69 insertions(+), 58 deletions(-) create mode 100644 .github/workflows/check.yaml diff --git a/.github/workflows/_armadillo-dsbase-shard.yaml b/.github/workflows/_armadillo-dsbase-shard.yaml index 3fa989d41..4944d851f 100644 --- a/.github/workflows/_armadillo-dsbase-shard.yaml +++ b/.github/workflows/_armadillo-dsbase-shard.yaml @@ -33,7 +33,7 @@ jobs: with: path: dsBaseClient - - uses: ./.github/actions/setup-armadillo-with-dsbase + - uses: ./dsBaseClient/.github/actions/setup-armadillo-with-dsbase - name: Run dsBase tests with coverage & JUnit report run: | diff --git a/.github/workflows/_opal-dsbase-shard.yaml b/.github/workflows/_opal-dsbase-shard.yaml index 55f7dc38d..7812ca308 100644 --- a/.github/workflows/_opal-dsbase-shard.yaml +++ b/.github/workflows/_opal-dsbase-shard.yaml @@ -33,7 +33,7 @@ jobs: with: path: dsBaseClient - - uses: ./.github/actions/setup-opal-with-dsbase + - uses: ./dsBaseClient/.github/actions/setup-opal-with-dsbase - name: Run dsBase tests with coverage & JUnit report run: | diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml new file mode 100644 index 000000000..c3fdaea58 --- /dev/null +++ b/.github/workflows/check.yaml @@ -0,0 +1,60 @@ +name: Check + +on: + push: + workflow_dispatch: + schedule: + - cron: '0 0 * * 0' # Weekly + - cron: '0 1 * * *' # Nightly + +permissions: + contents: read + +jobs: + check: + name: Package checks (doc sync, R CMD check) + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + + - uses: r-lib/actions/setup-pandoc@v2 + + - uses: r-lib/actions/setup-r@v2 + with: + r-version: release + http-user-agent: release + use-public-rspm: true + + - name: Install system libraries + run: | + sudo apt-get update -qq + sudo apt-get install -qq libxml2-dev libcurl4-openssl-dev libssl-dev libgsl-dev libgit2-dev -y + sudo apt-get install -qq libharfbuzz-dev libfribidi-dev libmagick++-dev libudunits2-dev libuv1-dev -y + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + dependencies: 'c("Depends", "Imports", "LinkingTo")' + extra-packages: | + any::rcmdcheck + cran::devtools + cran::usethis + github::datashield/dsDangerClient + needs: check + + - name: Check manual updated + run: | + orig_sum=$(find man -type f | sort -u | xargs cat | md5sum) + R -q -e "devtools::document()" + new_sum=$(find man -type f | sort -u | xargs cat | md5sum) + if [ "$orig_sum" != "$new_sum" ]; then + echo "Your committed man/*.Rd files are out of sync with the R headers." + exit 1 + fi + continue-on-error: true + + - name: Devtools checks + run: | + R -q -e "devtools::check(args = c('--no-examples', '--no-tests'))" | tee azure-pipelines_check.Rout + grep --quiet "^0 errors" azure-pipelines_check.Rout && grep --quiet " 0 warnings" azure-pipelines_check.Rout && grep --quiet " 0 notes" azure-pipelines_check.Rout + continue-on-error: true diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index 99e669031..b42d553e6 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -3,8 +3,12 @@ # DataSHIELD GHA test suite - dsBaseClient # Replaces azure-pipelines.yml / opal_azure-pipelines.yml / armadillo_azure-pipelines.yml. # +# This is one of three separate workflow files, each its own named check on a +# PR/run: this one (all the actual test execution), check.yaml (doc-sync + +# R CMD check), lint.yaml (lintr). Split so each shows as its own category +# rather than one graph mixing test execution with static checks. +# # Structure (all jobs below run in parallel except where "needs" says otherwise): -# r-checks - devtools::document()/check() sync checks. No backend needed. # opal-dsbase-smk (matrix x2) \ # opal-dsbase-perf (matrix x3) } dsBase suite against Opal, one category # opal-dsbase-other (matrix x2) / group per job - see below. @@ -73,59 +77,6 @@ env: jobs: - ################################################################################ - # Doc-sync / R CMD check - no backend needed, so this runs once, not per - # backend or per shard. - ################################################################################ - r-checks: - name: Package checks (doc sync, R CMD check) - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - - - uses: r-lib/actions/setup-pandoc@v2 - - - uses: r-lib/actions/setup-r@v2 - with: - r-version: release - http-user-agent: release - use-public-rspm: true - - - name: Install system libraries - run: | - sudo apt-get update -qq - sudo apt-get install -qq libxml2-dev libcurl4-openssl-dev libssl-dev libgsl-dev libgit2-dev -y - sudo apt-get install -qq libharfbuzz-dev libfribidi-dev libmagick++-dev libudunits2-dev libuv1-dev -y - - - uses: r-lib/actions/setup-r-dependencies@v2 - with: - dependencies: 'c("Depends", "Imports", "LinkingTo")' - extra-packages: | - any::rcmdcheck - cran::devtools - cran::usethis - github::datashield/dsDangerClient - needs: check - - - name: Check manual updated - run: | - orig_sum=$(find man -type f | sort -u | xargs cat | md5sum) - R -q -e "devtools::document()" - new_sum=$(find man -type f | sort -u | xargs cat | md5sum) - if [ "$orig_sum" != "$new_sum" ]; then - echo "Your committed man/*.Rd files are out of sync with the R headers." - exit 1 - fi - continue-on-error: true - - - name: Devtools checks - run: | - R -q -e "devtools::check(args = c('--no-examples', '--no-tests'))" | tee azure-pipelines_check.Rout - grep --quiet "^0 errors" azure-pipelines_check.Rout && grep --quiet " 0 warnings" azure-pipelines_check.Rout && grep --quiet " 0 notes" azure-pipelines_check.Rout - continue-on-error: true - - ################################################################################ # Opal - dsBase suite, sharded 4 ways. Each shard is a fully isolated job # with its own Opal instance. @@ -200,7 +151,7 @@ jobs: with: path: dsBaseClient - - uses: ./.github/actions/setup-opal-with-dsbase + - uses: ./dsBaseClient/.github/actions/setup-opal-with-dsbase with: dsbase-ref: v7.0-dev @@ -380,7 +331,7 @@ jobs: with: path: dsBaseClient - - uses: ./.github/actions/setup-armadillo-with-dsbase + - uses: ./dsBaseClient/.github/actions/setup-armadillo-with-dsbase - name: Install dsDangerClient run: | From 5ae770e8037dc8cffe368bcd5832a1bee3eaba04 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Mon, 31 Aug 2026 12:14:08 +0200 Subject: [PATCH 31/56] remove duplicate lint --- .github/workflows/lint.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index fd467b0f6..b3428ad04 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -2,6 +2,7 @@ name: Lint on: push: + branches: [main, master] pull_request: permissions: From c4c1a356217df72164d8fd877bc41e97c0ad785d Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Mon, 31 Aug 2026 12:29:47 +0200 Subject: [PATCH 32/56] Add missing dependency --- .github/workflows/lint.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index b3428ad04..4a071a588 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -29,6 +29,7 @@ jobs: dependencies: 'c("Depends", "Imports", "LinkingTo")' extra-packages: | cran::lintr + cran::jsonlite github::datashield/dsDangerClient - name: Lint @@ -43,7 +44,6 @@ jobs: cat("n lints:", length(lints), "\n") lintr::sarif_output(lints, "lintr_results.sarif") ' - continue-on-error: true - name: Upload lint results uses: github/codeql-action/upload-sarif@v3 From cf14b021c10531148591695125d4e9070686af46 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Mon, 31 Aug 2026 12:43:14 +0200 Subject: [PATCH 33/56] revert back to single github flow --- .../workflows/dsBaseClient_test_suite.yaml | 234 +++++++++++------- .github/workflows/lint.yaml | 29 +++ 2 files changed, 170 insertions(+), 93 deletions(-) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index b42d553e6..a9518b8ea 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -9,31 +9,31 @@ # rather than one graph mixing test execution with static checks. # # Structure (all jobs below run in parallel except where "needs" says otherwise): -# opal-dsbase-smk (matrix x2) \ -# opal-dsbase-perf (matrix x3) } dsBase suite against Opal, one category -# opal-dsbase-other (matrix x2) / group per job - see below. -# opal-dsdanger - dsDanger suite against Opal (small, not sharded). -# opal-report - needs the three opal-dsbase-* jobs + opal-dsdanger; merges results. -# armadillo-dsbase-smk (matrix x2) \ -# armadillo-dsbase-perf (matrix x3) } dsBase suite against Armadillo, same split. -# armadillo-dsbase-other(matrix x2) / -# armadillo-dsdanger - dsDanger suite against Armadillo. -# armadillo-report - needs the three armadillo-dsbase-* jobs + armadillo-dsdanger; merges results. -# test-summary - needs opal-report + armadillo-report; one combined -# coverage figure + pass/fail table + collapsible -# failure details, written to the job summary. +# opal-dsbase (matrix x7) - dsBase suite against Opal, one shard per +# category, all grouped under one summary box. +# opal-dsdanger - dsDanger suite against Opal (small, not sharded). +# opal-report - needs opal-dsbase + opal-dsdanger; merges results. +# armadillo-dsbase (matrix x7) - dsBase suite against Armadillo, same split. +# armadillo-dsdanger - dsDanger suite against Armadillo. +# armadillo-report - needs armadillo-dsbase + armadillo-dsdanger; merges results. +# test-summary - needs opal-report + armadillo-report; one combined +# coverage figure + pass/fail table + collapsible +# failure details, written to the job summary. # # The dsBase suite (matching TEST_FILTER_DSBASE - same 292 files the Azure -# pipelines run) is split into 7 shards across 3 category groups - smk (116 -# files, 2 shards), perf (51 fixed 30s-loop benchmarks, 3 shards - by far the -# slowest per-file), and other (arg + misc, 2 shards) - each shard with its -# own testthat filter substring. smk/perf are additionally split by the first -# letter of the function name (after any "ds." prefix) rather than an -# enumerated file list, so newly added test files fall into a bucket -# automatically. The 3 category groups are separate job definitions (not one -# 7-entry matrix) purely so they show as separate boxes in the run graph; -# each calls the shared _armadillo-dsbase-shard.yaml / _opal-dsbase-shard.yaml -# reusable workflow to avoid tripling the setup steps. +# pipelines run) is split into 7 shards - smk (116 files, 2 shards), perf (51 +# fixed 30s-loop benchmarks, 3 shards - by far the slowest per-file), arg (1 +# shard), and misc (1 shard) - each shard with its own testthat filter +# substring. smk/perf are split by the first letter of the function name +# (after any "ds." prefix) rather than an enumerated file list, so newly +# added test files fall into a bucket automatically. This is one job with a +# 7-entry matrix (not split into separate job definitions per category) so +# all shards share one summary box in the run graph, and so they don't share +# a reusable workflow name - GitHub's default same-name concurrency cap of 2 +# would otherwise throttle them to 2-at-a-time. Setup (checkout through +# installing dsBase) still lives in a shared composite action +# (setup-armadillo-with-dsbase / setup-opal-with-dsbase), reused by the +# dsdanger jobs too, so that part stays DRY without needing separate jobs. # # Each dsbase/dsdanger job spins up its OWN backend instance (isolated - no # shared server state / concurrency risk between categories running at once). @@ -81,56 +81,80 @@ jobs: # Opal - dsBase suite, sharded 4 ways. Each shard is a fully isolated job # with its own Opal instance. ################################################################################ - # Split into 3 caller jobs (smk / perf / other) rather than one 7-entry - # matrix, so each category group shows as its own box in the run graph. - # All three call the same reusable workflow (_opal-dsbase-shard.yaml) to - # avoid tripling the setup boilerplate. - opal-dsbase-smk: - name: Opal dsBase tests (smk) + # One job, 7-entry matrix, so all shards group under a single summary box + # in the run graph (previously split into 3 category-group jobs calling a + # reusable workflow - reverted since that lost the combined summary and, + # worse, made all 7 calls share the reusable workflow's name, which + # triggers GitHub's default same-name concurrency cap of 2 and throttled + # the shards to 2-at-a-time instead of running in parallel). + opal-dsbase: + name: Opal dsBase tests (${{ matrix.category }}) + runs-on: ubuntu-latest + timeout-minutes: 60 strategy: fail-fast: false + # smk and perf are split by the first letter of the function name + # (after any "ds." prefix) rather than an enumerated file list, so + # newly added test files fall into a bucket automatically. matrix: include: - category: smk-1 filter: 'smk-ds.[a-lA-L]|smk-(checkClass|isDefined)' - category: smk-2 filter: 'smk-ds.[m-vM-V]' - uses: ./.github/workflows/_opal-dsbase-shard.yaml - with: - category: ${{ matrix.category }} - filter: ${{ matrix.filter }} - - opal-dsbase-perf: - name: Opal dsBase tests (perf) - strategy: - fail-fast: false - matrix: - include: + - category: arg + filter: 'arg-' - category: perf-1 filter: 'perf-ds.[a-cA-C]' - category: perf-2 filter: 'perf-ds.[d-mD-M]' - category: perf-3 filter: 'perf-ds.[n-vN-V]|perf-(conndisconn|void)' - uses: ./.github/workflows/_opal-dsbase-shard.yaml - with: - category: ${{ matrix.category }} - filter: ${{ matrix.filter }} - - opal-dsbase-other: - name: Opal dsBase tests (other) - strategy: - fail-fast: false - matrix: - include: - - category: arg - filter: 'arg-' - category: misc filter: 'datachk-|disc-|expt-|smk_expt-|math-|_-' - uses: ./.github/workflows/_opal-dsbase-shard.yaml - with: - category: ${{ matrix.category }} - filter: ${{ matrix.filter }} + env: + PROJECT_NAME: dsBaseClient + DS_DRIVER: OpalDriver + steps: + - name: Checkout dsBaseClient + uses: actions/checkout@v4 + with: + path: dsBaseClient + + - uses: ./dsBaseClient/.github/actions/setup-opal-with-dsbase + + - name: Run dsBase tests with coverage & JUnit report + run: | + R -q -e "devtools::load_all();" + R -q -e ' + cov <- covr::package_coverage( + type = c("none"), + code = c('"'"' + output_file <- file("test_console_output_dsbase.txt"); + sink(output_file, split = TRUE); + junit_rep <- testthat::JunitReporter$new(file = file.path(getwd(), "test_results_dsbase.xml")); + progress_rep <- testthat::ProgressReporter$new(max_failures = 999999); + multi_rep <- testthat::MultiReporter$new(reporters = list(progress_rep, junit_rep)); + options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); + testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ matrix.filter }}", reporter = multi_rep, stop_on_failure = FALSE)'"'"' + ) + ) + saveRDS(cov, "coverage.rds") + write.csv(covr::coverage_to_list(cov), "coveragelist.csv")' || R_EXIT=$? + cat test_console_output_dsbase.txt + exit "${R_EXIT:-0}" + working-directory: dsBaseClient + + - name: Upload shard results + uses: actions/upload-artifact@v4 + with: + name: opal-dsbase-${{ matrix.category }} + path: | + dsBaseClient/test_results_dsbase.xml + dsBaseClient/test_console_output_dsbase.txt + dsBaseClient/coveragelist.csv + dsBaseClient/coverage.rds + dsBaseClient/tests/testthat/data_files/dsbase_version.txt ################################################################################ @@ -197,7 +221,7 @@ jobs: ################################################################################ opal-report: name: Opal report - needs: [opal-dsbase-smk, opal-dsbase-perf, opal-dsbase-other, opal-dsdanger] + needs: [opal-dsbase, opal-dsdanger] if: always() runs-on: ubuntu-latest timeout-minutes: 30 @@ -262,56 +286,80 @@ jobs: # run time. No process is restarted after installing dsBase - install then # whitelist directly. ################################################################################ - # Split into 3 caller jobs (smk / perf / other) rather than one 7-entry - # matrix, so each category group shows as its own box in the run graph. - # All three call the same reusable workflow (_armadillo-dsbase-shard.yaml) - # to avoid tripling the setup boilerplate. - armadillo-dsbase-smk: - name: Armadillo dsBase tests (smk) + # One job, 7-entry matrix, so all shards group under a single summary box + # in the run graph (previously split into 3 category-group jobs calling a + # reusable workflow - reverted since that lost the combined summary and, + # worse, made all 7 calls share the reusable workflow's name, which + # triggers GitHub's default same-name concurrency cap of 2 and throttled + # the shards to 2-at-a-time instead of running in parallel). + armadillo-dsbase: + name: Armadillo dsBase tests (${{ matrix.category }}) + runs-on: ubuntu-latest + timeout-minutes: 60 strategy: fail-fast: false + # smk and perf are split by the first letter of the function name + # (after any "ds." prefix) rather than an enumerated file list, so + # newly added test files fall into a bucket automatically. matrix: include: - category: smk-1 filter: 'smk-ds.[a-lA-L]|smk-(checkClass|isDefined)' - category: smk-2 filter: 'smk-ds.[m-vM-V]' - uses: ./.github/workflows/_armadillo-dsbase-shard.yaml - with: - category: ${{ matrix.category }} - filter: ${{ matrix.filter }} - - armadillo-dsbase-perf: - name: Armadillo dsBase tests (perf) - strategy: - fail-fast: false - matrix: - include: + - category: arg + filter: 'arg-' - category: perf-1 filter: 'perf-ds.[a-cA-C]' - category: perf-2 filter: 'perf-ds.[d-mD-M]' - category: perf-3 filter: 'perf-ds.[n-vN-V]|perf-(conndisconn|void)' - uses: ./.github/workflows/_armadillo-dsbase-shard.yaml - with: - category: ${{ matrix.category }} - filter: ${{ matrix.filter }} - - armadillo-dsbase-other: - name: Armadillo dsBase tests (other) - strategy: - fail-fast: false - matrix: - include: - - category: arg - filter: 'arg-' - category: misc filter: 'datachk-|disc-|expt-|smk_expt-|math-|_-' - uses: ./.github/workflows/_armadillo-dsbase-shard.yaml - with: - category: ${{ matrix.category }} - filter: ${{ matrix.filter }} + env: + PROJECT_NAME: dsBaseClient + DS_DRIVER: ArmadilloDriver + steps: + - name: Checkout dsBaseClient + uses: actions/checkout@v4 + with: + path: dsBaseClient + + - uses: ./dsBaseClient/.github/actions/setup-armadillo-with-dsbase + + - name: Run dsBase tests with coverage & JUnit report + run: | + R -q -e "devtools::load_all();" + R -q -e ' + cov <- covr::package_coverage( + type = c("none"), + code = c('"'"' + output_file <- file("test_console_output_dsbase.txt"); + sink(output_file, split = TRUE); + junit_rep <- testthat::JunitReporter$new(file = file.path(getwd(), "test_results_dsbase.xml")); + progress_rep <- testthat::ProgressReporter$new(max_failures = 999999); + multi_rep <- testthat::MultiReporter$new(reporters = list(progress_rep, junit_rep)); + options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); + testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ matrix.filter }}", reporter = multi_rep, stop_on_failure = FALSE)'"'"' + ) + ) + saveRDS(cov, "coverage.rds") + write.csv(covr::coverage_to_list(cov), "coveragelist.csv")' || R_EXIT=$? + cat test_console_output_dsbase.txt + exit "${R_EXIT:-0}" + working-directory: dsBaseClient + + - name: Upload shard results + uses: actions/upload-artifact@v4 + with: + name: armadillo-dsbase-${{ matrix.category }} + path: | + dsBaseClient/test_results_dsbase.xml + dsBaseClient/test_console_output_dsbase.txt + dsBaseClient/coveragelist.csv + dsBaseClient/coverage.rds + dsBaseClient/dsbase_version.txt ################################################################################ @@ -390,7 +438,7 @@ jobs: ################################################################################ armadillo-report: name: Armadillo report - needs: [armadillo-dsbase-smk, armadillo-dsbase-perf, armadillo-dsbase-other, armadillo-dsdanger] + needs: [armadillo-dsbase, armadillo-dsdanger] if: always() runs-on: ubuntu-latest timeout-minutes: 30 diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 4a071a588..ef9a391f7 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -43,6 +43,35 @@ jobs: lints <- lintr::lint_package(linters = linter_funs) cat("n lints:", length(lints), "\n") lintr::sarif_output(lints, "lintr_results.sarif") + + if (length(lints) > 0) { + linter_names <- vapply(lints, function(l) l$linter, character(1)) + counts <- sort(table(linter_names), decreasing = TRUE) + count_rows <- paste0("| ", names(counts), " | ", as.integer(counts), " |") + + finding_lines <- vapply(lints, function(l) { + sprintf("- `%s:%d` %s - %s", basename(l$filename), l$line_number, l$linter, l$message) + }, character(1)) + + summary <- c( + "## Lint results", + "", + sprintf("**%d findings** (correctness/robustness/common_mistakes linters)", length(lints)), + "", + "| Linter | Count |", + "|---|---|", + count_rows, + "", + "
All findings", + "", + finding_lines, + "", + "
" + ) + } else { + summary <- c("## Lint results", "", "No findings.") + } + writeLines(summary, Sys.getenv("GITHUB_STEP_SUMMARY")) ' - name: Upload lint results From 78a50642b4700035d33149ed507cbfbd166cde4a Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Mon, 31 Aug 2026 12:55:54 +0200 Subject: [PATCH 34/56] fail on codecov and test failure --- .../workflows/_armadillo-dsbase-shard.yaml | 69 ------------------- .github/workflows/_opal-dsbase-shard.yaml | 69 ------------------- .../workflows/dsBaseClient_test_suite.yaml | 25 ++++++- .github/workflows/lint.yaml | 8 +++ 4 files changed, 31 insertions(+), 140 deletions(-) delete mode 100644 .github/workflows/_armadillo-dsbase-shard.yaml delete mode 100644 .github/workflows/_opal-dsbase-shard.yaml diff --git a/.github/workflows/_armadillo-dsbase-shard.yaml b/.github/workflows/_armadillo-dsbase-shard.yaml deleted file mode 100644 index 4944d851f..000000000 --- a/.github/workflows/_armadillo-dsbase-shard.yaml +++ /dev/null @@ -1,69 +0,0 @@ -name: Armadillo dsBase shard - -# Reusable workflow: runs one dsBase testthat filter against a fresh Armadillo -# instance. Called (with a different category/filter per matrix entry) from -# dsBaseClient_test_suite.yaml's armadillo-dsbase-smk/-perf/-other jobs, so -# each category group shows as its own box in the run graph instead of one -# flat 7-shard matrix, without duplicating this setup 3x. The setup itself -# (checkout through installing dsBase) lives in the setup-armadillo-with- -# dsbase composite action, shared with armadillo-dsdanger too. - -on: - workflow_call: - inputs: - category: - required: true - type: string - filter: - required: true - type: string - -env: - PROJECT_NAME: dsBaseClient - DS_DRIVER: ArmadilloDriver - -jobs: - run-shard: - name: Armadillo dsBase tests (${{ inputs.category }}) - runs-on: ubuntu-latest - timeout-minutes: 60 - steps: - - name: Checkout dsBaseClient - uses: actions/checkout@v4 - with: - path: dsBaseClient - - - uses: ./dsBaseClient/.github/actions/setup-armadillo-with-dsbase - - - name: Run dsBase tests with coverage & JUnit report - run: | - R -q -e "devtools::load_all();" - R -q -e ' - cov <- covr::package_coverage( - type = c("none"), - code = c('"'"' - output_file <- file("test_console_output_dsbase.txt"); - sink(output_file, split = TRUE); - junit_rep <- testthat::JunitReporter$new(file = file.path(getwd(), "test_results_dsbase.xml")); - progress_rep <- testthat::ProgressReporter$new(max_failures = 999999); - multi_rep <- testthat::MultiReporter$new(reporters = list(progress_rep, junit_rep)); - options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); - testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ inputs.filter }}", reporter = multi_rep, stop_on_failure = FALSE)'"'"' - ) - ) - saveRDS(cov, "coverage.rds") - write.csv(covr::coverage_to_list(cov), "coveragelist.csv")' || R_EXIT=$? - cat test_console_output_dsbase.txt - exit "${R_EXIT:-0}" - working-directory: dsBaseClient - - - name: Upload shard results - uses: actions/upload-artifact@v4 - with: - name: armadillo-dsbase-${{ inputs.category }} - path: | - dsBaseClient/test_results_dsbase.xml - dsBaseClient/test_console_output_dsbase.txt - dsBaseClient/coveragelist.csv - dsBaseClient/coverage.rds - dsBaseClient/dsbase_version.txt diff --git a/.github/workflows/_opal-dsbase-shard.yaml b/.github/workflows/_opal-dsbase-shard.yaml deleted file mode 100644 index 7812ca308..000000000 --- a/.github/workflows/_opal-dsbase-shard.yaml +++ /dev/null @@ -1,69 +0,0 @@ -name: Opal dsBase shard - -# Reusable workflow: runs one dsBase testthat filter against a fresh Opal -# instance. Called (with a different category/filter per matrix entry) from -# dsBaseClient_test_suite.yaml's opal-dsbase-smk/-perf/-other jobs, so each -# category group shows as its own box in the run graph instead of one flat -# 7-shard matrix, without duplicating this setup 3x. The setup itself -# (checkout through installing dsBase) lives in the setup-opal-with-dsbase -# composite action, shared with opal-dsdanger too. - -on: - workflow_call: - inputs: - category: - required: true - type: string - filter: - required: true - type: string - -env: - PROJECT_NAME: dsBaseClient - DS_DRIVER: OpalDriver - -jobs: - run-shard: - name: Opal dsBase tests (${{ inputs.category }}) - runs-on: ubuntu-latest - timeout-minutes: 60 - steps: - - name: Checkout dsBaseClient - uses: actions/checkout@v4 - with: - path: dsBaseClient - - - uses: ./dsBaseClient/.github/actions/setup-opal-with-dsbase - - - name: Run dsBase tests with coverage & JUnit report - run: | - R -q -e "devtools::load_all();" - R -q -e ' - cov <- covr::package_coverage( - type = c("none"), - code = c('"'"' - output_file <- file("test_console_output_dsbase.txt"); - sink(output_file, split = TRUE); - junit_rep <- testthat::JunitReporter$new(file = file.path(getwd(), "test_results_dsbase.xml")); - progress_rep <- testthat::ProgressReporter$new(max_failures = 999999); - multi_rep <- testthat::MultiReporter$new(reporters = list(progress_rep, junit_rep)); - options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); - testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ inputs.filter }}", reporter = multi_rep, stop_on_failure = FALSE)'"'"' - ) - ) - saveRDS(cov, "coverage.rds") - write.csv(covr::coverage_to_list(cov), "coveragelist.csv")' || R_EXIT=$? - cat test_console_output_dsbase.txt - exit "${R_EXIT:-0}" - working-directory: dsBaseClient - - - name: Upload shard results - uses: actions/upload-artifact@v4 - with: - name: opal-dsbase-${{ inputs.category }} - path: | - dsBaseClient/test_results_dsbase.xml - dsBaseClient/test_console_output_dsbase.txt - dsBaseClient/coveragelist.csv - dsBaseClient/coverage.rds - dsBaseClient/tests/testthat/data_files/dsbase_version.txt diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index a9518b8ea..7ea5a666f 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -532,6 +532,7 @@ jobs: path: artifacts/shards - name: Write combined job summary + id: summary run: | Rscript -e ' find_version <- function(prefix) { @@ -576,20 +577,29 @@ jobs: fail_lines, "", "
" - ) + ), + n_failures = n_failures, + n_errors = n_errors ) } armadillo <- summarise_backend("Armadillo", "artifacts/reports/armadillo-report-results", find_version("armadillo-dsbase")) opal <- summarise_backend("Opal", "artifacts/reports/opal-report-results", find_version("opal-dsbase")) + coverage_threshold <- 80 rds_files <- list.files("artifacts/shards", pattern = "coverage\\.rds$", recursive = TRUE, full.names = TRUE) if (length(rds_files) > 0) { tallies <- lapply(rds_files, function(f) covr::tally_coverage(readRDS(f))) agg <- aggregate(value ~ filename + line, data = do.call(rbind, tallies), FUN = sum) totalcoverage <- round(sum(agg$value > 0) / nrow(agg) * 100, 1) - cov_line <- sprintf("**Combined coverage: %.1f%%**", totalcoverage) + coverage_ok <- totalcoverage >= coverage_threshold + cov_line <- if (coverage_ok) { + sprintf("**Combined coverage: %.1f%%**", totalcoverage) + } else { + sprintf("**Combined coverage: %.1f%%** - below the %d%% threshold", totalcoverage, coverage_threshold) + } } else { + coverage_ok <- FALSE cov_line <- "No coverage data found." } @@ -608,4 +618,15 @@ jobs: opal$details ) writeLines(summary, Sys.getenv("GITHUB_STEP_SUMMARY")) + + tests_ok <- (armadillo$n_failures + armadillo$n_errors + opal$n_failures + opal$n_errors) == 0 + cat(sprintf("coverage_ok=%s\ntests_ok=%s\n", tolower(coverage_ok), tolower(tests_ok)), + file = Sys.getenv("GITHUB_OUTPUT"), append = TRUE) ' + + - name: Enforce coverage & test thresholds + if: steps.summary.outputs.coverage_ok != 'true' || steps.summary.outputs.tests_ok != 'true' + run: | + echo "Coverage ok: ${{ steps.summary.outputs.coverage_ok }}" + echo "Tests ok: ${{ steps.summary.outputs.tests_ok }}" + exit 1 diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index ef9a391f7..0c9d913b5 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -33,6 +33,7 @@ jobs: github::datashield/dsDangerClient - name: Lint + id: lint run: | Rscript -e ' al <- lintr::available_linters() @@ -72,9 +73,16 @@ jobs: summary <- c("## Lint results", "", "No findings.") } writeLines(summary, Sys.getenv("GITHUB_STEP_SUMMARY")) + + cat(sprintf("has_findings=%s\n", tolower(length(lints) > 0)), + file = Sys.getenv("GITHUB_OUTPUT"), append = TRUE) ' - name: Upload lint results uses: github/codeql-action/upload-sarif@v3 with: sarif_file: lintr_results.sarif + + - name: Fail on lint findings + if: steps.lint.outputs.has_findings == 'true' + run: exit 1 From 9adab0c9b084f656c618b9d90b628153449d598c Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Mon, 31 Aug 2026 13:46:09 +0200 Subject: [PATCH 35/56] try: connect with codecov --- .../workflows/dsBaseClient_test_suite.yaml | 267 +++++++++--------- .github/workflows/lint.yaml | 5 +- 2 files changed, 138 insertions(+), 134 deletions(-) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index 7ea5a666f..fc39c8183 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -9,13 +9,12 @@ # rather than one graph mixing test execution with static checks. # # Structure (all jobs below run in parallel except where "needs" says otherwise): -# opal-dsbase (matrix x7) - dsBase suite against Opal, one shard per -# category, all grouped under one summary box. -# opal-dsdanger - dsDanger suite against Opal (small, not sharded). -# opal-report - needs opal-dsbase + opal-dsdanger; merges results. -# armadillo-dsbase (matrix x7) - dsBase suite against Armadillo, same split. -# armadillo-dsdanger - dsDanger suite against Armadillo. -# armadillo-report - needs armadillo-dsbase + armadillo-dsdanger; merges results. +# opal-dsbase (matrix x8) - dsBase suite against Opal, one shard per +# category, plus a dsdanger entry, all grouped under +# one summary box. +# opal-report - needs opal-dsbase; merges results. +# armadillo-dsbase (matrix x8) - dsBase suite against Armadillo, same split. +# armadillo-report - needs armadillo-dsbase; merges results. # test-summary - needs opal-report + armadillo-report; one combined # coverage figure + pass/fail table + collapsible # failure details, written to the job summary. @@ -26,14 +25,17 @@ # shard), and misc (1 shard) - each shard with its own testthat filter # substring. smk/perf are split by the first letter of the function name # (after any "ds." prefix) rather than an enumerated file list, so newly -# added test files fall into a bucket automatically. This is one job with a -# 7-entry matrix (not split into separate job definitions per category) so -# all shards share one summary box in the run graph, and so they don't share -# a reusable workflow name - GitHub's default same-name concurrency cap of 2 -# would otherwise throttle them to 2-at-a-time. Setup (checkout through -# installing dsBase) still lives in a shared composite action -# (setup-armadillo-with-dsbase / setup-opal-with-dsbase), reused by the -# dsdanger jobs too, so that part stays DRY without needing separate jobs. +# added test files fall into a bucket automatically. The small dsDanger suite +# is folded in as an 8th matrix entry (steps gated on matrix.category == +# 'dsdanger') rather than a standalone job, so it groups under the same +# summary box instead of its own. This is one job with a matrix (not split +# into separate job definitions per category) so all entries share one +# summary box in the run graph, and so they don't share a reusable workflow +# name - GitHub's default same-name concurrency cap of 2 would otherwise +# throttle them to 2-at-a-time. Setup (checkout through installing dsBase) +# lives in a shared composite action (setup-armadillo-with-dsbase / +# setup-opal-with-dsbase), used by every matrix entry including dsdanger, so +# that part stays DRY. # # Each dsbase/dsdanger job spins up its OWN backend instance (isolated - no # shared server state / concurrency risk between categories running at once). @@ -78,24 +80,27 @@ env: jobs: ################################################################################ - # Opal - dsBase suite, sharded 4 ways. Each shard is a fully isolated job - # with its own Opal instance. + # Opal - dsBase suite, sharded, plus dsDanger folded in as an extra matrix + # entry. Each entry is a fully isolated job with its own Opal instance. ################################################################################ - # One job, 7-entry matrix, so all shards group under a single summary box + # One job, 8-entry matrix, so all entries group under a single summary box # in the run graph (previously split into 3 category-group jobs calling a # reusable workflow - reverted since that lost the combined summary and, - # worse, made all 7 calls share the reusable workflow's name, which - # triggers GitHub's default same-name concurrency cap of 2 and throttled - # the shards to 2-at-a-time instead of running in parallel). + # worse, made all calls share the reusable workflow's name, which triggers + # GitHub's default same-name concurrency cap of 2 and throttled the shards + # to 2-at-a-time instead of running in parallel). opal-dsbase: - name: Opal dsBase tests (${{ matrix.category }}) + name: Opal tests (${{ matrix.category }}) runs-on: ubuntu-latest timeout-minutes: 60 strategy: fail-fast: false # smk and perf are split by the first letter of the function name # (after any "ds." prefix) rather than an enumerated file list, so - # newly added test files fall into a bucket automatically. + # newly added test files fall into a bucket automatically. dsdanger is + # a separate, smaller suite (steps below are gated on + # matrix.category == 'dsdanger') folded in here so it groups under this + # job's summary box instead of a standalone job. matrix: include: - category: smk-1 @@ -112,9 +117,11 @@ jobs: filter: 'perf-ds.[n-vN-V]|perf-(conndisconn|void)' - category: misc filter: 'datachk-|disc-|expt-|smk_expt-|math-|_-' + - category: dsdanger env: PROJECT_NAME: dsBaseClient DS_DRIVER: OpalDriver + DSDANGER_REF: '6.3.4' steps: - name: Checkout dsBaseClient uses: actions/checkout@v4 @@ -123,7 +130,23 @@ jobs: - uses: ./dsBaseClient/.github/actions/setup-opal-with-dsbase + - name: Install dsDangerClient + if: matrix.category == 'dsdanger' + run: | + R -q -e " + ref <- Sys.getenv('BRANCH_NAME') + ok <- tryCatch({ pak::pkg_install(sprintf('github::datashield/dsDangerClient@%s', ref)); TRUE }, error = function(e) FALSE) + if (!ok) pak::pkg_install('github::datashield/dsDangerClient')" + + - name: Install dsDanger package on Opal server + if: matrix.category == 'dsdanger' + run: | + R -q -e "library(opalr); opal <- opal.login(username = 'administrator', password = 'datashield_test&', url = 'http://localhost:8080'); opal.put(opal, 'system', 'conf', 'general', '_rPackage'); opal.logout(opal)" + R -q -e "library(opalr); opal <- opal.login('administrator','datashield_test&', url='http://localhost:8080/'); dsadmin.install_github_package(opal, 'dsDanger', username = 'datashield', ref = '${{ env.DSDANGER_REF }}'); opal.logout(opal)" + working-directory: dsBaseClient + - name: Run dsBase tests with coverage & JUnit report + if: matrix.category != 'dsdanger' run: | R -q -e "devtools::load_all();" R -q -e ' @@ -140,59 +163,14 @@ jobs: ) ) saveRDS(cov, "coverage.rds") - write.csv(covr::coverage_to_list(cov), "coveragelist.csv")' || R_EXIT=$? + write.csv(covr::coverage_to_list(cov), "coveragelist.csv") + covr::to_cobertura(cov, "cobertura.xml")' || R_EXIT=$? cat test_console_output_dsbase.txt exit "${R_EXIT:-0}" working-directory: dsBaseClient - - name: Upload shard results - uses: actions/upload-artifact@v4 - with: - name: opal-dsbase-${{ matrix.category }} - path: | - dsBaseClient/test_results_dsbase.xml - dsBaseClient/test_console_output_dsbase.txt - dsBaseClient/coveragelist.csv - dsBaseClient/coverage.rds - dsBaseClient/tests/testthat/data_files/dsbase_version.txt - - - ################################################################################ - # Opal - dsDanger suite. Small (~15 files), so it stays a single job rather - # than sharding further. - ################################################################################ - opal-dsdanger: - name: Opal dsDanger tests - runs-on: ubuntu-latest - timeout-minutes: 60 - env: - DS_DRIVER: OpalDriver - DSDANGER_REF: '6.3.4' - - steps: - - name: Checkout dsBaseClient - uses: actions/checkout@v4 - with: - path: dsBaseClient - - - uses: ./dsBaseClient/.github/actions/setup-opal-with-dsbase - with: - dsbase-ref: v7.0-dev - - - name: Install dsDangerClient - run: | - R -q -e " - ref <- Sys.getenv('BRANCH_NAME') - ok <- tryCatch({ pak::pkg_install(sprintf('github::datashield/dsDangerClient@%s', ref)); TRUE }, error = function(e) FALSE) - if (!ok) pak::pkg_install('github::datashield/dsDangerClient')" - - - name: Install dsDanger package on Opal server - run: | - R -q -e "library(opalr); opal <- opal.login(username = 'administrator', password = 'datashield_test&', url = 'http://localhost:8080'); opal.put(opal, 'system', 'conf', 'general', '_rPackage'); opal.logout(opal)" - R -q -e "library(opalr); opal <- opal.login('administrator','datashield_test&', url='http://localhost:8080/'); dsadmin.install_github_package(opal, 'dsDanger', username = 'datashield', ref = '${{ env.DSDANGER_REF }}'); opal.logout(opal)" - working-directory: dsBaseClient - - name: Run dsDanger tests with JUnit report + if: matrix.category == 'dsdanger' run: | R -q -e ' library(testthat); @@ -207,7 +185,20 @@ jobs: exit "${R_EXIT:-0}" working-directory: dsBaseClient + - name: Upload shard results + if: matrix.category != 'dsdanger' + uses: actions/upload-artifact@v4 + with: + name: opal-dsbase-${{ matrix.category }} + path: | + dsBaseClient/test_results_dsbase.xml + dsBaseClient/test_console_output_dsbase.txt + dsBaseClient/coveragelist.csv + dsBaseClient/coverage.rds + dsBaseClient/tests/testthat/data_files/dsbase_version.txt + - name: Upload dsDanger results + if: matrix.category == 'dsdanger' uses: actions/upload-artifact@v4 with: name: opal-dsdanger @@ -215,13 +206,22 @@ jobs: dsBaseClient/test_results_dsdanger.xml dsBaseClient/test_console_output_dsdanger.txt + - name: Upload coverage to Codecov + if: matrix.category != 'dsdanger' + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: dsBaseClient/cobertura.xml + flags: opal-${{ matrix.category }} + fail_ci_if_error: false + ################################################################################ - # Opal - merge all shard + dsDanger results and publish the report. + # Opal - merge all matrix entry results and publish the report. ################################################################################ opal-report: name: Opal report - needs: [opal-dsbase, opal-dsdanger] + needs: [opal-dsbase] if: always() runs-on: ubuntu-latest timeout-minutes: 30 @@ -286,21 +286,24 @@ jobs: # run time. No process is restarted after installing dsBase - install then # whitelist directly. ################################################################################ - # One job, 7-entry matrix, so all shards group under a single summary box + # One job, 8-entry matrix, so all entries group under a single summary box # in the run graph (previously split into 3 category-group jobs calling a # reusable workflow - reverted since that lost the combined summary and, - # worse, made all 7 calls share the reusable workflow's name, which - # triggers GitHub's default same-name concurrency cap of 2 and throttled - # the shards to 2-at-a-time instead of running in parallel). + # worse, made all calls share the reusable workflow's name, which triggers + # GitHub's default same-name concurrency cap of 2 and throttled the shards + # to 2-at-a-time instead of running in parallel). armadillo-dsbase: - name: Armadillo dsBase tests (${{ matrix.category }}) + name: Armadillo tests (${{ matrix.category }}) runs-on: ubuntu-latest timeout-minutes: 60 strategy: fail-fast: false # smk and perf are split by the first letter of the function name # (after any "ds." prefix) rather than an enumerated file list, so - # newly added test files fall into a bucket automatically. + # newly added test files fall into a bucket automatically. dsdanger is + # a separate, smaller suite (steps below are gated on + # matrix.category == 'dsdanger') folded in here so it groups under this + # job's summary box instead of a standalone job. matrix: include: - category: smk-1 @@ -317,62 +320,11 @@ jobs: filter: 'perf-ds.[n-vN-V]|perf-(conndisconn|void)' - category: misc filter: 'datachk-|disc-|expt-|smk_expt-|math-|_-' + - category: dsdanger env: PROJECT_NAME: dsBaseClient DS_DRIVER: ArmadilloDriver - steps: - - name: Checkout dsBaseClient - uses: actions/checkout@v4 - with: - path: dsBaseClient - - - uses: ./dsBaseClient/.github/actions/setup-armadillo-with-dsbase - - - name: Run dsBase tests with coverage & JUnit report - run: | - R -q -e "devtools::load_all();" - R -q -e ' - cov <- covr::package_coverage( - type = c("none"), - code = c('"'"' - output_file <- file("test_console_output_dsbase.txt"); - sink(output_file, split = TRUE); - junit_rep <- testthat::JunitReporter$new(file = file.path(getwd(), "test_results_dsbase.xml")); - progress_rep <- testthat::ProgressReporter$new(max_failures = 999999); - multi_rep <- testthat::MultiReporter$new(reporters = list(progress_rep, junit_rep)); - options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); - testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ matrix.filter }}", reporter = multi_rep, stop_on_failure = FALSE)'"'"' - ) - ) - saveRDS(cov, "coverage.rds") - write.csv(covr::coverage_to_list(cov), "coveragelist.csv")' || R_EXIT=$? - cat test_console_output_dsbase.txt - exit "${R_EXIT:-0}" - working-directory: dsBaseClient - - - name: Upload shard results - uses: actions/upload-artifact@v4 - with: - name: armadillo-dsbase-${{ matrix.category }} - path: | - dsBaseClient/test_results_dsbase.xml - dsBaseClient/test_console_output_dsbase.txt - dsBaseClient/coveragelist.csv - dsBaseClient/coverage.rds - dsBaseClient/dsbase_version.txt - - - ################################################################################ - # Armadillo - dsDanger suite. Small, so it stays a single job. - ################################################################################ - armadillo-dsdanger: - name: Armadillo dsDanger tests - runs-on: ubuntu-latest - timeout-minutes: 60 - env: - DS_DRIVER: ArmadilloDriver DSDANGER_TARBALL: dsDanger_6.3.4.tar.gz - steps: - name: Checkout dsBaseClient uses: actions/checkout@v4 @@ -382,6 +334,7 @@ jobs: - uses: ./dsBaseClient/.github/actions/setup-armadillo-with-dsbase - name: Install dsDangerClient + if: matrix.category == 'dsdanger' run: | R -q -e " ref <- Sys.getenv('BRANCH_NAME') @@ -389,6 +342,7 @@ jobs: if (!ok) pak::pkg_install('github::datashield/dsDangerClient')" - name: Install dsDanger package on Armadillo server + if: matrix.category == 'dsdanger' run: | curl -u admin:admin http://localhost:8080/whitelist install_status=$(curl -u admin:admin -H 'Content-Type: multipart/form-data' -F "file=@${{ env.DSDANGER_TARBALL }}" -o /dev/null -w '%{http_code}' -X POST http://localhost:8080/install-package) @@ -409,7 +363,32 @@ jobs: curl -u admin:admin http://localhost:8080/whitelist working-directory: dsBaseClient + - name: Run dsBase tests with coverage & JUnit report + if: matrix.category != 'dsdanger' + run: | + R -q -e "devtools::load_all();" + R -q -e ' + cov <- covr::package_coverage( + type = c("none"), + code = c('"'"' + output_file <- file("test_console_output_dsbase.txt"); + sink(output_file, split = TRUE); + junit_rep <- testthat::JunitReporter$new(file = file.path(getwd(), "test_results_dsbase.xml")); + progress_rep <- testthat::ProgressReporter$new(max_failures = 999999); + multi_rep <- testthat::MultiReporter$new(reporters = list(progress_rep, junit_rep)); + options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); + testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ matrix.filter }}", reporter = multi_rep, stop_on_failure = FALSE)'"'"' + ) + ) + saveRDS(cov, "coverage.rds") + write.csv(covr::coverage_to_list(cov), "coveragelist.csv") + covr::to_cobertura(cov, "cobertura.xml")' || R_EXIT=$? + cat test_console_output_dsbase.txt + exit "${R_EXIT:-0}" + working-directory: dsBaseClient + - name: Run dsDanger tests with JUnit report + if: matrix.category == 'dsdanger' run: | R -q -e ' library(testthat); @@ -424,7 +403,20 @@ jobs: exit "${R_EXIT:-0}" working-directory: dsBaseClient + - name: Upload shard results + if: matrix.category != 'dsdanger' + uses: actions/upload-artifact@v4 + with: + name: armadillo-dsbase-${{ matrix.category }} + path: | + dsBaseClient/test_results_dsbase.xml + dsBaseClient/test_console_output_dsbase.txt + dsBaseClient/coveragelist.csv + dsBaseClient/coverage.rds + dsBaseClient/dsbase_version.txt + - name: Upload dsDanger results + if: matrix.category == 'dsdanger' uses: actions/upload-artifact@v4 with: name: armadillo-dsdanger @@ -432,13 +424,22 @@ jobs: dsBaseClient/test_results_dsdanger.xml dsBaseClient/test_console_output_dsdanger.txt + - name: Upload coverage to Codecov + if: matrix.category != 'dsdanger' + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: dsBaseClient/cobertura.xml + flags: armadillo-${{ matrix.category }} + fail_ci_if_error: false + ################################################################################ - # Armadillo - merge all shard + dsDanger results and publish the report. + # Armadillo - merge all matrix entry results and publish the report. ################################################################################ armadillo-report: name: Armadillo report - needs: [armadillo-dsbase, armadillo-dsdanger] + needs: [armadillo-dsbase] if: always() runs-on: ubuntu-latest timeout-minutes: 30 diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 0c9d913b5..0e38c22f0 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -43,6 +43,7 @@ jobs: names(linter_funs) <- sel lints <- lintr::lint_package(linters = linter_funs) cat("n lints:", length(lints), "\n") + print(lints) # emits ::warning file=...,line=...:: annotations (auto-detects GitHub Actions) lintr::sarif_output(lints, "lintr_results.sarif") if (length(lints) > 0) { @@ -85,4 +86,6 @@ jobs: - name: Fail on lint findings if: steps.lint.outputs.has_findings == 'true' - run: exit 1 + run: | + echo "Lint found issues - see the annotations above, the job summary, or the SARIF upload for details." + exit 1 From 234a16ee0910e06753fc2bb94a7dc7778e9287e3 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Mon, 31 Aug 2026 14:18:40 +0200 Subject: [PATCH 36/56] Added codecov link and perf profiles --- .github/workflows/dsBaseClient_test_suite.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index fc39c8183..e742292c8 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -71,6 +71,11 @@ env: PROJECT_NAME: dsBaseClient BRANCH_NAME: ${{ github.head_ref || github.ref_name }} R_KEEP_PKG_SOURCE: yes + # Selects perf_files/__perf-profile.csv as the perf + # test reference rates/tolerances (see tests/testthat/perf_tests/perf_rate.R). + # Reuses the existing azure-pipeline reference files rather than adding new + # ones, matching what the old Azure pipelines set via perf.profile. + PERF_PROFILE: azure-pipeline # These should all be constant, except the two TEST_FILTER_* values. These can # be used to test subsets of test files in the testthat directory. Options are # like: '*' <- run all tests, '*_smk_*' <- run all the smoke tests. @@ -604,10 +609,13 @@ jobs: cov_line <- "No coverage data found." } + codecov_url <- sprintf("https://app.codecov.io/gh/%s/commit/%s", Sys.getenv("GITHUB_REPOSITORY"), Sys.getenv("GITHUB_SHA")) + summary <- c( "## dsBaseClient test suite results", "", cov_line, + sprintf("[View coverage report on Codecov](%s)", codecov_url), "", "| Backend | dsBase version | Tests | Failures | Errors | Skipped |", "|---|---|---|---|---|---|", From 02abcbd6ccc9acf8cde9d1dc9ec5021061ab4ab9 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Mon, 31 Aug 2026 15:24:34 +0200 Subject: [PATCH 37/56] cleaned up --- .../setup-armadillo-with-dsbase/action.yaml | 2 -- .../actions/setup-opal-with-dsbase/action.yaml | 2 -- codecov.yml | 15 +++++++++++++++ 3 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 codecov.yml diff --git a/.github/actions/setup-armadillo-with-dsbase/action.yaml b/.github/actions/setup-armadillo-with-dsbase/action.yaml index 77c49ca3d..826afe7e8 100644 --- a/.github/actions/setup-armadillo-with-dsbase/action.yaml +++ b/.github/actions/setup-armadillo-with-dsbase/action.yaml @@ -104,8 +104,6 @@ runs: dependencies: 'c("Depends", "Imports", "LinkingTo")' extra-packages: | cran::devtools - cran::usethis - cran::git2r cran::covr cran::fields cran::meta diff --git a/.github/actions/setup-opal-with-dsbase/action.yaml b/.github/actions/setup-opal-with-dsbase/action.yaml index bf9c45cbd..c97af580a 100644 --- a/.github/actions/setup-opal-with-dsbase/action.yaml +++ b/.github/actions/setup-opal-with-dsbase/action.yaml @@ -50,8 +50,6 @@ runs: dependencies: 'c("Depends", "Imports", "LinkingTo")' extra-packages: | cran::devtools - cran::usethis - cran::git2r cran::covr cran::fields cran::meta diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..8455adf15 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,15 @@ +coverage: + status: + # Whole-repo coverage trend - informational only, never fails a PR. The + # combined figure is still shown in the GitHub Actions job summary + # (test-summary job) alongside a link to the full Codecov report. + project: + default: + target: auto + informational: true + # Coverage of lines actually changed by the PR - this is the enforced + # gate, so new/changed code is held to a bar without blocking on the + # pre-existing coverage backlog elsewhere in the repo. + patch: + default: + target: 80% From d4e681d00e8e3a33727153a758729b028487c524 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:06:46 +0200 Subject: [PATCH 38/56] tidied reporting --- .github/workflows/check.yaml | 22 +++++-- .../workflows/dsBaseClient_test_suite.yaml | 62 +++++++++++++----- .github/workflows/lint.yaml | 65 +++++++++++++++---- 3 files changed, 112 insertions(+), 37 deletions(-) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index c3fdaea58..df1948669 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -39,22 +39,34 @@ jobs: any::rcmdcheck cran::devtools cran::usethis - github::datashield/dsDangerClient needs: check - name: Check manual updated + id: docsync run: | orig_sum=$(find man -type f | sort -u | xargs cat | md5sum) R -q -e "devtools::document()" new_sum=$(find man -type f | sort -u | xargs cat | md5sum) if [ "$orig_sum" != "$new_sum" ]; then echo "Your committed man/*.Rd files are out of sync with the R headers." - exit 1 + echo "ok=false" >> "$GITHUB_OUTPUT" + else + echo "ok=true" >> "$GITHUB_OUTPUT" fi - continue-on-error: true - name: Devtools checks + id: rcmdcheck run: | R -q -e "devtools::check(args = c('--no-examples', '--no-tests'))" | tee azure-pipelines_check.Rout - grep --quiet "^0 errors" azure-pipelines_check.Rout && grep --quiet " 0 warnings" azure-pipelines_check.Rout && grep --quiet " 0 notes" azure-pipelines_check.Rout - continue-on-error: true + if grep --quiet "^0 errors" azure-pipelines_check.Rout && grep --quiet " 0 warnings" azure-pipelines_check.Rout && grep --quiet " 0 notes" azure-pipelines_check.Rout; then + echo "ok=true" >> "$GITHUB_OUTPUT" + else + echo "ok=false" >> "$GITHUB_OUTPUT" + fi + + - name: Enforce check results + if: steps.docsync.outputs.ok != 'true' || steps.rcmdcheck.outputs.ok != 'true' + run: | + echo "Doc sync ok: ${{ steps.docsync.outputs.ok }}" + echo "R CMD check ok: ${{ steps.rcmdcheck.outputs.ok }}" + exit 1 diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index e742292c8..8b576d815 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -76,10 +76,7 @@ env: # Reuses the existing azure-pipeline reference files rather than adding new # ones, matching what the old Azure pipelines set via perf.profile. PERF_PROFILE: azure-pipeline - # These should all be constant, except the two TEST_FILTER_* values. These can - # be used to test subsets of test files in the testthat directory. Options are - # like: '*' <- run all tests, '*_smk_*' <- run all the smoke tests. - TEST_FILTER_DSBASE: '_-|datachk-|smk-|arg-|disc-|perf-|smk_expt-|expt-|math-' + # dsBase shards get their filter from matrix.filter per entry instead. TEST_FILTER_DSDANGER: '__dgr-|datachk_dgr-|smk_dgr-|arg_dgr-|disc_dgr-|smk_expt_dgr-|expt_dgr-|math_dgr-' jobs: @@ -246,7 +243,6 @@ jobs: dependencies: 'c("Depends", "Imports", "LinkingTo")' extra-packages: | cran::xml2 - github::datashield/dsDangerClient - name: Download shard/dsdanger results uses: actions/download-artifact@v4 @@ -464,7 +460,6 @@ jobs: dependencies: 'c("Depends", "Imports", "LinkingTo")' extra-packages: | cran::xml2 - github::datashield/dsDangerClient - name: Download shard/dsdanger results uses: actions/download-artifact@v4 @@ -589,9 +584,33 @@ jobs: ) } + console_block <- function(name, artifact_dir) { + files <- list.files(artifact_dir, pattern = "^test_console_output\\.txt$", recursive = TRUE, full.names = TRUE) + body <- if (length(files) == 0) { + "No console output found." + } else { + # Strip ANSI colour codes testthat'"'"'s ProgressReporter may emit. + paste(gsub("\033\\[[0-9;]*[a-zA-Z]", "", readLines(files[1], warn = FALSE)), collapse = "\n") + } + c( + sprintf("
Full testthat console output (%s)", name), + "", + "```", + body, + "```", + "", + "
" + ) + } + armadillo <- summarise_backend("Armadillo", "artifacts/reports/armadillo-report-results", find_version("armadillo-dsbase")) opal <- summarise_backend("Opal", "artifacts/reports/opal-report-results", find_version("opal-dsbase")) + tests_ok <- (armadillo$n_failures + armadillo$n_errors + opal$n_failures + opal$n_errors) == 0 + # Coverage threshold here is informational only, for the status + # line below - actual enforcement lives in Codecovs own + # patch-coverage check (see codecov.yml), scoped to lines changed + # by the PR rather than the whole repo, so it does not gate this job. coverage_threshold <- 80 rds_files <- list.files("artifacts/shards", pattern = "coverage\\.rds$", recursive = TRUE, full.names = TRUE) if (length(rds_files) > 0) { @@ -599,21 +618,26 @@ jobs: agg <- aggregate(value ~ filename + line, data = do.call(rbind, tallies), FUN = sum) totalcoverage <- round(sum(agg$value > 0) / nrow(agg) * 100, 1) coverage_ok <- totalcoverage >= coverage_threshold - cov_line <- if (coverage_ok) { - sprintf("**Combined coverage: %.1f%%**", totalcoverage) - } else { - sprintf("**Combined coverage: %.1f%%** - below the %d%% threshold", totalcoverage, coverage_threshold) - } + cov_line <- sprintf("**Combined coverage: %.1f%%** (target %d%%, informational - see Codecov patch check for enforcement)", totalcoverage, coverage_threshold) } else { - coverage_ok <- FALSE + coverage_ok <- NA cov_line <- "No coverage data found." } + status_icon <- function(ok) if (isTRUE(ok)) "✅" else if (isFALSE(ok)) "❌" else "❓" + codecov_url <- sprintf("https://app.codecov.io/gh/%s/commit/%s", Sys.getenv("GITHUB_REPOSITORY"), Sys.getenv("GITHUB_SHA")) summary <- c( "## dsBaseClient test suite results", "", + sprintf("%s Tests %s", status_icon(tests_ok), if (tests_ok) "passed" else "failed"), + sprintf( + "%s Coverage %s (informational)", + status_icon(coverage_ok), + if (is.na(coverage_ok)) "unknown - no coverage data found" else if (coverage_ok) sprintf("meets the %d%% target", coverage_threshold) else sprintf("is below the %d%% target", coverage_threshold) + ), + "", cov_line, sprintf("[View coverage report on Codecov](%s)", codecov_url), "", @@ -624,18 +648,20 @@ jobs: "", armadillo$details, "", - opal$details + opal$details, + "", + console_block("Armadillo", "artifacts/reports/armadillo-report-results"), + "", + console_block("Opal", "artifacts/reports/opal-report-results") ) writeLines(summary, Sys.getenv("GITHUB_STEP_SUMMARY")) - tests_ok <- (armadillo$n_failures + armadillo$n_errors + opal$n_failures + opal$n_errors) == 0 - cat(sprintf("coverage_ok=%s\ntests_ok=%s\n", tolower(coverage_ok), tolower(tests_ok)), + cat(sprintf("tests_ok=%s\n", tolower(tests_ok)), file = Sys.getenv("GITHUB_OUTPUT"), append = TRUE) ' - - name: Enforce coverage & test thresholds - if: steps.summary.outputs.coverage_ok != 'true' || steps.summary.outputs.tests_ok != 'true' + - name: Enforce test pass/fail + if: steps.summary.outputs.tests_ok != 'true' run: | - echo "Coverage ok: ${{ steps.summary.outputs.coverage_ok }}" echo "Tests ok: ${{ steps.summary.outputs.tests_ok }}" exit 1 diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 0e38c22f0..8f8c75cda 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -18,6 +18,8 @@ jobs: security-events: write steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - uses: r-lib/actions/setup-r@v2 with: @@ -30,10 +32,21 @@ jobs: extra-packages: | cran::lintr cran::jsonlite - github::datashield/dsDangerClient + + - name: Get PR changed files + if: github.event_name == 'pull_request' + id: changed + run: | + files=$(git diff --name-only "${{ github.event.pull_request.base.sha }}" "${{ github.event.pull_request.head.sha }}" -- '*.R' '*.Rmd') + echo "files<> "$GITHUB_ENV" + echo "$files" >> "$GITHUB_ENV" + echo "CHANGED_FILES_EOF" >> "$GITHUB_ENV" - name: Lint id: lint + env: + IS_PR: ${{ github.event_name == 'pull_request' }} + CHANGED_FILES: ${{ env.files }} run: | Rscript -e ' al <- lintr::available_linters() @@ -46,19 +59,45 @@ jobs: print(lints) # emits ::warning file=...,line=...:: annotations (auto-detects GitHub Actions) lintr::sarif_output(lints, "lintr_results.sarif") - if (length(lints) > 0) { - linter_names <- vapply(lints, function(l) l$linter, character(1)) - counts <- sort(table(linter_names), decreasing = TRUE) - count_rows <- paste0("| ", names(counts), " | ", as.integer(counts), " |") + is_pr <- Sys.getenv("IS_PR") == "true" + changed_files <- strsplit(Sys.getenv("CHANGED_FILES"), "\n")[[1]] + changed_files <- changed_files[nzchar(changed_files)] - finding_lines <- vapply(lints, function(l) { + lint_files <- vapply(lints, function(l) l$filename, character(1)) + is_new <- if (is_pr) lint_files %in% changed_files else rep(FALSE, length(lints)) + + format_findings <- function(idx) { + vapply(idx, function(i) { + l <- lints[[i]] sprintf("- `%s:%d` %s - %s", basename(l$filename), l$line_number, l$linter, l$message) }, character(1)) + } + if (length(lints) == 0) { + summary <- c("## Lint results", "", "No findings.") + } else if (is_pr) { + new_idx <- which(is_new) + existing_idx <- which(!is_new) summary <- c( "## Lint results", "", - sprintf("**%d findings** (correctness/robustness/common_mistakes linters)", length(lints)), + sprintf("**%d findings in files changed by this PR** (fails the check)", length(new_idx)), + if (length(new_idx) > 0) format_findings(new_idx) else "None.", + "", + sprintf("
%d pre-existing findings elsewhere in the repo (informational only)", length(existing_idx)), + "", + format_findings(existing_idx), + "", + "
" + ) + } else { + linter_names <- vapply(lints, function(l) l$linter, character(1)) + counts <- sort(table(linter_names), decreasing = TRUE) + count_rows <- paste0("| ", names(counts), " | ", as.integer(counts), " |") + summary <- c( + "## Lint results", + "", + sprintf("**%d findings** (correctness/robustness/common_mistakes linters, informational only outside PRs)", length(lints)), "", "| Linter | Count |", "|---|---|", @@ -66,16 +105,14 @@ jobs: "", "
All findings", "", - finding_lines, + format_findings(seq_along(lints)), "", "
" ) - } else { - summary <- c("## Lint results", "", "No findings.") } writeLines(summary, Sys.getenv("GITHUB_STEP_SUMMARY")) - cat(sprintf("has_findings=%s\n", tolower(length(lints) > 0)), + cat(sprintf("has_new_findings=%s\n", tolower(any(is_new))), file = Sys.getenv("GITHUB_OUTPUT"), append = TRUE) ' @@ -84,8 +121,8 @@ jobs: with: sarif_file: lintr_results.sarif - - name: Fail on lint findings - if: steps.lint.outputs.has_findings == 'true' + - name: Fail on lint findings in changed files + if: steps.lint.outputs.has_new_findings == 'true' run: | - echo "Lint found issues - see the annotations above, the job summary, or the SARIF upload for details." + echo "Lint found issues in files changed by this PR - see the annotations above, the job summary, or the SARIF upload for details." exit 1 From a3bb9e52a9f341eef770470aa2b919fe47ff5e24 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:18:52 +0200 Subject: [PATCH 39/56] fix dsDanger tests --- .../workflows/dsBaseClient_test_suite.yaml | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index 8b576d815..48caabadc 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -168,6 +168,11 @@ jobs: write.csv(covr::coverage_to_list(cov), "coveragelist.csv") covr::to_cobertura(cov, "cobertura.xml")' || R_EXIT=$? cat test_console_output_dsbase.txt + n_tests=$(grep -c ' entries in test_results_dsbase.xml) - treating as a failure rather than a silent pass." + R_EXIT=1 + fi exit "${R_EXIT:-0}" working-directory: dsBaseClient @@ -175,6 +180,7 @@ jobs: if: matrix.category == 'dsdanger' run: | R -q -e ' + devtools::load_all(quiet = TRUE); library(testthat); output_file <- file("test_console_output_dsdanger.txt"); sink(output_file, split = TRUE); @@ -182,8 +188,13 @@ jobs: progress_rep <- ProgressReporter$new(max_failures = 999999); multi_rep <- MultiReporter$new(reporters = list(progress_rep, junit_rep)); options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); - testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ env.TEST_FILTER_DSDANGER }}", reporter = multi_rep, stop_on_failure = FALSE)' || R_EXIT=$? + test_dir("tests/testthat", filter = "${{ env.TEST_FILTER_DSDANGER }}", reporter = multi_rep, stop_on_failure = FALSE)' || R_EXIT=$? cat test_console_output_dsdanger.txt + n_tests=$(grep -c ' entries in test_results_dsdanger.xml) - treating as a failure rather than a silent pass." + R_EXIT=1 + fi exit "${R_EXIT:-0}" working-directory: dsBaseClient @@ -385,6 +396,11 @@ jobs: write.csv(covr::coverage_to_list(cov), "coveragelist.csv") covr::to_cobertura(cov, "cobertura.xml")' || R_EXIT=$? cat test_console_output_dsbase.txt + n_tests=$(grep -c ' entries in test_results_dsbase.xml) - treating as a failure rather than a silent pass." + R_EXIT=1 + fi exit "${R_EXIT:-0}" working-directory: dsBaseClient @@ -392,6 +408,7 @@ jobs: if: matrix.category == 'dsdanger' run: | R -q -e ' + devtools::load_all(quiet = TRUE); library(testthat); output_file <- file("test_console_output_dsdanger.txt"); sink(output_file, split = TRUE); @@ -399,8 +416,13 @@ jobs: progress_rep <- ProgressReporter$new(max_failures = 999999); multi_rep <- MultiReporter$new(reporters = list(progress_rep, junit_rep)); options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); - testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ env.TEST_FILTER_DSDANGER }}", reporter = multi_rep, stop_on_failure = FALSE)' || R_EXIT=$? + test_dir("tests/testthat", filter = "${{ env.TEST_FILTER_DSDANGER }}", reporter = multi_rep, stop_on_failure = FALSE)' || R_EXIT=$? cat test_console_output_dsdanger.txt + n_tests=$(grep -c ' entries in test_results_dsdanger.xml) - treating as a failure rather than a silent pass." + R_EXIT=1 + fi exit "${R_EXIT:-0}" working-directory: dsBaseClient From a1b1209fab5144a595341c9c2e10aa1f5f7aa291 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:47:47 +0200 Subject: [PATCH 40/56] remove unnescessary dsdanger dep --- .../setup-armadillo-with-dsbase/action.yaml | 1 + .../setup-opal-with-dsbase/action.yaml | 1 + .github/workflows/check.yaml | 20 +++---------------- .../workflows/dsBaseClient_test_suite.yaml | 17 ++++++---------- .github/workflows/lint.yaml | 14 +++++-------- 5 files changed, 16 insertions(+), 37 deletions(-) diff --git a/.github/actions/setup-armadillo-with-dsbase/action.yaml b/.github/actions/setup-armadillo-with-dsbase/action.yaml index 826afe7e8..5a9a8a2c2 100644 --- a/.github/actions/setup-armadillo-with-dsbase/action.yaml +++ b/.github/actions/setup-armadillo-with-dsbase/action.yaml @@ -101,6 +101,7 @@ runs: - uses: r-lib/actions/setup-r-dependencies@v2 with: + working-directory: dsBaseClient dependencies: 'c("Depends", "Imports", "LinkingTo")' extra-packages: | cran::devtools diff --git a/.github/actions/setup-opal-with-dsbase/action.yaml b/.github/actions/setup-opal-with-dsbase/action.yaml index c97af580a..e3270891a 100644 --- a/.github/actions/setup-opal-with-dsbase/action.yaml +++ b/.github/actions/setup-opal-with-dsbase/action.yaml @@ -47,6 +47,7 @@ runs: - uses: r-lib/actions/setup-r-dependencies@v2 with: + working-directory: dsBaseClient dependencies: 'c("Depends", "Imports", "LinkingTo")' extra-packages: | cran::devtools diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index df1948669..ef803a1b1 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -42,31 +42,17 @@ jobs: needs: check - name: Check manual updated - id: docsync run: | orig_sum=$(find man -type f | sort -u | xargs cat | md5sum) R -q -e "devtools::document()" new_sum=$(find man -type f | sort -u | xargs cat | md5sum) if [ "$orig_sum" != "$new_sum" ]; then echo "Your committed man/*.Rd files are out of sync with the R headers." - echo "ok=false" >> "$GITHUB_OUTPUT" - else - echo "ok=true" >> "$GITHUB_OUTPUT" + exit 1 fi - name: Devtools checks - id: rcmdcheck + if: always() run: | R -q -e "devtools::check(args = c('--no-examples', '--no-tests'))" | tee azure-pipelines_check.Rout - if grep --quiet "^0 errors" azure-pipelines_check.Rout && grep --quiet " 0 warnings" azure-pipelines_check.Rout && grep --quiet " 0 notes" azure-pipelines_check.Rout; then - echo "ok=true" >> "$GITHUB_OUTPUT" - else - echo "ok=false" >> "$GITHUB_OUTPUT" - fi - - - name: Enforce check results - if: steps.docsync.outputs.ok != 'true' || steps.rcmdcheck.outputs.ok != 'true' - run: | - echo "Doc sync ok: ${{ steps.docsync.outputs.ok }}" - echo "R CMD check ok: ${{ steps.rcmdcheck.outputs.ok }}" - exit 1 + grep --quiet "^0 errors" azure-pipelines_check.Rout && grep --quiet " 0 warnings" azure-pipelines_check.Rout && grep --quiet " 0 notes" azure-pipelines_check.Rout diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index 48caabadc..8e0c6a83d 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -184,7 +184,7 @@ jobs: library(testthat); output_file <- file("test_console_output_dsdanger.txt"); sink(output_file, split = TRUE); - junit_rep <- JunitReporter$new(file = "test_results_dsdanger.xml"); + junit_rep <- JunitReporter$new(file = file.path(getwd(), "test_results_dsdanger.xml")); progress_rep <- ProgressReporter$new(max_failures = 999999); multi_rep <- MultiReporter$new(reporters = list(progress_rep, junit_rep)); options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); @@ -251,6 +251,7 @@ jobs: - uses: r-lib/actions/setup-r-dependencies@v2 with: + working-directory: dsBaseClient dependencies: 'c("Depends", "Imports", "LinkingTo")' extra-packages: | cran::xml2 @@ -412,7 +413,7 @@ jobs: library(testthat); output_file <- file("test_console_output_dsdanger.txt"); sink(output_file, split = TRUE); - junit_rep <- JunitReporter$new(file = "test_results_dsdanger.xml"); + junit_rep <- JunitReporter$new(file = file.path(getwd(), "test_results_dsdanger.xml")); progress_rep <- ProgressReporter$new(max_failures = 999999); multi_rep <- MultiReporter$new(reporters = list(progress_rep, junit_rep)); options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); @@ -479,6 +480,7 @@ jobs: - uses: r-lib/actions/setup-r-dependencies@v2 with: + working-directory: dsBaseClient dependencies: 'c("Depends", "Imports", "LinkingTo")' extra-packages: | cran::xml2 @@ -555,7 +557,6 @@ jobs: path: artifacts/shards - name: Write combined job summary - id: summary run: | Rscript -e ' find_version <- function(prefix) { @@ -678,12 +679,6 @@ jobs: ) writeLines(summary, Sys.getenv("GITHUB_STEP_SUMMARY")) - cat(sprintf("tests_ok=%s\n", tolower(tests_ok)), - file = Sys.getenv("GITHUB_OUTPUT"), append = TRUE) + if (!tests_ok) message("Tests failed - see the summary above for details.") + quit(save = "no", status = if (tests_ok) 0 else 1) ' - - - name: Enforce test pass/fail - if: steps.summary.outputs.tests_ok != 'true' - run: | - echo "Tests ok: ${{ steps.summary.outputs.tests_ok }}" - exit 1 diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 8f8c75cda..bca7f0b3c 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -43,7 +43,6 @@ jobs: echo "CHANGED_FILES_EOF" >> "$GITHUB_ENV" - name: Lint - id: lint env: IS_PR: ${{ github.event_name == 'pull_request' }} CHANGED_FILES: ${{ env.files }} @@ -112,17 +111,14 @@ jobs: } writeLines(summary, Sys.getenv("GITHUB_STEP_SUMMARY")) - cat(sprintf("has_new_findings=%s\n", tolower(any(is_new))), - file = Sys.getenv("GITHUB_OUTPUT"), append = TRUE) + if (any(is_new)) { + message("Lint found issues in files changed by this PR - see the annotations above, the job summary, or the SARIF upload for details.") + } + quit(save = "no", status = if (any(is_new)) 1 else 0) ' - name: Upload lint results + if: always() uses: github/codeql-action/upload-sarif@v3 with: sarif_file: lintr_results.sarif - - - name: Fail on lint findings in changed files - if: steps.lint.outputs.has_new_findings == 'true' - run: | - echo "Lint found issues in files changed by this PR - see the annotations above, the job summary, or the SARIF upload for details." - exit 1 From 5f87431abc8e5c4219e143fdd6114ac1e2de117d Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Mon, 31 Aug 2026 18:12:28 +0200 Subject: [PATCH 41/56] fix report format --- .../workflows/dsBaseClient_test_suite.yaml | 85 ++++++++----------- 1 file changed, 37 insertions(+), 48 deletions(-) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index 8e0c6a83d..140d0992e 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -579,26 +579,44 @@ jobs: n_failures <- sum(as.integer(xml2::xml_attr(suites, "failures")), na.rm = TRUE) n_errors <- sum(as.integer(xml2::xml_attr(suites, "errors")), na.rm = TRUE) n_skipped <- sum(as.integer(xml2::xml_attr(suites, "skipped")), na.rm = TRUE) + n_pass <- n_tests - n_failures - n_errors - n_skipped failed <- xml2::xml_find_all(doc, ".//testcase[failure or error]") - fail_lines <- if (length(failed) > 0) { + ok <- length(failed) == 0 + + if (ok) { + fail_block <- character(0) + fail_list <- "All tests passed." + } else { msgs <- vapply(failed, function(tc) { node <- xml2::xml_find_first(tc, "failure|error") m <- xml2::xml_attr(node, "message") if (is.na(m) || !nzchar(m)) m <- trimws(xml2::xml_text(node)) m }, character(1)) - paste0("- `", xml2::xml_attr(failed, "classname"), "::", xml2::xml_attr(failed, "name"), "` - ", msgs) - } else { - "All tests passed." + labels <- paste0(xml2::xml_attr(failed, "classname"), "::", xml2::xml_attr(failed, "name")) + fail_block <- unlist(lapply(seq_along(failed), function(i) { + c(sprintf("-- Failure (%s) %s", labels[i], strrep("-", max(1, 60 - nchar(labels[i])))), msgs[i], "") + })) + fail_list <- paste0("- `", labels, "` - ", msgs) } + tally <- sprintf("[ FAIL %d | WARN 0 | SKIP %d | PASS %d ]", n_failures + n_errors, n_skipped, n_pass) + list( - row = sprintf("| %s | %s | %d | %d | %d | %d |", name, version, n_tests, n_failures, n_errors, n_skipped), - details = c( - sprintf("
Failed tests (%s)", name), + block = c( + sprintf("### %s %s %s", if (ok) "✅" else "❌", name, if (ok) "passed" else "failed"), + "", + sprintf("dsBase %s", version), "", - fail_lines, + "```", + fail_block, + tally, + "```", + "", + "
Failed tests", + "", + fail_list, "", "
" ), @@ -607,25 +625,6 @@ jobs: ) } - console_block <- function(name, artifact_dir) { - files <- list.files(artifact_dir, pattern = "^test_console_output\\.txt$", recursive = TRUE, full.names = TRUE) - body <- if (length(files) == 0) { - "No console output found." - } else { - # Strip ANSI colour codes testthat'"'"'s ProgressReporter may emit. - paste(gsub("\033\\[[0-9;]*[a-zA-Z]", "", readLines(files[1], warn = FALSE)), collapse = "\n") - } - c( - sprintf("
Full testthat console output (%s)", name), - "", - "```", - body, - "```", - "", - "
" - ) - } - armadillo <- summarise_backend("Armadillo", "artifacts/reports/armadillo-report-results", find_version("armadillo-dsbase")) opal <- summarise_backend("Opal", "artifacts/reports/opal-report-results", find_version("opal-dsbase")) tests_ok <- (armadillo$n_failures + armadillo$n_errors + opal$n_failures + opal$n_errors) == 0 @@ -641,41 +640,31 @@ jobs: agg <- aggregate(value ~ filename + line, data = do.call(rbind, tallies), FUN = sum) totalcoverage <- round(sum(agg$value > 0) / nrow(agg) * 100, 1) coverage_ok <- totalcoverage >= coverage_threshold - cov_line <- sprintf("**Combined coverage: %.1f%%** (target %d%%, informational - see Codecov patch check for enforcement)", totalcoverage, coverage_threshold) } else { + totalcoverage <- NA coverage_ok <- NA - cov_line <- "No coverage data found." } status_icon <- function(ok) if (isTRUE(ok)) "✅" else if (isFALSE(ok)) "❌" else "❓" + cov_status_line <- if (is.na(coverage_ok)) { + sprintf("%s Coverage unknown: no coverage data found", status_icon(NA)) + } else { + sprintf("%s Coverage %s: %.1f%% vs %d%% target (informational - see Codecov patch check for enforcement)", + status_icon(coverage_ok), if (coverage_ok) "passed" else "failed", totalcoverage, coverage_threshold) + } + codecov_url <- sprintf("https://app.codecov.io/gh/%s/commit/%s", Sys.getenv("GITHUB_REPOSITORY"), Sys.getenv("GITHUB_SHA")) summary <- c( "## dsBaseClient test suite results", "", - sprintf("%s Tests %s", status_icon(tests_ok), if (tests_ok) "passed" else "failed"), - sprintf( - "%s Coverage %s (informational)", - status_icon(coverage_ok), - if (is.na(coverage_ok)) "unknown - no coverage data found" else if (coverage_ok) sprintf("meets the %d%% target", coverage_threshold) else sprintf("is below the %d%% target", coverage_threshold) - ), - "", - cov_line, + cov_status_line, sprintf("[View coverage report on Codecov](%s)", codecov_url), "", - "| Backend | dsBase version | Tests | Failures | Errors | Skipped |", - "|---|---|---|---|---|---|", - armadillo$row, - opal$row, - "", - armadillo$details, - "", - opal$details, - "", - console_block("Armadillo", "artifacts/reports/armadillo-report-results"), + armadillo$block, "", - console_block("Opal", "artifacts/reports/opal-report-results") + opal$block ) writeLines(summary, Sys.getenv("GITHUB_STEP_SUMMARY")) From c76bf3b464b52fd3fe0f45c107ddec11d616d8bb Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Mon, 31 Aug 2026 18:15:28 +0200 Subject: [PATCH 42/56] add link to summary report --- .github/workflows/dsBaseClient_test_suite.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index 140d0992e..9f1dc45e0 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -286,6 +286,11 @@ jobs: dsBaseClient/logs/test_results.xml dsBaseClient/logs/test_console_output.txt + - name: Print link to run summary + if: always() + run: | + echo "::notice title=Test summary::Job summaries only render on the run's own Summary page, not here - see ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + ################################################################################ # Armadillo - dsBase suite, sharded 4 ways. Each shard downloads and runs its @@ -515,6 +520,11 @@ jobs: dsBaseClient/logs/test_results.xml dsBaseClient/logs/test_console_output.txt + - name: Print link to run summary + if: always() + run: | + echo "::notice title=Test summary::Job summaries only render on the run's own Summary page, not here - see ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + ################################################################################ # Single consolidated summary across both backends: one combined coverage @@ -671,3 +681,8 @@ jobs: if (!tests_ok) message("Tests failed - see the summary above for details.") quit(save = "no", status = if (tests_ok) 0 else 1) ' + + - name: Print link to run summary + if: always() + run: | + echo "::notice title=Test summary::Job summaries only render on the run's own Summary page, not here - see ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" From 25d073ed63b6f95e5f881650bf09fbfe99ab57a8 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Mon, 31 Aug 2026 18:17:30 +0200 Subject: [PATCH 43/56] update ignore files --- .Rbuildignore | 1 + .gitignore | 1 + 2 files changed, 2 insertions(+) diff --git a/.Rbuildignore b/.Rbuildignore index 01c7d2805..17262dafc 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -25,3 +25,4 @@ ^\.github$ ^REFACTOR_GUIDE\.md$ ^man-roxygen$ +^codecov\.yml$ diff --git a/.gitignore b/.gitignore index 60d56797e..93decc0f6 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ azure-pipelines.Rout tests/testthat/connection_to_datasets/local_settings.csv tests/docker/armadillo/standard/logs/ tests/docker/armadillo/standard/data/ +lintr_results.sarif From 77f0133b75cf414da47837d4e0628645d333c964 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Mon, 31 Aug 2026 18:39:52 +0200 Subject: [PATCH 44/56] stop runs from previous push on new push --- .github/workflows/check.yaml | 7 +++++++ .github/workflows/dsBaseClient_test_suite.yaml | 7 +++++++ .github/workflows/lint.yaml | 6 ++++++ 3 files changed, 20 insertions(+) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index ef803a1b1..76631eae9 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -7,6 +7,13 @@ on: - cron: '0 0 * * 0' # Weekly - cron: '0 1 * * *' # Nightly +# A new push to the same ref supersedes any run still in progress for it, so +# we don't burn compute on stale commits. Scoped by event_name too, so a +# schedule/workflow_dispatch run is never auto-cancelled by an unrelated push. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }} + cancel-in-progress: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }} + permissions: contents: read diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index 9f1dc45e0..50014c290 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -63,6 +63,13 @@ on: - cron: '0 0 * * 0' # Weekly - cron: '0 1 * * *' # Nightly +# A new push to the same ref supersedes any run still in progress for it, so +# we don't burn compute on stale commits. Scoped by event_name too, so a +# schedule/workflow_dispatch run is never auto-cancelled by an unrelated push. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }} + cancel-in-progress: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }} + permissions: contents: read diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index bca7f0b3c..0f3a14a9a 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -5,6 +5,12 @@ on: branches: [main, master] pull_request: +# A new push to the same ref supersedes any run still in progress for it, so +# we don't burn compute on stale commits. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }} + cancel-in-progress: true + permissions: contents: read From e58a0680219e94b889b23b19b429afcf65022eec Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Mon, 31 Aug 2026 21:04:54 +0200 Subject: [PATCH 45/56] try: collate output --- .github/workflows/check.yaml | 35 +++++++++++++++++++ .../workflows/dsBaseClient_test_suite.yaml | 25 +++++++++++++ .github/workflows/lint.yaml | 26 ++++++++++++++ 3 files changed, 86 insertions(+) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 76631eae9..2961cdb09 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -22,6 +22,9 @@ jobs: name: Package checks (doc sync, R CMD check) runs-on: ubuntu-latest timeout-minutes: 30 + permissions: + contents: read + checks: write steps: - uses: actions/checkout@v4 @@ -49,6 +52,7 @@ jobs: needs: check - name: Check manual updated + id: docsync run: | orig_sum=$(find man -type f | sort -u | xargs cat | md5sum) R -q -e "devtools::document()" @@ -59,7 +63,38 @@ jobs: fi - name: Devtools checks + id: rcmdcheck if: always() run: | R -q -e "devtools::check(args = c('--no-examples', '--no-tests'))" | tee azure-pipelines_check.Rout grep --quiet "^0 errors" azure-pipelines_check.Rout && grep --quiet " 0 warnings" azure-pipelines_check.Rout && grep --quiet " 0 notes" azure-pipelines_check.Rout + + - name: Publish check results + if: always() + uses: actions/github-script@v7 + with: + script: | + const docsync = '${{ steps.docsync.outcome }}'; + const rcmdcheck = '${{ steps.rcmdcheck.outcome }}'; + const ok = docsync === 'success' && rcmdcheck === 'success'; + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const summary = [ + '| Check | Result |', + '|---|---|', + `| Doc sync (man/*.Rd vs R headers) | ${docsync === 'success' ? '✅ passed' : '❌ failed'} |`, + `| R CMD check (0 errors/warnings/notes) | ${rcmdcheck === 'success' ? '✅ passed' : '❌ failed'} |`, + '', + `[View full log](${runUrl})` + ].join('\n'); + await github.rest.checks.create({ + owner: context.repo.owner, + repo: context.repo.repo, + name: 'Check results', + head_sha: context.sha, + status: 'completed', + conclusion: ok ? 'success' : 'failure', + output: { + title: ok ? 'All checks passed' : 'Checks failed', + summary + } + }); diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index 50014c290..45a3f5f99 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -547,6 +547,9 @@ jobs: if: always() runs-on: ubuntu-latest timeout-minutes: 15 + permissions: + contents: read + checks: write steps: - uses: r-lib/actions/setup-r@v2 with: @@ -574,6 +577,7 @@ jobs: path: artifacts/shards - name: Write combined job summary + id: summary run: | Rscript -e ' find_version <- function(prefix) { @@ -689,6 +693,27 @@ jobs: quit(save = "no", status = if (tests_ok) 0 else 1) ' + - name: Publish check results + if: always() + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const ok = '${{ steps.summary.outcome }}' === 'success'; + const summary = fs.readFileSync(process.env.GITHUB_STEP_SUMMARY, 'utf8'); + await github.rest.checks.create({ + owner: context.repo.owner, + repo: context.repo.repo, + name: 'Test results', + head_sha: context.sha, + status: 'completed', + conclusion: ok ? 'success' : 'failure', + output: { + title: ok ? 'All tests passed' : 'Tests failed', + summary + } + }); + - name: Print link to run summary if: always() run: | diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 0f3a14a9a..3a14175f5 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -22,6 +22,7 @@ jobs: permissions: contents: read security-events: write + checks: write steps: - uses: actions/checkout@v4 with: @@ -49,6 +50,7 @@ jobs: echo "CHANGED_FILES_EOF" >> "$GITHUB_ENV" - name: Lint + id: lint env: IS_PR: ${{ github.event_name == 'pull_request' }} CHANGED_FILES: ${{ env.files }} @@ -128,3 +130,27 @@ jobs: uses: github/codeql-action/upload-sarif@v3 with: sarif_file: lintr_results.sarif + + - name: Publish check results + if: always() + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const ok = '${{ steps.lint.outcome }}' === 'success'; + const summary = fs.readFileSync(process.env.GITHUB_STEP_SUMMARY, 'utf8'); + const headSha = context.eventName === 'pull_request' + ? context.payload.pull_request.head.sha + : context.sha; + await github.rest.checks.create({ + owner: context.repo.owner, + repo: context.repo.repo, + name: 'Lint results', + head_sha: headSha, + status: 'completed', + conclusion: ok ? 'success' : 'failure', + output: { + title: ok ? 'No blocking lint findings' : 'Lint findings block this PR', + summary + } + }); From 42594f72c439e94d03adf6ea098b803af7ccb332 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Mon, 31 Aug 2026 21:56:00 +0200 Subject: [PATCH 46/56] try: test results as PR comment --- .github/workflows/check.yaml | 45 +++++++++++++------ .../workflows/dsBaseClient_test_suite.yaml | 44 ++++++++++++------ .github/workflows/lint.yaml | 42 +++++++++-------- 3 files changed, 85 insertions(+), 46 deletions(-) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 2961cdb09..f4541333d 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -24,7 +24,7 @@ jobs: timeout-minutes: 30 permissions: contents: read - checks: write + pull-requests: write steps: - uses: actions/checkout@v4 @@ -69,7 +69,7 @@ jobs: R -q -e "devtools::check(args = c('--no-examples', '--no-tests'))" | tee azure-pipelines_check.Rout grep --quiet "^0 errors" azure-pipelines_check.Rout && grep --quiet " 0 warnings" azure-pipelines_check.Rout && grep --quiet " 0 notes" azure-pipelines_check.Rout - - name: Publish check results + - name: Post PR comment if: always() uses: actions/github-script@v7 with: @@ -78,7 +78,11 @@ jobs: const rcmdcheck = '${{ steps.rcmdcheck.outcome }}'; const ok = docsync === 'success' && rcmdcheck === 'success'; const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; - const summary = [ + const marker = ''; + const body = [ + marker, + `## ${ok ? '✅' : '❌'} Package checks`, + '', '| Check | Result |', '|---|---|', `| Doc sync (man/*.Rd vs R headers) | ${docsync === 'success' ? '✅ passed' : '❌ failed'} |`, @@ -86,15 +90,28 @@ jobs: '', `[View full log](${runUrl})` ].join('\n'); - await github.rest.checks.create({ - owner: context.repo.owner, - repo: context.repo.repo, - name: 'Check results', - head_sha: context.sha, - status: 'completed', - conclusion: ok ? 'success' : 'failure', - output: { - title: ok ? 'All checks passed' : 'Checks failed', - summary - } + + let prNumber = context.payload.pull_request?.number; + if (!prNumber) { + const branch = context.ref.replace('refs/heads/', ''); + const prs = await github.rest.pulls.list({ + owner: context.repo.owner, repo: context.repo.repo, + head: `${context.repo.owner}:${branch}`, state: 'open' + }); + prNumber = prs.data[0]?.number; + } + if (!prNumber) return; + + const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber }); + const existing = comments.data.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, repo: context.repo.repo, comment_id: existing.id, body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, body + }); + } diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index 45a3f5f99..aa63c3a4c 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -549,7 +549,7 @@ jobs: timeout-minutes: 15 permissions: contents: read - checks: write + pull-requests: write steps: - uses: r-lib/actions/setup-r@v2 with: @@ -693,26 +693,42 @@ jobs: quit(save = "no", status = if (tests_ok) 0 else 1) ' - - name: Publish check results + - name: Post PR comment if: always() uses: actions/github-script@v7 with: script: | const fs = require('fs'); const ok = '${{ steps.summary.outcome }}' === 'success'; - const summary = fs.readFileSync(process.env.GITHUB_STEP_SUMMARY, 'utf8'); - await github.rest.checks.create({ - owner: context.repo.owner, - repo: context.repo.repo, - name: 'Test results', - head_sha: context.sha, - status: 'completed', - conclusion: ok ? 'success' : 'failure', - output: { - title: ok ? 'All tests passed' : 'Tests failed', - summary - } + const marker = ''; + const body = [ + marker, + `## ${ok ? '✅' : '❌'} Tests`, + '', + fs.readFileSync(process.env.GITHUB_STEP_SUMMARY, 'utf8') + ].join('\n'); + + const branch = context.ref.replace('refs/heads/', ''); + const prs = await github.rest.pulls.list({ + owner: context.repo.owner, repo: context.repo.repo, + head: `${context.repo.owner}:${branch}`, state: 'open' + }); + const prNumber = prs.data[0]?.number; + if (!prNumber) return; + + const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber }); + const existing = comments.data.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, repo: context.repo.repo, comment_id: existing.id, body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, body + }); + } - name: Print link to run summary if: always() diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 3a14175f5..f1c518ae1 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -22,7 +22,7 @@ jobs: permissions: contents: read security-events: write - checks: write + pull-requests: write steps: - uses: actions/checkout@v4 with: @@ -131,26 +131,32 @@ jobs: with: sarif_file: lintr_results.sarif - - name: Publish check results - if: always() + - name: Post PR comment + if: always() && github.event_name == 'pull_request' uses: actions/github-script@v7 with: script: | const fs = require('fs'); const ok = '${{ steps.lint.outcome }}' === 'success'; - const summary = fs.readFileSync(process.env.GITHUB_STEP_SUMMARY, 'utf8'); - const headSha = context.eventName === 'pull_request' - ? context.payload.pull_request.head.sha - : context.sha; - await github.rest.checks.create({ - owner: context.repo.owner, - repo: context.repo.repo, - name: 'Lint results', - head_sha: headSha, - status: 'completed', - conclusion: ok ? 'success' : 'failure', - output: { - title: ok ? 'No blocking lint findings' : 'Lint findings block this PR', - summary - } + const marker = ''; + const body = [ + marker, + `## ${ok ? '✅' : '❌'} Lint`, + '', + fs.readFileSync(process.env.GITHUB_STEP_SUMMARY, 'utf8') + ].join('\n'); + + const prNumber = context.payload.pull_request.number; + const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber }); + const existing = comments.data.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, repo: context.repo.repo, comment_id: existing.id, body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, body + }); + } From 3505ff251add89e8a3af36f122af523ccc90d34e Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Tue, 1 Sep 2026 07:35:31 +0200 Subject: [PATCH 47/56] revise format --- .github/workflows/check.yaml | 36 ++++++++++++------ .../workflows/dsBaseClient_test_suite.yaml | 38 ++++++++++++------- .github/workflows/lint.yaml | 34 +++++++++++------ 3 files changed, 73 insertions(+), 35 deletions(-) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index f4541333d..ba2991260 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -78,11 +78,7 @@ jobs: const rcmdcheck = '${{ steps.rcmdcheck.outcome }}'; const ok = docsync === 'success' && rcmdcheck === 'success'; const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; - const marker = ''; - const body = [ - marker, - `## ${ok ? '✅' : '❌'} Package checks`, - '', + const content = [ '| Check | Result |', '|---|---|', `| Doc sync (man/*.Rd vs R headers) | ${docsync === 'success' ? '✅ passed' : '❌ failed'} |`, @@ -102,16 +98,34 @@ jobs: } if (!prNumber) return; + // Shared summary comment: each workflow (check/lint/tests) owns one + // delimited section and only ever replaces its own, so they combine + // into a single comment regardless of which finishes first. + const topMarker = ''; + const sectionKey = 'check'; + const sectionBody = [ + ``, + `## ${ok ? '✅' : '❌'} Package checks`, + '', + content, + `` + ].join('\n'); + const comments = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber }); - const existing = comments.data.find(c => c.body.includes(marker)); - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, repo: context.repo.repo, comment_id: existing.id, body + const existing = comments.data.find(c => c.body.includes(topMarker)); + if (!existing) { + await github.rest.issues.createComment({ + owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, + body: [topMarker, '', sectionBody].join('\n') }); } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, body + const sectionRegex = new RegExp(`[\\s\\S]*?`); + const newBody = sectionRegex.test(existing.body) + ? existing.body.replace(sectionRegex, sectionBody) + : existing.body + '\n\n' + sectionBody; + await github.rest.issues.updateComment({ + owner: context.repo.owner, repo: context.repo.repo, comment_id: existing.id, body: newBody }); } diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index aa63c3a4c..9d49785e0 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -700,13 +700,7 @@ jobs: script: | const fs = require('fs'); const ok = '${{ steps.summary.outcome }}' === 'success'; - const marker = ''; - const body = [ - marker, - `## ${ok ? '✅' : '❌'} Tests`, - '', - fs.readFileSync(process.env.GITHUB_STEP_SUMMARY, 'utf8') - ].join('\n'); + const content = fs.readFileSync(process.env.GITHUB_STEP_SUMMARY, 'utf8'); const branch = context.ref.replace('refs/heads/', ''); const prs = await github.rest.pulls.list({ @@ -716,17 +710,35 @@ jobs: const prNumber = prs.data[0]?.number; if (!prNumber) return; + // Shared summary comment: each workflow (check/lint/tests) owns one + // delimited section and only ever replaces its own, so they combine + // into a single comment regardless of which finishes first. + const topMarker = ''; + const sectionKey = 'tests'; + const sectionBody = [ + ``, + `## ${ok ? '✅' : '❌'} Tests`, + '', + content, + `` + ].join('\n'); + const comments = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber }); - const existing = comments.data.find(c => c.body.includes(marker)); - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, repo: context.repo.repo, comment_id: existing.id, body + const existing = comments.data.find(c => c.body.includes(topMarker)); + if (!existing) { + await github.rest.issues.createComment({ + owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, + body: [topMarker, '', sectionBody].join('\n') }); } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, body + const sectionRegex = new RegExp(`[\\s\\S]*?`); + const newBody = sectionRegex.test(existing.body) + ? existing.body.replace(sectionRegex, sectionBody) + : existing.body + '\n\n' + sectionBody; + await github.rest.issues.updateComment({ + owner: context.repo.owner, repo: context.repo.repo, comment_id: existing.id, body: newBody }); } diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index f1c518ae1..0c36acf72 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -138,25 +138,37 @@ jobs: script: | const fs = require('fs'); const ok = '${{ steps.lint.outcome }}' === 'success'; - const marker = ''; - const body = [ - marker, + const content = fs.readFileSync(process.env.GITHUB_STEP_SUMMARY, 'utf8'); + const prNumber = context.payload.pull_request.number; + + // Shared summary comment: each workflow (check/lint/tests) owns one + // delimited section and only ever replaces its own, so they combine + // into a single comment regardless of which finishes first. + const topMarker = ''; + const sectionKey = 'lint'; + const sectionBody = [ + ``, `## ${ok ? '✅' : '❌'} Lint`, '', - fs.readFileSync(process.env.GITHUB_STEP_SUMMARY, 'utf8') + content, + `` ].join('\n'); - const prNumber = context.payload.pull_request.number; const comments = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber }); - const existing = comments.data.find(c => c.body.includes(marker)); - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, repo: context.repo.repo, comment_id: existing.id, body + const existing = comments.data.find(c => c.body.includes(topMarker)); + if (!existing) { + await github.rest.issues.createComment({ + owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, + body: [topMarker, '', sectionBody].join('\n') }); } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, body + const sectionRegex = new RegExp(`[\\s\\S]*?`); + const newBody = sectionRegex.test(existing.body) + ? existing.body.replace(sectionRegex, sectionBody) + : existing.body + '\n\n' + sectionBody; + await github.rest.issues.updateComment({ + owner: context.repo.owner, repo: context.repo.repo, comment_id: existing.id, body: newBody }); } From cb73be497dc398a55b90a9bce0f6fc63d03938ca Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Tue, 1 Sep 2026 09:34:57 +0200 Subject: [PATCH 48/56] fixed formatting --- .github/workflows/check.yaml | 59 +---- .../workflows/dsBaseClient_test_suite.yaml | 202 +++++++----------- .github/workflows/lint.yaml | 98 ++------- 3 files changed, 99 insertions(+), 260 deletions(-) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index ba2991260..20bd931cb 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -74,58 +74,11 @@ jobs: uses: actions/github-script@v7 with: script: | - const docsync = '${{ steps.docsync.outcome }}'; - const rcmdcheck = '${{ steps.rcmdcheck.outcome }}'; - const ok = docsync === 'success' && rcmdcheck === 'success'; + const ok = '${{ steps.docsync.outcome }}' === 'success' && '${{ steps.rcmdcheck.outcome }}' === 'success'; const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; - const content = [ - '| Check | Result |', - '|---|---|', - `| Doc sync (man/*.Rd vs R headers) | ${docsync === 'success' ? '✅ passed' : '❌ failed'} |`, - `| R CMD check (0 errors/warnings/notes) | ${rcmdcheck === 'success' ? '✅ passed' : '❌ failed'} |`, - '', - `[View full log](${runUrl})` - ].join('\n'); - let prNumber = context.payload.pull_request?.number; - if (!prNumber) { - const branch = context.ref.replace('refs/heads/', ''); - const prs = await github.rest.pulls.list({ - owner: context.repo.owner, repo: context.repo.repo, - head: `${context.repo.owner}:${branch}`, state: 'open' - }); - prNumber = prs.data[0]?.number; - } - if (!prNumber) return; - - // Shared summary comment: each workflow (check/lint/tests) owns one - // delimited section and only ever replaces its own, so they combine - // into a single comment regardless of which finishes first. - const topMarker = ''; - const sectionKey = 'check'; - const sectionBody = [ - ``, - `## ${ok ? '✅' : '❌'} Package checks`, - '', - content, - `` - ].join('\n'); - - const comments = await github.rest.issues.listComments({ - owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber - }); - const existing = comments.data.find(c => c.body.includes(topMarker)); - if (!existing) { - await github.rest.issues.createComment({ - owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, - body: [topMarker, '', sectionBody].join('\n') - }); - } else { - const sectionRegex = new RegExp(`[\\s\\S]*?`); - const newBody = sectionRegex.test(existing.body) - ? existing.body.replace(sectionRegex, sectionBody) - : existing.body + '\n\n' + sectionBody; - await github.rest.issues.updateComment({ - owner: context.repo.owner, repo: context.repo.repo, comment_id: existing.id, body: newBody - }); - } + const postCiComment = require('${{ github.workspace }}/.github/scripts/post-ci-comment.js'); + await postCiComment({ github, context, updates: { + 'row:check': `Devtools checks${ok ? '✅ passed' : '❌ failed'}`, + 'log:check': `[Devtools checks](${runUrl})` + }}); diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index 9d49785e0..90aa644b3 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -293,10 +293,13 @@ jobs: dsBaseClient/logs/test_results.xml dsBaseClient/logs/test_console_output.txt - - name: Print link to run summary + - name: Fail if any shard failed if: always() run: | - echo "::notice title=Test summary::Job summaries only render on the run's own Summary page, not here - see ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + if [ "${{ needs.opal-dsbase.result }}" != "success" ]; then + echo "One or more Opal dsbase/dsdanger matrix entries did not succeed - failing this report even though the merged JUnit results (from whichever shards did upload) may show 0 failures. A shard that never produced results must not look like a pass." + exit 1 + fi ################################################################################ @@ -527,10 +530,13 @@ jobs: dsBaseClient/logs/test_results.xml dsBaseClient/logs/test_console_output.txt - - name: Print link to run summary + - name: Fail if any shard failed if: always() run: | - echo "::notice title=Test summary::Job summaries only render on the run's own Summary page, not here - see ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + if [ "${{ needs.armadillo-dsbase.result }}" != "success" ]; then + echo "One or more Armadillo dsbase/dsdanger matrix entries did not succeed - failing this report even though the merged JUnit results (from whichever shards did upload) may show 0 failures. A shard that never produced results must not look like a pass." + exit 1 + fi ################################################################################ @@ -549,8 +555,11 @@ jobs: timeout-minutes: 15 permissions: contents: read + actions: read pull-requests: write steps: + - uses: actions/checkout@v4 + - uses: r-lib/actions/setup-r@v2 with: r-version: release @@ -576,8 +585,11 @@ jobs: pattern: '*-dsbase-*' path: artifacts/shards - - name: Write combined job summary + - name: Compute test/coverage results id: summary + env: + OPAL_REPORT_RESULT: ${{ needs.opal-report.result }} + ARMADILLO_REPORT_RESULT: ${{ needs.armadillo-report.result }} run: | Rscript -e ' find_version <- function(prefix) { @@ -593,7 +605,7 @@ jobs: files[1] } - summarise_backend <- function(name, artifact_dir, version) { + summarise_backend <- function(artifact_dir) { doc <- xml2::read_xml(find_results_xml(artifact_dir)) suites <- xml2::xml_find_all(doc, ".//testsuite") n_tests <- sum(as.integer(xml2::xml_attr(suites, "tests")), na.rm = TRUE) @@ -601,59 +613,26 @@ jobs: n_errors <- sum(as.integer(xml2::xml_attr(suites, "errors")), na.rm = TRUE) n_skipped <- sum(as.integer(xml2::xml_attr(suites, "skipped")), na.rm = TRUE) n_pass <- n_tests - n_failures - n_errors - n_skipped - - failed <- xml2::xml_find_all(doc, ".//testcase[failure or error]") - ok <- length(failed) == 0 - - if (ok) { - fail_block <- character(0) - fail_list <- "All tests passed." - } else { - msgs <- vapply(failed, function(tc) { - node <- xml2::xml_find_first(tc, "failure|error") - m <- xml2::xml_attr(node, "message") - if (is.na(m) || !nzchar(m)) m <- trimws(xml2::xml_text(node)) - m - }, character(1)) - labels <- paste0(xml2::xml_attr(failed, "classname"), "::", xml2::xml_attr(failed, "name")) - fail_block <- unlist(lapply(seq_along(failed), function(i) { - c(sprintf("-- Failure (%s) %s", labels[i], strrep("-", max(1, 60 - nchar(labels[i])))), msgs[i], "") - })) - fail_list <- paste0("- `", labels, "` - ", msgs) - } - - tally <- sprintf("[ FAIL %d | WARN 0 | SKIP %d | PASS %d ]", n_failures + n_errors, n_skipped, n_pass) - list( - block = c( - sprintf("### %s %s %s", if (ok) "✅" else "❌", name, if (ok) "passed" else "failed"), - "", - sprintf("dsBase %s", version), - "", - "```", - fail_block, - tally, - "```", - "", - "
Failed tests", - "", - fail_list, - "", - "
" - ), - n_failures = n_failures, - n_errors = n_errors + ok = (n_failures + n_errors) == 0, + tally = sprintf("[ FAIL %d | WARN 0 | SKIP %d | PASS %d ]", n_failures + n_errors, n_skipped, n_pass) ) } - armadillo <- summarise_backend("Armadillo", "artifacts/reports/armadillo-report-results", find_version("armadillo-dsbase")) - opal <- summarise_backend("Opal", "artifacts/reports/opal-report-results", find_version("opal-dsbase")) - tests_ok <- (armadillo$n_failures + armadillo$n_errors + opal$n_failures + opal$n_errors) == 0 + armadillo <- summarise_backend("artifacts/reports/armadillo-report-results") + opal <- summarise_backend("artifacts/reports/opal-report-results") - # Coverage threshold here is informational only, for the status - # line below - actual enforcement lives in Codecovs own - # patch-coverage check (see codecov.yml), scoped to lines changed - # by the PR rather than the whole repo, so it does not gate this job. + # A report job fails hard (see its "Fail if any shard failed" step) + # if any matrix shard did not succeed, even if the shards that DID + # upload results show 0 failures - a shard that never reported must + # not look like a pass. + reports_ok <- Sys.getenv("OPAL_REPORT_RESULT") == "success" && Sys.getenv("ARMADILLO_REPORT_RESULT") == "success" + tests_ok <- armadillo$ok && opal$ok && reports_ok + + # Coverage threshold here is informational only - actual + # enforcement lives in Codecovs own patch-coverage check (see + # codecov.yml), scoped to lines changed by the PR rather than the + # whole repo, so it does not gate this job. coverage_threshold <- 80 rds_files <- list.files("artifacts/shards", pattern = "coverage\\.rds$", recursive = TRUE, full.names = TRUE) if (length(rds_files) > 0) { @@ -661,35 +640,28 @@ jobs: agg <- aggregate(value ~ filename + line, data = do.call(rbind, tallies), FUN = sum) totalcoverage <- round(sum(agg$value > 0) / nrow(agg) * 100, 1) coverage_ok <- totalcoverage >= coverage_threshold + coverage_text <- sprintf("%.1f%% vs %d%% target", totalcoverage, coverage_threshold) } else { - totalcoverage <- NA coverage_ok <- NA + coverage_text <- "no coverage data found" } - - status_icon <- function(ok) if (isTRUE(ok)) "✅" else if (isFALSE(ok)) "❌" else "❓" - - cov_status_line <- if (is.na(coverage_ok)) { - sprintf("%s Coverage unknown: no coverage data found", status_icon(NA)) - } else { - sprintf("%s Coverage %s: %.1f%% vs %d%% target (informational - see Codecov patch check for enforcement)", - status_icon(coverage_ok), if (coverage_ok) "passed" else "failed", totalcoverage, coverage_threshold) - } - - codecov_url <- sprintf("https://app.codecov.io/gh/%s/commit/%s", Sys.getenv("GITHUB_REPOSITORY"), Sys.getenv("GITHUB_SHA")) - - summary <- c( - "## dsBaseClient test suite results", - "", - cov_status_line, - sprintf("[View coverage report on Codecov](%s)", codecov_url), - "", - armadillo$block, - "", - opal$block + coverage_icon <- if (isTRUE(coverage_ok)) "✅" else if (isFALSE(coverage_ok)) "❌" else "❓" + + out <- Sys.getenv("GITHUB_OUTPUT") + cat( + sprintf("tests_ok=%s\n", tolower(tests_ok)), + sprintf("armadillo_ok=%s\n", tolower(armadillo$ok)), + sprintf("armadillo_tally=%s\n", armadillo$tally), + sprintf("armadillo_version=%s\n", find_version("armadillo-dsbase")), + sprintf("opal_ok=%s\n", tolower(opal$ok)), + sprintf("opal_tally=%s\n", opal$tally), + sprintf("opal_version=%s\n", find_version("opal-dsbase")), + sprintf("coverage_icon=%s\n", coverage_icon), + sprintf("coverage_text=%s\n", coverage_text), + file = out, append = TRUE, sep = "" ) - writeLines(summary, Sys.getenv("GITHUB_STEP_SUMMARY")) - if (!tests_ok) message("Tests failed - see the summary above for details.") + if (!tests_ok) message("Tests failed - see the PR comment for details.") quit(save = "no", status = if (tests_ok) 0 else 1) ' @@ -698,51 +670,35 @@ jobs: uses: actions/github-script@v7 with: script: | - const fs = require('fs'); - const ok = '${{ steps.summary.outcome }}' === 'success'; - const content = fs.readFileSync(process.env.GITHUB_STEP_SUMMARY, 'utf8'); - - const branch = context.ref.replace('refs/heads/', ''); - const prs = await github.rest.pulls.list({ - owner: context.repo.owner, repo: context.repo.repo, - head: `${context.repo.owner}:${branch}`, state: 'open' + const s = { + armadillo_ok: '${{ steps.summary.outputs.armadillo_ok }}' === 'true', + armadillo_tally: '${{ steps.summary.outputs.armadillo_tally }}', + armadillo_version: '${{ steps.summary.outputs.armadillo_version }}', + opal_ok: '${{ steps.summary.outputs.opal_ok }}' === 'true', + opal_tally: '${{ steps.summary.outputs.opal_tally }}', + opal_version: '${{ steps.summary.outputs.opal_version }}', + coverage_icon: '${{ steps.summary.outputs.coverage_icon }}', + coverage_text: '${{ steps.summary.outputs.coverage_text }}' + }; + + const codecovUrl = `https://app.codecov.io/gh/${context.repo.owner}/${context.repo.repo}/commit/${context.sha}`; + + const jobs = await github.rest.actions.listJobsForWorkflowRun({ + owner: context.repo.owner, repo: context.repo.repo, run_id: context.runId }); - const prNumber = prs.data[0]?.number; - if (!prNumber) return; - - // Shared summary comment: each workflow (check/lint/tests) owns one - // delimited section and only ever replaces its own, so they combine - // into a single comment regardless of which finishes first. - const topMarker = ''; - const sectionKey = 'tests'; - const sectionBody = [ - ``, - `## ${ok ? '✅' : '❌'} Tests`, - '', - content, - `` - ].join('\n'); - - const comments = await github.rest.issues.listComments({ - owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber - }); - const existing = comments.data.find(c => c.body.includes(topMarker)); - if (!existing) { - await github.rest.issues.createComment({ - owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, - body: [topMarker, '', sectionBody].join('\n') - }); - } else { - const sectionRegex = new RegExp(`[\\s\\S]*?`); - const newBody = sectionRegex.test(existing.body) - ? existing.body.replace(sectionRegex, sectionBody) - : existing.body + '\n\n' + sectionBody; - await github.rest.issues.updateComment({ - owner: context.repo.owner, repo: context.repo.repo, comment_id: existing.id, body: newBody - }); + function jobUrl(name) { + const j = jobs.data.jobs.find(j => j.name === name); + return j ? j.html_url : `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; } - - name: Print link to run summary - if: always() - run: | - echo "::notice title=Test summary::Job summaries only render on the run's own Summary page, not here - see ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + const postCiComment = require('${{ github.workspace }}/.github/scripts/post-ci-comment.js'); + await postCiComment({ github, context, updates: { + 'row:tests-armadillo': `Armadillo unit tests${s.armadillo_ok ? '✅' : '❌'} ${s.armadillo_tally}`, + 'row:tests-opal': `Opal unit tests${s.opal_ok ? '✅' : '❌'} ${s.opal_tally}`, + 'row:coverage': `Test coverage${s.coverage_icon} ${s.coverage_text} (informational)`, + 'ver:armadillo': s.armadillo_version, + 'ver:opal': s.opal_version, + 'log:tests-armadillo': `[Armadillo unit tests](${jobUrl('Armadillo report')})`, + 'log:tests-opal': `[Opal unit tests](${jobUrl('Opal report')})`, + 'log:coverage': `[Codecov](${codecovUrl})` + }}); diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 0c36acf72..bf1a64be5 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -72,57 +72,14 @@ jobs: lint_files <- vapply(lints, function(l) l$filename, character(1)) is_new <- if (is_pr) lint_files %in% changed_files else rep(FALSE, length(lints)) + n_new <- sum(is_new) - format_findings <- function(idx) { - vapply(idx, function(i) { - l <- lints[[i]] - sprintf("- `%s:%d` %s - %s", basename(l$filename), l$line_number, l$linter, l$message) - }, character(1)) - } - - if (length(lints) == 0) { - summary <- c("## Lint results", "", "No findings.") - } else if (is_pr) { - new_idx <- which(is_new) - existing_idx <- which(!is_new) - summary <- c( - "## Lint results", - "", - sprintf("**%d findings in files changed by this PR** (fails the check)", length(new_idx)), - if (length(new_idx) > 0) format_findings(new_idx) else "None.", - "", - sprintf("
%d pre-existing findings elsewhere in the repo (informational only)", length(existing_idx)), - "", - format_findings(existing_idx), - "", - "
" - ) - } else { - linter_names <- vapply(lints, function(l) l$linter, character(1)) - counts <- sort(table(linter_names), decreasing = TRUE) - count_rows <- paste0("| ", names(counts), " | ", as.integer(counts), " |") - summary <- c( - "## Lint results", - "", - sprintf("**%d findings** (correctness/robustness/common_mistakes linters, informational only outside PRs)", length(lints)), - "", - "| Linter | Count |", - "|---|---|", - count_rows, - "", - "
All findings", - "", - format_findings(seq_along(lints)), - "", - "
" - ) - } - writeLines(summary, Sys.getenv("GITHUB_STEP_SUMMARY")) + cat(sprintf("n_new=%d\n", n_new), file = Sys.getenv("GITHUB_OUTPUT"), append = TRUE) - if (any(is_new)) { - message("Lint found issues in files changed by this PR - see the annotations above, the job summary, or the SARIF upload for details.") + if (n_new > 0) { + message(sprintf("Lint found %d issue(s) in files changed by this PR - see the annotations above or the SARIF upload for details.", n_new)) } - quit(save = "no", status = if (any(is_new)) 1 else 0) + quit(save = "no", status = if (n_new > 0) 1 else 0) ' - name: Upload lint results @@ -136,39 +93,12 @@ jobs: uses: actions/github-script@v7 with: script: | - const fs = require('fs'); - const ok = '${{ steps.lint.outcome }}' === 'success'; - const content = fs.readFileSync(process.env.GITHUB_STEP_SUMMARY, 'utf8'); - const prNumber = context.payload.pull_request.number; - - // Shared summary comment: each workflow (check/lint/tests) owns one - // delimited section and only ever replaces its own, so they combine - // into a single comment regardless of which finishes first. - const topMarker = ''; - const sectionKey = 'lint'; - const sectionBody = [ - ``, - `## ${ok ? '✅' : '❌'} Lint`, - '', - content, - `` - ].join('\n'); - - const comments = await github.rest.issues.listComments({ - owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber - }); - const existing = comments.data.find(c => c.body.includes(topMarker)); - if (!existing) { - await github.rest.issues.createComment({ - owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, - body: [topMarker, '', sectionBody].join('\n') - }); - } else { - const sectionRegex = new RegExp(`[\\s\\S]*?`); - const newBody = sectionRegex.test(existing.body) - ? existing.body.replace(sectionRegex, sectionBody) - : existing.body + '\n\n' + sectionBody; - await github.rest.issues.updateComment({ - owner: context.repo.owner, repo: context.repo.repo, comment_id: existing.id, body: newBody - }); - } + const nNew = parseInt('${{ steps.lint.outputs.n_new }}' || '0', 10); + const ok = nNew === 0; + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + + const postCiComment = require('${{ github.workspace }}/.github/scripts/post-ci-comment.js'); + await postCiComment({ github, context, updates: { + 'row:lint': `Code quality${ok ? '✅ 0 findings' : `❌ ${nNew} finding${nNew === 1 ? '' : 's'}`}`, + 'log:lint': `[Code quality](${runUrl})` + }}); From 2e5cbddbfff51a4765d6593bd6545b5ed462865f Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Tue, 1 Sep 2026 09:40:15 +0200 Subject: [PATCH 49/56] Add shared PR-comment upsert script --- .github/scripts/post-ci-comment.js | 86 ++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 .github/scripts/post-ci-comment.js diff --git a/.github/scripts/post-ci-comment.js b/.github/scripts/post-ci-comment.js new file mode 100644 index 000000000..fcfa80d2e --- /dev/null +++ b/.github/scripts/post-ci-comment.js @@ -0,0 +1,86 @@ +// Shared by check.yaml / lint.yaml / dsBaseClient_test_suite.yaml's "Post PR +// comment" steps. Each workflow owns a subset of the markers below and only +// ever replaces its own, so the three runs (independent workflows, no +// ordering guarantee) converge on one combined PR comment regardless of +// which finishes first. The headline is recomputed from the row markers' +// current state on every update. + +const ROW_KEYS = ['row:check', 'row:lint', 'row:tests-armadillo', 'row:tests-opal', 'row:coverage']; +const TOP_MARKER = ''; + +const SKELETON = [ + TOP_MARKER, + '⏳ Running checks...', + '', + 'Tested against dsBase versions:', + 'Armadillo: _pending_', + 'Opal: _pending_', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '
CheckResult
Devtools checks⏳ pending
Code quality⏳ pending
Armadillo unit tests⏳ pending
Opal unit tests⏳ pending
Test coverage⏳ pending
', + '', + 'Logs: _pending_ · _pending_ · _pending_ · _pending_ · _pending_' +].join('\n'); + +function replaceMarker(body, key, content) { + const re = new RegExp(`[\\s\\S]*?`); + const replacement = `${content}`; + return re.test(body) ? body.replace(re, replacement) : body; +} + +function computeHeadline(body) { + let pass = 0, fail = 0, pending = 0; + for (const key of ROW_KEYS) { + const re = new RegExp(`([\\s\\S]*?)`); + const m = body.match(re); + const text = m ? m[1] : ''; + if (text.includes('❌')) fail++; + else if (text.includes('✅')) pass++; + else pending++; + } + if (fail > 0) return `❌ ${fail} of ${ROW_KEYS.length} checks failed`; + if (pending > 0) return `⏳ ${pass} of ${ROW_KEYS.length} checks reported so far`; + return `✅ All ${ROW_KEYS.length} checks passed`; +} + +module.exports = async function postCiComment({ github, context, updates }) { + let prNumber = context.payload.pull_request?.number; + if (!prNumber) { + const branch = context.ref.replace('refs/heads/', ''); + const prs = await github.rest.pulls.list({ + owner: context.repo.owner, repo: context.repo.repo, + head: `${context.repo.owner}:${branch}`, state: 'open' + }); + prNumber = prs.data[0]?.number; + } + if (!prNumber) return; + + const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber + }); + const existing = comments.data.find(c => c.body.includes(TOP_MARKER)); + + let body = existing ? existing.body : SKELETON; + for (const [key, content] of Object.entries(updates)) { + body = replaceMarker(body, key, content); + } + body = replaceMarker(body, 'headline', computeHeadline(body)); + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, repo: context.repo.repo, comment_id: existing.id, body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, body + }); + } +}; From b7ebbc11be6b04657d085e53cee87a0769e7ae22 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Tue, 1 Sep 2026 09:43:33 +0200 Subject: [PATCH 50/56] fix grep --- .github/actions/setup-armadillo-with-dsbase/action.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/setup-armadillo-with-dsbase/action.yaml b/.github/actions/setup-armadillo-with-dsbase/action.yaml index 5a9a8a2c2..da6972c23 100644 --- a/.github/actions/setup-armadillo-with-dsbase/action.yaml +++ b/.github/actions/setup-armadillo-with-dsbase/action.yaml @@ -195,5 +195,5 @@ runs: - name: Dump Armadillo server log if: failure() shell: bash - run: grep "Caused by:" armadillo_home/logs/stdout.log | sort -u + run: grep "Caused by:" armadillo_home/logs/stdout.log | sort -u || true working-directory: dsBaseClient From 7d5b625b0ca5e868d6afc92f25a809b4656653e9 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Tue, 1 Sep 2026 10:48:42 +0200 Subject: [PATCH 51/56] fixed comment rendering --- .github/scripts/post-ci-comment.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/scripts/post-ci-comment.js b/.github/scripts/post-ci-comment.js index fcfa80d2e..ae5b2f2de 100644 --- a/.github/scripts/post-ci-comment.js +++ b/.github/scripts/post-ci-comment.js @@ -68,7 +68,12 @@ module.exports = async function postCiComment({ github, context, updates }) { }); const existing = comments.data.find(c => c.body.includes(TOP_MARKER)); - let body = existing ? existing.body : SKELETON; + // A comment from before this skeleton's format changed won't contain our + // current markers - patching it would silently no-op every replace below. + // Reset it to a fresh skeleton (still updating the same comment in place, + // not creating a new one) rather than leaving stale content untouched. + const isCompatible = existing && ROW_KEYS.some(key => existing.body.includes(``)); + let body = isCompatible ? existing.body : SKELETON; for (const [key, content] of Object.entries(updates)) { body = replaceMarker(body, key, content); } From 59ae979d412b67913d368baf0b94cfa0934fe9b7 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Tue, 1 Sep 2026 11:43:32 +0200 Subject: [PATCH 52/56] simplify workflow --- .../setup-opal-with-dsbase/action.yaml | 1 - .github/scripts/post-ci-comment.js | 8 +- .../workflows/dsBaseClient_test_suite.yaml | 329 +++++++++--------- 3 files changed, 160 insertions(+), 178 deletions(-) diff --git a/.github/actions/setup-opal-with-dsbase/action.yaml b/.github/actions/setup-opal-with-dsbase/action.yaml index e3270891a..6a82b447c 100644 --- a/.github/actions/setup-opal-with-dsbase/action.yaml +++ b/.github/actions/setup-opal-with-dsbase/action.yaml @@ -51,7 +51,6 @@ runs: dependencies: 'c("Depends", "Imports", "LinkingTo")' extra-packages: | cran::devtools - cran::covr cran::fields cran::meta cran::metafor diff --git a/.github/scripts/post-ci-comment.js b/.github/scripts/post-ci-comment.js index ae5b2f2de..b66006289 100644 --- a/.github/scripts/post-ci-comment.js +++ b/.github/scripts/post-ci-comment.js @@ -12,10 +12,6 @@ const SKELETON = [ TOP_MARKER, '⏳ Running checks...', '', - 'Tested against dsBase versions:', - 'Armadillo: _pending_', - 'Opal: _pending_', - '', '', '', '', @@ -27,6 +23,10 @@ const SKELETON = [ '', '
CheckResult
', '', + 'Tested against dsBase versions:', + 'Armadillo: _pending_', + 'Opal: _pending_', + '', 'Logs: _pending_ · _pending_ · _pending_ · _pending_ · _pending_' ].join('\n'); diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index 90aa644b3..885f84dc3 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -12,12 +12,18 @@ # opal-dsbase (matrix x8) - dsBase suite against Opal, one shard per # category, plus a dsdanger entry, all grouped under # one summary box. -# opal-report - needs opal-dsbase; merges results. +# opal-report - needs opal-dsbase; merges results, computes its own +# pass/fail, posts its own row into the shared PR +# comment (see .github/scripts/post-ci-comment.js). # armadillo-dsbase (matrix x8) - dsBase suite against Armadillo, same split. -# armadillo-report - needs armadillo-dsbase; merges results. -# test-summary - needs opal-report + armadillo-report; one combined -# coverage figure + pass/fail table + collapsible -# failure details, written to the job summary. +# armadillo-report - needs armadillo-dsbase; same as opal-report, plus +# coverage (computed here only - see comment at that +# step for why one backend's figure is sufficient). +# +# There is no separate combining/summary job: dsBaseClient's own R/ source +# never branches on backend, so Armadillo and Opal results are independently +# meaningful and each report job stands alone (gate on both being required +# checks in branch protection, rather than one job that waits on both). # # The dsBase suite (matching TEST_FILTER_DSBASE - same 292 files the Azure # pipelines run) is split into 7 shards - smk (116 files, 2 shards), perf (51 @@ -154,26 +160,19 @@ jobs: R -q -e "library(opalr); opal <- opal.login('administrator','datashield_test&', url='http://localhost:8080/'); dsadmin.install_github_package(opal, 'dsDanger', username = 'datashield', ref = '${{ env.DSDANGER_REF }}'); opal.logout(opal)" working-directory: dsBaseClient - - name: Run dsBase tests with coverage & JUnit report + - name: Run dsBase tests with JUnit report if: matrix.category != 'dsdanger' run: | - R -q -e "devtools::load_all();" R -q -e ' - cov <- covr::package_coverage( - type = c("none"), - code = c('"'"' - output_file <- file("test_console_output_dsbase.txt"); - sink(output_file, split = TRUE); - junit_rep <- testthat::JunitReporter$new(file = file.path(getwd(), "test_results_dsbase.xml")); - progress_rep <- testthat::ProgressReporter$new(max_failures = 999999); - multi_rep <- testthat::MultiReporter$new(reporters = list(progress_rep, junit_rep)); - options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); - testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ matrix.filter }}", reporter = multi_rep, stop_on_failure = FALSE)'"'"' - ) - ) - saveRDS(cov, "coverage.rds") - write.csv(covr::coverage_to_list(cov), "coveragelist.csv") - covr::to_cobertura(cov, "cobertura.xml")' || R_EXIT=$? + devtools::load_all(quiet = TRUE); + library(testthat); + output_file <- file("test_console_output_dsbase.txt"); + sink(output_file, split = TRUE); + junit_rep <- JunitReporter$new(file = file.path(getwd(), "test_results_dsbase.xml")); + progress_rep <- ProgressReporter$new(max_failures = 999999); + multi_rep <- MultiReporter$new(reporters = list(progress_rep, junit_rep)); + options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}"); + test_dir("tests/testthat", filter = "${{ matrix.filter }}", reporter = multi_rep, stop_on_failure = FALSE)' || R_EXIT=$? cat test_console_output_dsbase.txt n_tests=$(grep -c ' 0) { + msgs <- vapply(failed, function(tc) { + node <- xml2::xml_find_first(tc, "failure|error") + m <- xml2::xml_attr(node, "message") + if (is.na(m) || !nzchar(m)) m <- trimws(xml2::xml_text(node)) + m + }, character(1)) + labels <- paste0(xml2::xml_attr(failed, "classname"), "::", xml2::xml_attr(failed, "name")) + fail_block <- unlist(lapply(seq_along(failed), function(i) { + c(sprintf("-- Failure (%s) %s", labels[i], strrep("-", max(1, 60 - nchar(labels[i])))), msgs[i], "") + })) + } + writeLines(c("## Opal unit tests", "", "```", fail_block, tally, "```"), Sys.getenv("GITHUB_STEP_SUMMARY")) + + version_files <- list.files("artifacts", pattern = "dsbase_version\\.txt$", recursive = TRUE, full.names = TRUE) + version <- if (length(version_files) > 0) trimws(readLines(version_files[1], warn = FALSE)[1]) else "unknown" + + # needs.opal-dsbase.result is "failure" if ANY matrix shard did not + # succeed (even if the shards that DID upload results show 0 + # failures) - a shard that never reported must not look like a pass. + shard_ok <- Sys.getenv("OPAL_DSBASE_RESULT") == "success" + ok <- (n_failures + n_errors) == 0 && shard_ok + if (!shard_ok) message("One or more Opal dsbase/dsdanger matrix entries did not succeed.") + + out <- Sys.getenv("GITHUB_OUTPUT") + cat( + sprintf("ok=%s\n", tolower(ok)), + sprintf("tally=%s\n", tally), + sprintf("version=%s\n", version), + file = out, append = TRUE, sep = "" + ) + + if (!ok) message("Opal tests failed - see the PR comment / job summary for details.") + quit(save = "no", status = if (ok) 0 else 1) + ' + working-directory: dsBaseClient + + - name: Post PR comment + if: always() + uses: actions/github-script@v7 + with: + script: | + const ok = '${{ steps.results.outputs.ok }}' === 'true'; + const tally = '${{ steps.results.outputs.tally }}'; + const version = '${{ steps.results.outputs.version }}'; + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + + const postCiComment = require('${{ github.workspace }}/dsBaseClient/.github/scripts/post-ci-comment.js'); + await postCiComment({ github, context, updates: { + 'row:tests-opal': `Opal unit tests${ok ? '✅' : '❌'} ${tally}`, + 'ver:opal': version, + 'log:tests-opal': `[Opal unit tests](${runUrl})` + }}); ################################################################################ @@ -482,6 +536,9 @@ jobs: if: always() runs-on: ubuntu-latest timeout-minutes: 30 + permissions: + contents: read + pull-requests: write steps: - name: Checkout dsBaseClient uses: actions/checkout@v4 @@ -499,6 +556,7 @@ jobs: dependencies: 'c("Depends", "Imports", "LinkingTo")' extra-packages: | cran::xml2 + cran::covr - name: Download shard/dsdanger results uses: actions/download-artifact@v4 @@ -530,111 +588,47 @@ jobs: dsBaseClient/logs/test_results.xml dsBaseClient/logs/test_console_output.txt - - name: Fail if any shard failed - if: always() - run: | - if [ "${{ needs.armadillo-dsbase.result }}" != "success" ]; then - echo "One or more Armadillo dsbase/dsdanger matrix entries did not succeed - failing this report even though the merged JUnit results (from whichever shards did upload) may show 0 failures. A shard that never produced results must not look like a pass." - exit 1 - fi - - - ################################################################################ - # Single consolidated summary across both backends: one combined coverage - # figure (merged from every shard's raw coverage.rds, both backends - # together - coverage measures client-side code paths, which are largely - # backend-agnostic, so one figure is more meaningful than two near-duplicate - # ones), a pass/fail table per backend, and failures tucked into collapsible - #
blocks rather than cluttering the main summary. - ################################################################################ - test-summary: - name: Test summary - needs: [opal-report, armadillo-report] - if: always() - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: read - actions: read - pull-requests: write - steps: - - uses: actions/checkout@v4 - - - uses: r-lib/actions/setup-r@v2 - with: - r-version: release - use-public-rspm: true - - - uses: r-lib/actions/setup-r-dependencies@v2 - with: - dependencies: 'c("Depends", "Imports", "LinkingTo")' - packages: 'any::sessioninfo' - extra-packages: | - cran::xml2 - cran::covr - - - name: Download merged report results - uses: actions/download-artifact@v4 - with: - pattern: '*-report-results' - path: artifacts/reports - - - name: Download shard coverage and versions - uses: actions/download-artifact@v4 - with: - pattern: '*-dsbase-*' - path: artifacts/shards - - - name: Compute test/coverage results - id: summary + - name: Compute results & write summary + id: results env: - OPAL_REPORT_RESULT: ${{ needs.opal-report.result }} - ARMADILLO_REPORT_RESULT: ${{ needs.armadillo-report.result }} + ARMADILLO_DSBASE_RESULT: ${{ needs.armadillo-dsbase.result }} run: | Rscript -e ' - find_version <- function(prefix) { - files <- list.files("artifacts/shards", pattern = "dsbase_version\\.txt$", recursive = TRUE, full.names = TRUE) - files <- files[grepl(prefix, files)] - if (length(files) == 0) return("unknown") - trimws(readLines(files[1], warn = FALSE)[1]) - } - - find_results_xml <- function(artifact_dir) { - files <- list.files(artifact_dir, pattern = "^test_results\\.xml$", recursive = TRUE, full.names = TRUE) - if (length(files) == 0) stop(sprintf("No test_results.xml found under %s", artifact_dir)) - files[1] + doc <- xml2::read_xml("logs/test_results.xml") + suites <- xml2::xml_find_all(doc, ".//testsuite") + n_tests <- sum(as.integer(xml2::xml_attr(suites, "tests")), na.rm = TRUE) + n_failures <- sum(as.integer(xml2::xml_attr(suites, "failures")), na.rm = TRUE) + n_errors <- sum(as.integer(xml2::xml_attr(suites, "errors")), na.rm = TRUE) + n_skipped <- sum(as.integer(xml2::xml_attr(suites, "skipped")), na.rm = TRUE) + n_pass <- n_tests - n_failures - n_errors - n_skipped + tally <- sprintf("[ FAIL %d | WARN 0 | SKIP %d | PASS %d ]", n_failures + n_errors, n_skipped, n_pass) + + failed <- xml2::xml_find_all(doc, ".//testcase[failure or error]") + fail_block <- character(0) + if (length(failed) > 0) { + msgs <- vapply(failed, function(tc) { + node <- xml2::xml_find_first(tc, "failure|error") + m <- xml2::xml_attr(node, "message") + if (is.na(m) || !nzchar(m)) m <- trimws(xml2::xml_text(node)) + m + }, character(1)) + labels <- paste0(xml2::xml_attr(failed, "classname"), "::", xml2::xml_attr(failed, "name")) + fail_block <- unlist(lapply(seq_along(failed), function(i) { + c(sprintf("-- Failure (%s) %s", labels[i], strrep("-", max(1, 60 - nchar(labels[i])))), msgs[i], "") + })) } + writeLines(c("## Armadillo unit tests", "", "```", fail_block, tally, "```"), Sys.getenv("GITHUB_STEP_SUMMARY")) - summarise_backend <- function(artifact_dir) { - doc <- xml2::read_xml(find_results_xml(artifact_dir)) - suites <- xml2::xml_find_all(doc, ".//testsuite") - n_tests <- sum(as.integer(xml2::xml_attr(suites, "tests")), na.rm = TRUE) - n_failures <- sum(as.integer(xml2::xml_attr(suites, "failures")), na.rm = TRUE) - n_errors <- sum(as.integer(xml2::xml_attr(suites, "errors")), na.rm = TRUE) - n_skipped <- sum(as.integer(xml2::xml_attr(suites, "skipped")), na.rm = TRUE) - n_pass <- n_tests - n_failures - n_errors - n_skipped - list( - ok = (n_failures + n_errors) == 0, - tally = sprintf("[ FAIL %d | WARN 0 | SKIP %d | PASS %d ]", n_failures + n_errors, n_skipped, n_pass) - ) - } - - armadillo <- summarise_backend("artifacts/reports/armadillo-report-results") - opal <- summarise_backend("artifacts/reports/opal-report-results") - - # A report job fails hard (see its "Fail if any shard failed" step) - # if any matrix shard did not succeed, even if the shards that DID - # upload results show 0 failures - a shard that never reported must - # not look like a pass. - reports_ok <- Sys.getenv("OPAL_REPORT_RESULT") == "success" && Sys.getenv("ARMADILLO_REPORT_RESULT") == "success" - tests_ok <- armadillo$ok && opal$ok && reports_ok + version_files <- list.files("artifacts", pattern = "dsbase_version\\.txt$", recursive = TRUE, full.names = TRUE) + version <- if (length(version_files) > 0) trimws(readLines(version_files[1], warn = FALSE)[1]) else "unknown" - # Coverage threshold here is informational only - actual - # enforcement lives in Codecovs own patch-coverage check (see - # codecov.yml), scoped to lines changed by the PR rather than the - # whole repo, so it does not gate this job. + # Coverage is only computed here (not for Opal): dsBaseClients own + # R/ source never branches on backend (verified - the only + # Opal/Armadillo-specific references in R/ are inside roxygen + # @examples comments, not executable code), so a single backends + # coverage figure is equivalent to a combined one. coverage_threshold <- 80 - rds_files <- list.files("artifacts/shards", pattern = "coverage\\.rds$", recursive = TRUE, full.names = TRUE) + rds_files <- list.files("artifacts", pattern = "coverage\\.rds$", recursive = TRUE, full.names = TRUE) if (length(rds_files) > 0) { tallies <- lapply(rds_files, function(f) covr::tally_coverage(readRDS(f))) agg <- aggregate(value ~ filename + line, data = do.call(rbind, tallies), FUN = sum) @@ -647,58 +641,47 @@ jobs: } coverage_icon <- if (isTRUE(coverage_ok)) "✅" else if (isFALSE(coverage_ok)) "❌" else "❓" + # needs.armadillo-dsbase.result is "failure" if ANY matrix shard did + # not succeed (even if the shards that DID upload results show 0 + # failures) - a shard that never reported must not look like a pass. + shard_ok <- Sys.getenv("ARMADILLO_DSBASE_RESULT") == "success" + ok <- (n_failures + n_errors) == 0 && shard_ok + if (!shard_ok) message("One or more Armadillo dsbase/dsdanger matrix entries did not succeed.") + out <- Sys.getenv("GITHUB_OUTPUT") cat( - sprintf("tests_ok=%s\n", tolower(tests_ok)), - sprintf("armadillo_ok=%s\n", tolower(armadillo$ok)), - sprintf("armadillo_tally=%s\n", armadillo$tally), - sprintf("armadillo_version=%s\n", find_version("armadillo-dsbase")), - sprintf("opal_ok=%s\n", tolower(opal$ok)), - sprintf("opal_tally=%s\n", opal$tally), - sprintf("opal_version=%s\n", find_version("opal-dsbase")), + sprintf("ok=%s\n", tolower(ok)), + sprintf("tally=%s\n", tally), + sprintf("version=%s\n", version), sprintf("coverage_icon=%s\n", coverage_icon), sprintf("coverage_text=%s\n", coverage_text), file = out, append = TRUE, sep = "" ) - if (!tests_ok) message("Tests failed - see the PR comment for details.") - quit(save = "no", status = if (tests_ok) 0 else 1) + if (!ok) message("Armadillo tests failed - see the PR comment / job summary for details.") + quit(save = "no", status = if (ok) 0 else 1) ' + working-directory: dsBaseClient - name: Post PR comment if: always() uses: actions/github-script@v7 with: script: | - const s = { - armadillo_ok: '${{ steps.summary.outputs.armadillo_ok }}' === 'true', - armadillo_tally: '${{ steps.summary.outputs.armadillo_tally }}', - armadillo_version: '${{ steps.summary.outputs.armadillo_version }}', - opal_ok: '${{ steps.summary.outputs.opal_ok }}' === 'true', - opal_tally: '${{ steps.summary.outputs.opal_tally }}', - opal_version: '${{ steps.summary.outputs.opal_version }}', - coverage_icon: '${{ steps.summary.outputs.coverage_icon }}', - coverage_text: '${{ steps.summary.outputs.coverage_text }}' - }; - + const ok = '${{ steps.results.outputs.ok }}' === 'true'; + const tally = '${{ steps.results.outputs.tally }}'; + const version = '${{ steps.results.outputs.version }}'; + const coverageIcon = '${{ steps.results.outputs.coverage_icon }}'; + const coverageText = '${{ steps.results.outputs.coverage_text }}'; + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; const codecovUrl = `https://app.codecov.io/gh/${context.repo.owner}/${context.repo.repo}/commit/${context.sha}`; - const jobs = await github.rest.actions.listJobsForWorkflowRun({ - owner: context.repo.owner, repo: context.repo.repo, run_id: context.runId - }); - function jobUrl(name) { - const j = jobs.data.jobs.find(j => j.name === name); - return j ? j.html_url : `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; - } - - const postCiComment = require('${{ github.workspace }}/.github/scripts/post-ci-comment.js'); + const postCiComment = require('${{ github.workspace }}/dsBaseClient/.github/scripts/post-ci-comment.js'); await postCiComment({ github, context, updates: { - 'row:tests-armadillo': `Armadillo unit tests${s.armadillo_ok ? '✅' : '❌'} ${s.armadillo_tally}`, - 'row:tests-opal': `Opal unit tests${s.opal_ok ? '✅' : '❌'} ${s.opal_tally}`, - 'row:coverage': `Test coverage${s.coverage_icon} ${s.coverage_text} (informational)`, - 'ver:armadillo': s.armadillo_version, - 'ver:opal': s.opal_version, - 'log:tests-armadillo': `[Armadillo unit tests](${jobUrl('Armadillo report')})`, - 'log:tests-opal': `[Opal unit tests](${jobUrl('Opal report')})`, + 'row:tests-armadillo': `Armadillo unit tests${ok ? '✅' : '❌'} ${tally}`, + 'row:coverage': `Test coverage${coverageIcon} ${coverageText}`, + 'ver:armadillo': version, + 'log:tests-armadillo': `[Armadillo unit tests](${runUrl})`, 'log:coverage': `[Codecov](${codecovUrl})` }}); + From 3033ca0600dfa7d1da7b8828ff2f88713ed55528 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Tue, 1 Sep 2026 12:20:56 +0200 Subject: [PATCH 53/56] link back from summary to PR --- .github/workflows/check.yaml | 2 +- .github/workflows/dsBaseClient_test_suite.yaml | 6 +++--- .github/workflows/lint.yaml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 20bd931cb..bbc674bf2 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -80,5 +80,5 @@ jobs: const postCiComment = require('${{ github.workspace }}/.github/scripts/post-ci-comment.js'); await postCiComment({ github, context, updates: { 'row:check': `Devtools checks${ok ? '✅ passed' : '❌ failed'}`, - 'log:check': `[Devtools checks](${runUrl})` + 'log:check': `Devtools checks` }}); diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index 885f84dc3..391da6af3 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -352,7 +352,7 @@ jobs: await postCiComment({ github, context, updates: { 'row:tests-opal': `Opal unit tests${ok ? '✅' : '❌'} ${tally}`, 'ver:opal': version, - 'log:tests-opal': `[Opal unit tests](${runUrl})` + 'log:tests-opal': `Opal unit tests` }}); @@ -681,7 +681,7 @@ jobs: 'row:tests-armadillo': `Armadillo unit tests${ok ? '✅' : '❌'} ${tally}`, 'row:coverage': `Test coverage${coverageIcon} ${coverageText}`, 'ver:armadillo': version, - 'log:tests-armadillo': `[Armadillo unit tests](${runUrl})`, - 'log:coverage': `[Codecov](${codecovUrl})` + 'log:tests-armadillo': `Armadillo unit tests`, + 'log:coverage': `Codecov` }}); diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index bf1a64be5..a0615746c 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -100,5 +100,5 @@ jobs: const postCiComment = require('${{ github.workspace }}/.github/scripts/post-ci-comment.js'); await postCiComment({ github, context, updates: { 'row:lint': `Code quality${ok ? '✅ 0 findings' : `❌ ${nNew} finding${nNew === 1 ? '' : 's'}`}`, - 'log:lint': `[Code quality](${runUrl})` + 'log:lint': `Code quality` }}); From bd257199faa4c33d9c34cb2df5136a19cbe3d53f Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Tue, 1 Sep 2026 12:38:47 +0200 Subject: [PATCH 54/56] refactor: remove code duplication and unnecessary dependency install --- .../setup-opal-with-dsbase/action.yaml | 32 +++++++-- .github/scripts/post-ci-comment.js | 65 +++++++++++------ .github/workflows/check.yaml | 1 - .../workflows/dsBaseClient_test_suite.yaml | 72 ++++--------------- 4 files changed, 86 insertions(+), 84 deletions(-) diff --git a/.github/actions/setup-opal-with-dsbase/action.yaml b/.github/actions/setup-opal-with-dsbase/action.yaml index 6a82b447c..e60bec0db 100644 --- a/.github/actions/setup-opal-with-dsbase/action.yaml +++ b/.github/actions/setup-opal-with-dsbase/action.yaml @@ -72,7 +72,13 @@ runs: - name: Install test datasets to Opal shell: bash run: | - sleep 60 + for i in $(seq 1 18); do + if curl -sf -o /dev/null http://localhost:8080/; then + echo "Opal is responding" + break + fi + sleep 5 + done R -q -f "obiba_opal-upload_testing_datasets.R" working-directory: dsBaseClient/tests/testthat/data_files @@ -82,8 +88,6 @@ runs: R -q -e "library(opalr); opal <- opal.login(username = 'administrator', password = 'datashield_test&', url = 'http://localhost:8080/'); opal.put(opal, 'system', 'conf', 'general', '_rPackage'); opal.logout(opal)" R -q -e "library(opalr); opal <- opal.login('administrator','datashield_test&', url='http://localhost:8080/'); dsadmin.install_github_package(opal, 'dsBase', username = 'datashield', ref = '${{ inputs.dsbase-ref }}'); opal.logout(opal)" - sleep 60 - expected_version=$(curl -sf "https://raw.githubusercontent.com/datashield/dsBase/${{ inputs.dsbase-ref }}/DESCRIPTION" | sed -n 's/^Version: //p' | tr -d '\r') if [ -z "$expected_version" ]; then echo "Could not determine expected dsBase version from GitHub" @@ -92,7 +96,27 @@ runs: echo "Expected dsBase version: $expected_version" echo "$expected_version" > dsbase_version.txt - R -q -e "library(opalr); opal <- opal.login('administrator', 'datashield_test&', url='http://localhost:8080/'); desc <- dsadmin.package_description(opal, 'dsBase'); opal.logout(opal); installed_version <- desc[['Version']]; cat('Installed dsBase version:', installed_version, '\n'); if (is.null(installed_version) || installed_version != '$expected_version') stop('dsBase version mismatch: expected $expected_version, found ', installed_version)" + ok=false + for i in $(seq 1 18); do + if Rscript -e " + library(opalr) + opal <- opal.login('administrator', 'datashield_test&', url = 'http://localhost:8080/') + desc <- dsadmin.package_description(opal, 'dsBase') + opal.logout(opal) + installed <- desc[['Version']] + cat('Installed dsBase version:', if (is.null(installed)) '(none yet)' else installed, '\n') + quit(status = if (!is.null(installed) && installed == '$expected_version') 0 else 1) + "; then + ok=true + break + fi + sleep 10 + done + if [ "$ok" != "true" ]; then + echo "dsBase version mismatch or install not detected after polling: expected $expected_version" + exit 1 + fi + R -q -e "library(opalr); opal <- opal.login('administrator', 'datashield_test&', url='http://localhost:8080/'); dsadmin.profile_init(opal, name = 'default', packages = c('dsBase', 'dsTidyverse', 'resourcer')); opal.logout(opal)" R -q -e "library(opalr); opal <- opal.login('administrator', 'datashield_test&', url='http://localhost:8080/'); dsadmin.set_option(opal, 'default.datashield.privacyControlLevel', 'permissive'); opal.logout(opal)" working-directory: dsBaseClient/tests/testthat/data_files diff --git a/.github/scripts/post-ci-comment.js b/.github/scripts/post-ci-comment.js index b66006289..4a6abf442 100644 --- a/.github/scripts/post-ci-comment.js +++ b/.github/scripts/post-ci-comment.js @@ -51,6 +51,9 @@ function computeHeadline(body) { return `✅ All ${ROW_KEYS.length} checks passed`; } +const MAX_ATTEMPTS = 3; +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + module.exports = async function postCiComment({ github, context, updates }) { let prNumber = context.payload.pull_request?.number; if (!prNumber) { @@ -63,29 +66,49 @@ module.exports = async function postCiComment({ github, context, updates }) { } if (!prNumber) return; - const comments = await github.rest.issues.listComments({ - owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber - }); - const existing = comments.data.find(c => c.body.includes(TOP_MARKER)); + // Two workflows (e.g. opal-report/armadillo-report) can finish within + // moments of each other and both read-modify-write this same comment - + // there's no atomic compare-and-swap in the Issues API, so after writing + // we re-read and confirm OUR content actually landed. If a third write + // slipped in between our write and this check, retry the whole cycle + // against the latest body rather than silently losing the update. + for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { + const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber + }); + const existing = comments.data.find(c => c.body.includes(TOP_MARKER)); - // A comment from before this skeleton's format changed won't contain our - // current markers - patching it would silently no-op every replace below. - // Reset it to a fresh skeleton (still updating the same comment in place, - // not creating a new one) rather than leaving stale content untouched. - const isCompatible = existing && ROW_KEYS.some(key => existing.body.includes(``)); - let body = isCompatible ? existing.body : SKELETON; - for (const [key, content] of Object.entries(updates)) { - body = replaceMarker(body, key, content); - } - body = replaceMarker(body, 'headline', computeHeadline(body)); + // A comment from before this skeleton's format changed won't contain our + // current markers - patching it would silently no-op every replace below. + // Reset it to a fresh skeleton (still updating the same comment in place, + // not creating a new one) rather than leaving stale content untouched. + const isCompatible = existing && ROW_KEYS.some(key => existing.body.includes(``)); + let body = isCompatible ? existing.body : SKELETON; + for (const [key, content] of Object.entries(updates)) { + body = replaceMarker(body, key, content); + } + body = replaceMarker(body, 'headline', computeHeadline(body)); - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, repo: context.repo.repo, comment_id: existing.id, body - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, body + let commentId; + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, repo: context.repo.repo, comment_id: existing.id, body + }); + commentId = existing.id; + } else { + const created = await github.rest.issues.createComment({ + owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, body + }); + commentId = created.data.id; + } + + const verify = await github.rest.issues.getComment({ + owner: context.repo.owner, repo: context.repo.repo, comment_id: commentId }); + const landed = Object.entries(updates).every(([key, content]) => + verify.data.body.includes(`${content}`) + ); + if (landed) return; + if (attempt < MAX_ATTEMPTS) await sleep(1000 * attempt); } }; diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index bbc674bf2..dbe043284 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -48,7 +48,6 @@ jobs: extra-packages: | any::rcmdcheck cran::devtools - cran::usethis needs: check - name: Check manual updated diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index 391da6af3..5ebaa9ae7 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -250,7 +250,7 @@ jobs: - uses: r-lib/actions/setup-r-dependencies@v2 with: working-directory: dsBaseClient - dependencies: 'c("Depends", "Imports", "LinkingTo")' + packages: 'any::sessioninfo' extra-packages: | cran::xml2 @@ -290,45 +290,23 @@ jobs: OPAL_DSBASE_RESULT: ${{ needs.opal-dsbase.result }} run: | Rscript -e ' - doc <- xml2::read_xml("logs/test_results.xml") - suites <- xml2::xml_find_all(doc, ".//testsuite") - n_tests <- sum(as.integer(xml2::xml_attr(suites, "tests")), na.rm = TRUE) - n_failures <- sum(as.integer(xml2::xml_attr(suites, "failures")), na.rm = TRUE) - n_errors <- sum(as.integer(xml2::xml_attr(suites, "errors")), na.rm = TRUE) - n_skipped <- sum(as.integer(xml2::xml_attr(suites, "skipped")), na.rm = TRUE) - n_pass <- n_tests - n_failures - n_errors - n_skipped - tally <- sprintf("[ FAIL %d | WARN 0 | SKIP %d | PASS %d ]", n_failures + n_errors, n_skipped, n_pass) - - failed <- xml2::xml_find_all(doc, ".//testcase[failure or error]") - fail_block <- character(0) - if (length(failed) > 0) { - msgs <- vapply(failed, function(tc) { - node <- xml2::xml_find_first(tc, "failure|error") - m <- xml2::xml_attr(node, "message") - if (is.na(m) || !nzchar(m)) m <- trimws(xml2::xml_text(node)) - m - }, character(1)) - labels <- paste0(xml2::xml_attr(failed, "classname"), "::", xml2::xml_attr(failed, "name")) - fail_block <- unlist(lapply(seq_along(failed), function(i) { - c(sprintf("-- Failure (%s) %s", labels[i], strrep("-", max(1, 60 - nchar(labels[i])))), msgs[i], "") - })) - } - writeLines(c("## Opal unit tests", "", "```", fail_block, tally, "```"), Sys.getenv("GITHUB_STEP_SUMMARY")) + source(".github/scripts/summarise-junit.R") + res <- summarise_junit("logs/test_results.xml", "Opal") + writeLines(res$summary, Sys.getenv("GITHUB_STEP_SUMMARY")) - version_files <- list.files("artifacts", pattern = "dsbase_version\\.txt$", recursive = TRUE, full.names = TRUE) - version <- if (length(version_files) > 0) trimws(readLines(version_files[1], warn = FALSE)[1]) else "unknown" + version <- find_dsbase_version("artifacts") # needs.opal-dsbase.result is "failure" if ANY matrix shard did not # succeed (even if the shards that DID upload results show 0 # failures) - a shard that never reported must not look like a pass. shard_ok <- Sys.getenv("OPAL_DSBASE_RESULT") == "success" - ok <- (n_failures + n_errors) == 0 && shard_ok + ok <- res$ok && shard_ok if (!shard_ok) message("One or more Opal dsbase/dsdanger matrix entries did not succeed.") out <- Sys.getenv("GITHUB_OUTPUT") cat( sprintf("ok=%s\n", tolower(ok)), - sprintf("tally=%s\n", tally), + sprintf("tally=%s\n", res$tally), sprintf("version=%s\n", version), file = out, append = TRUE, sep = "" ) @@ -553,7 +531,7 @@ jobs: - uses: r-lib/actions/setup-r-dependencies@v2 with: working-directory: dsBaseClient - dependencies: 'c("Depends", "Imports", "LinkingTo")' + packages: 'any::sessioninfo' extra-packages: | cran::xml2 cran::covr @@ -594,33 +572,11 @@ jobs: ARMADILLO_DSBASE_RESULT: ${{ needs.armadillo-dsbase.result }} run: | Rscript -e ' - doc <- xml2::read_xml("logs/test_results.xml") - suites <- xml2::xml_find_all(doc, ".//testsuite") - n_tests <- sum(as.integer(xml2::xml_attr(suites, "tests")), na.rm = TRUE) - n_failures <- sum(as.integer(xml2::xml_attr(suites, "failures")), na.rm = TRUE) - n_errors <- sum(as.integer(xml2::xml_attr(suites, "errors")), na.rm = TRUE) - n_skipped <- sum(as.integer(xml2::xml_attr(suites, "skipped")), na.rm = TRUE) - n_pass <- n_tests - n_failures - n_errors - n_skipped - tally <- sprintf("[ FAIL %d | WARN 0 | SKIP %d | PASS %d ]", n_failures + n_errors, n_skipped, n_pass) - - failed <- xml2::xml_find_all(doc, ".//testcase[failure or error]") - fail_block <- character(0) - if (length(failed) > 0) { - msgs <- vapply(failed, function(tc) { - node <- xml2::xml_find_first(tc, "failure|error") - m <- xml2::xml_attr(node, "message") - if (is.na(m) || !nzchar(m)) m <- trimws(xml2::xml_text(node)) - m - }, character(1)) - labels <- paste0(xml2::xml_attr(failed, "classname"), "::", xml2::xml_attr(failed, "name")) - fail_block <- unlist(lapply(seq_along(failed), function(i) { - c(sprintf("-- Failure (%s) %s", labels[i], strrep("-", max(1, 60 - nchar(labels[i])))), msgs[i], "") - })) - } - writeLines(c("## Armadillo unit tests", "", "```", fail_block, tally, "```"), Sys.getenv("GITHUB_STEP_SUMMARY")) + source(".github/scripts/summarise-junit.R") + res <- summarise_junit("logs/test_results.xml", "Armadillo") + writeLines(res$summary, Sys.getenv("GITHUB_STEP_SUMMARY")) - version_files <- list.files("artifacts", pattern = "dsbase_version\\.txt$", recursive = TRUE, full.names = TRUE) - version <- if (length(version_files) > 0) trimws(readLines(version_files[1], warn = FALSE)[1]) else "unknown" + version <- find_dsbase_version("artifacts") # Coverage is only computed here (not for Opal): dsBaseClients own # R/ source never branches on backend (verified - the only @@ -645,13 +601,13 @@ jobs: # not succeed (even if the shards that DID upload results show 0 # failures) - a shard that never reported must not look like a pass. shard_ok <- Sys.getenv("ARMADILLO_DSBASE_RESULT") == "success" - ok <- (n_failures + n_errors) == 0 && shard_ok + ok <- res$ok && shard_ok if (!shard_ok) message("One or more Armadillo dsbase/dsdanger matrix entries did not succeed.") out <- Sys.getenv("GITHUB_OUTPUT") cat( sprintf("ok=%s\n", tolower(ok)), - sprintf("tally=%s\n", tally), + sprintf("tally=%s\n", res$tally), sprintf("version=%s\n", version), sprintf("coverage_icon=%s\n", coverage_icon), sprintf("coverage_text=%s\n", coverage_text), From ad179fae70376c2612e6e527dc5f5e378d14ef71 Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Tue, 1 Sep 2026 13:19:34 +0200 Subject: [PATCH 55/56] Add shared JUnit summary parsing script --- .github/scripts/summarise-junit.R | 43 +++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/scripts/summarise-junit.R diff --git a/.github/scripts/summarise-junit.R b/.github/scripts/summarise-junit.R new file mode 100644 index 000000000..b7eb420c1 --- /dev/null +++ b/.github/scripts/summarise-junit.R @@ -0,0 +1,43 @@ +# Shared by opal-report/armadillo-report's "Compute results & write summary" +# steps (dsBaseClient_test_suite.yaml) - parses one backend's merged JUnit +# XML into a pass/fail tally and, if any failures/errors, a testthat-style +# failure block. The two report jobs are otherwise near-identical, so this +# is the one piece that was previously duplicated between them. + +summarise_junit <- function(xml_path, label) { + doc <- xml2::read_xml(xml_path) + suites <- xml2::xml_find_all(doc, ".//testsuite") + n_tests <- sum(as.integer(xml2::xml_attr(suites, "tests")), na.rm = TRUE) + n_failures <- sum(as.integer(xml2::xml_attr(suites, "failures")), na.rm = TRUE) + n_errors <- sum(as.integer(xml2::xml_attr(suites, "errors")), na.rm = TRUE) + n_skipped <- sum(as.integer(xml2::xml_attr(suites, "skipped")), na.rm = TRUE) + n_pass <- n_tests - n_failures - n_errors - n_skipped + tally <- sprintf("[ FAIL %d | WARN 0 | SKIP %d | PASS %d ]", n_failures + n_errors, n_skipped, n_pass) + + failed <- xml2::xml_find_all(doc, ".//testcase[failure or error]") + fail_block <- character(0) + if (length(failed) > 0) { + msgs <- vapply(failed, function(tc) { + node <- xml2::xml_find_first(tc, "failure|error") + m <- xml2::xml_attr(node, "message") + if (is.na(m) || !nzchar(m)) m <- trimws(xml2::xml_text(node)) + m + }, character(1)) + labels <- paste0(xml2::xml_attr(failed, "classname"), "::", xml2::xml_attr(failed, "name")) + fail_block <- unlist(lapply(seq_along(failed), function(i) { + c(sprintf("-- Failure (%s) %s", labels[i], strrep("-", max(1, 60 - nchar(labels[i])))), msgs[i], "") + })) + } + + list( + ok = (n_failures + n_errors) == 0, + tally = tally, + summary = c(sprintf("## %s unit tests", label), "", "```", fail_block, tally, "```") + ) +} + +find_dsbase_version <- function(artifact_dir) { + files <- list.files(artifact_dir, pattern = "dsbase_version\\.txt$", recursive = TRUE, full.names = TRUE) + if (length(files) == 0) return("unknown") + trimws(readLines(files[1], warn = FALSE)[1]) +} From a9deef1e35e203168fbe00a48c82662751b41cde Mon Sep 17 00:00:00 2001 From: Tim Cadman <41470917+timcadman@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:04:07 +0200 Subject: [PATCH 56/56] fix imports --- .../workflows/dsBaseClient_test_suite.yaml | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/.github/workflows/dsBaseClient_test_suite.yaml b/.github/workflows/dsBaseClient_test_suite.yaml index 5ebaa9ae7..a8643f46a 100644 --- a/.github/workflows/dsBaseClient_test_suite.yaml +++ b/.github/workflows/dsBaseClient_test_suite.yaml @@ -250,7 +250,7 @@ jobs: - uses: r-lib/actions/setup-r-dependencies@v2 with: working-directory: dsBaseClient - packages: 'any::sessioninfo' + dependencies: 'c("Depends", "Imports", "LinkingTo")' extra-packages: | cran::xml2 @@ -321,9 +321,13 @@ jobs: uses: actions/github-script@v7 with: script: | + // steps.results.outputs.* come back empty (not "true"/"false") if + // that step never got far enough to write them - e.g. it errored + // or was skipped outright because an earlier step failed. Fall + // back to something informative rather than a blank tally. const ok = '${{ steps.results.outputs.ok }}' === 'true'; - const tally = '${{ steps.results.outputs.tally }}'; - const version = '${{ steps.results.outputs.version }}'; + const tally = '${{ steps.results.outputs.tally }}' || 'error - see log'; + const version = '${{ steps.results.outputs.version }}' || 'unknown'; const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; const postCiComment = require('${{ github.workspace }}/dsBaseClient/.github/scripts/post-ci-comment.js'); @@ -531,7 +535,7 @@ jobs: - uses: r-lib/actions/setup-r-dependencies@v2 with: working-directory: dsBaseClient - packages: 'any::sessioninfo' + dependencies: 'c("Depends", "Imports", "LinkingTo")' extra-packages: | cran::xml2 cran::covr @@ -624,11 +628,15 @@ jobs: uses: actions/github-script@v7 with: script: | + // steps.results.outputs.* come back empty (not "true"/"false") if + // that step never got far enough to write them - e.g. it errored + // or was skipped outright because an earlier step failed. Fall + // back to something informative rather than a blank tally. const ok = '${{ steps.results.outputs.ok }}' === 'true'; - const tally = '${{ steps.results.outputs.tally }}'; - const version = '${{ steps.results.outputs.version }}'; - const coverageIcon = '${{ steps.results.outputs.coverage_icon }}'; - const coverageText = '${{ steps.results.outputs.coverage_text }}'; + const tally = '${{ steps.results.outputs.tally }}' || 'error - see log'; + const version = '${{ steps.results.outputs.version }}' || 'unknown'; + const coverageIcon = '${{ steps.results.outputs.coverage_icon }}' || '❓'; + const coverageText = '${{ steps.results.outputs.coverage_text }}' || 'error - see log'; const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; const codecovUrl = `https://app.codecov.io/gh/${context.repo.owner}/${context.repo.repo}/commit/${context.sha}`;