← Back to blog2026-08-09

Never wait for the latest CI run

Trigger a workflow and then grab the latest green run, and you may be watching an old run, a concurrent run, or a different release entirely. A reliable pipeline carries one immutable identity from source intent to workflow attempt, then proves the public effect with version stamps.

The problem

A release script triggers a workflow, asks GitHub for the latest run, waits for it, sees green, and reports success. The code is short:

gh workflow run deploy-selfhost.yml --ref main
run_id=$(gh run list --workflow deploy-selfhost.yml --limit 1 --json databaseId --jq '.[0].databaseId')
gh run watch "$run_id" --exit-status

The bug is in one word: latest. Latest is a moving property of a shared queue. It is not the identity of the operation you just requested.

An older run can already exist for the same commit. Another operator can dispatch between your two commands. The API can take a moment to list the run you just created. In every case, the script can watch a real workflow and receive a real green result while answering the wrong question.

A wait is safe only when it is bound to an identity chosen before the wait begins.

Freeze the source first

“Deploy main” is not an identity because main can move. Our release path resolves the intended source to a full commit SHA. A manual dispatch carries that SHA as expected_sha, and the workflow refuses to proceed if GitHub is about to build a different commit.

This check belongs on the remote worker, not only in the caller. The caller can verify the branch and still lose a race before --ref main is resolved. Re-checking inside .github/workflows/deploy-selfhost.yml turns the SHA from a comment about intent into an executable boundary.

The rule is simple: resolve mutable names before approval, then make the system that performs the work reject any different immutable id.

Capture the run by set difference

A manual GitHub CLI dispatch does not return the new run id. In scripts/hotfix-deploy.sh, we create that missing correlation ourselves. The script lists runs filtered by workflow, branch, event type, and exact commit before dispatch. It dispatches once, lists the same filtered set again, and subtracts the old ids:

before = matching_runs(workflow, branch, event, commit)
dispatch(expected_sha=commit)

repeat until visible:
  after = matching_runs(workflow, branch, event, commit)
  created = after - before

require created.count == 1
run_id = created.only_item

Filtering only by commit is not enough because automatic and manual runs can share it. Filtering only by event is not enough because several manual runs can exist. Taking the newest item still reads a shared queue. The set difference asks the narrow question we care about: which matching id appeared because of this dispatch?

Before watching, the script reads that run back and checks its source SHA and event type. It checks them again after the run reaches a terminal state. The result being consumed must still belong to the operation that was approved.

Green execution is not public effect

Binding the correct workflow solves correlation, not delivery. A job can succeed while a route, cache, or coupled surface still exposes the previous release.

After the exact run succeeds, the hotfix lane checks /readyz and reads two public receipts: the app's build-stamp.txt and the marketing apex's release-source.json. Both must name the target SHA and the same release id. If they disagree, workflow success is not rounded up to a live release.

This gives us two separate proofs: an execution receipt from the orchestrator, and an effect receipt from the deployed system. The first proves what ran. Only the second proves what users can now reach.

The reusable checklist

  1. Freeze intent. Resolve a branch, tag, or label to an immutable id.
  2. Carry the id into the worker. Let the remote side reject a different source.
  3. Capture the created attempt. Prefer a returned id; otherwise use a filtered before/after set difference and require exactly one new id.
  4. Re-bind before consuming. Read the attempt back and verify source and trigger type before and after waiting.
  5. Prove the effect separately. Ask the deployed system for immutable version stamps, and require coupled surfaces to agree.
  6. Test wrong-success paths. Old green run, concurrent dispatch, delayed listing, and mismatched public stamps.

“Wait for latest” saves one line by deleting the fact the rest of the automation needs: which operation are we talking about? Keep that identity from intent to observable effect, and a green light finally means the thing you think it means.