When you manage versions for dependencies, GitHub Actions, or Terraform providers, one idea comes up again and again.
Whenever a new version comes out, why not just upgrade to the latest one every time?
At first glance, that sounds perfectly reasonable.
And indeed, updating continuously is far better than sitting on old versions for months, because it keeps you from deferring vulnerabilities and compatibility issues.
In real-world development operations, however,
"not pinning versions" and "always staying up to date" are two different things.
In fact,
pinning your versions and letting Dependabot open update PRs automatically
is an approach that makes it much easier to get both safety and update frequency.
In this post, I'll walk through a "to pin or not to pin" debate that actually happened on a development team, and lay out how Dependabot fits into the answer.
It started with a simple question: "Do we even need to pin at all?"
On one project, the topic of updating GitHub Actions and Terraform provider versions came up.
During that discussion, someone suggested:
Ideally we shouldn't pin anything. When a new version comes out, we should just always upgrade, right?
That was the argument.
And the thinking itself isn't wrong.
As a goal,
never let your dependencies rot
is exactly right.
But if the way you pursue that goal is "don't pin," a different set of problems shows up.
Which is how we arrived at the alternative:
Not "no pinning," but "pin + automated bumps."
That was the idea.
In other words,
pin the versions, but automate the work of updating them.
That is the reason to use Dependabot.
In GitHub Actions, floating tags are a real risk
In GitHub Actions, for example, you often see references written like this.
uses: actions/checkout@v4
It's readable, and it's the most common style.
Strictly speaking, though, a tag like v4 doesn't point to one specific piece of code.
If the tag is moved to a different commit, the exact same @v4 reference can end up running different code.
Actions that run in CI/CD often have access to your build environment and deploy credentials.
That makes this an important supply chain security concern as well.
If you want stricter control, you pin to a specific commit SHA.
uses: actions/checkout@<commit-sha>
This way, the code executed from a given configuration can never change out from under you.
But there's a catch.
If all you do is pin, that version will keep getting older forever.
This is where Dependabot comes in.
When a new version is released, the flow becomes:
- Dependabot detects the change
- It opens a Pull Request
- You review the diff
- CI runs
- If everything looks good, you merge
That's the whole cycle.
In short,
pinning gives you reproducibility, and Dependabot keeps things fresh.
That's the idea.
"Pinning" and "using the latest version" aren't contradictory
This is the key point.
When you pin a version, you might worry:
"Doesn't that mean we'll be stuck on an old version?"
It's a fair concern.
But with Dependabot, that's not what happens.
Say your current version is 1.2.3.
1.2.3
When 1.2.4 is released, Dependabot opens a PR.
1.2.3 → 1.2.4
You review that PR and merge it.
When 1.3.0 comes out later,
1.2.4 → 1.3.0
another PR shows up.
So in practice, you can keep tracking the latest version continuously.
The difference is that
instead of silently switching to the latest version, every update arrives as a Pull Request — a reviewable change.
That's the distinction.
And it's a big one.
Terraform is a slightly different story
A similar debate comes up with Terraform providers.
Suppose you declare the AWS provider like this.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
~> 6.0 is a constraint that says "use the AWS provider 6.x series."
The important thing here is that in Terraform, the provider version actually in use is recorded in .terraform.lock.hcl.
In other words,
the version constraint you write in versions.tf and similar files decides
which versions are allowed,
nothing more.
The lock file, on the other hand, pins
which version is actually used.
That's its job.
These two play different roles.
What happens if you drop constraints in Terraform entirely
Say you're on the AWS provider 6.x series, and version 7.x is released at some point in the future.
With no constraint, 7.x could get pulled in on your next update.
But major version upgrades can include breaking changes.
As a result, you could see things like:
- Changed resource behavior
- Removed deprecated attributes
- Drift against your state
- Unexpected Terraform plans
Any of these can happen.
With infrastructure especially, you need to avoid the situation where
"the next apply suddenly produced a mountain of changes."
That's a state you can't afford.
So instead, you constrain the major version, like this:
version = "~> 6.0"
You keep the constraint on the major version.
And on top of that, you let Dependabot open the update PRs.
With Terraform, make the flow "PR → plan → review → apply"
With Terraform, what matters isn't the update itself so much as
what changes the update will cause in your infrastructure.
That's the real question.
So for provider updates, a safe flow looks like this:
- Dependabot opens an update PR
- Terraform plan runs
- You inspect the speculative plan
- You review for breaking changes
- Merge if everything checks out
- Apply
Following that sequence keeps things safe.
For major version upgrades of the AWS provider in particular, this flow is essential.
Rather than simply jumping to the latest version with
6.x → 7.x
the point is to
verify the changes through both code review and plan review.
That's what it comes down to.
So what exactly is Dependabot?
Dependabot is GitHub's built-in automation for dependency management.
Its main features are the following.
Dependency graph
GitHub tracks which packages and libraries your repository depends on.
This is the foundation that Dependabot's vulnerability detection is built on.
Dependabot alerts
If a dependency you're using has a known vulnerability, GitHub notifies you.
Dependabot security updates
When a version that fixes a vulnerability is available, Dependabot automatically opens a Pull Request.
Dependabot version updates
Regardless of whether a vulnerability exists, Dependabot opens update PRs whenever new versions are released.
In practice, version updates is the feature at the heart of the "pin + automated bumps" approach.
Version updates are configured in dependabot.yml
To automate routine version updates, you add the following file to your repository.
.github/dependabot.yml
For example, to check GitHub Actions weekly, you'd write this.
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
It can watch Terraform too.
version: 2
updates:
- package-ecosystem: "terraform"
directory: "/"
schedule:
interval: "weekly"
For Gradle, the configuration looks like this.
version: 2
updates:
- package-ecosystem: "gradle"
directory: "/"
schedule:
interval: "weekly"
You can also combine multiple ecosystems in a single file.
Why Dependabot instead of manual updates?
Of course, a human could periodically check for new versions and update by hand.
But over the long run, that kind of process is very easy to let slip.
Right after a project kicks off, you might tell yourselves,
"Let's check every month."
But then,
- development gets busy
- the person responsible moves on
- the number of repositories grows
- the priority drops
and for reasons like these, the updates gradually stop happening.
With Dependabot, you get an operation where
updates no longer depend on human memory.
That's the shift.
And it's a substantial benefit.
The PR history itself becomes your update rationale
There was one more interesting point in this discussion.
The initial proposal was:
Whenever we pin a version, let's leave a comment explaining why we chose that version.
That was the plan.
It's not a bad idea.
But if you're using Dependabot, you don't necessarily need a comment on every single pin.
The reason is simple:
the update history itself is preserved as Pull Requests.
For example, if there's a PR titled
Bump hashicorp/aws from 6.12.0 to 6.13.0
then GitHub already records:
- when the change happened
- what was changed
- which version it came from
- which version it went to
- whether CI passed
- who reviewed it
All of it stays on GitHub.
In some cases, the PR history is actually more accurate than continually maintaining background notes in code comments.
The problems with a no-pin approach
Summing up what we've covered so far, a "no pinning" approach has several problems.
One is that dependencies change at unintended times.
A CI pipeline that worked yesterday can suddenly break today, even though nobody touched the code.
Another is that the changes themselves become hard to see.
When updates go through Dependabot, a change like
v1 → v2
is made explicit as a PR.
But when the thing you reference changes automatically, that change never surfaces in code review.
In software development,
being able to trace what changed and when
matters just as much.
Dependabot alone doesn't make you safe
There are caveats, though.
Adopting Dependabot doesn't automatically make everything safe.
At its core, Dependabot is
a mechanism that automatically surfaces update candidates.
Nothing more.
Just because a PR was opened doesn't mean it's fine to auto-merge it as is.
In particular, for things like:
- Major updates of Terraform providers
- Database libraries
- Authentication libraries
- Build tools
- GitHub Actions
you're better off actually reviewing what changed.
The important framing is:
not automatic updates, but automatic PRs.
That's the mindset.
In practice, "pin + bot + CI" is the workable combination
In the end, the basic shape of dependency management looks like this.
Version pin
↓
Dependabot
↓
Pull Request
↓
CI / Terraform plan
↓
Human review
↓
Merge
With this setup, you can:
- guarantee reproducibility
- keep tracking the latest versions
- retain a change history
- catch problems in CI
- leave the final call to a human
It strikes exactly that balance.
"Using the latest version" and "silently becoming the latest version" are different things
This, I think, is the most important takeaway from the whole discussion.
Actively using the latest version is the right instinct.
However,
automatically switching to the latest version is not necessarily right.
The ideal state is this:
Always chase new versions — but make every change visible as a Pull Request.
That's the target.
And as a mechanism for getting there, Dependabot is remarkably easy to work with.
Wrapping up
The reason to use Dependabot isn't simply "because updating is tedious."
Fundamentally, it's
to turn dependency updates into a reviewable, traceable part of your development process.
That's the point.
The "pinning means going stale" problem is solved by Dependabot.
The "latest version suddenly breaks things" problem is contained by pinning plus PR review.
Which is why, in practice,
pin + Dependabot + CI
is the combination that's easiest to operate.
Pinning versions and staying up to date are not in conflict.
On the contrary, putting Dependabot in the middle makes it possible to run an operation where you
pin for safety while continuously tracking the latest versions.
That kind of operation is exactly what it enables.
