Skip to content

build(deps): Relock plugin-build on Renovate PRs in CI - #1411

Merged
runningcode merged 2 commits into
mainfrom
no/renovate-relock-plugin-build
Aug 26, 2026
Merged

build(deps): Relock plugin-build on Renovate PRs in CI#1411
runningcode merged 2 commits into
mainfrom
no/renovate-relock-plugin-build

Conversation

@runningcode

@runningcode runningcode commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

#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 ./gradlew by walking up from the package file it edited, so a change to gradle/libs.versions.toml 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 can't reach. So Renovate opens its PRs with those files untouched, STRICT-mode locking rejects the new version, and pre-merge-checks fails — 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 its post-update-script.

  • Pushes over SSH with CI_DEPLOY_KEY, not GITHUB_TOKEN, whose commits don't re-trigger workflows. Re-triggering is the point: the check we're fixing is one that already failed.
  • Scoped to 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.

@runningcode
runningcode force-pushed the no/renovate-skip-snapshots-runtime branch from eb0cea8 to f4aa1f9 Compare August 21, 2026 10:36
@runningcode
runningcode force-pushed the no/renovate-relock-plugin-build branch from 6a4ca3d to 7abb998 Compare August 21, 2026 10:36
@runningcode
runningcode force-pushed the no/renovate-skip-snapshots-runtime branch from f4aa1f9 to fc8398a Compare August 21, 2026 11:58
@runningcode
runningcode force-pushed the no/renovate-relock-plugin-build branch from 7abb998 to d256526 Compare August 21, 2026 11:58
@runningcode
runningcode force-pushed the no/renovate-skip-snapshots-runtime branch from fc8398a to cdeef57 Compare August 21, 2026 12:09
@runningcode
runningcode force-pushed the no/renovate-relock-plugin-build branch from d256526 to 0d1915c Compare August 21, 2026 12:09
Base automatically changed from no/renovate-skip-snapshots-runtime to main August 21, 2026 15:11
@runningcode
runningcode force-pushed the no/renovate-relock-plugin-build branch 2 times, most recently from 627fc99 to 2e8d0ad Compare August 25, 2026 13:57
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.
@runningcode
runningcode force-pushed the no/renovate-relock-plugin-build branch from 2e8d0ad to bee70c9 Compare August 25, 2026 15:08
@runningcode
runningcode marked this pull request as ready for review August 25, 2026 15:11
@runningcode runningcode added the sanity-check PR needs a lightweight review for obvious issues label Aug 25, 2026

@markushi markushi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment thread .github/workflows/relock-plugin-build.yml
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>
Comment on lines +88 to +113
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 }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.toml content), then scripts/relock-plugin-build.sh runs 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/* or git config core.sshCommand before the push step.
  • The push step sets DEPLOY_KEY: ${{ secrets.CI_DEPLOY_KEY }} for the whole block, then runs git commit and git push in that worktree, so a planted pre-commit/pre-push hook or custom ssh command inherits the write deploy key.
  • Job if allows any same-repo renovate/* 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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, we're locking the gradle and verifying the checksum sha. we'd have deeper issues if we couldn't trust gradle.

@runningcode
runningcode merged commit 10e05e8 into main Aug 26, 2026
25 checks passed
@runningcode
runningcode deleted the no/renovate-relock-plugin-build branch August 26, 2026 11:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

sanity-check PR needs a lightweight review for obvious issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants