Renovate Buildkite Plugin
A Buildkite plugin that runs self-hosted Renovate in a Buildkite pipeline.
It is a port of the renovatebot/github-action
GitHub Action, with the parts of GitLab’s
renovate-runner templates
that a scheduled pipeline needs: an ndjson log kept as an artifact, and a
config-validator mode.
- Quick start
- Scheduling the run
- Configuration
- Execution modes
- Configuring Renovate itself
- Passing environment variables to Renovate
- Persisting the repository cache
- GitHub App permissions
- Hardening the app credentials
- Validating Renovate configuration
- How it works
- Requirements
- Troubleshooting
- Development
Quick start
Pair this plugin with
altana-ai/create-github-app-token:
it mints a GitHub App installation token, exports it as GITHUB_TOKEN, and
revokes it when the step finishes. This plugin picks that token up with no extra
configuration.
steps:
- label: ":robot_face: renovate"
# Renovate is a scheduled job; see Scheduling the run below.
if: build.source == "schedule"
concurrency: 1
concurrency_group: renovate
# Buildkite secrets, mapped to the environment variables the plugins read.
# Requires agent 3.106.0 or later; values are redacted from build logs.
secrets:
GITHUB_APP_ID: renovate_github_app_id
GITHUB_APP_PRIVATE_KEY: renovate_github_app_private_key
plugins:
# Must be listed first: its pre-command hook runs before this plugin's
# command hook, which is what puts GITHUB_TOKEN in scope.
- altana-ai/create-github-app-token#v0.6.0:
app-id: "$$GITHUB_APP_ID"
private-key: "$$GITHUB_APP_PRIVATE_KEY"
# With no permission-* options the token inherits the app's own
# permissions. See GitHub App permissions below for the explicit form.
- altana-ai/renovate#v0.1.3:
configuration-file: .buildkite/renovate-config.js
The step needs no command — this plugin provides it. The $$ keeps Buildkite
from interpolating the variable at pipeline-upload time, so the auth plugin
resolves it on the agent instead.
examples/pipeline.yml is the full production pipeline:
a scheduled run, a dry run for manual builds, and config validation on pull
requests.
[!IMPORTANT] Install the GitHub App on the runner repository as well as the repositories Renovate manages.
create-github-app-tokenresolves the installation from the repository the build is running in, so a dedicated runner repo without the app installed fails withGitHub App is not installed on …before Renovate starts.
Renovate’s own buildkite manager
updates plugin references in .buildkite/*.yml, so once this is running it keeps
its own version current.
Scheduling the run
Renovate is a scheduled job, not a per-commit one. Create a dedicated pipeline for it, then add a schedule under Pipeline Settings → Schedules:
| Field | Value |
|---|---|
| Cron | 3 * * * * |
| Branch | main |
Give the step concurrency: 1 with a concurrency_group, so two scheduled
builds can never run Renovate against the same repositories at once.
Then guard the step, or every push to the runner repository starts a real run — including the push that edits the pipeline:
if: build.source == "schedule"
That leaves the other build sources free for safer work, which is how
examples/pipeline.yml is arranged:
| Build source | Step |
|---|---|
schedule | Renovate runs for real |
ui | Renovate dry-runs (dry-run: full), so a config change can be checked by hand |
| push / PR | validate-config only — no token is granted to that step |
[!NOTE] An installation access token is valid for one hour and Renovate will not renew it, so a run that outgrows that window starts failing partway through. Keep
--autodiscover-filternarrow, or split wide estates across several steps.
Configuration
Renovate
configuration-file (string)
Path to Renovate’s global configuration file, in JS or JSON format, relative to
the checkout. Sets RENOVATE_CONFIG_FILE;
in docker mode the file is mounted into the container. See
examples/renovate-config.js.
Avoid naming it something Renovate would also read as repository config
(renovate.json, .renovaterc, …).
token (string)
The token Renovate authenticates to the platform with. Either a literal value or
a reference to an environment variable — "$MY_TOKEN", quoted as "$$MY_TOKEN"
in the pipeline YAML so Buildkite does not interpolate it first.
Resolution order, when the option is not set:
$RENOVATE_TOKEN$GITHUB_TOKEN— whatcreate-github-app-tokenleaves behind
The resolved token is registered with the agent’s redactor and reaches Renovate through the environment, never as a command-line argument.
For the github-actions manager
to update workflow files, the token needs the app’s Workflows: write
permission (or the workflow scope, for a PAT) — that is
permission-workflows: write on create-github-app-token, which is a different
permission from permission-actions. Without it GitHub rejects the push with
refusing to allow a GitHub App to create or update workflow, and Renovate
cannot raise PRs for actions/* updates.
github-com-token (string)
Token used for github.com changelog and release lookups, so those requests are
not rate-limited. Sets GITHUB_COM_TOKEN. Also accepts an environment variable
reference.
extra-flags (string or list)
Extra CLI arguments for renovate, the equivalent of the GitLab runner’s
RENOVATE_EXTRA_FLAGS:
extra-flags:
- "--autodiscover=true"
- "--autodiscover-filter=altana-ai/*"
dry-run (string)
extract, lookup or full. Sets RENOVATE_DRY_RUN — useful for a
non-scheduled build that should look but not touch.
log-level (string)
Console log level: trace, debug, info, warn, error, fatal. Sets
LOG_LEVEL.
log-file (string), log-file-level (string), upload-log-artifact (boolean)
Write Renovate’s machine-readable ndjson log to log-file (relative to the
checkout) at log-file-level (default debug), and upload it as a Buildkite
artifact unless upload-log-artifact is false. The log is kept and uploaded
even when Renovate fails, which is when it is most wanted.
base-dir (string)
Directory Renovate clones repositories and stores its cache in. Sets
RENOVATE_BASE_DIR. In docker mode the path must be inside one of
docker-volumes — the default /tmp:/tmp already covers Renovate’s own default
of /tmp/renovate.
print-version (boolean, default true)
Log the Renovate version before running it.
Config validation
validate-config (boolean, default false)
Run renovate-config-validator instead of renovate. No token is required. See
Validating Renovate configuration.
validator-flags (string or list)
Files to validate. Defaults to configuration-file when not set.
Execution
mode (string, default docker)
docker or native. See Execution modes.
renovate-image (string, default ghcr.io/renovatebot/renovate)
Image to run — for example a pull-through cache such as
proxy.example.com/renovate/renovate.
renovate-version (string, default 44)
Image tag, optionally with a digest. Use full for the image with every
lock-file tool preinstalled (much larger), or pin it:
renovate-version: "44.24.2@sha256:90759c92793143ae7db68cd14e690109d8ace3527e17c7d188c96a1ad628282e"
env-regex (string)
Which of the agent’s environment variables are passed into the container. See Passing environment variables to Renovate.
docker-pull (string, default always)
always, missing or never, passed to docker run --pull.
docker-tty (boolean, default true)
Allocate a pseudo-TTY, which keeps Renovate’s output coloured in the Buildkite log.
docker-volumes (string or list, default /tmp:/tmp)
Volume mounts. A string may hold several, separated by ;:
docker-volumes: "/tmp:/tmp ; /opt/cache:/opt/cache"
docker-volumes:
- /tmp:/tmp
- /opt/cache:/opt/cache
docker-user (string)
User or UID the container runs as. Defaults to the image’s own unprivileged
user; only set it (usually to root) alongside docker-cmd-file.
docker-network (string)
Docker network the container joins, e.g. host.
docker-cmd-file (string)
An executable file in the checkout to run inside the container instead of
renovate, for installing extra tooling first. It is mounted at the container
root. See examples/renovate-entrypoint.sh.
plugins:
- altana-ai/renovate#v0.1.3:
docker-cmd-file: .buildkite/renovate-entrypoint.sh
docker-user: root
The script is responsible for dropping privileges and for forwarding "$@" if
extra-flags are also in play.
mount-docker-socket (boolean, default false), docker-socket-host-path (string)
Mount the agent’s Docker socket into the container and add the container’s user
to the socket’s group, so Renovate’s
postUpgradeTasks
can use Docker. docker-socket-host-path overrides the host path
(/var/run/docker.sock).
Execution modes
docker (default) — the hook runs docker run ghcr.io/renovatebot/renovate
on the agent host. This is how the GitHub Action works, and it needs nothing of
the step but a Docker socket.
native — the hook runs the renovate binary already on PATH, for steps
that are themselves executed inside the Renovate image. This is how the GitLab
runner templates work, and it is the right mode for the Kubernetes agent stack
or the docker-compose plugin. The docker-* options are ignored.
steps:
- label: ":robot_face: renovate"
agents:
queue: kubernetes
plugins:
- kubernetes:
podSpec:
containers:
- image: ghcr.io/renovatebot/renovate:44
- altana-ai/renovate#v0.1.3:
mode: native
Configuring Renovate itself
Only the options above belong to this plugin. Everything else is Renovate’s own configuration, and there are two ways to give it:
configuration-file— global config as JS or JSON.RENOVATE_*environment variables — every self-hosted option has one, and they are forwarded to Renovate automatically.
steps:
- label: ":robot_face: renovate"
env:
RENOVATE_REPOSITORY_CACHE: enabled
RENOVATE_ONBOARDING: "false"
RENOVATE_PLATFORM_COMMIT: enabled
plugins:
- altana-ai/renovate#v0.1.3: ~
For a GitHub Enterprise Server instance, set RENOVATE_ENDPOINT to
https://git.example.com/api/v3.
Passing environment variables to Renovate
In docker mode only variables matching env-regex cross into the container.
The default is the GitHub Action’s:
^(RENOVATE_[[:alnum:]_]+|LOG_LEVEL|LOG_FILE|LOG_FILE_LEVEL|GITHUB_COM_TOKEN|NODE_OPTIONS|NO_COLOR|(HTTPS?|NO)_PROXY|(https?|no)_proxy)$
To pass something else — credentials for a
host rule, say
— widen the regex. Prefer a distinct prefix over RENOVATE_, which is reserved
for Renovate’s own options:
steps:
- label: ":robot_face: renovate"
env:
CUSTOM_TFE_TOKEN: "$$MY_TFE_TOKEN"
plugins:
- altana-ai/renovate#v0.1.3:
configuration-file: .buildkite/renovate-config.js
env-regex: "^(RENOVATE_[A-Za-z0-9_]+|LOG_LEVEL|CUSTOM_TFE_TOKEN)$"
module.exports = {
hostRules: [
{
hostType: 'terraform-module',
matchHost: 'app.terraform.io',
token: process.env.CUSTOM_TFE_TOKEN,
},
],
};
Bash matches POSIX extended regular expressions, so \w and (?:...) are not
native — but because they are what the GitHub Action’s docs use, a regex
containing them is translated rather than rejected.
Variables the plugin sets itself (the token, the config file path, the log
settings) are always passed, even if env-regex would not have matched them.
Values are passed by name (docker run --env NAME), so no secret appears in the
host’s process list.
Persisting the repository cache
Renovate’s repository cache
stops it re-proposing work it already decided about. It lives under
<base-dir>/cache/renovate/repository, i.e. /tmp/renovate/cache/renovate/repository
by default, which the default /tmp:/tmp mount already exposes to the host.
steps:
- label: ":robot_face: renovate"
env:
RENOVATE_REPOSITORY_CACHE: enabled
cache:
# Requires an agent with a cache backend configured.
paths:
- /tmp/renovate/cache/renovate/repository
plugins:
- altana-ai/renovate#v0.1.3: ~
Renovate reads the cache as an unprivileged user inside the container (UID
12021), so a cache restored by the agent user may need
sudo chown -R 12021:0 /tmp/renovate in a pre-command hook first. On an
ephemeral agent without a cache backend, leave it off: the cache is an
optimisation, not state Renovate depends on.
GitHub App permissions
Renovate’s platform documentation
gives the permission set a self-hosted app needs, and the permission-* option
that requests each one:
| GitHub App permission | Scope | create-github-app-token option | What loses if it is missing |
|---|---|---|---|
| Contents | read + write | permission-contents: write | Everything — branches and commits |
| Pull requests | read + write | permission-pull-requests: write | Everything — the PRs themselves |
| Issues | read + write | permission-issues: write | Dependency Dashboard, config-error issues |
| Checks | read + write | permission-checks: write | Branch status, so automerge cannot tell whether CI passed |
| Commit statuses | read + write | permission-statuses: write | Renovate’s own statuses, e.g. renovate/artifacts |
| Workflows | read + write | permission-workflows: write | Updates to files under .github/workflows |
| Administration | read | permission-administration: read | Branch-protection awareness for automerge decisions |
| Members | read | permission-members: read | Team reviewers and assignees |
| Dependabot alerts | read | permission-vulnerability-alerts: read | Vulnerability-remediation PRs |
| Metadata | read | — | Mandatory, granted implicitly |
The stock Renovate app also holds Packages:
read, which Renovate’s table omits. Add permission-packages: read only if your
dependencies resolve from GitHub Packages or ghcr.io.
Two things to get right:
- The app’s grants and the token’s request are separate. The app has to hold a
permission, and the step has to ask for it.
create-github-app-tokensends an explicitpermissionsobject as soon as onepermission-*option is set, which narrows the token to exactly what is listed — so a partial list silently drops the rest. Requesting more than the app holds is a hard failure: GitHub answers422,The permissions requested are not granted to this installation. Omitting everypermission-*option makes the token inherit the app’s own set, which is the simplest correct choice for an app dedicated to Renovate. - An installation token covers at most one organization. Renovate documents
this directly: running across several orgs needs a separate installation, token
and step for each, with
--autodiscover-filterscoped accordingly.
Hardening the app credentials
The private key is the one long-lived secret in this setup: anything that can read it can mint tokens for every repository the app is installed on. Two controls are worth applying.
Scope the token, not just the key. Grant the app only what
GitHub App permissions lists — drop Workflows if
Renovate should not touch .github/workflows — and add repositories: to
create-github-app-token to scope the token to a subset of the installation. Tokens are revoked when the step ends
and expire within the hour regardless.
Restrict which builds can read the key. A Buildkite secret is readable by every agent in its cluster by default. Attach an access policy to the private key so only this pipeline can fetch it — under the cluster’s secret, Access tab → Restrict access to agents matching a policy:
# Only the Renovate runner pipeline, and only when Buildkite itself started the
# build (a schedule, or someone clicking New Build).
- pipeline_id:
- "01234567-89ab-cdef-0123-456789abcdef"
build_source:
- "schedule"
- "ui"
Policy semantics: claims within a rule are ANDed, values within a claim are ORed,
and rules are ORed — if no rule matches, access is denied. Prefer the first-party
claims (pipeline_id, build_source, cluster_queue_id), which Buildkite
generates itself; the third-party ones (pipeline_slug, build_branch,
build_creator) are convenient but derive from user- or tool-supplied values, so
they are weaker ground for a security boundary. pipeline_id is on the pipeline’s
settings page.
Adding build_source matters here: it is what stops a pull request against the
runner repository from reading the key, so an untrusted branch cannot mint a token
even if it edits the pipeline. Combine it with the if: guard above — the guard
decides what runs, the policy decides what can be read.
Confirm the accepted build_source values against your own pipeline before
relying on the policy; the same vocabulary as build.source in step conditionals
applies, and Buildkite’s docs enumerate only examples.
Validating Renovate configuration
validate-config runs renovate-config-validator instead of Renovate, which is
worth a pull-request step in every repository that has Renovate config:
steps:
- label: ":mag: renovate config"
plugins:
- altana-ai/renovate#v0.1.3:
validate-config: true
validator-flags:
- renovate.json
- .buildkite/renovate-config.js
In docker mode the checkout is mounted read-only and the paths are resolved
inside it, so the files must live in the repository.
How it works
- Resolves the token from the
tokenoption,$RENOVATE_TOKENor$GITHUB_TOKEN, and registers it with the agent’s redactor. - Translates the plugin’s options into the
RENOVATE_*/LOG_*environment variables Renovate reads. - Logs the Renovate version.
- Runs Renovate —
docker runwith the environment, mounts and flags above, or therenovatebinary onPATHinnativemode. - Moves the ndjson log into the checkout and uploads it as an artifact.
- Fails the step with Renovate’s exit code.
Because the plugin provides the step’s command hook, a command on the step
itself is ignored (the hook warns when it finds one). Use docker-cmd-file, or a
separate step, for work that has to happen around the Renovate run.
Requirements
dockeron the agent (dockermode), or a step that runs inside the Renovate image (nativemode)buildkite-agentonPATH, for token redaction and artifact upload- A token with access to the repositories Renovate should manage
Troubleshooting
No token available — nothing supplied a token. Either set the token
option, or list create-github-app-token before this plugin so its
pre-command hook has exported GITHUB_TOKEN by the time this plugin’s
command hook runs.
GitHub App is not installed on … — from the auth plugin, before Renovate
starts. It resolves the installation from the repository the build is running in,
so the app has to be installed on the runner repository too, not only on the
repositories Renovate manages.
Authentication failure from Renovate — the token itself was rejected.
Check the GitHub App is installed on the target repositories and that
username/gitAuthor in the config file match the app.
Renovate fails partway through a long run — the installation token expired.
They last one hour and are not renewed; narrow --autodiscover-filter or split
the estate across steps.
Renovate opens PRs it already opened — the repository cache is not persisting; see Persisting the repository cache.
Nothing happens, no repositories processed — Renovate does not discover
repositories by default. Give it repositories in the config file, or
--autodiscover=true with an --autodiscover-filter in extra-flags.
Debugging a run — start with log-level: debug, and add
log-file: renovate-log.ndjson to get the full structured log as an artifact.
Development
bats tests # hook tests, with docker and the agent stubbed
shellcheck hooks/* lib/*.bash tests/test_helper.bash tests/stubs/*
The tests replace docker, renovate and buildkite-agent with stubs on
PATH that record their argument vectors, and assert on the command the hook
builds — that command is what this plugin produces. Both commands, plus
buildkite/plugin-linter, run in CI on every pull request.
Credits
Ported from renovatebot/github-action
(GPL-3.0-or-later), with ideas from
renovate-bot/renovate-runner
(MIT).