Kai Ole Hartwig — Blog
9 min read
By

Renovate from poll to push: a fast lane for your own packages, images, and CI components

Renovate is one of those tools that quietly does its job: point it at your repos and it opens tidy little MRs whenever something moves upstream. One structural property bugged me, though — Renovate is poll-based. When I cut a release of one of my own building blocks, every consumer waits for the next scan cycle. This post shows how a release becomes an event that reaches every consumer in minutes — without flooding the CI runner — and the sharp edges I hit on the way.

TL;DR

Renovate polls. When you release one of your own packages, images, or CI components, every consuming project waits for the next scan cycle before it gets an update MR — in my setup, up to an hour. The fix: the release job fires a pipeline trigger at the Renovate runner, which starts a minimal run for first-party dependencies only. The result: your own releases propagate in minutes instead of an hour, security updates keep running on every scan, external updates stay throttled to a 4-hour window with a 24-hour soak. CI load goes down, not up. Cost: one shell step in the release job and a second Renovate config.

The problem: Renovate only acts when it runs

On my self-hosted GitLab, Renovate covers everything: Composer and npm packages, the golden images I build myself, and the CI/CD components my pipelines include. When I cut a release of one of my own building blocks — a shared sitepackage, a base image, a CI component — every project that consumes it just waits. Up to a full scan cycle before a bump MR even appears. For third-party dependencies that's completely fine. For my own artifacts, where I pushed the change two minutes ago and know exactly what it is, waiting an hour feels backwards.

The naive fix is polling more often. Don't. A self-hosted Renovate that scans every repo every 15 minutes buries your CI runner in pipelines and lookups that find nothing 99% of the time. The updates you actually care about are a small, knowable subset.

So I went the other way: turn a release into an event.

Sort the cadence first

Before any trigger enters the picture, one observation pays for itself: not all updates deserve the same urgency. There are three tiers.

Security: instant

CVE, OSV, and advisory fixes ride every run, with no soak time and auto-merge at patch level. That is not up for debate.

First-party: as fast as possible

My own packages, images, and CI components. Here I want minutes, not hours — that is what the fast lane is for.

External: relaxed

Everybody else's stuff gets the opposite treatment: a cron window roughly every four hours plus a 24-hour soak before an MR even opens. The soak is deliberate defense against compromised upstream tags. The 4-hour window simply stops external churn from flooding the CI runner with pipelines nobody needs to look at right away.

In Renovate, "external deps only every four hours" is a cron schedule on a package rule. One catch: Renovate's schedule cron requires * in the minute field, there is no minute granularity.

 

{
  "matchPackageNames": ["!moselwal/**", "!koh/**", "!hn/typo3-mcp-server"],
  "schedule": ["* 0,4,8,12,16,20 * * *"]
}

 

That alone made the daily update noise bearable. It doesn't make my own stuff fast, though — it only makes everyone else's quieter. For the fast part, I needed the event.

The core idea: the release announces itself

Every one of my first-party artifacts is released the same way: a semantic-release job on a protected branch turns conventional commits into a tagged release. That shared release job is the perfect place to hang a side effect.

So I added an opt-in step to it: when a release is actually published, fire a pipeline trigger at the Renovate runner.

 

# runs only when semantic-release actually published (HEAD is the chore(release) commit + tag)
if [ "$TRIGGER_RENOVATE" = "true" ] && [ -n "$RENOVATE_TRIGGER_TOKEN" ]; then
  curl -sf -X POST \
    -F token="$RENOVATE_TRIGGER_TOKEN" -F ref=main \
    -F "variables[RELEASED_PACKAGE]=$CI_PROJECT_PATH" \
    "$CI_SERVER_URL/api/v4/projects/<renovate-runner>/trigger/pipeline" \
    || echo "trigger failed (non-fatal, ignored)"
fi

 

Three properties mattered to me, and all three are visible in those few lines. First: non-fatal. The release is already live by the time this step runs. A hiccup in the trigger must never turn a green release red, so every path exits 0. Second: gated. The trigger only fires where I explicitly opted the repo in and a token is present — a clean per-repo kill switch. Third: real releases only. semantic-release runs on every push to main and most of those runs publish nothing. The step keys off the actual chore(release) commit plus tag. No release, no trigger.

Now a release emits an event. The other half is making the Renovate side do the right, minimal thing when it receives one.

The triggered run: the datasource defines what "ours" means

The triggered Renovate run must do exactly one thing: bump first-party dependencies in consumers. Nothing else. No external deps (they have their own relaxed lane), no security (that runs everywhere anyway). A minimal, focused pass.

My first attempt was an allow list of package-name globs: moselwal/**, koh/**, and so on. It worked for Composer packages and then fell over immediately. My golden images resolve to depNames like devops/images/php-runtime and my CI components to devops/ci-cd-components/build-tools. None of those match a package glob. A hand-maintained list of every naming shape is exactly the kind of configuration that silently rots.

Then came the nice realization: every first-party artifact I have is looked up against my own GitLab — and nothing external is. Packages, images, and components are all tracked via gitlab-tags, gitlab-releases, or gitlab-packages. External stuff runs through packagist, npm, docker, github-tags, and friends. So I don't need to enumerate names at all, I invert on the datasource:

 

"packageRules": [
  { "matchPackageNames": ["/.*/"], "enabled": false },
  { "matchDatasources": ["gitlab-tags", "gitlab-releases", "gitlab-packages"], "enabled": true }
]

 

Disable everything, then re-enable only the self-hosted GitLab datasources. That one rule covers packages, images, components, and even an OCI sources artifact I would otherwise have forgotten. It is structurally correct instead of a list I have to remember to update. This is the kind of definition that keeps being right after you stop looking at it.

The sharp edges

A few things bit me, and they are the interesting part.

Security fixes override enabled: false

I had disabled external deps for the fast lane, ran a test, and Renovate cheerfully opened an MR for a phpunit security bump anyway. Renovate's vulnerability path can override a disabled package, and the OSV flag was still on via a CLI argument — CLI beats config file. Omitting the flag wasn't enough, I had to pass it explicitly as false. Lesson: a CLI flag wins against your config, and "disabled" doesn't mean "disabled" to a security fix.

The run tried to delete the scheduled run's work

Because the fast lane disables external deps, Renovate treated their existing branches as orphans and wanted to prune them — in other words, close the hourly run's external MRs. One line fixes it: pruneStaleBranches: false. The fast lane should only ever add first-party bumps. Pruning belongs to the boring hourly scan.

A trigger-variable encoding gotcha

Passing variables[KEY]=value to the pipeline trigger API works with curl -F but not with every CLI wrapper. One of them swallowed the bracket syntax and sent an empty variables object — my supposed dry run quietly ran for real. Always verify the variable actually arrived before trusting a dry run.

Debounce the bursts

Releasing one thing can cascade into releasing several (a component bumps the next one). A dedicated resource group plus interruptible collapses a burst of triggers into a single run. And since Renovate is idempotent, a dropped trigger costs nothing.

The payoff

The shape is now:

 

first-party release  ──▶  release:semver publishes
        │  (only on a real release)
        ▼
   pipeline trigger  ──▶  Renovate fast-lane run (own-only, datasource-scoped)
        ▼
   consumer bump MRs  ──▶  auto-merge at patch/minor  ──▶  deployed

 

End to end: a release reaches its consumers in minutes, not up to an hour. Security keeps its always-on, no-soak lane. External dependencies stay in their relaxed 4-hour window, so the CI runner isn't drowning in noise. And the whole first-party fast lane is additive: the plain hourly scan is still there underneath as the safety net. A missed trigger self-heals on the next cycle.

Frequently asked questions

Does this only work with GitLab?+

The pattern transfers. On GitHub, the trigger would be a repository_dispatch or workflow_dispatch call from the release workflow. The core stays the same: a release event instead of a shorter poll interval, plus a focused Renovate run that only sees your own datasources.

What happens if a trigger gets lost?+

Nothing dramatic. The hourly scan picks up the update on the next cycle at the latest. The trigger accelerates the common case, it carries no responsibility for correctness.

Why not just poll every 15 minutes?+

Because 99% of those runs find nothing and you pay for them anyway, in pipelines and registry lookups across all repos. The trigger costs a single focused run, exactly when there is something to do.

Conclusion

If you catch yourself reaching for a shorter poll interval, ask what event you could listen to instead. The updates you actually care about are almost always a small, knowable set. And let the data model define your categories: "first-party" wasn't a list of names I had to maintain, it was a property that was already there — the datasource. Invert on that and it stays correct without anyone watching it.

The rest is small-scale discipline: a side effect hanging off a release must never break the release, a duplicate trigger must be harmless, and the boring hourly scan stays in place — it is what makes the fast lane safe to fail.

Poll-based tools don't have to feel poll-based. You just have to give them something to listen to.

Before the next internal release sits idle for an hour again — let's talk about your pipeline architecture.

CI/CD pipelines that keep themselves up to date

If your pipelines wait for the next scan after every internal release — or your Renovate floods the CI runner — I'll work through with you which updates deserve which cadence. The result: security instantly, your own artifacts in minutes, external churn throttled, and a release process that propagates itself.

Request a consultation

Freelance · DevSecOps consulting, training & software development

About the author

[Translate to English:] Foto von Kai Ole Hartwig.

Kai Ole Hartwig

Freelance DevSecOps consultant · OnlyOle Consulting

Programming since 2002 – self-taught, set up my own business with KO-Web in 2012. Over 100 projects, with a focus on security, performance, automation and quality. Today freelance: DevSecOps consulting, training and software development.

Composer 2.10, Packagist, supply chain security, stable version immutability, MFA, FIDO2, dependency policies, minimum release age, cooldown, SLSA, Sigstore, OpenSSF, Sovereign Tech Agency, TYPO3, Sylius, PHP, German Mittelstand, NIS-2, GDPR, BSI, DORA, MaRisk

Composer 2.10 & Packagist roadmap

Nils Adermann and Igor Benko have published the Packagist/Composer supply chain roadmap on 27 May 2026. Composer 2.10 with its dependency policy framework ships this week, stable version immutability on Packagist.org goes live in the same deploy, MFA status moves into the transparency log and onto maintainer profiles. Looking further ahead, a minimum release age policy, FIDO2-backed staged releases for packages with a large userbase and an alignment with OpenSSF L3/L4 plus SLSA build provenance are on the plan. For TYPO3 and Sylius operators, enabling MFA is now an operational prerequisite for the next twelve weeks.

TYPO3, OCI artifact, container, Docker, FrankenPHP, golden image, runtime, supply chain, signed, cosign, reproducible, rollback, Composer, Kubernetes, DevSecOps, digital sovereignty

TYPO3 as a signed OCI artifact

Opinion/architecture: instead of deploying TYPO3 as an individual container image per project, separate runtime (hardened golden image: FrankenPHP, PHP, extensions, Caddy) and application (TYPO3 core, vendor, extensions, sitepackage) and ship the application as a signed OCI artifact. No composer install at start time. Benefits: smaller deployments, faster rollbacks, better supply chain, reproducibility — plus open limits.