Data before code is a graph problem
“Run the data pipeline before deploying the code” sounds like a checklist item. It is actually a graph invariant.
That distinction matters when releases are assembled by agents, approvals happen asynchronously, and more than one commit can land while a workflow is waiting. A checklist describes the happy path. A graph invariant keeps holding when the order changes.
File diffs do not carry obligations
Consider this history:
o---A---B main
|
data contract changes
pipeline pending
B changes only copy
The application at B contains everything introduced at A. It also inherits A's unmet data dependency.
This check misses it:
git diff --name-only A B | grep '^pipeline/'
B did not change the pipeline, so the gate concludes there is nothing to wait for. The question is locally accurate and globally wrong.
Release safety depends on the newest dependency-bearing commit reachable from the release:
required_sha="$(git log -1 --format=%H -- \
data/production-snapshot.json \
pipeline/parse.ts \
pipeline/load.ts \
src/data/read-model.ts)"
That commit is the obligation. Any production release containing it must be covered by a successful data workflow.
Green needs a location
Let R be the latest required data commit, W the commit used by a successful data workflow, and D the release commit. The safe relationship is:
R <= W <= D
Here <= means “is an ancestor of.”
git merge-base --is-ancestor "$required_sha" "$workflow_sha"
git merge-base --is-ancestor "$workflow_sha" "$release_sha"
The first line rejects a green run that predates the data change. The second rejects a run from a branch or future commit that the release does not contain.
Put the gate before the deploy
The GitHub Actions shape is deliberately small:
jobs:
await-data:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Resolve newest data obligation
id: required
run: |
sha="$(git log -1 --format=%H -- pipeline/ data/ src/data/)"
echo "sha=$sha" >> "$GITHUB_OUTPUT"
- name: Wait for a covering pipeline run
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
scripts/wait-for-workflow-ancestor.sh \
"Production Data Pipeline" \
"${{ steps.required.outputs.sha }}" \
"$GITHUB_SHA"
deploy:
needs: await-data
Full history matters. A shallow checkout cannot answer arbitrary ancestry questions. The complete reconstructed data before code workflow also uses a deployment concurrency group. When a new push arrives, it replaces the older wait and recomputes the obligation.
Fail closed when proof is unavailable
Failure to read workflow state cannot mean permission to deploy:
if ! successful_shas="$(gh "${args[@]}")"; then
echo "Could not read workflow runs." >&2
exit 1
fi
A timeout blocks for the same reason. The prerequisite being slow may be harmless. The deploy guessing that it probably succeeded is not.
Test the bypass
An ancestry helper looks correct when it accepts the current commit. The useful fixtures are:
before required -> reject
at required -> accept
after required -> accept
outside release line -> reject
invalid commit -> reject
API unavailable -> reject
The release ancestry gate creates a temporary Git history and a fake workflow API. If the production failure depends on ancestry, the test fixture should have ancestry.
The same pattern covers schema before query code, a model artifact before inference code, search index before a new search surface, and access policy before a route becomes reachable.
“Run this first” relies on memory. “No release can pass without a covering success in its ancestry” is a system.
Questions, corrections, and useful disagreement
Sign in with GitHub to join the conversation. Comments are public. Article views are anonymous and counted once per browser, per article, each day.