build(deps): Relock plugin-build on Renovate PRs in CI - #1411
Conversation
eb0cea8 to
f4aa1f9
Compare
6a4ca3d to
7abb998
Compare
f4aa1f9 to
fc8398a
Compare
7abb998 to
d256526
Compare
fc8398a to
cdeef57
Compare
d256526 to
0d1915c
Compare
627fc99 to
2e8d0ad
Compare
Renovate resolves ./gradlew by walking up from the package file it edited, so its lock refresh always runs against the root build. Every lockfile and verification metadata entry we have lives in plugin-build, a separate build the root `dependencies` task cannot reach, so Renovate opens its PRs with those files untouched and STRICT-mode locking rejects the new version in pre-merge. #1363 has been sitting red for a month for exactly this reason. No amount of settings.gradle.kts tuning fixes that, because the command Renovate runs is aimed at the wrong build. Regenerate the files in CI instead and push the result back onto the PR branch, reusing the script the Android SDK updater already runs. Push over SSH rather than with GITHUB_TOKEN, whose commits would not re-trigger the checks that are failing on the stale lock. The resulting `synchronize` event runs the job once more, finds nothing to regenerate and stops.
2e8d0ad to
bee70c9
Compare
The job runs Gradle on a PR-supplied tree, and Gradle executes that tree's build logic: buildSrc, settings and build scripts, the wrapper, and the relock script itself. It also runs with checksum verification in write mode, since that is what regenerating the metadata means, so plugin-build's STRICT verification cannot reject a hostile artifact during that window. Three changes, in order of what they buy: Load the deploy key in the push step via ssh-agent instead of passing it to actions/checkout. Previously the key sat in RUNNER_TEMP with core.sshCommand pointing at it for the whole job, readable by anything the build executed -- including a Gradle plugin from a compromised upstream, which needs no access to this repository at all. Now no credential is on disk while Gradle runs. Refuse to run Gradle unless the PR touches only the version catalog and the two files we generate. Renovate rewrites version strings and nothing else, so any other path in the diff means running unreviewed code. This also covers a maintainer pushing a commit onto a genuine Renovate branch, which fires synchronize while the PR author stays renovate[bot]. Drop cache-encryption-key. It encrypts saved configuration-cache entries and has no business being handed to a build whose tree we do not fully trust. Also commit the two generated paths explicitly rather than all of plugin-build, so a build that writes elsewhere in that tree cannot smuggle the change into the pushed commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| if: steps.guard.outputs.run == 'true' | ||
| run: | | ||
| generated=(plugin-build/gradle.lockfile plugin-build/gradle/verification-metadata.xml) | ||
| if git diff --quiet -- "${generated[@]}"; then | ||
| echo "Lockfile and verification metadata are already up to date." | ||
| exit 0 | ||
| fi | ||
| # The identity every CI-authored commit in this repo already carries, from | ||
| # getsentry/github-workflows/updater. Cosmetic: what re-triggers the checks is the | ||
| # deploy key we push with, not the author. | ||
| git -c user.name='github-actions[bot]' \ | ||
| -c user.email='41898282+github-actions[bot]@users.noreply.github.com' \ | ||
| commit -m 'build(deps): Regenerate plugin-build lockfile and verification metadata' \ | ||
| -- "${generated[@]}" | ||
| # Only now, with Gradle finished, does the key reach the runner. Pushed over SSH rather | ||
| # than with GITHUB_TOKEN, whose commits would not re-trigger the checks that are failing | ||
| # on the stale lock. | ||
| eval "$(ssh-agent -s)" | ||
| printf '%s\n' "$DEPLOY_KEY" | ssh-add - | ||
| # The resulting `synchronize` event runs this job again, which finds nothing to do. | ||
| GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=accept-new' \ | ||
| git push "git@github.com:$GITHUB_REPOSITORY.git" "HEAD:$HEAD_REF" | ||
| ssh-agent -k | ||
| env: | ||
| DEPLOY_KEY: ${{ secrets.CI_DEPLOY_KEY }} | ||
| HEAD_REF: ${{ github.event.pull_request.head.ref }} |
There was a problem hiding this comment.
Untrusted Gradle build can steal CI_DEPLOY_KEY via git hooks/config
DEPLOY_KEY is injected into the push step while still using the same checkout that Gradle just executed from, so a malicious or compromised plugin run during relock can plant .git/hooks or core.sshCommand and exfiltrate the write-capable deploy key on git commit/push. Push only the two generated files from a fresh clone (or scrub hooks/config and avoid putting DEPLOY_KEY in the env until a verified ssh-add), and do not reuse the untrusted worktree’s .git for the privileged push.
Evidence
- The guard only allowlists changed paths (not
gradle/libs.versions.tomlcontent), thenscripts/relock-plugin-build.shruns Gradle with--write-verification-metadata, which downloads and executes plugin-build plugins from that catalog. - Plugin code runs as the runner user on the same worktree and can write
.git/hooks/*orgit config core.sshCommandbefore the push step. - The push step sets
DEPLOY_KEY: ${{ secrets.CI_DEPLOY_KEY }}for the whole block, then runsgit commitandgit pushin that worktree, so a planted pre-commit/pre-push hook or custom ssh command inherits the write deploy key. - Job
ifallows any same-reporenovate/*head (not only the Renovate app), so a branch that only edits the toml can pass the guard and reach this path.
Identified by Warden · security-review · KTU-MEJ
There was a problem hiding this comment.
well, we're locking the gradle and verifying the checksum sha. we'd have deeper issues if we couldn't trust gradle.
#skip-changelog
#1410 stopped Renovate's Gradle run from erroring, but Renovate still can't do the job it runs Gradle for.
Problem
Renoate is opening PRs without refreshing the lockfiles. It seems it is due to not supporting nested/composite builds. So this PR opens a new workflow that tries to update the lockfiles using the same script we use to update the lockfiles as the other dependency updater in this repo.
Here's what claude says the reason renovate isn't working is:
Renovate resolves
./gradlewby walking up from the package file it edited, so a change togradle/libs.versions.tomlalways runs against the root build. Every lockfile and verification metadata entry we have lives inplugin-build, a separate build the rootdependenciestask can't reach. So Renovate opens its PRs with those files untouched, STRICT-mode locking rejects the new version, andpre-merge-checksfails — which is why #1363 has been red for a month.Fix
Regenerate the files in CI on Renovate's branches and push the result back onto the PR, reusing
scripts/relock-plugin-build.sh— the same script the Android SDK updater already runs as itspost-update-script.CI_DEPLOY_KEY, notGITHUB_TOKEN, whose commits don't re-trigger workflows. Re-triggering is the point: the check we're fixing is one that already failed.renovate/branches from this repository. Forks have no deploy key, and pushing to someone else's branch uninvited isn't the point. Could be widened later to cover humans who forget to relock.Verification
We know the script works. But the only way to verify this end to end is by merging it.