The Deploy That Didn't Need to Happen
There is a particular kind of waste that looks like productivity. A deployment pipeline spins up, pulls the code, runs the checks, packages the artifact, pushes to the target, and reports success. Minutes of wall-clock time, gone. The logs are clean. The status badge is green. And if you look closely at what just happened, the thing that got deployed is identical to what was already running. Not close. Identical. Same commit, same everything.
The pipeline reported success. Nothing changed.
The pipeline knows when it runs
The reason it happens is simple once you see it. The pipeline knows when it runs. It does not know what is already deployed. Those are two different questions, and most workflows only ask the first one. Did something trigger me? Yes. Then deploy.
The smarter question is: has anything actually changed since the last time this succeeded? That question has a real answer. The platform already knows it. The deployment history is sitting there, queryable, with commit identifiers attached to every run.
Asking in the wrong place
The naive fix is to check timestamps. If a file was touched recently, deploy. If not, skip. That breaks within a week. Timestamps lie. Restores lie. Anything that touches the filesystem without changing the code triggers a deploy that should not happen, and anything that changes the code without bumping a timestamp skips one that should.
I tried branch comparisons next. If the branch is ahead of the last tag, deploy. That one holds longer, but tags are manual. The first time someone forgets to tag, the check silently fails open and you are back to deploying nothing for the cost of deploying something.
Both of those are workarounds for a question I was asking in the wrong place.
The answer was in the run history
The answer was not in the code at all. It was in the run history.
Every successful deployment leaves a record: when it ran, what triggered it, and which exact commit it built from. That record is an API call away. Query the last successful run on the target environment. Read the commit identifier it deployed. Compare it to the one you are about to deploy. If they match, you are done. Not "skip for now and hope it is fine." Done, with proof, logged, traceable, auditable.
The whole decision fits before the expensive steps start. Check first, then commit to the work.
But here is what I did not expect. The first time the skip condition fired on a live pipeline, I did not believe it. The run finished in seconds. Nothing built, nothing pushed, no artifact in the queue. I checked the logs twice. Then a third time. The commit identifier in the run history matched exactly. The platform had known this the entire time. Not because of anything I had put there. It had always known. I had just never asked.
I sat with that for a moment. The system was not broken. I had been interrogating it with the wrong question for months, and every green run it returned was an honest answer to a question I should not have been asking.
What changed was not just speed
What changed was not just speed. It was honesty.
The pipeline stopped lying to me about what it had accomplished. Before, a successful run meant "we ran the steps." After, a successful run means "we changed something, or we confirmed nothing needed changing." Those are different things, and I had been conflating them for years without noticing.
The confidence the fingerprint check gives is real, but bounded. The run history is the ground truth the platform generated when the last real deployment happened, and it answers one question cleanly: was this exact commit already deployed? That covers the common case. It does not cover every case.
A rollback lands you at a commit the history already shows as deployed, but the environment still needs a run. A change applied outside the pipeline matches the same fingerprint for different reasons. An environment rebuilt from scratch needs a full deploy regardless of what the record says. For all of these, you need a force-deploy path: an explicit override that bypasses the skip check and runs unconditionally. Without it, the fingerprint comparison becomes a blind spot exactly where you cannot afford one. The check and the override are not competing ideas. They describe the same discipline: know what you are doing and be deliberate about it.
Systems aware of their own state
Systems that are aware of their own state do not need people to compensate for their ignorance.
Most pipelines are aware of half their state. They know when they run, what they build, and whether the steps pass. Adding state-awareness on the deployment side, knowing what code is actually behind the last green run, closes a loop that was always open.
If your pipeline redeploys the same code every night because it ran last night and nothing broke, that is not stability. That is a loop with no exit condition. It looks like a working system. It is a system that does not know what "done" means.
Ask the question the platform already knows how to answer. Query the history. Read the fingerprint. Build the override for when you genuinely need to force a run. The answer costs one API call and saves everything that comes after.
Top comments (2)
seen this same thing with AI coding agents that redeploy after every prompt even when nothing changed. same fix works there, check the last deployed commit before running the build step instead of deploying just because a message came in. saves the confusion of an app restarting for no real reason
"A successful run means we changed something, or we confirmed nothing needed changing" — that reframing is the whole post, and it matches what I found when I started reading exit codes as claims rather than outcomes. A green run that says "we ran the steps" is honest and useless; the fingerprint check replaces it with a claim the platform can actually answer.
The bounded-confidence part is where I'd add one: rollback is the case that quietly defeats a pure fingerprint skip, and so is an environment rebuilt from scratch where the run history is technically right and practically misleading. Curious how you scoped the force-deploy path — a label, a variable, or an explicit override job? I've seen teams bury it in a variable nobody remembers during an incident.