|
| 1 | +#!/usr/bin/env bash |
| 2 | +# Catetus pre-push guard: block partnership/standards-outreach docs from |
| 3 | +# being pushed. |
| 4 | +# |
| 5 | +# Background: 2026-05-15 leak — docs/partnerships/{adobe-spz-memo,contact-map, |
| 6 | +# outreach-sequence}.md and docs/standards-outreach/{khronos-issue, |
| 7 | +# openusd-forum-post}.md were pushed to the public test-hero-fast branch |
| 8 | +# for ~4 days, leading to Khronos issue #2580 being closed citing the leaked |
| 9 | +# outreach doc as evidence the submission was premature. |
| 10 | +# |
| 11 | +# This hook reads the list of ref updates from stdin (git's pre-push protocol) |
| 12 | +# and, for each update, compares the new SHA against the remote SHA. Any added |
| 13 | +# or modified path under docs/partnerships/ or docs/standards-outreach/ aborts |
| 14 | +# the push. |
| 15 | +# |
| 16 | +# Override: CATETUS_PARTNERSHIP_OVERRIDE=1 git push ... |
| 17 | +# (Use ONLY for legitimate cases like deleting a leaked file from history. |
| 18 | +# An audit line is written to stderr when override fires.) |
| 19 | +# |
| 20 | +# Design notes: |
| 21 | +# - FAST: only runs `git diff --name-only` once per ref update. |
| 22 | +# - FAIL-OPEN on diff machinery errors (we don't want to block legit work |
| 23 | +# if the repo is in a weird state — CI is the belt-and-suspenders). |
| 24 | +# - First-push: when the remote SHA is all-zeros, we diff against |
| 25 | +# origin/HEAD (or origin/main / origin/master as fallback) to avoid |
| 26 | +# diffing the entire branch history against /dev/null. |
| 27 | + |
| 28 | +set -u |
| 29 | + |
| 30 | +remote="${1:-origin}" |
| 31 | +# shellcheck disable=SC2034 # url passed by git, unused |
| 32 | +url="${2:-}" |
| 33 | + |
| 34 | +PROTECTED_PATTERN='^docs/(partnerships|standards-outreach)/' |
| 35 | +ZERO='0000000000000000000000000000000000000000' |
| 36 | + |
| 37 | +# Fast bail-out: not pushing? nothing to do. |
| 38 | +if [ -t 0 ]; then |
| 39 | + exit 0 |
| 40 | +fi |
| 41 | + |
| 42 | +# Resolve a fallback base ref for first-push case. |
| 43 | +fallback_base="" |
| 44 | +for candidate in "${remote}/HEAD" "${remote}/main" "${remote}/master"; do |
| 45 | + if git rev-parse --verify --quiet "${candidate}" >/dev/null 2>&1; then |
| 46 | + fallback_base="${candidate}" |
| 47 | + break |
| 48 | + fi |
| 49 | +done |
| 50 | + |
| 51 | +violations="" |
| 52 | +while read -r local_ref local_sha remote_ref remote_sha; do |
| 53 | + # Branch deletion: local_sha is zeros, nothing to check. |
| 54 | + if [ "${local_sha}" = "${ZERO}" ]; then |
| 55 | + continue |
| 56 | + fi |
| 57 | + |
| 58 | + # Determine the diff range. |
| 59 | + if [ "${remote_sha}" = "${ZERO}" ]; then |
| 60 | + # First push of this branch. Diff against fallback base if available; |
| 61 | + # otherwise diff against the empty tree (covers the no-fallback case |
| 62 | + # without erroring out). |
| 63 | + if [ -n "${fallback_base}" ]; then |
| 64 | + range="${fallback_base}..${local_sha}" |
| 65 | + else |
| 66 | + # Empty tree SHA — every file becomes "added". |
| 67 | + empty_tree="$(git hash-object -t tree /dev/null 2>/dev/null || echo '4b825dc642cb6eb9a060e54bf8d69288fbee4904')" |
| 68 | + range="${empty_tree}..${local_sha}" |
| 69 | + fi |
| 70 | + else |
| 71 | + range="${remote_sha}..${local_sha}" |
| 72 | + fi |
| 73 | + |
| 74 | + # FAIL-OPEN: if diff machinery errors, let the push through (CI catches it). |
| 75 | + changed="$(git diff --name-only --diff-filter=AM "${range}" 2>/dev/null)" || { |
| 76 | + echo "pre-push: warning — git diff failed for ${range}; skipping protection (CI will still check)" >&2 |
| 77 | + continue |
| 78 | + } |
| 79 | + |
| 80 | + hits="$(printf '%s\n' "${changed}" | grep -E "${PROTECTED_PATTERN}" || true)" |
| 81 | + if [ -n "${hits}" ]; then |
| 82 | + violations="${violations}${violations:+$'\n'}--- ${local_ref} -> ${remote_ref} ---"$'\n'"${hits}" |
| 83 | + fi |
| 84 | +done |
| 85 | + |
| 86 | +if [ -z "${violations}" ]; then |
| 87 | + exit 0 |
| 88 | +fi |
| 89 | + |
| 90 | +if [ "${CATETUS_PARTNERSHIP_OVERRIDE:-}" = "1" ]; then |
| 91 | + who="$(git config user.email 2>/dev/null || echo 'unknown')" |
| 92 | + when="$(date -u +%Y-%m-%dT%H:%M:%SZ)" |
| 93 | + echo "==============================================================" >&2 |
| 94 | + echo "pre-push: OVERRIDE engaged (CATETUS_PARTNERSHIP_OVERRIDE=1)" >&2 |
| 95 | + echo " user: ${who}" >&2 |
| 96 | + echo " when: ${when}" >&2 |
| 97 | + echo " remote: ${remote}" >&2 |
| 98 | + echo " paths overridden:" >&2 |
| 99 | + printf '%s\n' "${violations}" | sed 's/^/ /' >&2 |
| 100 | + echo "==============================================================" >&2 |
| 101 | + exit 0 |
| 102 | +fi |
| 103 | + |
| 104 | +cat >&2 <<EOF |
| 105 | +============================================================== |
| 106 | +pre-push: BLOCKED — partnership/outreach docs in push |
| 107 | +
|
| 108 | +The following paths are protected and MUST NOT be pushed to any |
| 109 | +remote (the repo flips public eventually; these docs never should): |
| 110 | +
|
| 111 | +${violations} |
| 112 | +
|
| 113 | +Why: a 2026-05-15 leak of docs/partnerships/* on the public |
| 114 | +test-hero-fast branch produced concrete external fallout |
| 115 | +(Khronos issue #2580 closed citing the leaked outreach doc). |
| 116 | +See experiments/partnership-docs-audit/AUDIT.md. |
| 117 | +
|
| 118 | +Options: |
| 119 | + 1) Drop these paths from the push: |
| 120 | + git restore --staged docs/partnerships docs/standards-outreach |
| 121 | + git commit --amend # or rebase to remove the commits |
| 122 | + 2) Legitimate exception (e.g. deleting a leaked file from |
| 123 | + history, intentional internal-only remote): |
| 124 | + CATETUS_PARTNERSHIP_OVERRIDE=1 git push ... |
| 125 | + (An audit line will be written to stderr.) |
| 126 | +============================================================== |
| 127 | +EOF |
| 128 | +exit 1 |
0 commit comments