Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 5s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 5s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 5m6s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 8s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 5s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 15m9s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 18m34s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
The changed-set git diff had rename detection on by default, so a code->docs rename (Foo.cs -> docs/Foo.md) showed only the destination and was misclassified as docs-only, skipping required tests on a code change. --no-renames surfaces the source deletion -> full matrix. Empirically verified. This is the cold-review MEDIUM; it was applied in the working tree but never committed before the first push (index/worktree mismatch) — committing it now. Refs #416
107 lines
4.7 KiB
Bash
Executable File
107 lines
4.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# scripts/ci-detect-docs-only.sh — emit `docs_only=true|false` to $GITHUB_OUTPUT for the
|
|
# docker-build.yml heavy-job gate (ersatztv#416). A change is "docs" iff every changed path is
|
|
# under docs/ or is a *.md file anywhere. docs_only=true ONLY when EVERY changed path is docs;
|
|
# any code path, an undeterminable diff, a tag build, or a non-PR/push event => false (run the
|
|
# full matrix).
|
|
#
|
|
# The bias is ALWAYS toward running MORE, never less: a false 'true' would skip the real
|
|
# test/migrations/build work on a code change, so every ambiguous case resolves to
|
|
# docs_only=false. It is fine (just wasteful) to run the full matrix on a docs change; it is a
|
|
# correctness bug to skip it on a code change.
|
|
#
|
|
# Why this is the merge-gate-safe half of #416: the two REQUIRED contexts (`Build & test (.NET)`,
|
|
# `EF migration integrity (SQLite + MySql)`) are gated by SKIPPING STEPS inside a job that always
|
|
# runs and always reports `success` — never by an `if:`-skipped job. On Gitea 1.25.4 an
|
|
# `if:`-skipped job reports commit-status state `skipped` (verified, PR #418), and how branch
|
|
# protection treats a `skipped` REQUIRED context is not something we rely on. Non-required jobs may
|
|
# skip freely (production already proves a `skipped` non-required context — e.g. `build` on every
|
|
# PR — does not block merge).
|
|
#
|
|
# Runs identically locally and in CI. Locally (no $GITHUB_OUTPUT) it prints the decision to stdout;
|
|
# e.g. GITHUB_EVENT_NAME=pull_request GITHUB_BASE_REF=main scripts/ci-detect-docs-only.sh
|
|
set -euo pipefail
|
|
|
|
out="${GITHUB_OUTPUT:-/dev/stdout}"
|
|
event="${GITHUB_EVENT_NAME:-}"
|
|
|
|
emit() {
|
|
echo "docs_only=$1" >> "$out"
|
|
echo "-> docs_only=$1"
|
|
}
|
|
|
|
# A release tag must NEVER be treated as docs-only, whatever it touches.
|
|
if [ "${GITHUB_REF_TYPE:-}" = "tag" ]; then
|
|
echo "tag build (${GITHUB_REF_NAME:-?}); never docs-only"
|
|
emit false
|
|
exit 0
|
|
fi
|
|
|
|
range=""
|
|
case "$event" in
|
|
pull_request)
|
|
base="${GITHUB_BASE_REF:-}"
|
|
if [ -z "$base" ]; then
|
|
echo "pull_request with no base ref; running full matrix (safe default)"
|
|
emit false
|
|
exit 0
|
|
fi
|
|
git fetch --no-tags --depth=200 origin "$base" || true
|
|
range="origin/${base}...HEAD"
|
|
;;
|
|
push)
|
|
# Only branch pushes reach here (tags handled above). We skip the heavy matrix on a docs-only
|
|
# push only when the range is UNAMBIGUOUS — i.e. HEAD is a merge commit, which is how every
|
|
# update to main lands (PR merge). Its first parent is the pre-merge branch tip, so
|
|
# HEAD^1...HEAD is exactly the merged delta. A non-merge (direct/multi-commit) push is rare and
|
|
# its true range is ambiguous here, so we fall back to running the full matrix.
|
|
git fetch --no-tags --depth=200 origin "${GITHUB_REF_NAME:-main}" || true
|
|
nfields="$(git rev-list --parents -n1 HEAD | wc -w | tr -d ' ')" # 1 (self) + parent count
|
|
if [ "${nfields:-0}" -ge 3 ]; then
|
|
range="HEAD^1...HEAD"
|
|
else
|
|
echo "non-merge push (parents=$(( nfields - 1 ))); running full matrix (safe default)"
|
|
emit false
|
|
exit 0
|
|
fi
|
|
;;
|
|
*)
|
|
echo "event '${event:-<none>}' is not pull_request/push; running full matrix (safe default)"
|
|
emit false
|
|
exit 0
|
|
;;
|
|
esac
|
|
|
|
# --no-renames is load-bearing: with rename detection ON (git's default) a code->docs rename
|
|
# (e.g. Foo.cs -> docs/Foo.md) shows ONLY the destination `docs/Foo.md`, hiding that a source file
|
|
# left the build -> misclassified as docs-only -> required tests skipped on a code change.
|
|
# --no-renames surfaces the deletion (`Foo.cs`, non-docs) so it correctly forces the full matrix,
|
|
# keeping the "any code path => run everything" invariant total.
|
|
changed="$(git diff --no-renames --name-only "$range" 2>/dev/null || true)"
|
|
echo "Range: $range"
|
|
echo "Changed files:"
|
|
printf '%s\n' "$changed"
|
|
|
|
if [ -z "$changed" ]; then
|
|
echo "empty/undeterminable diff; running full matrix (safe default)"
|
|
emit false
|
|
exit 0
|
|
fi
|
|
|
|
# docs_only unless SOME changed path is NOT docs. "docs" = under docs/ OR ends in .md (anywhere:
|
|
# README.md, CLAUDE.md, AGENTS.md, docs/handoffs/*.md, ...). Everything else — .cs, web/**,
|
|
# .gitea/**, Dockerfiles, scripts, csproj — is a code change and forces the full matrix.
|
|
#
|
|
# Capture the non-docs lines and test for emptiness rather than `grep -qv`: the combination of
|
|
# `-q` and `-v` early-exits inconsistently across grep implementations (BSD grep on macOS returned
|
|
# the wrong exit code here). `|| true` guards `set -e` when grep matches nothing (exit 1).
|
|
nondocs="$(printf '%s\n' "$changed" | grep -vE '(^docs/|\.md$)' || true)"
|
|
if [ -n "$nondocs" ]; then
|
|
echo "non-docs path(s) present in the diff -> NOT docs-only:"
|
|
printf '%s\n' "$nondocs" | sed 's/^/ /'
|
|
emit false
|
|
else
|
|
echo "every changed path is docs/ or *.md -> docs-only"
|
|
emit true
|
|
fi
|