Getting started
pinup is one static binary. It needs git on the machine, a token for the platform it writes to, and a configuration in Renovate's language. Four steps from the download to a scheduled job.
- Binary:
go install github.com/ohartwig/pinup/cmd/pinup@latestor the signed releases (pinup-linux-amd64,-arm64,pinup-darwin-arm64,-amd64,SHA256SUMS) - Check:
pinup versionprints what you have
From the binary to a scheduled job
A plan without a token
The first thing to run is whatif: it resolves the configuration, extracts the dependencies, looks versions up and writes a plan. It changes nothing and needs no platform token (private registries aside).
cd a-repository
pinup whatif --repo . --config .pinup.yaml --report plan.json
The plan says what pinup would do, and for everything it would not do, why: every held update carries its reason, its thaw time and the rule that holds it. --now 2026-01-01T00:00:00Z plans as of another moment; that is how you check a schedule or a release age without waiting.
A run against a project
run does what whatif does and then pushes the branches, opens or updates the merge requests and writes the dashboard issue. It needs the platform and a token:
export PINUP_GITLAB_URL=https://gitlab.example.org
export PINUP_GITLAB_TOKEN=glpat-… # api, write_repository
export PINUP_GIT_NAME="Dependency Bot" PINUP_GIT_EMAIL=bot@example.org
export PINUP_SIGNING_FORMAT=none # or openpgp / ssh with PINUP_SIGNING_KEY
pinup run --project group/project --config 'local>group/runner-config' --report plan.json
--dry-run stops after the plan, with the platform's view of the existing merge requests. --repo . runs against a checkout instead of cloning. On GitHub the same command reads PINUP_GITHUB_TOKEN and --project owner/repository.
--config is a file or a local> preset the platform serves: a default.json in a runner project that every repository's own configuration file extends. The repositories keep their files; pinup answers the runner's name from the file it was started with, without a fetch.
Every project the token can see
pinup run --autodiscover '["group/**", "!group/archive/**"]' --config … --report 'reports/%s.json'
The list is every project the token can see that is not archived, filtered by the globs (! negates); eight projects run at a time, each in its own clone. A %s in --report becomes the project path.
A CI job
The shape of a scheduled job on GitLab: the toolchain image carries pinup and whatever the lock refreshes need (composer, npm, go), the token is a masked variable, the plan is an artefact.
pinup:scan:
image: ghcr.io/ohartwig/pinup-toolchain:0
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
variables:
PINUP_CACHE: .pinup/cache.db
cache:
key: pinup-$CI_JOB_NAME
paths: [.pinup/cache.db]
script:
- pinup run --autodiscover '["group/**"]' --config 'local>group/runner' --report 'reports/%s.json'
artifacts:
paths: [reports/]
when: always
PINUP_CACHE keeps release lists, first-seen records, advisories and release notes between runs: a release whose registry publishes no timestamp gets its age from when this cache first saw it. That is why a run with an empty cache warns about minimumReleaseAge rather than trusting it. Complete files for GitLab and GitHub Actions are under docs/examples.
Next
Configuration
Renovate's language, the three layers, the merge semantics, every supported key, the environment.