Baobaobaolin.com
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 any curl -i or 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

  1. First published