From 71ac1284dc5571f46cf2af06bdd48a42af05522e Mon Sep 17 00:00:00 2001 From: Amber Arcadia Date: Mon, 17 Aug 2026 11:22:25 -0400 Subject: [PATCH 1/2] fix(check-for-epoch-bump): compare against origin/main, not local main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hook read its baseline with `git show main:`, which is the local `main` branch. Package repos tell contributors never to commit to main, so that branch is only as fresh as the last time someone checked it out. Mine in chainguard-dev/stereo is far enough behind that `git show main:enterprise-packages/gitlab-cng-19.0.yaml` returns `fatal: path ... exists on disk, but not in 'main'`. When that `git show` failed the script fell through to `version_main=0` and `epoch_main=0`, so every package compared as increased. On a real stereo file the old code printed: ✅ Version has been increased compared to main: 19.0.5 > 0 for a package that has been on main for months and is at 19.0.5-r2 there. The `> 0` was the only hint that no comparison had happened. With the fix, the same file reports the truth: ⚠️ Epoch HAS NOT been increased compared to origin/main: 0 <= 2 (version: 19.0.5) A stale local main is the quieter half of the bug. While the branch still contains the file but lags a few epochs, the baseline is simply too low, so an epoch that is already taken on main passes as a fresh bump. Changes: - Resolve the baseline once, preferring origin/main, then main, then origin/HEAD. No `git fetch`: a pre-commit hook has to stay fast and work offline, and the last fetched origin/main still beats a branch nobody visits. - Separate "cannot resolve any base ref" from "file is absent from the base ref". The first now says so and skips, instead of silently passing everything. The second keeps the 0-r0 baseline, which is correct for a new package, but prints `ℹ️ Not found on (new package?)` so a real comparison is not implied. - Restore the missing-field warnings the version/epoch rewrite dropped. A yaml with no top-level `version:` or `epoch:` said nothing and silently became 0. - Name the ref in every verdict line, so what was compared is visible. - Drop a redundant `version_local != version_main` re-test inside the branch that had already established it. Exit status is unchanged: still always 0, matching `verbose: true` and the hook's advisory role. This only changes which baseline is used and what gets reported. Adds tests/test-check-for-epoch-bump.sh, which builds throwaway repos with a real origin remote so local main and origin/main can disagree. It covers a local main that is one commit behind, a local main predating the file, no staleness at all, a genuinely new package, and a repo with no resolvable base ref. Five of its ten cases fail against the previous script and all ten pass now. The scratch repos set commit.gpgsign=false: gitsign is enabled globally on dev machines, so without that every throwaway commit round-trips to Fulcio and Rekor. --- scripts/check-for-epoch-bump.sh | 59 +++++++-- tests/test-check-for-epoch-bump.sh | 191 +++++++++++++++++++++++++++++ 2 files changed, 239 insertions(+), 11 deletions(-) create mode 100755 tests/test-check-for-epoch-bump.sh diff --git a/scripts/check-for-epoch-bump.sh b/scripts/check-for-epoch-bump.sh index 0c4f669..61dc299 100755 --- a/scripts/check-for-epoch-bump.sh +++ b/scripts/check-for-epoch-bump.sh @@ -26,6 +26,37 @@ epoch_sed() { sed -r 's/^ epoch:[[:space:]]+([0-9]+).*$/\1/' } +# Pick the ref to compare against, preferring the remote-tracking branch. +# +# `main` on its own is the wrong baseline in a package repo: the convention there +# is to never commit to main, so the local branch is only as fresh as the last +# time someone happened to check it out. It can sit thousands of commits behind, +# which hands this check an old, lower epoch and lets an already-taken epoch look +# like a real bump. Worse, once local main predates a package's creation, +# `git show` finds nothing, the baseline collapses to 0, and every version +# compares as increased, so the hook passes unconditionally. +# +# Deliberately no `git fetch`: a pre-commit hook has to stay fast and keep working +# offline. Whatever the last fetch left in origin/main is still far closer to the +# truth than a branch nobody visits. +resolve_base_ref() { + local candidate + for candidate in "origin/main" "main" "origin/HEAD"; do + if git rev-parse --verify --quiet "${candidate}^{commit}" >/dev/null 2>&1; then + printf '%s' "$candidate" + return 0 + fi + done + return 1 +} + +if ! base_ref="$(resolve_base_ref)"; then + echo "⚠️ Could not resolve a base ref (tried origin/main, main, origin/HEAD)." + echo " Skipping the epoch check rather than comparing against an empty" + echo " baseline, which would report every file as bumped." + exit 0 +fi + for yaml_file in "$@"; do echo "Checking $yaml_file:" @@ -34,17 +65,18 @@ for yaml_file in "$@"; do version_line="$(version_grep < "$yaml_file")" version_local="$(echo "$version_line" | version_sed)" if [ -z "$version_local" ]; then + echo "⚠️ No top-level 'version:' field found in $yaml_file; treating it as 0" version_local="0" fi epoch_line="$(epoch_grep < "$yaml_file")" epoch_local="$(echo "$epoch_line" | epoch_sed)" if [ -z "$epoch_local" ]; then + echo "⚠️ No top-level 'epoch:' field found in $yaml_file; treating it as 0" epoch_local="0" fi - # Extract version and epoch from the file on the main branch using git show - # Treat missing file as version 0 and epoch 0 + # Extract version and epoch from the file on the base ref using git show. # Try all three known package directories so epoch bumps are enforced when # a package is moved between os/, enterprise-packages/, and extra-packages/. first_component="${yaml_file%%/*}" @@ -52,13 +84,18 @@ for yaml_file in "$@"; do main_content="" if [[ "$first_component" == "os" || "$first_component" == "enterprise-packages" || "$first_component" == "extra-packages" ]]; then for prefix in "os" "enterprise-packages" "extra-packages"; do - main_content="$(git show main:"${prefix}/${rest_of_path}" 2>/dev/null)" + main_content="$(git show "${base_ref}:${prefix}/${rest_of_path}" 2>/dev/null)" [ -n "$main_content" ] && break done else - main_content="$(git show main:"$yaml_file" 2>/dev/null)" + main_content="$(git show "${base_ref}:${yaml_file}" 2>/dev/null)" fi + if [ -z "$main_content" ]; then + # Genuinely absent from the base ref, so a 0 baseline is the right answer. + # Say so out loud: a bare "✅ increased" here reads as though a real + # comparison happened. + echo "ℹ️ Not found on ${base_ref} (new package?); comparing against 0-r0" version_main="0" epoch_main="0" else @@ -77,19 +114,19 @@ for yaml_file in "$@"; do # Compare version first, then epoch only if versions are the same if [ "$version_local" != "$version_main" ]; then - # Versions are different - version comparison is sufficient - # Use sort -V for version comparison - if [ "$(printf '%s\n' "$version_local" "$version_main" | sort -V | head -n1)" = "$version_main" ] && [ "$version_local" != "$version_main" ]; then - echo "✅ Version has been increased compared to main: $version_local > $version_main" + # Versions differ, so the version comparison settles it. sort -V puts the + # lower version first; if that is the base ref's, the local one is newer. + if [ "$(printf '%s\n' "$version_local" "$version_main" | sort -V | head -n1)" = "$version_main" ]; then + echo "✅ Version has been increased compared to ${base_ref}: $version_local > $version_main" else - echo "⚠️ Version HAS NOT been increased compared to main: $version_local <= $version_main" + echo "⚠️ Version HAS NOT been increased compared to ${base_ref}: $version_local < $version_main" fi else # Versions are the same - check epoch if (( epoch_local > epoch_main )); then - echo "✅ Epoch has been increased compared to main: $epoch_local > $epoch_main (version: $version_local)" + echo "✅ Epoch has been increased compared to ${base_ref}: $epoch_local > $epoch_main (version: $version_local)" else - echo "⚠️ Epoch HAS NOT been increased compared to main: $epoch_local <= $epoch_main (version: $version_local)" + echo "⚠️ Epoch HAS NOT been increased compared to ${base_ref}: $epoch_local <= $epoch_main (version: $version_local)" fi fi diff --git a/tests/test-check-for-epoch-bump.sh b/tests/test-check-for-epoch-bump.sh new file mode 100755 index 0000000..b0d1fc5 --- /dev/null +++ b/tests/test-check-for-epoch-bump.sh @@ -0,0 +1,191 @@ +#!/usr/bin/env bash +# Tests for scripts/check-for-epoch-bump.sh +# +# Builds a throwaway repo with a real `origin` remote so the two refs can +# disagree, which is the situation that matters: in package repos nobody checks +# out `main` locally, so the local branch rots while `origin/main` stays current. +# +# Run: tests/test-check-for-epoch-bump.sh [path-to-script] +# Exits non-zero on the first failing case and leaves the scratch repo in place +# for inspection. +set -uo pipefail + +SCRIPT="${1:-$(cd "$(dirname "$0")/.." && pwd)/scripts/check-for-epoch-bump.sh}" +ROOT=/tmp/epoch-hook-test +PKG=enterprise-packages/foo.yaml + +pass=0 +fail=0 + +say() { printf '%s\n' "$*"; } + +# Scratch repos must never sign. gitsign is enabled globally on dev machines +# (gpg.format=x509, commit.gpgsign=true), so an unconfigured test repo would +# round-trip to Fulcio and Rekor for every throwaway commit: slow, offline-hostile, +# and it writes junk into the transparency log. Call this right after init/clone. +init_repo_config() { + git config commit.gpgsign false + git config tag.gpgsign false + git config user.name "epoch hook test" + git config user.email "epoch-hook-test@example.invalid" +} + +# write a melange-ish yaml with the given version and epoch +write_pkg() { + mkdir -p "$(dirname "$2")" + cat > "$2" <, local main is left at +# args: ahead_version ahead_epoch stale_mode +# stale_mode=behind -> local main points one commit back (file present, older epoch) +# stale_mode=nofile -> local main points at a commit before the file existed +# stale_mode=current -> local main == origin/main +build_repo() { + local av=$1 ae=$2 mode=$3 + rm -rf "$ROOT" + mkdir -p "$ROOT" + git init -q --bare "$ROOT/origin.git" + git clone -q "$ROOT/origin.git" "$ROOT/work" 2>/dev/null + cd "$ROOT/work" || exit 1 + init_repo_config + git symbolic-ref HEAD refs/heads/main + + echo seed > README.md + git add README.md + git commit -qm "seed" + local seed + seed=$(git rev-parse HEAD) + + write_pkg "1.0.0" "$PKG" 3 + git add "$PKG" + git commit -qm "add foo 1.0.0-r3" + local older + older=$(git rev-parse HEAD) + + write_pkg "$av" "$PKG" "$ae" + git add "$PKG" + git commit -qm "foo $av-r$ae" + git push -q origin main + + # Leave main before moving it: git refuses to force-update a branch that is + # checked out in the current worktree. + git checkout -q -b feature + case "$mode" in + behind) git branch -f main "$older" ;; + nofile) git branch -f main "$seed" ;; + current) : ;; + esac + + # sanity-check that the scenario is actually set up, so a silently broken + # harness cannot report a green run + local local_main origin_main + local_main=$(git rev-parse main) + origin_main=$(git rev-parse origin/main) + case "$mode" in + behind|nofile) + if [ "$local_main" = "$origin_main" ]; then + say " HARNESS BROKEN: local main was not made stale for mode=$mode" + exit 1 + fi ;; + current) + if [ "$local_main" != "$origin_main" ]; then + say " HARNESS BROKEN: local main should equal origin/main for mode=current" + exit 1 + fi ;; + esac +} + +# run_case