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 main without 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
Environment refs, deploy tags and notesThe staging and production refs each point at the commit currently running in that environment and move on every deploy, including rollbacks. Each production deploy also creates a tag that never moves, so past deployments can be found. Deploy notes are appended to the deployed commit and record who deployed it, when and from which run.Points toWhen it changesrefs/deployed/stagingcommit running on stagingevery staging deployrefs/deployed/productioncommit running in productionevery deploy and rollbackdeploy-production-<time>commit deployed thenneverrefs/notes/deployswho and when, per commitappended on each deployrefs answer now, tags answer then, notes answer who

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
Which question are you asking?To learn what is running now, read the environment ref. To learn whether a commit is deployed, check whether it is an ancestor of the environment ref. To learn what a deployment shipped, log the range between two deploy tags. To learn who deployed a commit and when, read its deploy notes.What do you need to know?what is running nowEnvironment refrefs/deployed/*is commit X deployedAncestry checkmerge-base --is-ancestorwhat did a deploy shipRange of tagsgit log tag1..tag2who and when come from the notes on the commit

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"
What the deploy job recordsThe deploy job deploys a commit to production. On success it moves the production environment ref, creates an immutable deploy tag, appends a deploy note to the commit, and posts the list of shipped changes to the team channel.deploy jobproductiongit serverteam channeldeploy abc1234healthyrefs/deployed/productiontag deploy-production-…notes: deploysshipped: 6 changesrecord only after the deploy succeeds — failed deploys must not move the ref

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.