Summary
In .github/workflows/claude.yml, Restore renv (line 487) runs before
Checkout PR branch (line 531). So on a PR, the renv library is restored from
the base branch's renv.lock, never the PR's.
Two consequences, and the second is the one that cost a day:
- A PR that changes
renv.lock is never validated against its own
lockfile. The one workflow that calls renv::restore() tests the base
branch's lockfile instead.
- A PR that adds a dependency gets a library that lacks it. Claude then
runs the repo's checks against a library that does not satisfy the
lockfile in the working tree.
How it surfaced
ucdavis/bcs had two rotted renv.lock records
(bcs#602): a GitHub pin whose
upstream commit had been garbage-collected, and a Repository/CRAN record at
a development version CRAN never published. renv::restore() failed on a cold
cache.
Fixing them in bcs#603 and firing
this workflow on that PR reproduced the failure verbatim, with the old
version numbers:
✖ snapr 0.0.0.9000
✖ cards 0.8.0.9004
Those are the values from main, not from the PR. The run confirms it
directly --- event: issue_comment, head_branch: main,
head_sha: 448971f8 (the PR head is a different commit) --- and the job's step
list shows why:
| # |
Step |
Result |
| 13 |
Restore renv |
failure |
| 15 |
Capture PR head SHA before Claude |
skipped |
| 16 |
Checkout PR branch |
skipped |
The restore fails at step 13, so steps 15 and 16 never run --- but even on a
passing run the ordering holds and the lockfile read is the base branch's.
Why this is worth fixing rather than documenting
It makes a whole class of change unverifiable by the only workflow that could
verify it, and it does so silently: the run fails (or passes) on evidence
from a different tree, with nothing in the output saying which lockfile it
read. In bcs's case it also helps explain why the rot went unnoticed for so
long --- even a PR written to fix it could not demonstrate the fix.
Suggested fix
Move the PR-branch checkout before the R/renv setup block, so everything
downstream reads the PR's own tree. The steps in between (Install packages,
Set up pandoc/R/Quarto) do not depend on repository content, so the move
looks low-risk, but I have not tested it.
Two details worth handling in the same change:
- The renv cache key is derived from
renv.lock, so reading the PR's
lockfile changes the key. That is correct behaviour --- a different lockfile
should not hit the base branch's cache --- but it means the first run after
this fix on any renv PR will be a cold restore.
Checkout PR branch currently carries a downstream fallback (lines
1102-1105) for the case where it "failed or was skipped (e.g. setup-renv
failed)". Moving the checkout earlier removes that particular dependency,
since the checkout would no longer sit behind a step that can fail first.
Same ordering question applies to preview/action.yml and
quarto-publish/action.yml, which set the same RENV_CONFIG_INSTALL_REMOTES
env; I have not checked whether they have an equivalent PR-checkout step.
Summary
In
.github/workflows/claude.yml,Restore renv(line 487) runs beforeCheckout PR branch(line 531). So on a PR, the renv library is restored fromthe base branch's
renv.lock, never the PR's.Two consequences, and the second is the one that cost a day:
renv.lockis never validated against its ownlockfile. The one workflow that calls
renv::restore()tests the basebranch's lockfile instead.
runs the repo's checks against a library that does not satisfy the
lockfile in the working tree.
How it surfaced
ucdavis/bcshad two rottedrenv.lockrecords(bcs#602): a GitHub pin whose
upstream commit had been garbage-collected, and a
Repository/CRANrecord ata development version CRAN never published.
renv::restore()failed on a coldcache.
Fixing them in bcs#603 and firing
this workflow on that PR reproduced the failure verbatim, with the old
version numbers:
Those are the values from
main, not from the PR. The run confirms itdirectly ---
event: issue_comment,head_branch: main,head_sha: 448971f8(the PR head is a different commit) --- and the job's steplist shows why:
Restore renvCheckout PR branchThe restore fails at step 13, so steps 15 and 16 never run --- but even on a
passing run the ordering holds and the lockfile read is the base branch's.
Why this is worth fixing rather than documenting
It makes a whole class of change unverifiable by the only workflow that could
verify it, and it does so silently: the run fails (or passes) on evidence
from a different tree, with nothing in the output saying which lockfile it
read. In bcs's case it also helps explain why the rot went unnoticed for so
long --- even a PR written to fix it could not demonstrate the fix.
Suggested fix
Move the PR-branch checkout before the R/renv setup block, so everything
downstream reads the PR's own tree. The steps in between (
Install packages,Set up pandoc/R/Quarto) do not depend on repository content, so the movelooks low-risk, but I have not tested it.
Two details worth handling in the same change:
renv.lock, so reading the PR'slockfile changes the key. That is correct behaviour --- a different lockfile
should not hit the base branch's cache --- but it means the first run after
this fix on any renv PR will be a cold restore.
Checkout PR branchcurrently carries a downstream fallback (lines1102-1105) for the case where it "failed or was skipped (e.g. setup-renv
failed)". Moving the checkout earlier removes that particular dependency,
since the checkout would no longer sit behind a step that can fail first.
Same ordering question applies to
preview/action.ymlandquarto-publish/action.yml, which set the sameRENV_CONFIG_INSTALL_REMOTESenv; I have not checked whether they have an equivalent PR-checkout step.