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
22 changes: 22 additions & 0 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,25 @@ jobs:
shell: bash
- run: pre-commit run --show-diff-on-failure --color=always --all-files
shell: bash

test:
name: Shell script tests
runs-on: ubuntu-latest

permissions:
contents: read

steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
with:
egress-policy: audit

- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false

# The tests build throwaway repos under /tmp with their own origin remote and
# their own committer identity, so they need no runner git config.
- run: tests/test-check-for-epoch-bump.sh
shell: bash
8 changes: 8 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,11 @@ repos:
hooks:
- id: misspell
exclude: '^example\.pre-commit-config\.yaml$'
# This repo ships shell scripts as its product, but nothing was linting them.
# shellcheck-py rather than the Docker-based mirror, so it installs alongside
# the pip packages the Lint job already sets up. Default severity is `style`,
# which reports everything; all current scripts are clean at that level.
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: 745eface02aef23e168a8afb6b5737818efbea95 # frozen: v0.11.0.1
hooks:
- id: shellcheck
59 changes: 48 additions & 11 deletions scripts/check-for-epoch-bump.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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:"

Expand All @@ -34,31 +65,37 @@ 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%%/*}"
rest_of_path="${yaml_file#*/}"
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
Expand All @@ -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

Expand Down
191 changes: 191 additions & 0 deletions tests/test-check-for-epoch-bump.sh
Original file line number Diff line number Diff line change
@@ -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" <<YAML
package:
name: foo
version: "$1"
epoch: $3
description: test package
YAML
}

# build_repo: origin/main ends at <ahead spec>, local main is left at <stale spec>
# 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 <label> <expect: PASS|WARN|SKIP|NEW> <local version> <local epoch>
run_case() {
local label=$1 expect=$2 lv=$3 le=$4
write_pkg "$lv" "$PKG" "$le"
local out
out=$("$SCRIPT" "$PKG" 2>&1)

local got=UNKNOWN
case "$out" in
*"Could not resolve a base ref"*) got=SKIP ;;
*"not found on"*|*"new package"*) got=NEW ;;
*"HAS NOT been increased"*) got=WARN ;;
*"has been increased"*) got=PASS ;;
esac

if [ "$got" = "$expect" ]; then
say " ok $label (expected $expect)"
pass=$((pass + 1))
else
say " FAIL $label: expected $expect, got $got"
say " ---- script output ----"
printf '%s\n' "$out" | sed 's/^/ /'
fail=$((fail + 1))
fi
}

say "script under test: $SCRIPT"

say ""
say "scenario: local main one commit stale, origin/main has 1.0.0-r5"
build_repo "1.0.0" 5 behind
run_case "epoch 4 is NOT a bump over origin/main r5" WARN "1.0.0" 4
run_case "epoch 6 IS a bump over origin/main r5" PASS "1.0.0" 6
run_case "epoch 5 equals origin/main r5" WARN "1.0.0" 5
run_case "new version resets epoch to 0" PASS "1.0.1" 0

say ""
say "scenario: local main predates the file, origin/main has 1.0.0-r5"
build_repo "1.0.0" 5 nofile
run_case "epoch 4 still compared against origin/main" WARN "1.0.0" 4
run_case "epoch 6 still recognised as a bump" PASS "1.0.0" 6

say ""
say "scenario: local main == origin/main (no staleness)"
build_repo "1.0.0" 5 current
run_case "epoch 4 rejected" WARN "1.0.0" 4
run_case "epoch 6 accepted" PASS "1.0.0" 6

say ""
say "scenario: genuinely new package, absent from origin/main"
build_repo "1.0.0" 5 current
rm -f "$PKG"
write_pkg "2.0.0" "enterprise-packages/brand-new.yaml" 0
out=$("$SCRIPT" enterprise-packages/brand-new.yaml 2>&1)
if printf '%s' "$out" | grep -qiE 'not found on|new package'; then
say " ok absent-from-main is reported explicitly, not silently compared"
pass=$((pass + 1))
else
say " FAIL absent-from-main was not called out; output was:"
printf '%s\n' "$out" | sed 's/^/ /'
fail=$((fail + 1))
fi

say ""
say "scenario: no resolvable base ref at all"
rm -rf "$ROOT/norefs"
mkdir -p "$ROOT/norefs"
cd "$ROOT/norefs" || exit 1
git init -q .
init_repo_config
git symbolic-ref HEAD refs/heads/feature
write_pkg "1.0.0" "$PKG" 1
git add "$PKG" >/dev/null 2>&1
git commit -qm "initial" >/dev/null 2>&1
out=$("$SCRIPT" "$PKG" 2>&1)
if printf '%s' "$out" | grep -qi 'could not resolve a base ref'; then
say " ok missing base ref is reported instead of passing everything"
pass=$((pass + 1))
else
say " FAIL missing base ref was not reported; output was:"
printf '%s\n' "$out" | sed 's/^/ /'
fail=$((fail + 1))
fi

say ""
say "passed: $pass failed: $fail"
[ "$fail" -eq 0 ]
Loading