- date
- entry
- 011
- topic
- tooling
- rev
- —
Every service needs /version: confirm which commit is running
You fixed it, you pushed it, the problem is still there. Two possibilities: the fix is wrong, or the fix is not running. Most people assume the first and spend two hours rereading the logic they just wrote.
Between "I pushed it" and "it is running" sits an entire deploy chain: did CI trigger, did the build succeed, did the image reach the registry, did the container actually roll, did a cache clear. Break any link and the symptom is indistinguishable from a wrong fix.
One endpoint removes the ambiguity. Give every backend service a /version:
curl -s https://api.example.com/version
{
"git_sha": "dea001c",
"git_branch": "main",
"build_time": "2026-09-05T11:32:04Z",
"version": "1.0.0"
}
Compare against git log and within thirty seconds you know whether to keep reading code.
Sha matches: the problem is in the logic or the config. Sha does not match: the problem is in the
deploy. Those two lead to completely different investigations.
Baking the sha into the binary
Go writes it in at compile time with -ldflags -X. No library required:
go build -ldflags "\
-X main.gitSHA=$(git rev-parse --short HEAD) \
-X main.gitBranch=$(git rev-parse --abbrev-ref HEAD) \
-X main.buildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
-o server ./cmd/server
var (
gitSHA = "unknown"
gitBranch = "unknown"
buildTime = "unknown"
)
r.GET("/version", func(c *gin.Context) {
c.JSON(200, gin.H{
"git_sha": gitSHA, "git_branch": gitBranch,
"build_time": buildTime, "version": appVersion,
})
})
Default them to "unknown" rather than an empty string — seeing unknown tells
you this binary was not produced by the normal pipeline, which is itself a lead.
In a container, pass the sha through a build arg:
ARG GIT_SHA=unknown
RUN go build -ldflags "-X main.gitSHA=${GIT_SHA}" -o /server ./cmd/server
docker build --build-arg GIT_SHA=$(git rev-parse --short HEAD) -t app:prod-$(git rev-parse --short HEAD) .
Put the sha in the image tag too. Then docker ps alone shows which container runs which
build, without asking it.
The frontend needs this more
At least a backend can be interrogated with docker ps. A frontend cannot — you have no
visibility into which build is running in someone's browser, and
an old one still running is the norm, not
the exception.
Write the sha into the HTML at build time and it becomes answerable:
<meta name="build-sha" content="dea001c" />
curl -s https://app.example.com/ | grep build-sha
That curl answers "which build is the CDN currently serving". If it is stale, this is not a code problem, it is a cache or deploy problem — located just as quickly.
Further: carry the sha into logs and headers
/version answers "now". Incident work usually needs "then" — which build was running
when that batch of 500s happened three days ago?
Two cheap habits:
- Log the full version line at startup. Every restart leaves a timestamped version marker in the log, so an incident timeline can be traced back to the nearest preceding start
- Add a response header such as
X-Build-SHA. Then anycurl -ior DevTools screenshot a reporter attaches carries the build with it, and nobody has to ask "when did you try this?"
The second is especially useful across teams. When someone says "your API is broken", the header tells you which build they reached — one round trip saved.
Do not return a hand-maintained version string
Plenty of services have a /version that returns {"version": "1.0.0"} from a
constant last touched two years ago. That endpoint is worse than none, because it looks like it
answered the question.
The test is simple: the value must be produced by the build, never maintained by a person. Human-maintained fields go stale, and they go stale without telling anyone — the same failure mode as a stored zone id or a drifted schema.
Should /version be public? Mine are. What it discloses is "they use git and are
currently on some short sha", which buys an attacker very little — unless your repo is public, in
which case it is worth thinking about. Put it behind authentication and the people who need it most
during an incident (support, a client-side engineer, you on a different machine) cannot get it.
It is also a deploy probe
With the endpoint in place, the last step of a deploy script can be verification instead of hope:
EXPECTED=$(git rev-parse --short HEAD)
for i in $(seq 1 30); do
ACTUAL=$(curl -s https://api.example.com/version | jq -r .git_sha)
[ "$ACTUAL" = "$EXPECTED" ] && { echo "deployed $ACTUAL"; exit 0; }
sleep 5
done
echo "TIMEOUT: still serving $ACTUAL, expected $EXPECTED" >&2
exit 1
The value of that block is not in the success case, it is in the failure case: it redefines "deploy finished" from "the script did not error" to "production actually rolled". The gap between those two is the same gap as between judging a site healthy by status code and actually comparing content.
If you remember one thing
The first debugging question is not "where is the mistake", it is "am I even running the code I think I am". The first takes time to read. The second is one curl. Ask the cheap one first.
Revision history
- First published