pakkasys logo
← Back to blog

Dependency Updates as Routine Maintenance, Not Emergency Work

Small, regular upgrades reduce security and stability risk. This post explains why a maintenance loop matters more than any one bot, and how update tooling must be paired with controlled delivery and rollback.

Aatu Harju9 min read

Dependency Updates as Routine Maintenance, Not Emergency Work

A dependency bot can create fifty pull requests and still leave a system difficult to patch.

The tool found updates. The organization did not create a maintenance loop.

I have seen the opposite too: no update bot, no schedule, and one heroic upgrade every year. The pull request changes half the dependency graph, the runtime version, the build image, and several APIs at once. Everyone is cautious because caution is rational. The change is enormous.

The useful goal is not "all dependencies are always current."

It is:

The system practices small upgrades often enough that an urgent upgrade is an ordinary release.

That is a reliability property, a security control, and a way to keep software changeable.

Detection and delivery are different systems

A scanner or update bot answers:

text

A newer version exists. A known vulnerability affects this version.

It does not answer:

text

Does the application still work? Can this change reach production safely? Who owns the decision? How quickly can we roll it back?

That second set is the delivery system.

This is why an SBOM or vulnerability report can be accurate while the practical response capability remains weak. I covered that broader distinction in Software supply chain security in 2026: inventory matters, but the benefit appears when evidence is connected to action.

A dependency-maintenance loop needs both sides:

text

discover -> classify -> update -> test -> review -> release -> observe -> close or roll back

If work stops at "open a pull request," the queue merely changed location.

The painful backlog is usually a policy problem

Dependency automation becomes noisy when every update receives the same treatment.

A patch to a test assertion library, a new Go toolchain, an OpenSSL base-image update, and a major database driver release should not all enter the same undifferentiated queue.

I classify updates by operational meaning.

Routine updates

  • patch releases
  • low-risk minor releases
  • lock-file maintenance
  • GitHub Action digest changes
  • development tooling with good tests

Review-required updates

  • major versions
  • runtime and compiler changes
  • database drivers
  • authentication and cryptography libraries
  • ORM or migration tooling
  • framework changes that alter lifecycle behaviour
  • packages with weak release notes or unstable APIs

Urgent updates

  • exploitable vulnerabilities in deployed code
  • compromised or malicious releases
  • revoked credentials or signing material
  • fixes required by an active incident

The tool should encode as much of this policy as practical. Humans should spend judgment where judgment changes the result.

A boring weekly cadence works surprisingly well

For a small SaaS team, I like this rhythm:

  • bots check continuously or daily
  • routine version-update pull requests arrive in a weekly window
  • security updates bypass the normal batch
  • one owner reviews the queue each week
  • small passing updates merge quickly
  • major changes become explicitly scheduled work
  • production observation closes the loop

Weekly is not sacred. The important property is that the interval is short enough to prevent geological layers.

GitHub's Dependabot version-update documentation supports schedules per ecosystem. A minimal configuration for a Go service can remain small:

yaml

version: 2 updates: - package-ecosystem: "gomod" directory: "/" schedule: interval: "weekly" open-pull-requests-limit: 5 - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" open-pull-requests-limit: 5 - package-ecosystem: "docker" directory: "/" schedule: interval: "weekly" open-pull-requests-limit: 3

This is only a starting point. The repository still needs a human-readable policy: which updates may merge automatically, which require release notes and manual testing, and what happens when an update remains blocked.

At the time of writing, GitHub also applies a default three-day cooldown to Dependabot version updates, while security updates still open immediately. GitHub introduced this in July 2026 to reduce the chance of adopting a newly broken or compromised release immediately. The behaviour and override are described in the GitHub changelog.

That is a useful default, but cooldown is not due diligence. A bad release can remain bad after three days.

Group updates by how they are tested, not merely by package manager

Grouping reduces pull-request noise, but one giant "update everything" pull request recreates the annual-upgrade problem every week.

I group packages when they move and fail together:

  • packages from the same monorepo
  • a framework and its official adapters
  • development-only linting packages
  • related cloud SDK modules
  • a base image and its generated lock or digest metadata

I separate packages when a failure needs a different diagnosis or rollback.

Renovate is particularly flexible here. Its documentation covers package grouping, schedules, and a Dependency Dashboard for updates that need approval.

A small policy might look like this:

json

{ "extends": [ "config:recommended", ":dependencyDashboard", "schedule:earlyMondays" ], "packageRules": [ { "description": "Group routine non-major updates", "matchUpdateTypes": ["patch", "minor"], "groupName": "routine non-major dependencies" }, { "description": "Require explicit approval for major updates", "matchUpdateTypes": ["major"], "dependencyDashboardApproval": true } ] }

This is not universally ideal. A single grouped failure can make it harder to identify the offending package. I split the group when that happens repeatedly.

The policy should reduce noise without hiding causality.

CI must prove something relevant

A green dependency pull request is valuable only if the checks cover the behaviour that could have changed.

For a Go backend, my baseline is:

  • compile all binaries
  • run unit tests
  • run race-sensitive tests where justified
  • run static analysis
  • run contract tests for public API shapes
  • apply migrations to a fresh database
  • apply migrations to a representative previous schema
  • start the service with production-shaped configuration
  • run a small HTTP smoke test
  • build the actual release image
  • scan the resulting artifact where the threat model requires it

Not every repository needs every check. But go test ./... is not proof that a container starts, a migration works, or the public error contract remained stable.

I pay special attention to packages near operational boundaries:

  • HTTP routers and middleware
  • JSON encoders
  • database drivers
  • migration libraries
  • authentication libraries
  • telemetry exporters
  • cloud SDKs
  • container base images

A small behavioural contract test around these boundaries often buys more safety than hundreds of tests over internal getters.

Automerge should be an earned privilege

Automerge is useful when three things are true:

  • the update class is narrow and understood
  • required checks are meaningful
  • rollback is routine

I may automerge:

  • patch updates to development-only tools
  • action digests after required checks
  • low-risk patch updates with a strong history
  • generated lock-file maintenance

I normally review:

  • major versions
  • runtime changes
  • database and network libraries
  • authentication, authorization, and cryptography
  • packages with unusual maintainer or ownership changes
  • updates that modify generated code or migrations
  • anything that changes the release artifact substantially

The distinction is not "security update equals automatic." A rushed vulnerable-dependency fix can still break authentication or corrupt data. Urgency should shorten waiting time, not erase verification.

Release notes are evidence, not a ritual

I scan release notes for a small set of questions:

  • Are there breaking changes?
  • Did defaults change?
  • Were deprecated paths removed?
  • Did retry, timeout, pooling, or connection behaviour change?
  • Did supported runtime versions change?
  • Are there migration steps?
  • Are security fixes intentionally vague?
  • Did ownership or publishing infrastructure change?

I do not expect a human to deeply audit every transitive package. That does not scale.

I do expect deeper scrutiny when the package sits on a high-impact boundary or the release itself looks unusual.

The review depth follows blast radius.

Keep dependency changes separate from feature work

A dependency update hidden inside a product feature is hard to reason about and hard to roll back.

I prefer small, identifiable dependency commits or pull requests. This gives me:

  • a clear diff
  • focused release notes
  • isolated CI evidence
  • easier bisecting
  • safer rollback
  • a clean deployment marker

When an update requires application changes, the pull request can contain them. It should still have one purpose: adopt dependency version X and adapt to its known changes.

"Upgrade dependencies while we are here" is how unrelated risk becomes invisible.

Production observation completes the update

The update is not done when the pull request merges.

After deployment, I compare:

  • request error rate
  • latency
  • memory and CPU
  • database connection behaviour
  • retries and timeouts
  • queue depth
  • domain success metrics
  • logs by deployment version

The deployment version in request logs is especially useful. It lets me ask whether a failure began with the dependency update rather than merely around the same time.

For high-risk updates, I use a canary or small traffic slice and define the rollback signal before release.

Blocked updates need visible ownership

Some updates cannot merge immediately.

A major library may require refactoring. A new runtime may break a transitive tool. A vendor SDK may remove an API. That is normal.

What becomes dangerous is an ignored update with no decision.

I record:

text

Dependency: Current version: Target version: Why blocked: Risk of staying: Owner: Next review date: Planned removal or upgrade:

An ignore rule without this context is a future surprise generator.

I also distinguish temporary pinning from abandonment. If a package is no longer maintained or no longer fits the architecture, the answer may be replacement rather than an increasingly elaborate exception policy.

The measures I care about

I do not optimize for the number of bot pull requests merged.

I care about:

  • median age of routine dependency updates
  • age of the oldest unowned update
  • time from critical advisory to production fix
  • percentage of release artifacts built from supported runtimes
  • number of repositories where automation is failing silently
  • update rollback rate
  • percentage of high-risk dependencies with an explicit owner
  • frequency of production releases, because updates cannot ship through a closed door

These measures describe response capability.

A dashboard showing 312 detected vulnerabilities may be accurate and still tell me little about whether the team can improve the situation.

Start by making one repository boring

For a neglected service, I would not enable every ecosystem and open fifty pull requests on day one.

I would:

  • record the current runtime and direct dependencies
  • update the build and test path until it is trustworthy
  • enable automation for one ecosystem
  • clear routine patch updates
  • isolate major upgrades
  • deploy a small update
  • observe it
  • establish the weekly owner and cadence
  • expand only after the loop works

The purpose is to build maintenance muscle, not to produce activity.

Dependency freshness is not purity. Old versions are not automatically vulnerable, and new versions are not automatically safe.

The useful state is simpler:

The distance to the next safe release stays small.

When that is true, dependency updates become ordinary maintenance. When it is false, every urgent fix becomes an emergency project built on top of months of avoided change.

Need help with this in your own stack?

If reliability or delivery friction is slowing your team down, we can fix it in focused steps.

Related posts

10 min read

Before You Add a Queue: The Failure Modes You Are Agreeing to Own

Asynchronous processing introduces concrete operational risks like retries, ordering, poison failures, and visibility debt. This post argues for adding queues only when asynchronous ownership is part of the product, not a default optimization.

ArchitectureQueuesReliability
11 min read

A Boring API Error Model That Survives Five Years of Clients

Stable APIs usually fail on inconsistent error contracts. This post proposes a boring structure using status, problem code, human-safe detail, and request identifiers to reduce contract drift over years.

API DesignError HandlingGo