From d49be536a66bd2e131215d6c33914148c7e822c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Shixiang=20Wang=20=28=E7=8E=8B=E8=AF=97=E7=BF=94=29?= Date: Sun, 11 Oct 2026 01:00:35 +0800 Subject: [PATCH 1/2] feat(ci): install R dependencies from renv.lock via setup-renv Switch all three render workflows (quarto-publish, quarto-publish-update- cache, ci-benchmark) from setup-r-dependencies to r-lib/actions/setup-renv (issue #306, B1 stage 2): - setup-renv restores the renv.lock-pinned library with its package cache keyed on the lockfile hash: an unchanged lockfile restores in minutes instead of ~26 min of pak resolution + install; a changed lockfile only pays the delta via restore-keys. - Drop the "Drop cached babelquarto" workaround: a repinned GitHub package changes the lockfile hash, so renv::restore() reinstalls it naturally. - Replace PKG_SYSREQS=true with explicit apt packages (libudunits2-dev, libglpk-dev added) plus a pak::pkg_sysreqs() step that runs the returned install scripts, covering the full dependency closure. - Delta-install QMD-scan-added packages after update-dependencies.R: the scan can append imports newer than renv.lock, so probe loadability and pak-install anything missing before rendering. - Disable renv's install sandbox on ephemeral runners (same as generate-renv-lock.yml). Co-Authored-By: Claude Code --- .github/workflows/ci-benchmark.yml | 27 +++++-- .../workflows/quarto-publish-update-cache.yml | 57 ++++++++++++-- .github/workflows/quarto-publish.yml | 75 ++++++++++++++++--- 3 files changed, 138 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci-benchmark.yml b/.github/workflows/ci-benchmark.yml index b5382dd29..ae41c7097 100644 --- a/.github/workflows/ci-benchmark.yml +++ b/.github/workflows/ci-benchmark.yml @@ -84,18 +84,33 @@ jobs: with: use-public-rspm: true + # R dependencies via renv lockfile (issue #306, B1) — mirrors + # quarto-publish.yml: setup-renv keys its cache on the renv.lock + # hash; a changed lockfile only pays the delta. - name: Install system dependencies run: | sudo apt-get update -y - sudo apt-get install -y libcurl4-openssl-dev + sudo apt-get install -y \ + libcurl4-openssl-dev \ + libudunits2-dev \ + libglpk-dev - - name: Drop cached babelquarto before dependency install - run: Rscript -e 'if (requireNamespace("babelquarto", quietly = TRUE)) remove.packages("babelquarto")' - - - uses: r-lib/actions/setup-r-dependencies@v2 + - uses: r-lib/actions/setup-renv@v2 env: - PKG_SYSREQS: "true" + RENV_CONFIG_SANDBOX_ENABLED: false + + # pkg_sysreqs() only returns the install commands (it never installs); + # run them through a privileged shell, as in quarto-publish.yml. + - name: Install system requirements for locked packages + run: | + SCRIPTS=$(Rscript -e 'cat(pak::pkg_sysreqs("deps::.")$install_scripts, sep = "\n")') + if [ -n "$SCRIPTS" ]; then + echo "$SCRIPTS" | sudo bash + else + echo "No additional system requirements needed" + fi + # Mark AFTER setup-renv + sysreqs so r_deps measures the new pipeline. - name: Mark phase — R dependencies done run: echo "r_deps=$(date +%s)" >> /tmp/benchmark-clock diff --git a/.github/workflows/quarto-publish-update-cache.yml b/.github/workflows/quarto-publish-update-cache.yml index f656415e1..baa70b346 100644 --- a/.github/workflows/quarto-publish-update-cache.yml +++ b/.github/workflows/quarto-publish-update-cache.yml @@ -96,14 +96,37 @@ jobs: with: use-public-rspm: true - # See quarto-publish.yml: ensure a repinned babelquarto is actually - # reinstalled instead of reused from the restored package library. - - name: Drop cached babelquarto before dependency install - run: Rscript -e 'if (requireNamespace("babelquarto", quietly = TRUE)) remove.packages("babelquarto")' + # ── R dependencies via renv (issue #306, B1) ───────────── + # See quarto-publish.yml for the full rationale: renv.lock pins the + # exact versions resolved from DESCRIPTION, setup-renv keys its + # package cache on the lockfile hash, and a changed lockfile only + # pays the delta thanks to the restore-keys prefix. A repinned + # GitHub package (e.g. our babelquarto fork) therefore takes effect + # without any manual cache-busting step. + - name: Install system dependencies + run: | + sudo apt-get update -y + sudo apt-get install -y \ + libcurl4-openssl-dev \ + libudunits2-dev \ + libglpk-dev - - uses: r-lib/actions/setup-r-dependencies@v2 + - uses: r-lib/actions/setup-renv@v2 env: - PKG_SYSREQS: "true" + # See quarto-publish.yml: renv's sandbox doubles install time on + # ephemeral runners for no benefit. + RENV_CONFIG_SANDBOX_ENABLED: false + + # pkg_sysreqs() only returns the install commands (it never installs); + # run them through a privileged shell, as in quarto-publish.yml. + - name: Install system requirements for locked packages + run: | + SCRIPTS=$(Rscript -e 'cat(pak::pkg_sysreqs("deps::.")$install_scripts, sep = "\n")') + if [ -n "$SCRIPTS" ]; then + echo "$SCRIPTS" | sudo bash + else + echo "No additional system requirements needed" + fi # ── Python Setup ───────────────────────────────────────── - name: Set up Python @@ -189,6 +212,28 @@ jobs: Rscript update-dependencies.R python manage-dependencies.py --update + # The scan above may append QMD-discovered install-call packages to + # DESCRIPTION that are newer than renv.lock; delta-install just the + # difference before rendering. See quarto-publish.yml for detail. + - name: Install scan-added dependencies (delta) + run: | + Rscript - <<'RS' + dcf <- read.dcf("DESCRIPTION") + imports <- if ("Imports" %in% colnames(dcf)) { + x <- trimws(strsplit(dcf[1, "Imports"], ",")[[1]]) + gsub("\\(.*$", "", x) + } else character(0) + imports <- setdiff(imports[imports != "" & imports != "R"], "R") + missing <- imports[!vapply(imports, function(p) + requireNamespace(p, quietly = TRUE), logical(1))] + if (length(missing) == 0) { + message("All Imports loadable - no delta install needed.") + } else { + message("Delta-installing: ", paste(missing, collapse = ", ")) + pak::pak(missing) + } + RS + - name: Commit updated dependency files run: | git config --local user.name "$GITHUB_ACTOR" diff --git a/.github/workflows/quarto-publish.yml b/.github/workflows/quarto-publish.yml index c0e818f5d..c509dd36c 100644 --- a/.github/workflows/quarto-publish.yml +++ b/.github/workflows/quarto-publish.yml @@ -16,6 +16,10 @@ # ║ pushes cancel stale in-progress builds. ║ # ║ 5. RESOURCE CONSERVATION: Failed builds still save their freeze ║ # ║ progress, so the next run picks up where it left off. ║ +# ║ 6. RENV LOCKFILE INSTALLS (issue #306, B1): R dependencies come ║ +# ║ from renv.lock via setup-renv, cache keyed on the lockfile ║ +# ║ hash — an unchanged lockfile restores in minutes instead of a ║ +# ║ ~26 min pak resolution + install. ║ # ╚══════════════════════════════════════════════════════════════════════╝ on: @@ -183,20 +187,45 @@ jobs: with: use-public-rspm: true + # ── R dependencies via renv (issue #306, B1) ───────────── + # renv.lock pins the exact versions resolved from DESCRIPTION by + # generate-renv-lock.yml, and setup-renv keys its package cache on + # the lockfile hash: an unchanged lockfile restores the whole + # library in minutes instead of re-resolving and installing ~650 + # packages (~26 min). When the lockfile changes, the restore-keys + # prefix still restores the previous cache and renv::restore() + # installs only the delta — which is also why a repinned GitHub + # package (e.g. our babelquarto fork) takes effect without any + # manual cache-busting step. - name: Install system dependencies run: | sudo apt-get update -y - sudo apt-get install -y libcurl4-openssl-dev + sudo apt-get install -y \ + libcurl4-openssl-dev \ + libudunits2-dev \ + libglpk-dev - # The restored R package library may carry a previously installed - # build of a GitHub-pinned package; without this, pak reuses the old - # binary and a new pin (e.g. our babelquarto fork) never takes effect. - - name: Drop cached babelquarto before dependency install - run: Rscript -e 'if (requireNamespace("babelquarto", quietly = TRUE)) remove.packages("babelquarto")' - - - uses: r-lib/actions/setup-r-dependencies@v2 + - uses: r-lib/actions/setup-renv@v2 env: - PKG_SYSREQS: "true" + # renv's install sandbox copies packages into a private library + # before promoting them; on ephemeral CI runners it doubles + # install time for no benefit, so switch it off. + RENV_CONFIG_SANDBOX_ENABLED: false + + # pak derives the system requirements of the full dependency closure + # from DESCRIPTION — the same job PKG_SYSREQS=true used to do inside + # setup-r-dependencies. pkg_sysreqs() only returns the install + # commands (it never installs), so run them through a privileged + # shell here. The extra apt line above already covers the libraries + # known to be needed; this catches anything else in the closure. + - name: Install system requirements for locked packages + run: | + SCRIPTS=$(Rscript -e 'cat(pak::pkg_sysreqs("deps::.")$install_scripts, sep = "\n")') + if [ -n "$SCRIPTS" ]; then + echo "$SCRIPTS" | sudo bash + else + echo "No additional system requirements needed" + fi # ── Python Setup ───────────────────────────────────────── - name: Set up Python @@ -292,6 +321,34 @@ jobs: Rscript update-dependencies.R python manage-dependencies.py --update + # The scan above appends QMD-discovered install-call packages to + # DESCRIPTION, but those are newer than renv.lock, so renv::restore() + # did not install them. Delta-install just the difference against + # the lockfile before anything renders. No-op on a typical run. + - name: Install scan-added dependencies (delta) + run: | + Rscript - <<'RS' + dcf <- read.dcf("DESCRIPTION") + imports <- if ("Imports" %in% colnames(dcf)) { + x <- trimws(strsplit(dcf[1, "Imports"], ",")[[1]]) + gsub("\\(.*$", "", x) + } else character(0) + remotes_field <- if ("Remotes" %in% colnames(dcf)) { + trimws(strsplit(dcf[1, "Remotes"], ",")[[1]]) + } else character(0) + # Remote refs (user/repo) are handled by renv from the Remotes + # field of the packages renv restores, not installed directly. + imports <- setdiff(imports[imports != "" & imports != "R"], "R") + missing <- imports[!vapply(imports, function(p) + requireNamespace(p, quietly = TRUE), logical(1))] + if (length(missing) == 0) { + message("All Imports loadable - no delta install needed.") + } else { + message("Delta-installing: ", paste(missing, collapse = ", ")) + pak::pak(missing) + } + RS + - name: Commit updated dependency files if: github.event_name != 'pull_request' run: | From 5a1372f0a0fe4d03bd2b8689c3ccbd58d1988b10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Shixiang=20Wang=20=28=E7=8E=8B=E8=AF=97=E7=BF=94=29?= Date: Sun, 11 Oct 2026 01:10:20 +0800 Subject: [PATCH 2/2] fix(ci): install sysreqs before renv restore and add librsvg2-dev MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #321's first run failed inside renv::restore(): rsvg load-tested at install time with 'librsvg-2.so.2: cannot open shared object file' — the pak pkg_sysreqs() step ran after setup-renv, so the system library arrived too late (and pak itself was not installed before restore). Move the sysreqs step before setup-renv (bootstrapping pak from PPM as a fast binary), and add librsvg2-dev to the explicit apt list as defense-in-depth. Co-Authored-By: Claude Code --- .github/workflows/ci-benchmark.yml | 26 +++++++++++----- .../workflows/quarto-publish-update-cache.yml | 30 ++++++++++++------- .github/workflows/quarto-publish.yml | 26 +++++++++------- 3 files changed, 54 insertions(+), 28 deletions(-) diff --git a/.github/workflows/ci-benchmark.yml b/.github/workflows/ci-benchmark.yml index ae41c7097..c69f51ceb 100644 --- a/.github/workflows/ci-benchmark.yml +++ b/.github/workflows/ci-benchmark.yml @@ -93,16 +93,21 @@ jobs: sudo apt-get install -y \ libcurl4-openssl-dev \ libudunits2-dev \ - libglpk-dev - - - uses: r-lib/actions/setup-renv@v2 - env: - RENV_CONFIG_SANDBOX_ENABLED: false - - # pkg_sysreqs() only returns the install commands (it never installs); - # run them through a privileged shell, as in quarto-publish.yml. + libglpk-dev \ + librsvg2-dev + + # pak derives the system requirements of the full dependency closure + # from DESCRIPTION — the same job PKG_SYSREQS=true used to do inside + # setup-r-dependencies. This MUST run before setup-renv: renv + # load-tests every package during restore, so missing system + # libraries (e.g. librsvg behind rsvg) would abort it. pak installs + # as a PPM binary in seconds; pkg_sysreqs() only returns the install + # commands (it never installs), so run them through a privileged + # shell here. The explicit apt line above covers the libraries + # known to be needed; this catches anything else in the closure. - name: Install system requirements for locked packages run: | + Rscript -e 'if (!requireNamespace("pak", quietly = TRUE)) install.packages("pak")' SCRIPTS=$(Rscript -e 'cat(pak::pkg_sysreqs("deps::.")$install_scripts, sep = "\n")') if [ -n "$SCRIPTS" ]; then echo "$SCRIPTS" | sudo bash @@ -110,6 +115,11 @@ jobs: echo "No additional system requirements needed" fi + - uses: r-lib/actions/setup-renv@v2 + env: + RENV_CONFIG_SANDBOX_ENABLED: false + + # Mark AFTER setup-renv + sysreqs so r_deps measures the new pipeline. - name: Mark phase — R dependencies done run: echo "r_deps=$(date +%s)" >> /tmp/benchmark-clock diff --git a/.github/workflows/quarto-publish-update-cache.yml b/.github/workflows/quarto-publish-update-cache.yml index baa70b346..c94d2140c 100644 --- a/.github/workflows/quarto-publish-update-cache.yml +++ b/.github/workflows/quarto-publish-update-cache.yml @@ -109,18 +109,21 @@ jobs: sudo apt-get install -y \ libcurl4-openssl-dev \ libudunits2-dev \ - libglpk-dev - - - uses: r-lib/actions/setup-renv@v2 - env: - # See quarto-publish.yml: renv's sandbox doubles install time on - # ephemeral runners for no benefit. - RENV_CONFIG_SANDBOX_ENABLED: false - - # pkg_sysreqs() only returns the install commands (it never installs); - # run them through a privileged shell, as in quarto-publish.yml. + libglpk-dev \ + librsvg2-dev + + # pak derives the system requirements of the full dependency closure + # from DESCRIPTION — the same job PKG_SYSREQS=true used to do inside + # setup-r-dependencies. This MUST run before setup-renv: renv + # load-tests every package during restore, so missing system + # libraries (e.g. librsvg behind rsvg) would abort it. pak installs + # as a PPM binary in seconds; pkg_sysreqs() only returns the install + # commands (it never installs), so run them through a privileged + # shell here. The explicit apt line above covers the libraries + # known to be needed; this catches anything else in the closure. - name: Install system requirements for locked packages run: | + Rscript -e 'if (!requireNamespace("pak", quietly = TRUE)) install.packages("pak")' SCRIPTS=$(Rscript -e 'cat(pak::pkg_sysreqs("deps::.")$install_scripts, sep = "\n")') if [ -n "$SCRIPTS" ]; then echo "$SCRIPTS" | sudo bash @@ -128,6 +131,13 @@ jobs: echo "No additional system requirements needed" fi + - uses: r-lib/actions/setup-renv@v2 + env: + # See quarto-publish.yml: renv's sandbox doubles install time on + # ephemeral runners for no benefit. + RENV_CONFIG_SANDBOX_ENABLED: false + + # ── Python Setup ───────────────────────────────────────── - name: Set up Python uses: actions/setup-python@v7 diff --git a/.github/workflows/quarto-publish.yml b/.github/workflows/quarto-publish.yml index c509dd36c..50792c36b 100644 --- a/.github/workflows/quarto-publish.yml +++ b/.github/workflows/quarto-publish.yml @@ -203,23 +203,21 @@ jobs: sudo apt-get install -y \ libcurl4-openssl-dev \ libudunits2-dev \ - libglpk-dev - - - uses: r-lib/actions/setup-renv@v2 - env: - # renv's install sandbox copies packages into a private library - # before promoting them; on ephemeral CI runners it doubles - # install time for no benefit, so switch it off. - RENV_CONFIG_SANDBOX_ENABLED: false + libglpk-dev \ + librsvg2-dev # pak derives the system requirements of the full dependency closure # from DESCRIPTION — the same job PKG_SYSREQS=true used to do inside - # setup-r-dependencies. pkg_sysreqs() only returns the install + # setup-r-dependencies. This MUST run before setup-renv: renv + # load-tests every package during restore, so missing system + # libraries (e.g. librsvg behind rsvg) would abort it. pak installs + # as a PPM binary in seconds; pkg_sysreqs() only returns the install # commands (it never installs), so run them through a privileged - # shell here. The extra apt line above already covers the libraries + # shell here. The explicit apt line above covers the libraries # known to be needed; this catches anything else in the closure. - name: Install system requirements for locked packages run: | + Rscript -e 'if (!requireNamespace("pak", quietly = TRUE)) install.packages("pak")' SCRIPTS=$(Rscript -e 'cat(pak::pkg_sysreqs("deps::.")$install_scripts, sep = "\n")') if [ -n "$SCRIPTS" ]; then echo "$SCRIPTS" | sudo bash @@ -227,6 +225,14 @@ jobs: echo "No additional system requirements needed" fi + - uses: r-lib/actions/setup-renv@v2 + env: + # renv's install sandbox copies packages into a private library + # before promoting them; on ephemeral CI runners it doubles + # install time for no benefit, so switch it off. + RENV_CONFIG_SANDBOX_ENABLED: false + + # ── Python Setup ───────────────────────────────────────── - name: Set up Python uses: actions/setup-python@v7