231 lines
23 KiB
Bash
Executable file
231 lines
23 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# The one gate. Every fast tree check CI runs, in one script, so the local gate and CI can never drift: the `site` job
|
|
# of .github/workflows/ci.yml calls `tools/ci/pre-push.sh --ci`, and the pre-push hook (tools/ci/install-hooks.sh) calls
|
|
# `tools/ci/pre-push.sh --hook` before any push to master or a release-* branch and refuses the push on red.
|
|
#
|
|
# tools/ci/pre-push.sh # the full gate over this working tree (any worktree; reads the tree, writes nothing in it)
|
|
# tools/ci/pre-push.sh --ci # the same, with the site built in place (a CI checkout is disposable)
|
|
# tools/ci/pre-push.sh --hook # from .git/hooks/pre-push: full gate for master and release-*; the light gate for every
|
|
# # other ref: the two structural checks (conflict markers, Windows paths) and the two
|
|
# # never-push classes (the no-secrets check, the identity grep), about 20 s on the Mac
|
|
# tools/ci/pre-push.sh --self-test # the runner fires on a known failure, passes a known success, the hook picks the
|
|
# # right gate from the ref lines, and the light gate carries the never-push classes
|
|
# tools/ci/pre-push.sh --list # the check names, one per line
|
|
#
|
|
# What is NOT here, on purpose: the three compile-or-compute jobs (igneum-pow tests, igneum-census, the simulators) and
|
|
# the live public API check (master only, a network call), which stay separate steps in ci.yml. Everything here finished
|
|
# in under 25 s on the Mac on 6 October 2026 (no-secrets 10 s, everything else under 3 s each).
|
|
#
|
|
# Local mode never writes into the worktree: the site is built in a temporary copy with SITE_DOWNLOADS_OFFLINE=1
|
|
# (063bbca, 6 October 2026: a hook that built in place rewrote the downloads snapshot in five worktrees). The link, ledger
|
|
# and identity checks then read the committed pages; CI builds in place and checks the rebuilt pages, the one difference.
|
|
set -uo pipefail
|
|
cd "$(git rev-parse --show-toplevel)" || exit 1
|
|
# git hands a hook its own repository through the environment (GIT_DIR, GIT_INDEX_FILE, GIT_PREFIX, ...). Left in place,
|
|
# every nested git inside the self-tests (remote-run.sh builds a mirror and pushes into it) would act on THIS repository
|
|
# and fire this hook again inside the fixture: the first master push through the gate died that way (6 October 2026).
|
|
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_PREFIX GIT_COMMON_DIR GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_QUARANTINE_PATH GIT_PUSH_OPTION_COUNT
|
|
MODE="${1:-local}"; MODE="${MODE#--}"
|
|
RED=0; N=0; LOG="$(mktemp)"; trap 'rm -rf "$LOG" "${SITE_TMP:-}"' EXIT
|
|
T0=$(date +%s)
|
|
|
|
run() {
|
|
# run <name> <command...>: one line per check; the output of a red check is shown in full
|
|
local name="$1"; shift; N=$((N + 1))
|
|
local s=$(date +%s)
|
|
if "$@" >"$LOG" 2>&1; then
|
|
printf ' ok %3ds %s\n' "$(( $(date +%s) - s ))" "$name"
|
|
else
|
|
printf ' RED %3ds %s\n' "$(( $(date +%s) - s ))" "$name"; sed 's/^/ /' "$LOG" | cut -c1-240; RED=1
|
|
fi
|
|
}
|
|
run_quiet() { "$@" >/dev/null 2>&1; }
|
|
|
|
# In place ONLY inside GitHub Actions (a disposable checkout); a `--ci` run on a lane's Mac builds in the temporary copy like the hook,
|
|
# because the in-place build rewrote four tracked site files (bench.html, index.html, journey.html, journey.json) in the running
|
|
# worktree and every lane had to discard them before a merge (the horizon lane, 7 October 2026). --self-test proves the tree is
|
|
# unchanged after a site build outside Actions.
|
|
site_in_place() { [ "$MODE" = ci ] && [ "${GITHUB_ACTIONS:-}" = true ]; }
|
|
site_build() {
|
|
if site_in_place; then node site/build.mjs; return; fi
|
|
SITE_TMP="$(mktemp -d)"; cp -R site "$SITE_TMP/site"
|
|
(cd "$SITE_TMP/site" && SITE_DOWNLOADS_OFFLINE=1 node build.mjs)
|
|
}
|
|
|
|
overlap_sweep() {
|
|
# tools/ci/overlap-check.mjs: the known-failed fixture first, then the built site (the gate's temporary copy locally, the tree in
|
|
# CI). A browser is needed: CI installs Playwright in the workflow; a machine without one ships the pages to a build box
|
|
# (nothing heavy on the Mac). IGNEUM_OVERLAP_APPS=1 adds the miner and wallet UIs through their mocks (slower, the box).
|
|
local dir="site"; [ "$MODE" = ci ] || dir="$SITE_TMP/site"
|
|
if [ "${IGNEUM_OVERLAP_APPS:-0}" = 1 ]; then node tools/ci/overlap-check.mjs --self-test --site "$dir" --apps .; else node tools/ci/overlap-check.mjs --self-test --site "$dir"; fi
|
|
}
|
|
|
|
structural_checks() {
|
|
run "no conflict markers in tracked files" bash tools/ci/no-conflict-markers.sh
|
|
run "every tracked path is valid on Windows (colon, trailing dot, reserved names, length)" bash tools/ci/windows-paths-check.sh
|
|
}
|
|
|
|
never_push_checks() {
|
|
# The two never-push classes, on EVERY ref (7 October 2026: three gate summaries on ca3-v4-node carried a 64-hex key
|
|
# through eight red CI runs in 80 minutes; the feature-branch hook ran only the structural checks, so no lane saw it).
|
|
# A secret or an identity leak must not reach the remote on any branch; together about 20 s on the Mac.
|
|
run "identity grep of the public export list and the served site" bash tools/ci/identity-check.sh
|
|
run "no secret file names and no 64-hex secrets in the tree" bash -c 'bash tools/ci/no-secrets-check.sh --self-test && bash tools/ci/no-secrets-check.sh'
|
|
}
|
|
|
|
tree_checks() {
|
|
run "site build (in a temporary copy here, in place only inside GitHub Actions)" site_build
|
|
run "internal link check of site/*.html" node tools/ci/link-check.mjs
|
|
run "every served page carries the slim bar (mark, Mine, Network, Learn, Download) with every route in its panels and the sheet (self-test, then the tree)" bash -c 'node tools/ci/site-nav-check.mjs --self-test && node tools/ci/site-nav-check.mjs'
|
|
run "vendor marks: site/lib/marks.mjs is brand/marks/vendor-marks.mjs byte for byte (the app and the site draw one set)" cmp brand/marks/vendor-marks.mjs site/lib/marks.mjs
|
|
run "the phone menu opens and is seen at 390 px on every page (self-test first; needs the box or CI browser, says so without one)" node tools/site/sheet-test.mjs --self-test
|
|
run "ledger sentences present verbatim on their public pages" node tools/ci/ledger-text-check.mjs
|
|
never_push_checks
|
|
run "shell inside .github/workflows parses (bash -n, the PowerShell 5.1 rule)" node tools/ci/check-workflow-shell.mjs
|
|
run "PowerShell drive-reference check (\$name: in a double-quoted string)" bash tools/ci/ps-drive-ref-check.sh
|
|
run "copied sources are re-stamped before a build" bash tools/ci/copied-sources-check.sh
|
|
run "override params files parse with no duplicate key" bash tools/ci/override-json-check.sh
|
|
run "second-engine playbooks log to a file and end their tree (C35)" bash tools/ci/second-engine-check.sh
|
|
run "no playbook quits, pauses or resumes the installed app" bash -c 'bash tools/ci/playbook-quit-check.sh --self-test && bash tools/ci/playbook-quit-check.sh'
|
|
run "no script writes into another worktree or walks Projects" bash -c 'bash tools/ci/no-foreign-tree-writes.sh --self-test && bash tools/ci/no-foreign-tree-writes.sh'
|
|
run "the signer is never piped into head" bash tools/ci/signer-pipe-check.sh
|
|
run "bash bodies in PowerShell job scripts pass bash -n" bash -c 'bash tools/ci/bash-body-check.sh --self-test && bash tools/ci/bash-body-check.sh'
|
|
run "run jobs test their fetched kit before use" bash -c 'bash tools/ci/kit-path-check.sh --self-test && bash tools/ci/kit-path-check.sh'
|
|
run "every Windows spawn of the app runs with a hidden console" bash -c 'node tools/ci/windows-spawn-check.mjs --self-test && node tools/ci/windows-spawn-check.mjs'
|
|
run "pinned guest programs match their manifest" bash tools/ci/pinned-guests-check.sh
|
|
run "root prover playbooks kill the GPU server and unlink its socket" bash tools/ci/prover-socket-check.sh
|
|
run "commit-string gate self-test" bash tools/ci/commit-string-check.sh --self-test
|
|
run "engine gate self-test (igneumd and igneum-miner must carry igneum-pow/src/ paths)" bash tools/ci/engine-check.sh --self-test
|
|
run "build server remote checkout self-test" bash infra/build-server/remote-run.sh --self-test
|
|
run "a slot holder keeps its own line for the whole run (the watcher-trust rule)" bash infra/build-server/remote-run.sh --self-test-keeper
|
|
run "the remote checkout resets the mirror's tree before the branch checkout (the stale-overlay class)" bash -c 'bash tools/ci/mirror-reset-check.sh --self-test && bash tools/ci/mirror-reset-check.sh'
|
|
run "the remote checkout's clean spares a lane's scratch (.igneum-scratch-spare, the fixed prefixes, never -x; the lost-scratch class)" bash -c 'bash tools/ci/scratch-spare-check.sh --self-test && bash tools/ci/scratch-spare-check.sh'
|
|
run "long-running tools keep their body in one parsed block (the edited-while-running class)" bash -c 'bash tools/ci/whole-body-check.sh --self-test && bash tools/ci/whole-body-check.sh'
|
|
run "build-remote without a priority flag bounds suites and benches (nice 10, 32 cores); a gate runs unbounded" bash tools/ci/build-kind-default-check.sh
|
|
run "the class router is a preference with spill-over (a held or overloaded box hands the job to the other one)" bash tools/ci/route-spill-check.sh
|
|
run "per-core leases, the quiet class and the reaper pass on the box (lease.sh and remote-run.sh self-tests over ssh)" bash tools/ci/box-locks-check.sh $( [ "$MODE" = ci ] && echo --ci )
|
|
run "the simulators job runs on master and release-* pushes and pull requests into them only" bash tools/ci/sims-branch-check.sh
|
|
run "no shell assignment hides behind a trailing comment (the swallowed-defaults class)" bash -c 'bash tools/ci/defaults-line-check.sh --self-test && bash tools/ci/defaults-line-check.sh'
|
|
run "no script kills or finds a process by a plain name or a file name (pgrep/pkill -f literals, ps | grep)" bash -c 'bash tools/ci/kill-by-name-check.sh --self-test && bash tools/ci/kill-by-name-check.sh'
|
|
run "the identity check's own self-test (excluded research path passes, exported leak fails)" bash tools/ci/identity-check.sh --self-test
|
|
run "the Windows paths check's own self-test" bash tools/ci/windows-paths-check.sh --self-test
|
|
run "the red watcher's own self-test (one line per run, posted once)" node tools/ci/red-watch.mjs --self-test
|
|
run "faucet unit tests" node --test site/api/faucet.test.mjs
|
|
run "redesign package data tests (the /api/live contract the pages read)" node tools/site-redesign/tests/data-tests.cjs
|
|
run "the home hero's loop never idles in view, stops hidden, resumes without a jump" node tools/site-redesign/tests/hero-loop-test.cjs
|
|
run "chain scene: the site's and the app's copies are scene/ byte for byte, the palette tokens live once (self-test, then the tree)" bash -c 'node tools/scene/sync.mjs --self-test && node tools/scene/sync.mjs --check'
|
|
run "chain scene: a push paints with the document hidden and no animation frame (the blank /live of 7 Oct 2026; known-failed first)" node tools/scene/paint-test.cjs
|
|
run "chain scene: the live feed contract (the recorded reply validates; a rewritten miner, a float now, a stray key refused)" node --test tools/scene/feed-contract.test.mjs
|
|
run "chain scene parity: one recorded feed through the home fold, /live and the app's Inspect view on build-2, three frames each pixel-equal apart from the app's own-key overlay (a changed token fails first; skipped with no box and no Playwright)" bash tools/scene/parity-remote.sh
|
|
run "no text overlaps: every served page at 390 to 1600 px, light and dark, the hero at each step (self-test first; IGNEUM_OVERLAP_APPS=1 adds the miner and wallet UIs)" overlap_sweep
|
|
run "explorer, emission and public stats unit tests" node --test site/lib/explorer.test.mjs site/lib/emission.test.mjs site/lib/money.test.mjs site/api/public-stats.test.mjs
|
|
run "ship tool self-test" node tools/ship-app.mjs --self-test
|
|
run "relay unit tests" node --test relay/test/parse.test.mjs relay/test/auth.test.mjs relay/test/wake.test.mjs relay/test/ember.test.mjs
|
|
run "miner app notice strip and update card tests" node --test app/igneum-app/ui/notices.test.mjs app/igneum-app/ui/update-card.test.mjs app/igneum-app/ui/view.test.mjs app/igneum-app/ui/tune-line.test.mjs
|
|
run "launch gates: every row with its check, the handoff text clean (self-test, then the tree)" bash -c 'node tools/ci/launch-gates-check.mjs --self-test && node tools/ci/launch-gates-check.mjs'
|
|
run "income per tier: the public table equals its inputs, the schedule arithmetic" bash -c 'node tools/launch/income-tiers.mjs --check && node --test tools/launch/income-tiers.test.mjs'
|
|
run "hash-origin report: a known-finished day and a known-failed day" node --test tools/observer/hash-origin.test.mjs
|
|
run "harness summaries never carry a raw 64-hex key (the writer's own redaction and check)" node infra/fast-time/lib/redact-keys.mjs --self-test
|
|
run "docs-only pushes skip the compile-or-compute CI jobs (the changes job's classifier)" bash tools/ci/docs-only-check.sh --self-test
|
|
}
|
|
|
|
gated_refs() {
|
|
# stdin: the pre-push hook's lines "<local ref> <local sha> <remote ref> <remote sha>". Prints "full" when any remote
|
|
# ref is master or release-*, else "light".
|
|
local lref lsha rref rsha full=0
|
|
while read -r lref lsha rref rsha; do
|
|
case "$rref" in refs/heads/master|refs/heads/release-*) full=1 ;; esac
|
|
done
|
|
[ "$full" = 1 ] && echo full || echo light
|
|
}
|
|
|
|
# The green stamp (main, 7 October 2026, the push-race class: a 100 s gate on the merge commit against a master that moves every
|
|
# minute starved every merge, six rejections in a row). A full gate that ends GREEN over a CLEAN tree records the commit it ran on
|
|
# in .git/igneum-gate-green/<sha> (the repository's own .git, shared by its worktrees). The hook then lets a MERGE commit through
|
|
# on the light gate when its first parent is exactly the remote tip being replaced (nothing unknown underneath) and its second
|
|
# parent carries a stamp younger than 12 hours: the branch's own gate was green on that commit, master's tip was green by its
|
|
# CI, and CI runs the same full gate on the merge the moment it lands (ci.yml), which is the backstop for the union.
|
|
# tools/ci/merge-to-master.sh is the merge tool that uses it; its merge message names the stamped commit.
|
|
stamp_dir() { local g; g=$(git rev-parse --git-common-dir 2>/dev/null) || return 1; ( cd "$g" 2>/dev/null && printf '%s/igneum-gate-green' "$(pwd -P)" ); } # absolute: a worktree's answer is relative
|
|
stamp_green() { # [repo dir]: after a GREEN full gate; only when no tracked file differs from HEAD (a dirty tree would lie)
|
|
local d="${1:-.}" sha dir
|
|
[ -z "$(git -C "$d" status --porcelain --untracked-files=no 2>/dev/null)" ] || return 0
|
|
sha=$(git -C "$d" rev-parse HEAD 2>/dev/null) || return 0; dir=$(cd "$d" && stamp_dir); mkdir -p "$dir" 2>/dev/null || return 0
|
|
date -u +%Y-%m-%dT%H:%M:%SZ > "$dir/$sha" 2>/dev/null && echo " (green stamp recorded for ${sha:0:8})"
|
|
}
|
|
deferred_merge() { # <local sha> <remote sha> [repo dir] -> "defer <branch sha>" or "full <reason>"
|
|
local lsha="$1" rsha="$2" d="${3:-.}" parents p1 p2 dir f age
|
|
parents=$(git -C "$d" rev-list --parents -n 1 "$lsha" 2>/dev/null | cut -d' ' -f2-) || { echo "full unknown commit"; return; }
|
|
set -- $parents; p1="${1:-}"; p2="${2:-}"; [ -n "$p2" ] && [ -z "${3:-}" ] || { echo "full not a two-parent merge"; return; }
|
|
[ "$p1" = "$rsha" ] || { echo "full first parent ${p1:0:8} is not the remote tip ${rsha:0:8}"; return; }
|
|
dir=$(cd "$d" && stamp_dir); f="$dir/$p2"; [ -f "$f" ] || { echo "full no green stamp for ${p2:0:8}"; return; }
|
|
age=$(( $(date +%s) - $(stat -f %m "$f" 2>/dev/null || stat -c %Y "$f") )); [ "$age" -le 43200 ] || { echo "full the stamp for ${p2:0:8} is $age s old"; return; }
|
|
echo "defer $p2"
|
|
}
|
|
finish() {
|
|
local what="$1" secs=$(( $(date +%s) - T0 ))
|
|
if [ "$RED" = 0 ]; then echo "pre-push gate ($what): GREEN, $N checks in ${secs}s"; [ "${STAMP:-0}" = 1 ] && stamp_green; exit 0; fi
|
|
echo "pre-push gate ($what): RED after $N checks in ${secs}s. Fix it before pushing (every check above is one CI runs; the same script runs there)." >&2
|
|
exit 1
|
|
}
|
|
|
|
case "$MODE" in
|
|
self-test)
|
|
fails=0
|
|
st="$(mktemp)" # run in this shell, not a $(...) subshell, so RED is visible here
|
|
run "known failure" false >"$st" 2>&1; out="$(cat "$st")"; case "$out" in *"RED"*"known failure"*) ;; *) echo "self-test failed: a failing check was not reported RED"; fails=1 ;; esac
|
|
[ "$RED" = 1 ] || { echo "self-test failed: a failing check did not set RED"; fails=1; }
|
|
RED=0
|
|
run "known success" true >"$st" 2>&1; out="$(cat "$st")"; rm -f "$st"; case "$out" in *"ok"*"known success"*) ;; *) echo "self-test failed: a passing check was not reported ok"; fails=1 ;; esac
|
|
[ "$RED" = 0 ] || { echo "self-test failed: a passing check set RED"; fails=1; }
|
|
[ "$(printf 'refs/heads/x 1 refs/heads/master 2\n' | gated_refs)" = full ] || { echo "self-test failed: a push to master did not select the full gate"; fails=1; }
|
|
[ "$(printf 'refs/heads/x 1 refs/heads/release-0.3.15 2\n' | gated_refs)" = full ] || { echo "self-test failed: a push to release-0.3.15 did not select the full gate"; fails=1; }
|
|
[ "$(printf 'refs/heads/x 1 refs/heads/x 2\n' | gated_refs)" = light ] || { echo "self-test failed: a push to a feature branch selected the full gate"; fails=1; }
|
|
[ "$(printf '' | gated_refs)" = light ] || { echo "self-test failed: a push with no refs selected the full gate"; fails=1; }
|
|
# the light gate carries the two never-push classes beside the structural checks, and the full gate runs them too
|
|
declare -f never_push_checks | grep -q 'tools/ci/no-secrets-check.sh' || { echo "self-test failed: the never-push checks do not run the no-secrets check"; fails=1; }
|
|
declare -f never_push_checks | grep -q 'tools/ci/identity-check.sh' || { echo "self-test failed: the never-push checks do not run the identity grep"; fails=1; }
|
|
grep -qE '^\s+structural_checks; never_push_checks; finish "feature branch"' "$0" || { echo "self-test failed: the hook's light gate does not run the never-push checks"; fails=1; }
|
|
# the green stamp and the deferral, in a fixture repository: a merge of a stamped branch onto the remote tip defers; an
|
|
# unstamped branch, a stale stamp, a merge onto an older tip and a plain commit all take the full gate
|
|
fx=$(mktemp -d); ( cd "$fx" && git init -q -b master . && git -c user.name=t -c user.email=t@t commit -q --allow-empty -m a ) 2>/dev/null
|
|
A=$(git -C "$fx" rev-parse HEAD); git -C "$fx" checkout -q -b b; git -C "$fx" -c user.name=t -c user.email=t@t commit -q --allow-empty -m b; B=$(git -C "$fx" rev-parse HEAD)
|
|
git -C "$fx" checkout -q master; git -C "$fx" -c user.name=t -c user.email=t@t merge -q --no-ff -m "merge b" b; M=$(git -C "$fx" rev-parse HEAD)
|
|
[ "$(deferred_merge "$M" "$A" "$fx")" = "full no green stamp for ${B:0:8}" ] || { echo "self-test failed: an unstamped branch merge was not sent to the full gate: $(deferred_merge "$M" "$A" "$fx")"; fails=1; }
|
|
( cd "$fx" && git checkout -q "$B" && stamp_green . >/dev/null && git checkout -q master )
|
|
[ "$(deferred_merge "$M" "$A" "$fx")" = "defer $B" ] || { echo "self-test failed: a stamped branch merge onto the remote tip was not deferred: $(deferred_merge "$M" "$A" "$fx")"; fails=1; }
|
|
[ "$(deferred_merge "$M" "$B" "$fx")" = "full first parent ${A:0:8} is not the remote tip ${B:0:8}" ] || { echo "self-test failed: a merge onto another tip was deferred"; fails=1; }
|
|
[ "$(deferred_merge "$B" "$A" "$fx")" = "full not a two-parent merge" ] || { echo "self-test failed: a plain commit was deferred"; fails=1; }
|
|
touch -t 202001010000 "$(cd "$fx" && stamp_dir)/$B"; case "$(deferred_merge "$M" "$A" "$fx")" in "full the stamp for ${B:0:8} is "*) ;; *) echo "self-test failed: a stale stamp was honoured"; fails=1 ;; esac
|
|
( cd "$fx" && echo x > dirty && git add dirty && git -c user.name=t -c user.email=t@t commit -q -m d && echo y > dirty && stamp_green . | grep -q recorded ) && { echo "self-test failed: a dirty tree was stamped"; fails=1; }
|
|
rm -rf "$fx"
|
|
# a --ci site build outside GitHub Actions leaves the tracked site files as they are (the horizon lane's four rewritten files)
|
|
before=$(git status --porcelain -- site | sort); ( MODE=ci GITHUB_ACTIONS= site_build >/dev/null 2>&1 ); after=$(git status --porcelain -- site | sort)
|
|
[ "$before" = "$after" ] || { echo "self-test failed: a --ci site build outside GitHub Actions changed tracked site files: $(git status --porcelain -- site | tr '\n' ' ')"; fails=1; }
|
|
MODE=ci GITHUB_ACTIONS= site_in_place && { echo "self-test failed: --ci outside GitHub Actions chose the in-place build"; fails=1; }
|
|
MODE=ci GITHUB_ACTIONS=true site_in_place || { echo "self-test failed: --ci inside GitHub Actions did not choose the in-place build"; fails=1; }
|
|
declare -f tree_checks | grep -q 'never_push_checks' || { echo "self-test failed: the full gate does not run the never-push checks"; fails=1; }
|
|
[ "$fails" = 0 ] && echo "self-test passed: a failing check is RED and fails the gate, a passing one is ok; master and release-* select the full gate, other refs the light one; a merge of a green-stamped branch onto the remote tip defers to CI, every other shape takes the full gate; a --ci site build outside GitHub Actions leaves the tree unchanged (structural checks, no-secrets, identity grep)"
|
|
exit $fails ;;
|
|
list)
|
|
grep -E '^\s+run "' "$0" | sed -E 's/^\s+run "([^"]+)".*/\1/' ;;
|
|
hook)
|
|
REFS="$(cat)"; which="$(printf '%s\n' "$REFS" | gated_refs)"
|
|
if [ "$which" = full ]; then
|
|
# a merge of a green-stamped branch onto the exact remote tip goes through on the light gate (CI runs the full one)
|
|
verdict=""; while read -r lref lsha rref rsha; do case "$rref" in refs/heads/master|refs/heads/release-*) verdict=$(deferred_merge "$lsha" "$rsha"); break ;; esac; done <<<"$REFS"
|
|
case "$verdict" in
|
|
defer*) echo "pre-push gate: a merge of green-stamped ${verdict#defer } onto the remote tip: the light gate here, the full gate in CI on landing:"
|
|
structural_checks; never_push_checks; finish "merge of a green branch (full gate deferred to CI)" ;;
|
|
*) echo "pre-push gate: a push to master or release-*, the full gate (the same checks CI runs)${verdict:+ ($verdict)}:"
|
|
STAMP=1; structural_checks; tree_checks; finish "push to master or release-*" ;;
|
|
esac
|
|
else
|
|
echo "pre-push gate: a feature branch, the light gate (the two structural checks, the no-secrets check, the identity grep):"
|
|
structural_checks; never_push_checks; finish "feature branch"
|
|
fi ;;
|
|
ci|local)
|
|
[ "$MODE" = ci ] && echo "pre-push gate in CI (the same script as the local hook):" || echo "pre-push gate over this working tree:"
|
|
[ "$MODE" = local ] && STAMP=1; structural_checks; tree_checks; finish "$MODE" ;;
|
|
*) echo "usage: tools/ci/pre-push.sh [--ci|--hook|--self-test|--list]" >&2; exit 2 ;;
|
|
esac
|