#!/usr/bin/env bash # scripts/ci-detect-already-validated.sh — emit `skip=true|false` to $GITHUB_OUTPUT for the # docker-build.yml cross-run tree-identity gate (ersatztv#420). On a merge-to-main push, if the # merged tree is byte-identical to a PR head that ALREADY passed `test`+`migrations` (a GREEN # combined commit status), the heavy compile/test/migrations work is redundant -- the exact same # source was already validated. `build` still runs and still builds+pushes the image, so no image # ever ships from unvalidated source. # # The bias is ALWAYS toward running MORE, never less: a false 'true' would ship (or claim to # validate) unreviewed/unvalidated source, so every ambiguous or unverifiable case resolves to # skip=false. It is fine (just wasteful) to re-run on an identical tree; it is a correctness bug # to skip validation on a tree that differs or was never proven green. # # Runs identically locally and in CI. Locally (no $GITHUB_OUTPUT) it prints the decision to # stdout; e.g. GITHUB_EVENT_NAME=push GITHUB_REF=refs/heads/main scripts/ci-detect-already-validated.sh set -euo pipefail out="${GITHUB_OUTPUT:-/dev/stdout}" event="${GITHUB_EVENT_NAME:-}" ref="${GITHUB_REF:-}" emit() { echo "skip=$1" >> "$out" echo "-> skip=$1" } # Only a push directly to main can possibly be a merge-to-main we can cross-check against an # already-validated PR head. Everything else (pull_request, tag push, workflow_dispatch, a push # to any other branch) -> always run. if [ "$event" != "push" ] || [ "$ref" != "refs/heads/main" ]; then echo "event='${event:-}' ref='${ref:-}' (need push to refs/heads/main); running full validation (safe default)" emit false exit 0 fi # Deepen history so HEAD's second parent (and its tree) are resolvable -- a shallow checkout may # have neither. Mirrors the docs-only/docs-reminder jobs' fetch style. Non-fatal: a failed # deepen still falls through to the HEAD^2 check below, which then fails safe. git fetch --deepen=2 origin 2>/dev/null || git fetch --unshallow origin 2>/dev/null || true # HEAD must be a real merge commit with a second parent -- that second parent is the PR head CI # actually validated. No second parent (a direct/fast-forward/squash push) -> nothing to compare # against -> always run. pr_head="$(git rev-parse --verify -q HEAD^2 || true)" if [ -z "$pr_head" ]; then echo "HEAD has no second parent (not a merge commit); running full validation (safe default)" emit false exit 0 fi merge_tree="$(git rev-parse --verify -q 'HEAD^{tree}' || true)" pr_tree="$(git rev-parse --verify -q "${pr_head}^{tree}" || true)" if [ -z "$merge_tree" ] || [ -z "$pr_tree" ]; then echo "could not resolve HEAD or HEAD^2 tree; running full validation (safe default)" emit false exit 0 fi if [ "$merge_tree" != "$pr_tree" ]; then echo "merged tree ($merge_tree) != PR head $pr_head tree ($pr_tree) -- main advanced since the PR was validated; running full validation" emit false exit 0 fi # Merge tree matches the PR head tree exactly. Confirm that PR head was actually validated green # before trusting it -- query the Gitea combined commit status API. # # THE READ IS ANONYMOUS (ersatztv#885, `ci.pr-route-carries-no-stored-credential`). The three jobs # that call this run on the `pull_request` route as well as on the main push, and Gitea resolves a # `pull_request` run from the PR HEAD -- so those jobs may hold no stored secret, and the # `ETV_STATUS_AUTH` they used to carry is gone. `timothy/ersatztv` is public and answers this GET # unauthenticated (measured 2026-09-04: HTTP 200 carrying the combined state). No credential # override is kept in its place: the URL below names ONE instance, that instance is public, so an # override would be a code path with no caller -- and an unusable one (":", the shape an absent # secret interpolates to) would draw a 401 and turn a working read into a permanent skip=false. # # Every failure direction here is safe: a missing, failing or non-success response falls through to # skip=false, which re-runs validation. Nothing about this step can cause a skip that was not earned. status_url="http://192.168.1.95:3000/api/v1/repos/timothy/ersatztv/commits/${pr_head}/status" status_json="$(curl -sf "$status_url" || true)" if [ -z "$status_json" ]; then echo "status API request for PR head ${pr_head} failed; running full validation (safe default)" emit false exit 0 fi state="$(printf '%s' "$status_json" | jq -r '.state // empty' 2>/dev/null || true)" if [ "$state" != "success" ]; then echo "PR head ${pr_head} combined status is '${state:-}', not 'success'; running full validation (safe default)" emit false exit 0 fi echo "merged tree == green PR head ${pr_head} (status=success) -> skipping re-validation" emit true