Tracking what is deployed with tags and notes Jump to heading
“Is my fix in production yet?” should be a one-command question. Usually it is not: the answer lives in a deploy tool’s history, a chat channel or someone’s memory. Git can hold the answer itself. A ref per environment that moves to each deployed commit answers “what is running now”. An immutable tag per deployment answers “what was running at this time”. A git note attached to the commit records who deployed it, when and from which pipeline. With those in place, git merge-base --is-ancestor tells you whether a commit is deployed, and git log between two deploy tags lists exactly what a deployment shipped. This page sets up all three, keeps them written by the pipeline rather than by hand, and shows the queries they enable, within environment and deployment branches.
When to use this approach Jump to heading
- People regularly ask whether a change has reached staging or production.
- Incident response needs “what changed in the last deployment” quickly.
- Deploy history lives only in a deploy tool that not everyone can access.
- You deploy from
mainwithout per-environment branches, as in promoting a release from staging to production.
Step 1 — Move an environment ref on each deploy Jump to heading
Keep one ref per environment pointing at the deployed commit. Use a namespace outside branches and tags, so it does not clutter branch lists and cannot be targeted by pull requests.
# In the deploy job, after a successful deploy
sha=$(git rev-parse HEAD)
git push --force origin "$sha:refs/deployed/production"
# Anyone can then fetch it
git fetch origin '+refs/deployed/*:refs/remotes/origin/deployed/*'
git log -1 --oneline origin/deployed/production The force push is intended here: the ref represents “currently deployed” and must move backwards on a rollback too. Restrict who can push to refs/deployed/* to the deploy identity.
Step 2 — Tag each production deployment Jump to heading
The moving ref loses history. Add an immutable, timestamped tag for each production deployment, so you can see what was running at any time and diff between deployments.
tag="deploy-production-$(date -u +%Y%m%dT%H%MZ)"
git tag -a "$tag" "$sha" -m "Deployed to production by $GITHUB_ACTOR via run $GITHUB_RUN_ID"
git push origin "$tag" If tag volume becomes a nuisance in tag listings, use a separate ref namespace such as refs/deploys/production/<timestamp> instead of refs/tags.
Step 3 — Attach deploy details as notes Jump to heading
Git notes attach extra information to a commit without changing it. Record each deployment of a commit in a dedicated notes ref, so git log can show deployment history inline.
git fetch origin refs/notes/deploys:refs/notes/deploys 2>/dev/null || true
git notes --ref=deploys append -m "production $(date -u +%FT%TZ) run=$GITHUB_RUN_ID actor=$GITHUB_ACTOR" "$sha"
git push origin refs/notes/deploys
git log --notes=deploys -3 Notes refs can conflict when two deploys append at once; serialise deploy jobs with a concurrency group, and retry the push after fetching on rejection.
Step 4 — Answer “is my commit deployed?” Jump to heading
With the environment refs fetched, the question becomes an ancestry check.
git fetch origin '+refs/deployed/*:refs/remotes/origin/deployed/*'
c=abc1234
for envref in $(git for-each-ref --format='%(refname:short)' refs/remotes/origin/deployed/); do
if git merge-base --is-ancestor "$c" "$envref"; then echo "$c is in ${envref##*/}"; else echo "$c is NOT in ${envref##*/}"; fi
done Step 5 — List what a deployment shipped Jump to heading
Log the range between the previous and current deploy tags. With merge commits or squash merges on main, first-parent gives one line per pull request.
prev=$(git tag --list 'deploy-production-*' --sort=-creatordate | sed -n 2p)
curr=$(git tag --list 'deploy-production-*' --sort=-creatordate | sed -n 1p)
git log --oneline --first-parent "$prev..$curr" This is the first command to run in an incident that starts after a deploy, and pairs with rolling back a deployment with Git.
Step 6 — Make it visible without Git Jump to heading
Not everyone runs Git commands. Post the shipped list from Step 5 to the team channel after each production deploy, and expose the environment refs on a small status page.
{
echo "Production deploy $curr"
git log --format='• %s (%an)' --first-parent "$prev..$curr"
} > "$TMPDIR/deploy-message.txt" Step 7 — Protect the records Jump to heading
The refs, tags and notes are only useful if they are accurate. Allow only the deploy identity to write them, and never update them by hand except during a documented rollback procedure.
# Ruleset: restrict updates to refs/deployed/* and creation of deploy-* tags to the deploy app
gh api "repos/$OWNER/$REPO/rulesets" --jq '.[] | {name, target, enforcement}' Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Why not use environment branches instead of refs? Jump to heading
Branches invite commits and pull requests. Refs outside refs/heads are clearly records, not places to work. Branch-per-environment is a different model, described in branch-per-environment GitOps patterns.
Do clones fetch these refs and notes automatically? Jump to heading
No. Default fetch refspecs cover branches and tags only. Add the refs/deployed/* and refs/notes/deploys refspecs to the remote configuration of anyone who needs them.
What happens on a rollback? Jump to heading
The environment ref moves back to the earlier commit, a new deploy tag records the rollback, and a note on that commit says it was redeployed. History stays complete.
Related Jump to heading
- Environment and Deployment Branches — the parent topic.
- Iterating Refs with for-each-ref — listing deploy refs and tags in scripts.
- Deriving Versions from git describe — tagging releases, as opposed to deploys.
- Verifying Attestations Before Deploy — checking what you are about to record.