Kai Ole Hartwig
Go · Apache-2.0 · GitLab and GitHub

pinup — dependency updates as one static binary.

pinup reads your existing Renovate configuration, plans every change before it writes one, and opens merge requests that say what they bring. One Go binary, no Node, no sidecars: the datasources and managers are built in, the platform is an interface with GitLab (in production across 200 repositories) and GitHub.

  • Binary:go install github.com/ohartwig/pinup/cmd/pinup@latest or the signed releases
  • Configuration:renovate.json stays as it is · .pinup.yaml works too
  • Licence: Apache-2.0 · two dependencies (a YAML parser, bbolt) · Go 1.27

Renovate runs. Why does the update not come?

Until now

  • An update is missing, and nobody knows whether a rule, a schedule or a bug holds it
  • Twenty to thirty minutes an hour on a Node runner with a sidecar for every foreign datasource
  • A chart vendor raises the major on every release, and every one of them waits for approval
  • A postUpgradeTasks command outside the allowlist is skipped, the branch pushed anyway

With pinup

  • Every run writes a plan in which every held update carries its reason, its thaw time and the rule
  • The same 200 repositories in ten instead of forty minutes of wall time, one binary, no runtime
  • An analyzer reads what actually changed in the chart; the stricter label decides unless a rule trusts the analyzer
  • A refused command holds the branch and names itself in the plan

Does this fit my setup?

Use it when …

  • you run Renovate and want to know why an update does not come
  • your runner spends its time in a Node runtime with sidecars
  • charts or packages raise the major on every release and you want to merge on your own where nothing changed
  • it matters to you that a plugin never sees the platform token and a task writes only in its scope

Do not use it when …

  • you need Bitbucket, Azure DevOps or Gitea: today there are GitLab and GitHub
  • your stack needs managers pinup does not have: Maven, Gradle, NuGet, Cargo, pip requirements
  • you want Mend Merge Confidence: pinup classifies on its own and calls no third-party service

What pinup brings

A plan before any write

pinup whatif stops there, pinup run continues

Every run produces a machine-readable plan before a byte is written. Every held update carries its reason, its thaw time and the rule that holds it. Apply consumes byte-range edits, never dependencies: nothing downstream of the plan can invent a change.

Effective label

For chart vendors that raise the major on every release

Beside the declared label (major, minor, patch), a rule with analyze: true asks an analyzer what actually changed: for a Helm chart the application version, removed values keys, the subcharts. The stricter label wins unless the rule says trustEffective.

Renovate-compatible

pinup migrate says, key by key, what is read

The same configuration language: extends, presets, packageRules, custom managers with Handlebars and JSONata, schedules, minimumReleaseAge, the dashboard. The same branch names, so open merge requests are adopted rather than duplicated.

Shadow mode

A switch without an advance of trust

pinup shadow compares pinup's plans with the merge requests Renovate has open, run after run, and calls itself right only when both agree. A control repository must differ, so the comparison proves it can fail.

Plugins without credentials

The core decides, tasks apply

A lock refresh or a postUpgradeTasks command runs in a HOME of its own, without the platform token and without the signing key, against an allowlist on the compiled command. What it writes outside its scope is discarded.

Clean room, Apache-2.0

Two dependencies: a YAML parser, bbolt

Renovate is AGPL-3.0. Nothing was copied: behaviour was observed in the pinned container and rewritten from recorded input/output pairs, which are the test suite at the same time.

A configuration audit

Four categories, 38 checks, each with a case that fires it

pinup advise reads the configuration the way a run does and names what should change: rules that never fire, values a preset already sets, an automerge over majors. --fix writes the fixes into the file byte for byte, behind a gate on the resolution.

Why

I ran Renovate across an estate of 200 repositories: seven scan partitions on an hourly schedule, a release fast lane, one default.json of 88 KB with 25 custom managers carrying the incident history of a year. It worked, and it cost: 40 minutes of wall time and 128 runner minutes per hourly run, a Node sidecar for the apk datasource Renovate does not have, mirrored toolchains so that install-tool never touches GitHub, and a class of silent failures: a composer lock never refreshed for a year; a command the allowlist rejected and that was simply skipped without a red job; thirteen security merge requests merged green around a task that never ran.

Renovate says what it did. It does not say what it did not do and why. That is where pinup starts. Renovate is also AGPL-3.0: I could keep the configuration language, not read the code. pinup is a clean-room re-implementation from recorded input/output pairs of the pinned container, which is why it can be permissively licensed: Apache-2.0, like all my Go tooling.

Measured after the switch, hourly run over the whole estate: 75 instead of 128 runner minutes, 10.6 instead of 40 minutes of wall time, about half per partition. A first-party release reaches its consumers within minutes, an OSV advisory is acted on within fifteen minutes.

Quickstart

1

Plan a repository you already have

whatif resolves the configuration, reads the manifests, looks the versions up and writes a plan. It changes nothing and needs no token. Without a file of its own, a .pinup.yaml holding extends: [config:recommended] is enough to start.

 

cd a-repository
pinup whatif --repo . --config .pinup.yaml --report plan.json

# what would move, and what is held back and why
jq -r '.updates[] | "\(.dep.depName) \(.dep.currentValue) -> \(.newValue)  \(.blocks[0].reason // "ready")"' plan.json

 

lodash 4.17.20 -> 4.17.21 ready becomes a branch on the next run; redis 20.13.4 -> 28.1.0 dependencyDashboardApproval says what holds the major. --now 2026-01-01T00:00:00Z plans as of another moment.

2

Run against one project

run does what whatif does and then pushes the branches, opens or updates the merge requests and keeps the dashboard issue.

 

export PINUP_GITLAB_URL=https://gitlab.example.org
export PINUP_GITLAB_TOKEN=glpat-…              # scopes: api, write_repository
export PINUP_GIT_NAME="Dependency Bot" PINUP_GIT_EMAIL=bot@example.org
export PINUP_SIGNING_FORMAT=none               # or openpgp / ssh + PINUP_SIGNING_KEY

pinup run --project group/project --config .pinup.yaml --report plan.json

 

On GitHub the same command reads PINUP_GITHUB_TOKEN and --project owner/repository. --dry-run stops after the plan: the safe first run against a real project. What you get: one branch per update or group, named the way Renovate names it, a merge request with the edits and the release notes, automerge where a rule says so, and a dashboard issue with a checkbox per held update.

3

One configuration for many repositories

The estate's rules live in a runner project, every repository extends it; a repository with no file at all gets the runner's defaults.

 

// runner project: default.json
{ "extends": ["config:recommended"], "packageRules": [ … ] }

# any repository: .pinup.yaml
extends: ["local>group/pinup-runner"]

pinup run --autodiscover '["group/**", "!group/archive/**"]' \
          --config 'local>group/pinup-runner' --report 'reports/%s.json'

 

--autodiscover runs every non-archived project the token can see that matches the globs, eight at a time. A complete runner configuration is under docs/examples in the repository.

4

Schedule it

docs/examples holds a scheduled GitLab job (a toolchain image with pinup, composer, npm and go, the token as a masked variable, the lookup cache kept between runs, the plans as artefacts) and the same as a GitHub Actions workflow. Hourly is fine: a run over two hundred repositories takes ten minutes of wall time, and a run that finds nothing new writes nothing.

The documentation

Getting started

The binary, a token, the first plan, the first run, a CI job.

Read →
Getting started

Container images

The two images on ghcr.io, their tags, what is in them, where the binary comes from and how to verify a signature.

Read →
Container images

Configuration

Renovate's language, the three layers, the merge semantics, every supported key and the environment variables.

Read →
Configuration

Managers, datasources, versionings

Which files are read, which registries are asked, how versions are ordered.

Read →
Managers, datasources, versionings

The plan

The JSON every run writes before it writes anything else: fields, block reasons, and how to read it with jq.

Read →
The plan

Effective label

The second label on every update: the Helm chart analyzer, the safety rule, matchEffective and trustEffective.

Read →
Effective label

Tasks and plugins

Lock refreshes and post-upgrade commands: what runs, in what scope, with what environment.

Read →
Tasks and plugins

Platforms

GitLab and GitHub behind one interface: credentials, what each can and cannot do, how a third one is added.

Read →
Platforms

Security

Where the token goes, what a repository can and cannot make the bot do, how commits are signed.

Read →
Security

Source and releases

Go module

github.com/ohartwig/pinup · CGO off, one binary

go install github.com/ohartwig/pinup/cmd/pinup@latest

pkg.go.dev →
Go module

Documentation

English in the repository, German and English here

Getting started, every command, the configuration keys, the plan format, platforms, security: in the repository under docs/, and here on the subpages.

docs/ on GitHub →
Documentation

GitHub

Linux and macOS, amd64 and arm64

Source, releases with signed binaries, issues. Apache-2.0-licensed.

View on GitHub →
GitHub

Running Renovate, but the runner bill or the silent misses bother you?

I help with shadow operation, the switch and the runner setup: compare first, then cut over, with a fallback at every step.

Get in touch →