Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions ci/check_actions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ if [ ! -x "$HOME/.cargo/bin/action-validator" ]; then
fi
export PATH="$HOME/.cargo/bin:$PATH"

# The pre-push hook coordinates concurrent repository checks and is itself
# production code. Exercise it with fake check scripts so this validation does
# not recursively run the real hook or depend on installed Rust toolchains.
python3 githooks/test_pre_push.py

# Files to exclude from validation (e.g., because they are not Actions/Workflows)
# Use relative paths matching `find .github` output
EXCLUDE_FILES=(
Expand Down
161 changes: 135 additions & 26 deletions githooks/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,122 @@

set -eo pipefail
echo "Running pre-push git hook: $0"
REPO_ROOT="$(git rev-parse --show-toplevel)"
cd "$REPO_ROOT"

# These checks assert properties of tracked source; none should rewrite a
# first-party workspace lockfile as a side effect. Discover the inventory so a
# newly added workspace is protected automatically. Vendored packages and test
# fixtures own independent lockfiles which these repository checks never use,
# so exclude those source snapshots from this workspace-level contract.
LOCKFILES=()
while IFS= read -r -d '' lockfile; do
case "$lockfile" in
*/vendor/*|*/tests/fixtures/*) continue ;;
esac
LOCKFILES+=("$lockfile")
done < <(git ls-files -z -- '*Cargo.lock')

if [[ ${#LOCKFILES[@]} -eq 0 ]]; then
echo "No first-party Cargo.lock files found; refusing to run unguarded" >&2
exit 1
fi

lockfile_hash() {
local lockfile="$1"
if [[ -f "$lockfile" ]]; then
# Unlike sha256sum, `git hash-object` is available on both Linux and
# macOS wherever this Git hook can run. Bypass clean filters so this
# snapshot represents the bytes that a check could mutate on disk.
git hash-object --no-filters -- "$lockfile"
else
# Preserve a pre-existing deletion. If a check recreates the file, the
# post-check value will still differ from this marker.
echo '<missing>'
fi
}

LOCKFILE_HASHES_BEFORE=()
for lockfile in "${LOCKFILES[@]}"; do
LOCKFILE_HASHES_BEFORE+=("$(lockfile_hash "$lockfile")")
done

# check_fmt.sh uses cargo-zerocopy's pinned nightly toolchain. On a fresh
# runner, it previously reached rustup concurrently with other checks. Rustup
# shares download and rollback paths across toolchains, so one installation
# could remove another installation's partial file. Bootstrap nightly before
# retaining parallelism between the checks themselves.
#
# Keep this list coordinated with the backgrounded scripts below. If another
# check starts using a different cargo-zerocopy descriptor, initialize it here
# before that check may run in parallel.
CHECKS_FAILED=0
TOOLCHAINS_READY=1
bootstrap_toolchain() {
local descriptor="$1"
local status
# Git sends its ref-update protocol to pre-push hooks on stdin. Bootstrap
# is deliberately noninteractive, and neither cargo-zerocopy nor rustup may
# consume input needed by a chained git-lfs or GHerrit hook.
if CARGO_ZEROCOPY_AUTO_INSTALL_TOOLCHAIN=1 \
./zerocopy/cargo.sh "+$descriptor" --version \
</dev/null >/dev/null; then
return
else
status=$?
fi
echo "zerocopy/cargo.sh +$descriptor --version failed with status $status" \
>&2
CHECKS_FAILED=1
TOOLCHAINS_READY=0
}

bootstrap_toolchain nightly

# Forego redirecting stdout to /dev/null on check_fmt.sh because the output from
# `cargo fmt` is useful (and the good stuff is not delivered by stderr).
#
# Background all jobs and wait for them so they can run in parallel.
./ci/check_actions.sh & ACTIONS_PID=$!
./ci/check_fmt.sh & FMT_PID=$!
./ci/check_job_dependencies.sh >/dev/null & JOB_DEPS_PID=$!
./zerocopy/ci/check_all_toolchains_tested.sh >/dev/null & TOOLCHAINS_PID=$!
./zerocopy/ci/check_readme.sh >/dev/null & README_PID=$!
./zerocopy/ci/check_stale_stderr.sh >/dev/null & STALE_STDERR_PID=$!
./zerocopy/ci/check_versions.sh >/dev/null & VERSIONS_PID=$!
./zerocopy/ci/check_msrv_is_minimal.sh >/dev/null & MSRV_PID=$!

# `wait <pid>` exits with the same status code as the job it's waiting for.
# Since we `set -e` above, this will have the effect of causing the entire
# script to exit with a non-zero status code if any of these jobs does the same.
# Note that, while `wait` (with no PID argument) waits for all backgrounded
# jobs, it exits with code 0 even if one of the backgrounded jobs does not, so
# we can't use it here.
wait $ACTIONS_PID
wait $FMT_PID
wait $TOOLCHAINS_PID
wait $JOB_DEPS_PID
wait $README_PID
wait $STALE_STDERR_PID
wait $VERSIONS_PID
wait $MSRV_PID
# Background all jobs and wait for them so they can run in parallel. Do not
# launch them after a bootstrap failure: they could otherwise race to repair
# the same incomplete toolchain state. Static script-inventory and lockfile
# checks below still run, including when bootstrap fails.
wait_for_check() {
local name="$1"
local pid="$2"
local status
if wait "$pid"; then
return
else
status=$?
fi
echo "$name failed with status $status" >&2
CHECKS_FAILED=1
}

if [[ "$TOOLCHAINS_READY" -eq 1 ]]; then
./ci/check_actions.sh & ACTIONS_PID=$!
./ci/check_fmt.sh & FMT_PID=$!
./ci/check_job_dependencies.sh >/dev/null & JOB_DEPS_PID=$!
./zerocopy/ci/check_all_toolchains_tested.sh >/dev/null & TOOLCHAINS_PID=$!
./zerocopy/ci/check_readme.sh >/dev/null & README_PID=$!
./zerocopy/ci/check_stale_stderr.sh >/dev/null & STALE_STDERR_PID=$!
./zerocopy/ci/check_versions.sh >/dev/null & VERSIONS_PID=$!
./zerocopy/ci/check_msrv_is_minimal.sh >/dev/null & MSRV_PID=$!

# A bare `wait` loses individual failures. Sequential bare waits under
# `set -e` abandon the remaining children after the first failure. Record
# each status explicitly so every child is reaped and every useful
# diagnostic has a chance to finish.
wait_for_check "ci/check_actions.sh" "$ACTIONS_PID"
wait_for_check "ci/check_fmt.sh" "$FMT_PID"
wait_for_check "ci/check_job_dependencies.sh" "$JOB_DEPS_PID"
wait_for_check \
"zerocopy/ci/check_all_toolchains_tested.sh" "$TOOLCHAINS_PID"
wait_for_check "zerocopy/ci/check_readme.sh" "$README_PID"
wait_for_check "zerocopy/ci/check_stale_stderr.sh" "$STALE_STDERR_PID"
wait_for_check "zerocopy/ci/check_versions.sh" "$VERSIONS_PID"
wait_for_check "zerocopy/ci/check_msrv_is_minimal.sh" "$MSRV_PID"
fi

# Ensure that this script calls all scripts in `ci/*` and `zerocopy/ci/*`. This
# isn't a foolproof check since it just checks for the string in this script
Expand All @@ -50,13 +139,33 @@ wait $MSRV_PID
shopt -s extglob
GLOBIGNORE="./*/@(release_crate_version|check_todo|release_anneal_version).sh" # We don't want to run these
for f in ./ci/*; do
grep "$f" githooks/pre-push >/dev/null || { echo "$f not called from githooks/pre-push" >&2 ; exit 1; }
if ! grep "$f" githooks/pre-push >/dev/null; then
echo "$f not called from githooks/pre-push" >&2
CHECKS_FAILED=1
fi
done
# We don't want to run release_crate_version here, and zerocopy/ci/check_fmt.sh
# is called by ci/check_fmt.sh above rather than directly.
GLOBIGNORE="./zerocopy/ci/@(release_crate_version|check_fmt).sh"
for f in ./zerocopy/ci/*; do
grep "$f" githooks/pre-push >/dev/null || { echo "$f not called from githooks/pre-push" >&2 ; exit 1; }
if ! grep "$f" githooks/pre-push >/dev/null; then
echo "$f not called from githooks/pre-push" >&2
CHECKS_FAILED=1
fi
done
unset GLOBIGNORE
shopt -u extglob

# Keep this comparison at the end of the hook so any future synchronous check
# added after the parallel fan-out remains inside the same mutation guard.
for index in "${!LOCKFILES[@]}"; do
lockfile="${LOCKFILES[$index]}"
hash_after="$(lockfile_hash "$lockfile")"
if [[ "$hash_after" != "${LOCKFILE_HASHES_BEFORE[$index]}" ]]; then
echo "$lockfile was modified by a nominally read-only pre-push check" \
>&2
CHECKS_FAILED=1
fi
done

exit "$CHECKS_FAILED"
Loading
Loading