Toolkit
WorkflowVerify

Release ancestry gate

A tested shell gate that proves a prerequisite workflow run covers the required commit and belongs to the release.

Git ancestry · shell · release gate
Use this

Read and copy the source

Read it before copying. Replace example paths, workflow names, product dimensions, and authority boundaries with those in your own system.

Release ancestry gaterelease-ancestry-gate.sh · shell
#!/usr/bin/env bash
set -euo pipefail

workflow_name="${1:-}"
required_sha="${2:-}"
release_sha="${3:-}"
branch_filter="${BRANCH_FILTER:-main}"
max_attempts="${MAX_ATTEMPTS:-120}"
poll_seconds="${POLL_SECONDS:-30}"

if [[ -z "$workflow_name" || -z "$required_sha" || -z "$release_sha" ]]; then
  echo "usage: release-ancestry-gate.sh <workflow> <required-sha> <release-sha>" >&2
  exit 2
fi

git cat-file -e "${required_sha}^{commit}"
git cat-file -e "${release_sha}^{commit}"

for ((attempt = 1; attempt <= max_attempts; attempt += 1)); do
  if ! successful_shas="$(gh run list \
    --workflow "$workflow_name" \
    --branch "$branch_filter" \
    --status success \
    --limit 100 \
    --json headSha \
    --jq '.[].headSha')"; then
    echo "Could not read workflow runs." >&2
    exit 1
  fi

  while IFS= read -r run_sha; do
    [[ -n "$run_sha" ]] || continue
    git cat-file -e "${run_sha}^{commit}" 2>/dev/null || continue

    if git merge-base --is-ancestor "$required_sha" "$run_sha" \
      && git merge-base --is-ancestor "$run_sha" "$release_sha"; then
      echo "$workflow_name succeeded at $run_sha and covers $required_sha."
      exit 0
    fi
  done <<< "$successful_shas"

  if [[ "$attempt" -lt "$max_attempts" ]]; then
    sleep "$poll_seconds"
  fi
done

echo "Timed out waiting for $workflow_name to cover $required_sha." >&2
exit 1
Negative-path testsrelease-ancestry-gate.test.sh · shell
#!/usr/bin/env bash
set -euo pipefail

script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
subject="$script_dir/release-ancestry-gate.sh"
fixture_dir="$(mktemp -d)"
trap 'rm -rf -- "$fixture_dir"' EXIT

git -C "$fixture_dir" init -q
git -C "$fixture_dir" config user.email "test@example.com"
git -C "$fixture_dir" config user.name "Release Gate Test"

printf 'zero\n' > "$fixture_dir/value.txt"
git -C "$fixture_dir" add value.txt
git -C "$fixture_dir" commit -qm "before requirement"
before_sha="$(git -C "$fixture_dir" rev-parse HEAD)"

printf 'one\n' > "$fixture_dir/value.txt"
git -C "$fixture_dir" commit -qam "required"
required_sha="$(git -C "$fixture_dir" rev-parse HEAD)"

printf 'two\n' >> "$fixture_dir/value.txt"
git -C "$fixture_dir" commit -qam "pipeline"
pipeline_sha="$(git -C "$fixture_dir" rev-parse HEAD)"

printf 'three\n' >> "$fixture_dir/value.txt"
git -C "$fixture_dir" commit -qam "release"
release_sha="$(git -C "$fixture_dir" rev-parse HEAD)"

fake_bin="$fixture_dir/bin"
mkdir -p "$fake_bin"
# The generated fake reads FAKE_SUCCESS_SHA when it runs, not while this fixture is created.
# shellcheck disable=SC2016
printf '#!/usr/bin/env bash\nprintf "%%s\\n" "$FAKE_SUCCESS_SHA"\n' > "$fake_bin/gh"
chmod +x "$fake_bin/gh"

run_gate() {
  (
    cd "$fixture_dir"
    PATH="$fake_bin:$PATH" \
      FAKE_SUCCESS_SHA="$1" \
      MAX_ATTEMPTS=1 \
      POLL_SECONDS=0 \
      bash "$subject" "Example Data Pipeline" "$required_sha" "$release_sha"
  )
}

run_gate "$required_sha"
run_gate "$pipeline_sha"

if run_gate "$before_sha" 2>/dev/null; then
  echo "expected a run before the requirement to fail" >&2
  exit 1
fi

echo "workflow ancestry tests passed"