A few months ago someone asked me a question I couldn’t answer fast: “when did the address-validation fix actually go live in the portal?” I knew it merged. I did not know when it shipped, because merging to main and shipping to production were the same event, and the record of that event was a green checkmark I’d have to go dig for.
So I changed the rule. Now merging to main doesn’t ship anything. To get code into production on our portal, you have to cut a git tag — a small named bookmark you attach to one exact commit — in a specific format. Pushing that tag is the only thing that fires the production deploy. No tag, no ship.
If you’re not an engineer: think of it like a printing press that only runs when you physically stamp the batch with a date. You can write and edit all day, but nothing gets printed until someone stamps it. The stamp is the release.
The stamp format is the part I like most. It’s calendar versioning: v2026.W28.1. Year, week number, then a counter for how many times we’ve shipped that week. So v2026.W28.3 is the third production release in the 28th week of 2026. You don’t decode anything — you glance at it and you know roughly when it shipped and that two releases came before it that week. Compare that to v4.11.2, which tells you nothing about when.
The old manual path still exists. GitHub Actions lets you trigger a workflow by hand with workflow_dispatch — basically a “run it now” button in the UI. I kept that, but only as the break-glass option for when prod is on fire and I need to redeploy a known-good build right now. The default, traceable path is the tag.
Here’s the mechanism that makes this worth it. A git tag points at an immutable commit SHA — a fixed 40-character fingerprint of the exact code. So every production release now maps one-to-one to a versioned name and a frozen snapshot of the tree. The audit trail isn’t something I maintain; it falls out of the process for free. “When did the fix ship?” becomes “which tag contains that SHA?” — a lookup, not an investigation.
Cutting the next calendar tag give me the detail
The only fiddly bit is computing the next sequential number. Read the latest tag for the current week from the GitHub API, then bump:
WEEK=$(date +%V) # ISO week, zero-padded
PREFIX="v$(date +%Y).W${WEEK}."
# highest existing sequence for this week, or 0
LAST=$(gh api repos/:owner/:repo/tags --jq \
"[.[].name | select(startswith(\"$PREFIX\"))
| ltrimstr(\"$PREFIX\") | tonumber] | max // 0")
NEXT="${PREFIX}$((LAST + 1))"
git tag "$NEXT" && git push origin "$NEXT"And the workflow listens on the tag, keeping dispatch as the manual escape hatch:
on:
push:
tags: ["v20*.W*.*"]
workflow_dispatch: {}date +%V gives you the ISO week so the number lines up with how humans talk about “week 28.”
The general move: make your default release trigger something named and immutable, not something ephemeral like a button press or a branch merge. A merge is a decision to integrate code. A tag is a decision to ship it — and those deserve to be two different acts with two different records. If you can’t answer “when did this go live” in one lookup, your release trigger is the thing to change.