From bbc9307a58e3a9bcd160e20244b5a2f022d82096 Mon Sep 17 00:00:00 2001 From: ms609-agent <313734811+ms609-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 08:52:38 +0100 Subject: [PATCH 01/11] ci(asan): adopt ms609/actions/asan in the r-hub gcc-asan container gcc-ASAN has failed on every main commit since 2026-09-11, dying in dependency installation before any sanitizer run. The hand-rolled rocker/r-devel-san job had three stacked faults: fs could not find libuv (FS_USE_BUNDLED_LIBUV is not the variable fs's configure reads; it wants USE_BUNDLED_LIBUV), a stray `-e 'remotes::install_local()'` line with no preceding continuation, and `sudo sysctl` inside an unprivileged container. Follow TreeTools / Coreset / ConsTree / TreeSearch in delegating to ms609/actions/asan, which installs hard deps with pak (sysreqs resolved for the Fedora image), immediate Suggests only on the vignettes leg, and Config/Needs/memcheck. Rogue is excluded to avoid Rfast's ~30-min ASAN source build; its vignette use is guarded. Co-Authored-By: Claude Opus 5 --- .github/workflows/ASan.yml | 47 ++++++++++++-------------------------- 1 file changed, 15 insertions(+), 32 deletions(-) diff --git a/.github/workflows/ASan.yml b/.github/workflows/ASan.yml index 7c564c2f..cb12ff45 100644 --- a/.github/workflows/ASan.yml +++ b/.github/workflows/ASan.yml @@ -1,4 +1,7 @@ # Address Sanitizer: Replicate CRAN's gcc-ASAN 'Additional Test' +# Uses the r-hub gcc-asan container (R-devel built with ASAN/UBSAN). +# Additional_repositories in DESCRIPTION (https://ms609.github.io/packages/) is +# read automatically by ms609/actions/asan, so TreeDistData needs no extra input. on: workflow_dispatch: push: @@ -26,9 +29,11 @@ name: gcc-ASAN jobs: mem-check: - runs-on: ubuntu-24.04 # Update RSPM when increasing + # The r-hub gcc-asan image has no arm64 variant: stay on x86. + runs-on: ubuntu-latest + timeout-minutes: 120 container: - image: rocker/r-devel-san + image: ghcr.io/r-hub/containers/gcc-asan:latest name: AddressSanitizer ${{ matrix.config.test }} @@ -41,36 +46,14 @@ jobs: - {test: 'vignettes'} env: - R_REMOTES_NO_ERRORS_FROM_WARNINGS: true - _R_CHECK_FORCE_SUGGESTS_: false - RSPM: https://packagemanager.rstudio.com/cran/__linux__/noble/latest - USING_ASAN: true - STRINGI_DISABLE_PKG_CONFIG: true - BIOCONDUCTOR_USE_CONTAINER_REPOSITORY: FALSE # For stringi GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} - FS_USE_BUNDLED_LIBUV: 1 - ASAN_OPTIONS: detect_container_overflow=1:verify_asan_link_order=0 steps: - - uses: actions/checkout@v6 - - - name: Install system dependencies - run: | - apt-get update && apt-get install -y --no-install-recommends \ - libxml2-dev \ - libssl-dev \ - libcurl4-openssl-dev - - - name: Install R package dependencies - run: | - Rscript -e 'options(repos = c(CRAN = "https://cloud.r-project.org", ms609 = "https://ms609.github.io/packages/"))' \ - -e 'install.packages(c("remotes", "testthat"))' \ - -e 'remotes::install_github("ms609/TreeDistData")' \ - -e 'remotes::install_deps(dependencies = TRUE)' \ - -e 'remotes::install_local(build_opts = c("--no-build-vignettes", "--no-manual", "--no-resave-data"))' - -e 'remotes::install_local()' - - - name: ASAN - memcheck ${{ matrix.config.test }} - run: | - sudo sysctl -w vm.mmap_rnd_bits=16 - Rscript memcheck/${{ matrix.config.test }}.R + - uses: ms609/actions/asan@main + with: + test: ${{ matrix.config.test }} + token: ${{ secrets.GITHUB_TOKEN }} + # Rogue hard-imports Rfast, a ~30-min source build under ASAN that + # adds no coverage to TreeDist's own compiled code. Its only use (the + # compare-treesets vignette) is requireNamespace()-guarded. + exclude-packages: Rogue From e25ea15d79ae52a11d33a8b7d3d1e7d0ae03af23 Mon Sep 17 00:00:00 2001 From: ms609-agent <313734811+ms609-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 08:58:16 +0100 Subject: [PATCH 02/11] ci(asan): pre-install TreeDistData for the vignettes leg The action's Suggests install cannot see Additional_repositories, so the vignettes leg failed its dependency solve on TreeDistData. Install it from GitHub first, as ConsTree does for TreeSearch, and exclude it from the action's own solve. Co-Authored-By: Claude Opus 5 --- .github/workflows/ASan.yml | 13 ++++++++++++- .github/workflows/codemeta.yml | 19 ------------------- 2 files changed, 12 insertions(+), 20 deletions(-) delete mode 100644 .github/workflows/codemeta.yml diff --git a/.github/workflows/ASan.yml b/.github/workflows/ASan.yml index cb12ff45..65fe96c8 100644 --- a/.github/workflows/ASan.yml +++ b/.github/workflows/ASan.yml @@ -49,6 +49,17 @@ jobs: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} steps: + # TreeDistData is not on CRAN. The action's pak::pkg_install() of the + # vignettes leg's Suggests does not see Additional_repositories (the + # action sets options(repos) in a separate R process), so the solve fails + # with "Can't find package called TreeDistData". Install it from GitHub + # here and exclude it below. The r-hub container ships R + pak, so this + # runs without a prior checkout. + - name: Pre-install TreeDistData + if: matrix.config.test == 'vignettes' + run: Rscript -e 'pak::pak("ms609/TreeDistData")' + shell: bash + - uses: ms609/actions/asan@main with: test: ${{ matrix.config.test }} @@ -56,4 +67,4 @@ jobs: # Rogue hard-imports Rfast, a ~30-min source build under ASAN that # adds no coverage to TreeDist's own compiled code. Its only use (the # compare-treesets vignette) is requireNamespace()-guarded. - exclude-packages: Rogue + exclude-packages: Rogue,TreeDistData diff --git a/.github/workflows/codemeta.yml b/.github/workflows/codemeta.yml deleted file mode 100644 index 161215cd..00000000 --- a/.github/workflows/codemeta.yml +++ /dev/null @@ -1,19 +0,0 @@ -on: - push: - branches: - - main - - master - paths: - - 'DESCRIPTION' - - '**codemeta.yml' - -name: codemeta - -jobs: - codemeta: - runs-on: ubuntu-latest - env: - GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} - - steps: - - uses: ms609/actions/codemeta@main From 349f64fe73e052df7494e7879b9f667a9c182bf8 Mon Sep 17 00:00:00 2001 From: ms609-agent <313734811+ms609-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 08:58:16 +0100 Subject: [PATCH 03/11] ci: bring workflows up to the Coreset / TreeTools model R-CMD-check moves to Coreset's wave structure: - codemeta (PRs only) via ms609/actions/update-codemeta, replacing the standalone codemeta.yml that pushed straight to main; - sense-check on ubuntu-24.04-arm, never cancelled, as the fast signal; - core (Windows + codecov, R 4.1, R devel) and benchmark gated on it; - full (macOS x2, ubuntu-24.04 release) and a per-dependency-type revdepcheck matrix gated on core. Expensive legs get per-leg concurrency groups and every job a timeout; apt gets transfer timeouts so a stalled mirror retries instead of burning six hours. Push triggers are limited to main/master (PRs and workflow_dispatch cover branches). The scheduled-failure notifier is shared through a YAML anchor. The benchmark now measures both trees twice (PR, base, PR, base) and benchmark/_compare_results.R is Coreset's, which sizes the noise band from both replicate pairs and ignores sub-0.2 ms differences. Smaller refreshes: memcheck, update-csl and copilot-setup-steps run on arm (as TreeTools); pkgdown gains contents: write and replaces dev/ with redirects after a release; RcppDeepState also runs on PRs and uses actions/checkout@v6. Co-Authored-By: Claude Opus 5 --- .github/workflows/R-CMD-check.yml | 443 ++++++++++++++++++---- .github/workflows/RcppDeepState.yml | 13 +- .github/workflows/copilot-setup-steps.yml | 2 +- .github/workflows/memcheck.yml | 3 +- .github/workflows/pkgdown.yml | 61 +++ .github/workflows/update-csl.yml | 2 +- benchmark/_compare_results.R | 92 +++-- 7 files changed, 498 insertions(+), 118 deletions(-) diff --git a/.github/workflows/R-CMD-check.yml b/.github/workflows/R-CMD-check.yml index d31c33b1..4ef3c62d 100644 --- a/.github/workflows/R-CMD-check.yml +++ b/.github/workflows/R-CMD-check.yml @@ -5,7 +5,9 @@ on: - cron: "24 06 * * 1" workflow_dispatch: push: - branches: ["*"] + branches: + - main + - master paths-ignore: - "Meta**" - "AGENT**" @@ -25,7 +27,7 @@ on: - "**.R[dD]ata" - "**.Rpro*" pull_request: - branches: ["*"] + branches: ["**"] paths-ignore: - "Meta**" - "AGENT**" @@ -47,18 +49,66 @@ on: name: R-CMD-check +# NB: there is deliberately NO workflow-level `concurrency`. Workflow-level +# cancel-in-progress would kill the whole run (sense-check included) on every +# new push. Instead, each EXPENSIVE job carries its own per-leg concurrency +# group so superseded Windows/macOS work is cancelled, while the cheap +# `sense-check` job has no concurrency and therefore always runs to completion +# -- giving a per-commit "did this build?" signal even under rapid iteration. + +# Waves: +# codemeta -> PRs only, ~15 s; pushes a stale codemeta.json to the PR +# branch and holds back later waves. +# sense-check -> arm (cheapest), never cancelled. Fast signal. Skipped only +# when codemeta pushed (the fresh run covers the new head). +# core -> Windows (+codecov) / R 4.1 / R devel; only if sense-check passed. +# benchmark -> PR-vs-base timings; also only if sense-check passed, and only +# on pull_request / workflow_dispatch. +# full -> macOS x2 + ubuntu-24.04 (release) backstop; only once core is green. +# revdepcheck -> reverse-dependency checks; also gated on core being green. jobs: - check-release: - runs-on: ubuntu-24.04 - name: ubuntu-24.04 (release) + # --------------------------------------------------------------------------- + # Wave 0 (PRs only): is codemeta.json current? If stale, pushes the + # regenerated file to the PR branch (as the codemeta-writer App, so the push + # starts a fresh run) and fails. See ms609/actions/update-codemeta. + # --------------------------------------------------------------------------- + codemeta: + if: github.event_name == 'pull_request' + runs-on: ubuntu-24.04-arm + timeout-minutes: 5 # healthy run is ~15 s + permissions: + contents: read + outputs: + pushed: ${{ steps.codemeta.outputs.pushed }} + + steps: + - id: codemeta + uses: ms609/actions/update-codemeta@main + with: + client-id: ${{ vars.CODEMETA_APP_ID }} + private-key: ${{ secrets.CODEMETA_APP_KEY }} + + # --------------------------------------------------------------------------- + # Wave 1: cheapest leg as a fail-fast sense-check. No concurrency group, so a + # rapid follow-up push does NOT cancel it -- each commit gets its own signal. + # Waits ~15 s for codemeta, and skips if codemeta pushed an update: the + # fresh run checks the new head. Still runs if codemeta failed otherwise. + # --------------------------------------------------------------------------- + sense-check: + needs: codemeta + if: ${{ !cancelled() && needs.codemeta.outputs.pushed != 'true' }} + runs-on: ubuntu-24.04-arm + timeout-minutes: 30 # healthy run is ~10 min + name: sense-check (ubuntu-24.04-arm, release) env: - _R_CHECK_CRAN_INCOMING_: true # Seemingly not set by --as-cran + _R_CHECK_CRAN_INCOMING_: ${{ github.event_name == 'pull_request' }} _R_CHECK_FORCE_SUGGESTS_: false # CRAN settings R_COMPILE_AND_INSTALL_PACKAGES: 'never' _R_CHECK_THINGS_IN_CHECK_DIR_: false R_REMOTES_STANDALONE: true R_REMOTES_NO_ERRORS_FROM_WARNINGS: true + _R_CHECK_CRAN_INCOMING_USE_ASPELL_: false RSPM: "https://packagemanager.posit.co/cran/__linux__/noble/latest" GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} @@ -70,17 +120,34 @@ jobs: run: | old_version=$(grep "Version:" DESCRIPTION | awk '{print $2}') if [[ $(echo "$old_version" | tr -cd '.' | wc -c) -eq 2 ]]; then - new_version="${old_version}.8888" - if [[ "$RUNNER_OS" == "macOS" ]]; then - sed -i "" "s/Version: .*/Version: ${new_version}/" DESCRIPTION - else - sed -i "s/Version: .*/Version: ${new_version}/" DESCRIPTION - fi + new_version="${old_version}.9999" + sed -i "s/Version: .*/Version: ${new_version}/" DESCRIPTION fi shell: bash - + + # setup-r's first act on Linux is an internal `apt-get update`, and apt + # applies no transfer timeout by default: when a mirror accepts the + # connection and then stalls, the fetch never returns and the step burns + # the job's entire 6-hour budget. Observed 2026-08-19, when + # azure.archive.ubuntu.com went dark and the archive.ubuntu.com fallback + # stalled part-way through the index fetch -- a step whose healthy cost is + # ~35 s sat at 75 minutes. Capping the timeout turns that into a retry. + - name: Make apt fail fast on a stalled mirror + shell: bash + run: | + sudo tee /etc/apt/apt.conf.d/99-ci-timeouts > /dev/null <<'EOF' + Acquire::Retries "3"; + Acquire::http::Timeout "20"; + Acquire::https::Timeout "20"; + Acquire::ftp::Timeout "20"; + EOF + - name: Set up R uses: r-lib/actions/setup-r@v2 + timeout-minutes: 15 + with: + r-version: release + extra-repositories: https://ms609.github.io/packages/ - name: Set up R dependencies uses: r-lib/actions/setup-r-dependencies@v2 @@ -94,20 +161,69 @@ jobs: - name: Check package uses: r-lib/actions/check-r-package@v2 + - name: Notify on failure + if: failure() && github.event_name == 'schedule' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: ¬ify | + const issue_number = 164; + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue_number, + per_page: 3 + }); + + const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000); + const recentFailureNotified = comments.some(comment => + new Date(comment.created_at) > oneHourAgo && + comment.body.includes('Scheduled workflow has failed') + ); - check-all: + if (recentFailureNotified) { + console.log("Recently notified; don't bombard"); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue_number, + body: 'Scheduled workflow has failed: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}' + }); + + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue_number, + state: 'open' + }); + } + + # --------------------------------------------------------------------------- + # Wave 2: Windows (+codecov), R 4.1, R devel. Runs only if the sense-check + # passed and codemeta.json is current (or not checked, outside PRs). + # Per-leg concurrency so a superseding push cancels in-flight legs. + # --------------------------------------------------------------------------- + core: + needs: [sense-check, codemeta] + if: >- + !cancelled() && needs.sense-check.result == 'success' && + contains(fromJSON('["success", "skipped"]'), needs.codemeta.result) runs-on: ${{ matrix.config.os }} + timeout-minutes: 60 # healthy run is ~20 min (Windows, with coverage) name: ${{ matrix.config.os }} (${{ matrix.config.r }}) + concurrency: + group: ${{ github.workflow }}-core-${{ matrix.config.os }}-${{ matrix.config.r }}-${{ github.ref }} + cancel-in-progress: true + strategy: fail-fast: false matrix: config: - {os: windows-latest, r: "release"} - - {os: macos-15-intel, r: "release"} # Until Intel architecture retired 2027-11 - - {os: macOS-latest, r: "release"} - - {os: ubuntu-22.04, r: '4.1', rspm: "https://packagemanager.posit.co/cran/__linux__/jammy/latest"} - - {os: ubuntu-24.04-arm, r: "release", rspm: "https://packagemanager.posit.co/cran/__linux__/noble/latest"} + - {os: ubuntu-22.04, r: "4.1", rspm: "https://packagemanager.posit.co/cran/__linux__/jammy/latest"} - {os: ubuntu-24.04, r: "devel", rspm: "https://packagemanager.posit.co/cran/__linux__/noble/latest"} env: @@ -118,6 +234,7 @@ jobs: R_REMOTES_STANDALONE: true R_REMOTES_NO_ERRORS_FROM_WARNINGS: true R_REALLY_FORCE_SYMBOLS: true # Until R4.3 + _R_CHECK_CRAN_INCOMING_USE_ASPELL_: false # Set to true when can figure how to install aspell on Windows RSPM: ${{ matrix.config.rspm }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} @@ -130,17 +247,28 @@ jobs: run: | old_version=$(grep "Version:" DESCRIPTION | awk '{print $2}') if [[ $(echo "$old_version" | tr -cd '.' | wc -c) -eq 2 ]]; then - new_version="${old_version}.8888" - if [[ "$RUNNER_OS" == "macOS" ]]; then - sed -i "" "s/Version: .*/Version: ${new_version}/" DESCRIPTION - else - sed -i "s/Version: .*/Version: ${new_version}/" DESCRIPTION - fi + new_version="${old_version}.9999" + sed -i "s/Version: .*/Version: ${new_version}/" DESCRIPTION fi + echo "Current package version is now: $(grep "Version:" DESCRIPTION | awk '{print $2}')" shell: bash - + + # See sense-check: without a transfer timeout a stalled apt mirror burns + # the job's whole 6-hour budget. + - name: Make apt fail fast on a stalled mirror + if: runner.os == 'Linux' + shell: bash + run: | + sudo tee /etc/apt/apt.conf.d/99-ci-timeouts > /dev/null <<'EOF' + Acquire::Retries "3"; + Acquire::http::Timeout "20"; + Acquire::https::Timeout "20"; + Acquire::ftp::Timeout "20"; + EOF + - name: Set up R uses: r-lib/actions/setup-r@v2 + timeout-minutes: 15 with: r-version: ${{ matrix.config.r }} extra-repositories: https://ms609.github.io/packages/ @@ -155,7 +283,9 @@ jobs: needs: | check coverage - + + # Suggests whose current CRAN releases need a newer R than the 4.1 leg + # offers are skipped there rather than pinned. - name: Set up R dependencies (Non-Windows) if: runner.os != 'Windows' uses: r-lib/actions/setup-r-dependencies@v2 @@ -173,7 +303,7 @@ jobs: uwot=?ignore-before-r=4.4.0 needs: | check - + - name: Check package uses: r-lib/actions/check-r-package@v2 @@ -187,62 +317,35 @@ jobs: uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const issue_number = 164; - - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue_number, - per_page: 3 - }); - - const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000); - const recentFailureNotified = comments.some(comment => - new Date(comment.created_at) > oneHourAgo && - comment.body.includes('Scheduled workflow has failed') - ); - - if (recentFailureNotified) { - console.log("Recently notified; don't bombard"); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue_number, - body: 'Scheduled workflow has failed: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}' - }); - - await github.rest.issues.update({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue_number, - state: 'open' - }); - } - rev-dep-check: - runs-on: ubuntu-latest - needs: check-release - - name: revdepcheck - - env: - R_COMPILE_AND_INSTALL_PACKAGES: 'never' - R_REMOTES_STANDALONE: true - R_REMOTES_NO_ERRORS_FROM_WARNINGS: true - GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} - - steps: - - uses: ms609/actions/revdepcheck@main - with: - extra-packages: ms609/TreeDistData - + script: *notify + + # --------------------------------------------------------------------------- + # Also wave 2: the benchmark comparison, gated on sense-check for the same + # reason as `core` -- a tree that does not build cannot be timed, so four + # benchmark passes over a broken package only rediscover what the cheap leg + # already said. Never runs on push or schedule: outside a PR there is no base + # ref to compare against. + # --------------------------------------------------------------------------- benchmark: + needs: [sense-check, codemeta] + if: >- + !cancelled() && needs.sense-check.result == 'success' && + contains(fromJSON('["success", "skipped"]'), needs.codemeta.result) && + (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch') runs-on: ubuntu-latest - needs: check-release - if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' + # A hung setup or a wedged benchmark otherwise runs to the 6-hour default, + # holding a runner the whole time. + timeout-minutes: 45 name: benchmark + concurrency: + group: ${{ github.workflow }}-benchmark-${{ github.ref }} + cancel-in-progress: true + + permissions: + contents: read + pull-requests: write + env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} RSPM: "https://packagemanager.posit.co/cran/__linux__/noble/latest" @@ -252,7 +355,7 @@ jobs: R_REMOTES_STANDALONE: true R_REMOTES_NO_ERRORS_FROM_WARNINGS: true R_REALLY_FORCE_SYMBOLS: true - PKG_CXXFLAGS: "-O3" + PKG_CXXFLAGS: "-O3" # release-grade optimisation for the compiled kernels steps: - name: Checkout PR branch @@ -266,16 +369,35 @@ jobs: ref: ${{ github.event.pull_request.base.ref || 'main' }} path: main + # See sense-check: without a transfer timeout a stalled apt mirror burns + # the job's whole 6-hour budget. + - name: Make apt fail fast on a stalled mirror + shell: bash + run: | + sudo tee /etc/apt/apt.conf.d/99-ci-timeouts > /dev/null <<'EOF' + Acquire::Retries "3"; + Acquire::http::Timeout "20"; + Acquire::https::Timeout "20"; + Acquire::ftp::Timeout "20"; + EOF + - name: Set up R uses: r-lib/actions/setup-r@v2 + timeout-minutes: 15 with: extra-repositories: https://ms609.github.io/packages/ + # A cache of its own: the default key prefix is shared with every + # ubuntu/R-release job, so a partial restore can bring back a library + # where the package itself is installed, and benchmark-step then refuses + # to run. - name: Install R dependencies uses: r-lib/actions/setup-r-dependencies@v2 with: + working-directory: pr dependencies: '"hard"' needs: benchmark + cache-version: benchmark - name: Benchmark PR uses: ms609/actions/benchmark-step@main @@ -295,6 +417,15 @@ jobs: path: pr output: pr2 + # Both trees are measured twice, alternating, so the comparison can size + # its noise band from replicates of each: a single sample of the target + # cannot tell a real change from the runner's between-step drift. + - name: Benchmark target again + uses: ms609/actions/benchmark-step@main + with: + path: main + output: main2 + - run: dir pr-benchmark-results working-directory: pr - run: dir main-benchmark-results @@ -323,7 +454,7 @@ jobs: - name: Comment on PR if: always() && github.event_name == 'pull_request' && steps.compare_results.outputs.report != '' - uses: actions/github-script@v7 + uses: actions/github-script@v9 env: BENCHMARK_MESSAGE: ${{ steps.compare_results.outputs.report }} with: @@ -390,3 +521,153 @@ jobs: issue_number: context.issue.number, body: newCommentBody }); + + # --------------------------------------------------------------------------- + # Wave 3 (backstop): the expensive, mostly-duplicate-failure legs. Only runs + # once the core wave is green, which in practice is "about to merge". + # macOS is ~10x runner-minutes, so keeping it off every push is the big save. + # --------------------------------------------------------------------------- + full: + needs: core + runs-on: ${{ matrix.config.os }} + timeout-minutes: 60 # macOS legs are the slow ones + name: ${{ matrix.config.os }} (${{ matrix.config.r }}) + + concurrency: + group: ${{ github.workflow }}-full-${{ matrix.config.os }}-${{ matrix.config.r }}-${{ github.ref }} + cancel-in-progress: true + + strategy: + fail-fast: false + matrix: + config: + - {os: ubuntu-24.04, r: "release", rspm: "https://packagemanager.posit.co/cran/__linux__/noble/latest"} + - {os: macos-15-intel, r: "release"} # Until Intel architecture retired 2027-11 + - {os: macOS-latest, r: "release"} + + env: + _R_CHECK_CRAN_INCOMING_: ${{ github.event_name == 'pull_request' }} + _R_CHECK_FORCE_SUGGESTS_: false # CRAN settings + R_COMPILE_AND_INSTALL_PACKAGES: 'never' + _R_CHECK_THINGS_IN_CHECK_DIR_: false + R_REMOTES_STANDALONE: true + R_REMOTES_NO_ERRORS_FROM_WARNINGS: true + _R_CHECK_CRAN_INCOMING_USE_ASPELL_: false + RSPM: ${{ matrix.config.rspm }} + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + + steps: + - name: Checkout git repo + uses: actions/checkout@v6 + + - name: Temporarily bump package version + run: | + old_version=$(grep "Version:" DESCRIPTION | awk '{print $2}') + if [[ $(echo "$old_version" | tr -cd '.' | wc -c) -eq 2 ]]; then + new_version="${old_version}.9999" + if [[ "$RUNNER_OS" == "macOS" ]]; then + sed -i "" "s/Version: .*/Version: ${new_version}/" DESCRIPTION + else + sed -i "s/Version: .*/Version: ${new_version}/" DESCRIPTION + fi + fi + echo "Current package version is now: $(grep "Version:" DESCRIPTION | awk '{print $2}')" + shell: bash + + # See sense-check: without a transfer timeout a stalled apt mirror burns + # the job's whole 6-hour budget. + - name: Make apt fail fast on a stalled mirror + if: runner.os == 'Linux' + shell: bash + run: | + sudo tee /etc/apt/apt.conf.d/99-ci-timeouts > /dev/null <<'EOF' + Acquire::Retries "3"; + Acquire::http::Timeout "20"; + Acquire::https::Timeout "20"; + Acquire::ftp::Timeout "20"; + EOF + + - name: Set up R + uses: r-lib/actions/setup-r@v2 + timeout-minutes: 15 + with: + r-version: ${{ matrix.config.r }} + extra-repositories: https://ms609.github.io/packages/ + + - name: Set up R dependencies + uses: r-lib/actions/setup-r-dependencies@v2 + with: + cache-version: 2-${{ runner.arch }} + extra-packages: | + ms609/TreeDistData + needs: | + check + + - name: Check package + uses: r-lib/actions/check-r-package@v2 + + - name: Notify on failure + if: failure() && github.event_name == 'schedule' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: *notify + + # --------------------------------------------------------------------------- + # Reverse-dependency checks: also a wave-3 backstop, gated on core being green. + # One leg per dependency type, so a slow Suggests sweep neither blocks nor + # masks the hard-dependency results. + # --------------------------------------------------------------------------- + rev-dep-check: + needs: core + runs-on: ubuntu-latest + timeout-minutes: 120 # revdepchecks are genuinely long + + name: revdepcheck (${{ matrix.config.deps }}) + + concurrency: + group: ${{ github.workflow }}-revdep-${{ matrix.config.deps }}-${{ github.ref }} + cancel-in-progress: true + + strategy: + fail-fast: false + matrix: + config: + - {deps: '"Depends"'} + - {deps: '"Suggests"'} + - {deps: '"Imports"'} + - {deps: '"LinkingTo"'} + + env: + _R_CHECK_CRAN_INCOMING_: false + _R_CHECK_FORCE_SUGGESTS_: false # CRAN settings + R_COMPILE_AND_INSTALL_PACKAGES: 'never' + _R_CHECK_THINGS_IN_CHECK_DIR_: false + R_REMOTES_STANDALONE: true + R_REMOTES_NO_ERRORS_FROM_WARNINGS: true + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + + steps: + # See sense-check: without a transfer timeout a stalled apt mirror burns + # the job's whole 6-hour budget. + - name: Make apt fail fast on a stalled mirror + shell: bash + run: | + sudo tee /etc/apt/apt.conf.d/99-ci-timeouts > /dev/null <<'EOF' + Acquire::Retries "3"; + Acquire::http::Timeout "20"; + Acquire::https::Timeout "20"; + Acquire::ftp::Timeout "20"; + EOF + + - uses: ms609/actions/revdepcheck@main + with: + deps: ${{ matrix.config.deps }} + extra-packages: ms609/TreeDistData + + - name: Notify on failure + if: failure() && github.event_name == 'schedule' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: *notify diff --git a/.github/workflows/RcppDeepState.yml b/.github/workflows/RcppDeepState.yml index a15b6a79..7bb9d869 100644 --- a/.github/workflows/RcppDeepState.yml +++ b/.github/workflows/RcppDeepState.yml @@ -11,6 +11,17 @@ on: - 'memcheck/**' - 'tests/testthat/**.R' - 'vignettes/**.Rmd' + pull_request: + branches: + - main + - master + paths: + - '.github/workflows/RcppDeepState.yml' + - 'src/**' + - 'inst/include/**' + - 'memcheck/**' + - 'tests/testthat/**.R' + - 'vignettes/**.Rmd' name: 'RcppDeepState analysis' jobs: @@ -21,7 +32,7 @@ jobs: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 - uses: FabrizioSandri/RcppDeepState-action@main with: diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index de903b35..24b421ae 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -14,7 +14,7 @@ on: jobs: # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. copilot-setup-steps: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04-arm # Set the permissions to the lowest permissions possible needed for your steps. # Copilot will be given its own token for its operations. diff --git a/.github/workflows/memcheck.yml b/.github/workflows/memcheck.yml index dbf58e22..69be0549 100644 --- a/.github/workflows/memcheck.yml +++ b/.github/workflows/memcheck.yml @@ -30,7 +30,8 @@ name: mem-check jobs: mem-check: - runs-on: ubuntu-24.04 + runs-on: ubuntu-24.04-arm + timeout-minutes: 180 name: valgrind ${{ matrix.config.test }} diff --git a/.github/workflows/pkgdown.yml b/.github/workflows/pkgdown.yml index 85e25ffa..454197e3 100644 --- a/.github/workflows/pkgdown.yml +++ b/.github/workflows/pkgdown.yml @@ -22,6 +22,10 @@ name: pkgdown jobs: pkgdown: runs-on: ubuntu-latest + timeout-minutes: 60 + + permissions: + contents: write env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} @@ -156,3 +160,60 @@ jobs: folder: docs branch: gh-pages clean: false + + # ── Point /dev/ at the release site once a release is deployed ── + - name: Check if release version + id: version-check + run: | + VERSION=$(grep '^Version:' DESCRIPTION | sed 's/Version: *//') + # Release = exactly 3 numeric components (e.g. 2.2.0) + # Dev = 4+ components (e.g. 2.2.0.9001) + if echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+[.\-][0-9]+$'; then + echo "is_release=true" >> "$GITHUB_OUTPUT" + echo "Release version: $VERSION" + else + echo "is_release=false" >> "$GITHUB_OUTPUT" + echo "Dev version: $VERSION" + fi + shell: bash + + - name: Replace dev docs with redirects + if: steps.version-check.outputs.is_release == 'true' + run: | + TMPDIR=$(mktemp -d) + git clone --single-branch --branch gh-pages \ + "https://x-access-token:${GITHUB_PAT}@github.com/${GITHUB_REPOSITORY}.git" \ + "$TMPDIR" + cd "$TMPDIR" + + git config user.email "actions@github.com" + git config user.name "GitHub Actions" + + # Remove old dev docs from git + git rm -rf dev || true + + # Redirect template: strips /dev/ from the current URL + cat > /tmp/redirect.html << 'EOF' + + + + +

Redirecting to release documentation…

+ + + EOF + + # Create a redirect stub for every HTML page in the release site + find . -name '*.html' -not -path './dev/*' -not -path './.git/*' \ + | while read -r f; do + rel="${f#./}" + mkdir -p "dev/$(dirname "$rel")" + cp /tmp/redirect.html "dev/$rel" + done + + git add dev/ + git commit -m "Replace dev/ docs with redirects to release" || true + git push origin gh-pages + + rm -rf "$TMPDIR" + shell: bash diff --git a/.github/workflows/update-csl.yml b/.github/workflows/update-csl.yml index baac81b6..8103bd5f 100644 --- a/.github/workflows/update-csl.yml +++ b/.github/workflows/update-csl.yml @@ -11,7 +11,7 @@ on: jobs: update-csl: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04-arm steps: - uses: actions/checkout@v6 diff --git a/benchmark/_compare_results.R b/benchmark/_compare_results.R index 21e0b736..2cb48c10 100644 --- a/benchmark/_compare_results.R +++ b/benchmark/_compare_results.R @@ -5,7 +5,7 @@ output <- paste0( "report< min_meaningful_diff && + median_pr < median_main - 2 * noise + is_slower <- matched && - median_pr > median_main + 2 * mad_main && - median_pr > noise_range[[2]] + abs_diff > min_meaningful_diff && + median_pr > median_main + 2 * noise report[[fn_name]] <- list( matched = matched, @@ -66,6 +86,8 @@ for (pr_file in pr_files) { median_pr = median(pr1_times), median_cf = median(pr2_times), median_main = median_main, + median_main1 = median(main1_times), + median_main2 = median(main2_times), change = percentage_change ) } @@ -73,25 +95,28 @@ for (pr_file in pr_files) { # Create a markdown-formatted message has_significant_regression <- FALSE - for (fn_name in names(report)) { - res <- report[[fn_name]] - status <- if (res$matched) { + for (i in seq_along(report)) { + fn_name <- names(report)[[i]] + res <- report[[i]] + if (is.null(res) || is.null(res$matched)) next + + status <- if (isTRUE(res$matched)) { if (res$slower) { - if (abs(percentage_change) > threshold_percent) { + if (abs(res$change) > threshold_percent) { has_significant_regression <- TRUE "\U1F7E0 Slower \U1F641" } else { "\U1F7E3 ~same" } } else if (res$faster) { - if (abs(percentage_change) > threshold_percent) { + if (abs(res$change) > threshold_percent) { "\U1F7E2 Faster!" } else { "\U1F7E3 ~same" } } else { "\U26AA NSD" - } + } } else { "\U1F7E4 ?Mismatch" } @@ -100,19 +125,20 @@ for (pr_file in pr_files) { message <- paste0( "| `", fn_name, "` | ", status, " | ", - bold, round(res$change, 2), "%", bold, " | ", - signif(res$median_main * 1e3, 3), " \u2192
", + bold, round(res$change, 2), "%", bold, " | ", + signif(res$median_main1 * 1e3, 3), ", ", + signif(res$median_main2 * 1e3, 3), " \u2192
", signif(res$median_pr * 1e3, 3), ", ", signif(res$median_cf * 1e3, 3), " |\n" ) + + cat(message) + output <- paste0(output, message) } if (has_significant_regression) { regressions <- TRUE } - - cat(message) - output <- paste0(output, message) } cat(paste0(output, "\nEOF"), file = Sys.getenv("GITHUB_OUTPUT"), append = TRUE) From d62a0fa41d9dae213c37bde0a22b3368ca3567c9 Mon Sep 17 00:00:00 2001 From: ms609-agent <313734811+ms609-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 08:58:36 +0100 Subject: [PATCH 04/11] Regenerate codemeta.json with update-codemeta's generator The PR-time codemeta job uses ms609/actions/update-codemeta's stdlib Python port of write_codemeta(). Its output differs from codemetar's in known, deliberate ways (no runtimePlatform or fileSize; softwareRequirements as an array), so regenerate once here rather than have the first PR flagged stale. Co-Authored-By: Claude Opus 5 --- codemeta.json | 59 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/codemeta.json b/codemeta.json index 2c7972c7..5af80745 100644 --- a/codemeta.json +++ b/codemeta.json @@ -14,7 +14,6 @@ "name": "R", "url": "https://r-project.org" }, - "runtimePlatform": "R version 4.6.1 (2026-06-24)", "provider": { "@id": "https://cran.r-project.org", "@type": "Organization", @@ -397,19 +396,19 @@ "sameAs": "https://CRAN.R-project.org/package=withr" } ], - "softwareRequirements": { - "1": { + "softwareRequirements": [ + { "@type": "SoftwareApplication", "identifier": "R", "name": "R", "version": ">= 4.0" }, - "2": { + { "@type": "SoftwareApplication", "identifier": "stats", "name": "stats" }, - "3": { + { "@type": "SoftwareApplication", "identifier": "ape", "name": "ape", @@ -422,7 +421,7 @@ }, "sameAs": "https://CRAN.R-project.org/package=ape" }, - "4": { + { "@type": "SoftwareApplication", "identifier": "cli", "name": "cli", @@ -435,7 +434,7 @@ }, "sameAs": "https://CRAN.R-project.org/package=cli" }, - "5": { + { "@type": "SoftwareApplication", "identifier": "Rdpack", "name": "Rdpack", @@ -448,7 +447,7 @@ }, "sameAs": "https://CRAN.R-project.org/package=Rdpack" }, - "6": { + { "@type": "SoftwareApplication", "identifier": "shiny", "name": "shiny", @@ -460,7 +459,7 @@ }, "sameAs": "https://CRAN.R-project.org/package=shiny" }, - "7": { + { "@type": "SoftwareApplication", "identifier": "shinyjs", "name": "shinyjs", @@ -472,7 +471,7 @@ }, "sameAs": "https://CRAN.R-project.org/package=shinyjs" }, - "8": { + { "@type": "SoftwareApplication", "identifier": "TreeTools", "name": "TreeTools", @@ -485,10 +484,12 @@ }, "sameAs": "https://CRAN.R-project.org/package=TreeTools" }, - "SystemRequirements": "C++17, pandoc-citeproc" - }, - "keywords": ["phylogenetics", "tree-distance"], - "fileSize": "2086.252KB", + "C++17, pandoc-citeproc" + ], + "keywords": [ + "phylogenetics", + "tree-distance" + ], "citation": [ { "@type": "ScholarlyArticle", @@ -496,7 +497,10 @@ "author": [ { "@type": "Person", - "givenName": ["Martin", "R."], + "givenName": [ + "Martin", + "R." + ], "familyName": "Smith", "email": "martin.smith@durham.ac.uk" } @@ -511,7 +515,10 @@ "issueNumber": "20", "datePublished": "2020", "isPartOf": { - "@type": ["PublicationVolume", "Periodical"], + "@type": [ + "PublicationVolume", + "Periodical" + ], "volumeNumber": "36", "name": "Bioinformatics" } @@ -523,7 +530,10 @@ "author": [ { "@type": "Person", - "givenName": ["Martin", "R."], + "givenName": [ + "Martin", + "R." + ], "familyName": "Smith", "email": "martin.smith@durham.ac.uk" } @@ -538,7 +548,10 @@ "issueNumber": "5", "datePublished": "2022", "isPartOf": { - "@type": ["PublicationVolume", "Periodical"], + "@type": [ + "PublicationVolume", + "Periodical" + ], "volumeNumber": "71", "name": "Systematic Biology" } @@ -550,7 +563,10 @@ "author": [ { "@type": "Person", - "givenName": ["Martin", "R."], + "givenName": [ + "Martin", + "R." + ], "familyName": "Smith", "email": "martin.smith@durham.ac.uk" } @@ -563,7 +579,10 @@ "@type": "PublicationIssue", "datePublished": "2020", "isPartOf": { - "@type": ["PublicationVolume", "Periodical"], + "@type": [ + "PublicationVolume", + "Periodical" + ], "name": "Comprehensive R Archive Network" } } From 2669e3a2d666eb61aea92a2fc5f8707cc5abf217 Mon Sep 17 00:00:00 2001 From: ms609-agent <313734811+ms609-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 09:02:48 +0100 Subject: [PATCH 05/11] ci(asan): drop TreeDistData workaround; validate ms609/actions fix ms609/actions branch asan-persist-repos makes the action's Additional_repositories setting persist into its pak install, so TreeDistData resolves from https://ms609.github.io/packages/ without a pre-install. Pinned to that branch for validation; revert to @main once it merges. Co-Authored-By: Claude Opus 5 --- .github/workflows/ASan.yml | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ASan.yml b/.github/workflows/ASan.yml index 65fe96c8..f629d4b7 100644 --- a/.github/workflows/ASan.yml +++ b/.github/workflows/ASan.yml @@ -49,22 +49,11 @@ jobs: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} steps: - # TreeDistData is not on CRAN. The action's pak::pkg_install() of the - # vignettes leg's Suggests does not see Additional_repositories (the - # action sets options(repos) in a separate R process), so the solve fails - # with "Can't find package called TreeDistData". Install it from GitHub - # here and exclude it below. The r-hub container ships R + pak, so this - # runs without a prior checkout. - - name: Pre-install TreeDistData - if: matrix.config.test == 'vignettes' - run: Rscript -e 'pak::pak("ms609/TreeDistData")' - shell: bash - - - uses: ms609/actions/asan@main + - uses: ms609/actions/asan@asan-persist-repos with: test: ${{ matrix.config.test }} token: ${{ secrets.GITHUB_TOKEN }} # Rogue hard-imports Rfast, a ~30-min source build under ASAN that # adds no coverage to TreeDist's own compiled code. Its only use (the # compare-treesets vignette) is requireNamespace()-guarded. - exclude-packages: Rogue,TreeDistData + exclude-packages: Rogue From 112b16a6182daa90f14fdc71016960a1b9a9c49e Mon Sep 17 00:00:00 2001 From: ms609-agent <313734811+ms609-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 09:10:34 +0100 Subject: [PATCH 06/11] ci: install macOS dependencies without pak CRAN's macOS binaries are now partly zstd-compressed, which pak cannot extract ("Cannot extract ... knitr_1.52.tgz, unknown archive type"; r-lib/pkgdepends#485, r-lib/pak#915). Base R's untar() reads zstd, so the macOS legs of the `full` wave install through install.packages() / remotes, fail explicitly on anything missing (install.packages() only warns), and set up pandoc, which setup-r-dependencies provides elsewhere. Ported from perf/hoist-canonical-splits (d2418b2c, 11e9893a), green on both macOS legs there, so the workflow has a single owner and that branch can drop its CI commits. Co-Authored-By: Claude Opus 5 --- .github/workflows/R-CMD-check.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/.github/workflows/R-CMD-check.yml b/.github/workflows/R-CMD-check.yml index 4ef3c62d..a1c91036 100644 --- a/.github/workflows/R-CMD-check.yml +++ b/.github/workflows/R-CMD-check.yml @@ -595,6 +595,7 @@ jobs: extra-repositories: https://ms609.github.io/packages/ - name: Set up R dependencies + if: runner.os != 'macOS' uses: r-lib/actions/setup-r-dependencies@v2 with: cache-version: 2-${{ runner.arch }} @@ -603,6 +604,36 @@ jobs: needs: | check + # Temporary: CRAN's macOS binaries are now partly zstd-compressed, which + # pak cannot extract ("unknown archive type"; r-lib/pkgdepends#485, + # r-lib/pak#915). Base R's untar() reads zstd, so install through + # install.packages() on macOS, and set up the pandoc that + # setup-r-dependencies would otherwise provide. Once pak supports zstd, + # delete both macOS steps and the `if` on the pak step above. + - name: Set up pandoc (macOS, without pak) + if: runner.os == 'macOS' + uses: r-lib/actions/setup-pandoc@v2 + + - name: Set up R dependencies (macOS, without pak) + if: runner.os == 'macOS' + run: | + lib <- Sys.getenv("R_LIBS_USER") + dir.create(lib, recursive = TRUE, showWarnings = FALSE) + .libPaths(c(lib, .libPaths())) + + install.packages(c("remotes", "rcmdcheck")) + remotes::install_deps(dependencies = TRUE, upgrade = "never") + remotes::install_github("ms609/TreeDistData", upgrade = "never") + + # install.packages() only warns on failure: fail the step instead. + deps <- remotes::local_package_deps(dependencies = TRUE) + wanted <- c(deps, "rcmdcheck", "TreeDistData") + missing <- setdiff(wanted, rownames(installed.packages())) + if (length(missing)) { + stop("Failed to install: ", paste(missing, collapse = ", ")) + } + shell: Rscript {0} + - name: Check package uses: r-lib/actions/check-r-package@v2 From 8184ddc6d1736cd503689147fb90b1e0d5a04cb7 Mon Sep 17 00:00:00 2001 From: ms609-agent <313734811+ms609-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 09:16:08 +0100 Subject: [PATCH 07/11] Declare Config/Needs/benchmark The benchmark job installs `needs: benchmark` from the PR checkout. TreeDist declared no such field, so bench was never installed ("there is no package called 'bench'"). The job only worked before because it ran setup-r-dependencies from a root with no DESCRIPTION, where pak scans scripts instead, which also pulled in CRAN TreeDist and tripped benchmark-step's installed-package guard. List what benchmark/ loads, as TreeTools does. Co-Authored-By: Claude Opus 5 --- DESCRIPTION | 1 + 1 file changed, 1 insertion(+) diff --git a/DESCRIPTION b/DESCRIPTION index c23b529c..a69a6da9 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -83,6 +83,7 @@ LinkingTo: RdMacros: Rdpack VignetteBuilder: knitr Config/Needs/app/optional: uwot +Config/Needs/benchmark: bench, phangorn, testthat Config/Needs/check: rcmdcheck Config/Needs/coverage: covr Config/Needs/memcheck: pkgdown, testthat From 96578367a7f32c61441c7a4d49dc6bdbb438f774 Mon Sep 17 00:00:00 2001 From: ms609-agent <313734811+ms609-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 09:19:02 +0100 Subject: [PATCH 08/11] test: don't require TBRDist in "SPR calculated correctly" The test's expect_exact() helper called TBRDist::USPRDist() unconditionally, though TBRDist is only in Suggests. Surfaced by the gcc-ASAN tests leg, which installs hard dependencies only ("there is no package called 'TBRDist'"); CRAN's no-Suggests check would hit it too. The helper, and the expect_failure() wrapped around it, now run only when TBRDist is available, so the fixed-value .SPRRogue() expectations in the same test still run everywhere. Co-Authored-By: Claude Opus 5 --- tests/testthat/test-tree_distance_spr.R | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/testthat/test-tree_distance_spr.R b/tests/testthat/test-tree_distance_spr.R index ff0a3ee8..1a2ffc9d 100644 --- a/tests/testthat/test-tree_distance_spr.R +++ b/tests/testthat/test-tree_distance_spr.R @@ -177,7 +177,13 @@ test_that("SPR shortcuts okay - larger trees", { test_that("SPR calculated correctly", { library("TreeTools", quietly = TRUE) + # Compares against TBRDist's exact uSPR distance, when available (Suggests); + # the fixed-value expectations below run regardless. + haveTBRDist <- requireNamespace("TBRDist", quietly = TRUE) expect_exact <- function(x, y, method = "rogue") { + if (!haveTBRDist) { + return(invisible()) + } if (is.character(x)) x <- Tree(x) if (is.character(y)) y <- Tree(y) expect_equal( @@ -204,9 +210,11 @@ test_that("SPR calculated correctly", { expect_exact("(a,(d,(b,(c,X))));", "(a,((b,c),(X,d)));", "rogue") # distance = 1 expect_exact("((((b,c),d),e),a);", "(a,(b,((e,c),d)));", "rogue") - expect_failure( - expect_exact("((((b,c),d),e),a);", "(a,(b,((e,c),d)));", "deo") - ) + if (haveTBRDist) { + expect_failure( + expect_exact("((((b,c),d),e),a);", "(a,(b,((e,c),d)));", "deo") + ) + } # Passes with ami, joint, vi # Fails with viNorm - should tiebreaker be non-normalized? From c45b3898dd279d91d06914695858b397a6207e4f Mon Sep 17 00:00:00 2001 From: ms609-agent <313734811+ms609-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 09:24:56 +0100 Subject: [PATCH 09/11] ci: declare least-privilege GITHUB_TOKEN permissions Addresses code scanning's "Workflow does not contain permissions" on every workflow that lacked a block. Read-only (contents: read) for gcc-ASAN, mem-check, R-hub, rev-dep-check and wasm-build. Write scopes only where a job writes: - update-csl pushes the refreshed CSL file (contents: write); - R-CMD-check's scheduled-failure notifier comments on and reopens issue #164 (issues: write); codemeta and benchmark keep their job-level blocks, which override this; - RcppDeepState posts its results to the PR (pull-requests: write). Co-Authored-By: Claude Opus 5 --- .github/workflows/ASan.yml | 3 +++ .github/workflows/R-CMD-check.yml | 4 ++++ .github/workflows/RcppDeepState.yml | 4 ++++ .github/workflows/memcheck.yml | 3 +++ .github/workflows/revdepcheck.yml | 3 +++ .github/workflows/rhub.yaml | 3 +++ .github/workflows/update-csl.yml | 3 +++ .github/workflows/wasm-build.yml | 3 +++ 8 files changed, 26 insertions(+) diff --git a/.github/workflows/ASan.yml b/.github/workflows/ASan.yml index f629d4b7..e7b0e11d 100644 --- a/.github/workflows/ASan.yml +++ b/.github/workflows/ASan.yml @@ -27,6 +27,9 @@ on: name: gcc-ASAN +permissions: + contents: read + jobs: mem-check: # The r-hub gcc-asan image has no arm64 variant: stay on x86. diff --git a/.github/workflows/R-CMD-check.yml b/.github/workflows/R-CMD-check.yml index a1c91036..33a86cdd 100644 --- a/.github/workflows/R-CMD-check.yml +++ b/.github/workflows/R-CMD-check.yml @@ -66,6 +66,10 @@ name: R-CMD-check # on pull_request / workflow_dispatch. # full -> macOS x2 + ubuntu-24.04 (release) backstop; only once core is green. # revdepcheck -> reverse-dependency checks; also gated on core being green. +permissions: + contents: read + issues: write # scheduled-failure notifier comments on and reopens #164 + jobs: # --------------------------------------------------------------------------- # Wave 0 (PRs only): is codemeta.json current? If stale, pushes the diff --git a/.github/workflows/RcppDeepState.yml b/.github/workflows/RcppDeepState.yml index 7bb9d869..1e1f354c 100644 --- a/.github/workflows/RcppDeepState.yml +++ b/.github/workflows/RcppDeepState.yml @@ -24,6 +24,10 @@ on: - 'vignettes/**.Rmd' name: 'RcppDeepState analysis' +permissions: + contents: read + pull-requests: write # comment: true posts results to the PR + jobs: RcppDeepState: runs-on: ubuntu-latest diff --git a/.github/workflows/memcheck.yml b/.github/workflows/memcheck.yml index 69be0549..44932b0a 100644 --- a/.github/workflows/memcheck.yml +++ b/.github/workflows/memcheck.yml @@ -28,6 +28,9 @@ on: name: mem-check +permissions: + contents: read + jobs: mem-check: runs-on: ubuntu-24.04-arm diff --git a/.github/workflows/revdepcheck.yml b/.github/workflows/revdepcheck.yml index 53a46a38..4e9fdeb7 100644 --- a/.github/workflows/revdepcheck.yml +++ b/.github/workflows/revdepcheck.yml @@ -4,6 +4,9 @@ name: rev-dep-check on: workflow_dispatch: +permissions: + contents: read + jobs: mem-check: runs-on: ubuntu-latest diff --git a/.github/workflows/rhub.yaml b/.github/workflows/rhub.yaml index e3fa9124..b10fe0af 100644 --- a/.github/workflows/rhub.yaml +++ b/.github/workflows/rhub.yaml @@ -25,6 +25,9 @@ on: description: 'Run name (optional)' type: string +permissions: + contents: read + jobs: setup: diff --git a/.github/workflows/update-csl.yml b/.github/workflows/update-csl.yml index 8103bd5f..24e8878b 100644 --- a/.github/workflows/update-csl.yml +++ b/.github/workflows/update-csl.yml @@ -9,6 +9,9 @@ on: paths: - '.github/workflows/update-csl.yml' +permissions: + contents: write # pushes the refreshed CSL file + jobs: update-csl: runs-on: ubuntu-24.04-arm diff --git a/.github/workflows/wasm-build.yml b/.github/workflows/wasm-build.yml index 76a6f441..d0d73691 100644 --- a/.github/workflows/wasm-build.yml +++ b/.github/workflows/wasm-build.yml @@ -29,6 +29,9 @@ on: description: "Comma-separated packages to build (deps resolved automatically)" default: "fastmatch" +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest From da478c7beca58795933b3c860192bb8a9fc5ecc9 Mon Sep 17 00:00:00 2001 From: ms609-agent <313734811+ms609-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 09:52:26 +0100 Subject: [PATCH 10/11] ci: take CRAN's macOS binaries, not P3M's, in the pak-free install Both macOS legs of the full wave failed to install TreeDistData: the TreeDist it depends on came from P3M (packagemanager.posit.co/cran/latest, sonoma-arm64 and big-sur-x86_64 TreeDist_2.15.0.tgz), whose binary references libomp's __kmpc_* symbols without linking libomp, so dyn.load() fails with "symbol not found in flat namespace '___kmpc_dispatch_init_4'". The same step passed on perf/hoist-canonical-splits, where install.packages() drew TreeDist from CRAN's own binaries. Set the repos explicitly rather than inheriting whichever setup-r chose. Co-Authored-By: Claude Opus 5 --- .github/workflows/R-CMD-check.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/R-CMD-check.yml b/.github/workflows/R-CMD-check.yml index 33a86cdd..002b0a85 100644 --- a/.github/workflows/R-CMD-check.yml +++ b/.github/workflows/R-CMD-check.yml @@ -625,6 +625,13 @@ jobs: dir.create(lib, recursive = TRUE, showWarnings = FALSE) .libPaths(c(lib, .libPaths())) + # CRAN's own binaries, not P3M's: P3M's macOS build of TreeDist + # (needed here by TreeDistData) references libomp's __kmpc_* + # symbols without linking libomp, so it fails to load ("symbol not + # found in flat namespace '___kmpc_dispatch_init_4'"). + options(repos = c(CRAN = "https://cloud.r-project.org", + ms609 = "https://ms609.github.io/packages/")) + install.packages(c("remotes", "rcmdcheck")) remotes::install_deps(dependencies = TRUE, upgrade = "never") remotes::install_github("ms609/TreeDistData", upgrade = "never") From 178ebfcff9f76b4983072c10ac11d4ebce83da8d Mon Sep 17 00:00:00 2001 From: ms609-agent <313734811+ms609-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 10:00:05 +0100 Subject: [PATCH 11/11] ci(asan): return to ms609/actions/asan@main; cancel superseded legs ms609/actions#6 (Additional_repositories now reach the dependency install) is merged, so drop the validation pin. Validated on run 35575870744: TreeDistData resolved from the ms609 repo and all nine articles built under ASan/UBSan with no reports. Each leg runs for up to an hour, and on pull requests every push re-triggers all three. Per-leg concurrency cancels the stale runs. Co-Authored-By: Claude Opus 5 --- .github/workflows/ASan.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ASan.yml b/.github/workflows/ASan.yml index e7b0e11d..507f46b9 100644 --- a/.github/workflows/ASan.yml +++ b/.github/workflows/ASan.yml @@ -40,6 +40,11 @@ jobs: name: AddressSanitizer ${{ matrix.config.test }} + # Each leg runs for up to an hour: a superseding push cancels the stale one. + concurrency: + group: ${{ github.workflow }}-${{ matrix.config.test }}-${{ github.ref }} + cancel-in-progress: true + strategy: fail-fast: false matrix: @@ -52,7 +57,7 @@ jobs: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: ms609/actions/asan@asan-persist-repos + - uses: ms609/actions/asan@main with: test: ${{ matrix.config.test }} token: ${{ secrets.GITHUB_TOKEN }}