fix(hooks): resolve Go lint base for forks (#3074)

Co-authored-by: ayattara <alassane.yattara@savoirfairelinux.com>
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
This commit is contained in:
Alassane
2026-08-27 10:08:04 +00:00
committed by GitHub
parent 335d80bf66
commit b3cdbf858c
3 changed files with 253 additions and 1 deletions
+1 -1
View File
@@ -43,7 +43,7 @@ repos:
- id: go-lint - id: go-lint
name: Go lint (PR changed code) name: Go lint (PR changed code)
entry: bash -c 'unset GIT_DIR GIT_WORK_TREE; merge_head=$(git rev-parse --verify MERGE_HEAD 2>/dev/null || true); base_tip=$(git rev-parse --verify origin/main 2>/dev/null || true); if [[ -n "$merge_head" && "$merge_head" == "$base_tip" ]]; then base="$merge_head"; else base=$(git merge-base HEAD origin/main 2>/dev/null || git rev-parse HEAD); fi; cd apps/backend && golangci-lint run ./... --new-from-rev="$base" --timeout=5m' entry: bash -c 'unset GIT_DIR GIT_WORK_TREE; base=$(scripts/resolve-go-lint-base --comparison-base) || exit $?; cd apps/backend && golangci-lint run ./... --new-from-rev="$base" --timeout=5m'
language: system language: system
files: ^apps/backend/.*\.go$ files: ^apps/backend/.*\.go$
pass_filenames: false pass_filenames: false
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env bash
# resolve-go-lint-base prints the local remote-tracking ref used by the
# pre-commit Go lint hook. Fork worktrees conventionally retain the canonical
# repository as `upstream`; do not silently fall back when the base is unclear.
set -euo pipefail
canonical_repository='kdlbs/kandev'
comparison_base=false
die() {
echo "Go lint base resolution failed: $*" >&2
exit 1
}
case "${1:-}" in
'') ;;
--comparison-base) comparison_base=true; shift ;;
*) die "unknown argument: $1" ;;
esac
[ "$#" -eq 0 ] || die "unexpected arguments: $*"
normalize_remote_ref() {
local ref=$1 remote_branch remote branch normalized
case "$ref" in
refs/remotes/*) remote_branch=${ref#refs/remotes/} ;;
refs/*) die "base ref must be a remote-tracking ref: $ref" ;;
*/*) remote_branch=$ref ;;
*) die "base ref must be a remote-tracking ref: $ref" ;;
esac
remote=${remote_branch%%/*}
branch=${remote_branch#*/}
[ -n "$remote" ] && [ "$branch" != "$remote_branch" ] && [ -n "$branch" ] || die "base ref must name a remote and branch: $ref"
normalized="refs/remotes/$remote/$branch"
git check-ref-format "$normalized" >/dev/null 2>&1 || die "base ref does not have a safe ref format: $ref"
printf '%s\n' "$normalized"
}
is_canonical_remote() {
local url=$1 path
case "$url" in
https://github.com/*) path=${url#https://github.com/} ;;
http://github.com/*) path=${url#http://github.com/} ;;
ssh://git@github.com/*) path=${url#ssh://git@github.com/} ;;
git+ssh://git@github.com/*) path=${url#git+ssh://git@github.com/} ;;
git://github.com/*) path=${url#git://github.com/} ;;
git@github.com:*) path=${url#git@github.com:} ;;
*) return 1 ;;
esac
path=${path%.git}
path=${path%/}
[ "$(printf '%s' "$path" | tr '[:upper:]' '[:lower:]')" = "$canonical_repository" ]
}
configured_refs=()
while IFS= read -r ref; do
[ -n "$ref" ] && configured_refs+=("$ref")
done < <(git config --get-all kandev.lintBaseRef || true)
if [ "${#configured_refs[@]}" -gt 1 ]; then
die "ambiguous kandev.lintBaseRef values: ${configured_refs[*]}"
fi
if [ "${#configured_refs[@]}" -eq 1 ]; then
base_ref=$(normalize_remote_ref "${configured_refs[0]}")
else
origin_head=$(git symbolic-ref --quiet refs/remotes/origin/HEAD || true)
[ -n "$origin_head" ] || die "origin's default branch is unavailable; set kandev.lintBaseRef to a local remote-tracking ref"
case "$origin_head" in
refs/remotes/origin/*) base_branch=${origin_head#refs/remotes/origin/} ;;
*) die "origin's default ref is invalid: $origin_head" ;;
esac
[ -n "$base_branch" ] || die "origin's default ref is invalid: $origin_head"
if upstream_url=$(git remote get-url upstream 2>/dev/null); then
is_canonical_remote "$upstream_url" || die "upstream remote is not the canonical $canonical_repository repository"
base_ref=$(normalize_remote_ref "refs/remotes/upstream/$base_branch")
git rev-parse --verify --quiet "$base_ref^{commit}" >/dev/null || die "canonical upstream ref does not resolve locally: $base_ref"
else
base_ref=$(normalize_remote_ref "$origin_head")
fi
fi
git rev-parse --verify --quiet "$base_ref^{commit}" >/dev/null || die "base ref does not resolve locally: $base_ref"
if [ "$comparison_base" = true ]; then
merge_head=$(git rev-parse --verify MERGE_HEAD 2>/dev/null || true)
base_tip=$(git rev-parse --verify --quiet "$base_ref^{commit}")
if [ -n "$merge_head" ] && [ "$merge_head" = "$base_tip" ]; then
printf '%s\n' "$merge_head"
else
merge_base=$(git merge-base HEAD "$base_ref" 2>/dev/null) || die "base ref has no merge base with HEAD: $base_ref"
printf '%s\n' "$merge_base"
fi
else
printf '%s\n' "$base_ref"
fi
+154
View File
@@ -0,0 +1,154 @@
#!/usr/bin/env bash
# resolve-go-lint-base.test.sh — regression coverage for the Go lint hook's
# comparison base. A fork's origin can be stale, so the resolver must prefer
# the conventional canonical upstream ref without weakening lint enforcement.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
RESOLVER="$ROOT_DIR/scripts/resolve-go-lint-base"
status=0
fail() {
printf 'FAIL %s\n' "$*"
status=1
}
expect_eq() {
local label=$1 want=$2 got=$3
if [ "$got" = "$want" ]; then
printf 'ok %-34s %s\n' "$label" "$got"
else
fail "$label (want $want, got $got)"
fi
}
expect_fail() {
local label=$1 needle=$2 repo=$3 output
if output=$(cd "$repo" && "$RESOLVER" 2>&1); then
fail "$label (unexpected success: $output)"
elif [[ "$output" == *"$needle"* ]]; then
printf 'ok %-34s %s\n' "$label" "$needle"
else
fail "$label (expected $needle in: $output)"
fi
}
new_repo() {
local repo
repo=$(mktemp -d)
git -C "$repo" init -q
git -C "$repo" config user.email test@example.com
git -C "$repo" config user.name 'Hook test'
git -C "$repo" commit --allow-empty -qm initial
git -C "$repo" branch -M main
git -C "$repo" remote add origin https://example.test/fork.git
git -C "$repo" update-ref refs/remotes/origin/main HEAD
git -C "$repo" symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main
echo "$repo"
}
fork_repo="" single_repo="" release_repo="" configured_repo=""
dangling_repo="" ambiguous_repo="" unsafe_repo="" history_repo="" missing_repo=""
full_ref_repo="" collision_repo="" untrusted_repo="" namespace_repo=""
diverged_repo="" merge_repo=""
cleanup() {
local repo
for repo in "$fork_repo" "$single_repo" "$release_repo" "$configured_repo" \
"$dangling_repo" "$ambiguous_repo" "$unsafe_repo" "$history_repo" "$missing_repo" \
"$full_ref_repo" "$collision_repo" "$untrusted_repo" "$namespace_repo" \
"$diverged_repo" "$merge_repo"; do
[ -z "$repo" ] || rm -rf "$repo"
done
}
trap cleanup EXIT
fork_repo=$(new_repo)
git -C "$fork_repo" remote add upstream git@github.com:kdlbs/kandev.git
git -C "$fork_repo" update-ref refs/remotes/upstream/main HEAD
expect_eq 'fork prefers canonical upstream' 'refs/remotes/upstream/main' "$(cd "$fork_repo" && "$RESOLVER")"
single_repo=$(new_repo)
expect_eq 'single remote uses origin default' 'refs/remotes/origin/main' "$(cd "$single_repo" && "$RESOLVER")"
release_repo=$(new_repo)
git -C "$release_repo" update-ref -d refs/remotes/origin/main
git -C "$release_repo" update-ref refs/remotes/origin/release HEAD
git -C "$release_repo" symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/release
git -C "$release_repo" remote add upstream https://github.com/kdlbs/kandev.git
git -C "$release_repo" update-ref refs/remotes/upstream/release HEAD
expect_eq 'non-main default follows upstream' 'refs/remotes/upstream/release' "$(cd "$release_repo" && "$RESOLVER")"
configured_repo=$(new_repo)
git -C "$configured_repo" remote add upstream https://github.com/kdlbs/kandev.git
git -C "$configured_repo" update-ref refs/remotes/upstream/release HEAD
git -C "$configured_repo" config kandev.lintBaseRef upstream/release
expect_eq 'configured ref is honored' 'refs/remotes/upstream/release' "$(cd "$configured_repo" && "$RESOLVER")"
dangling_repo=$(new_repo)
git -C "$dangling_repo" config kandev.lintBaseRef upstream/main
expect_fail 'missing configured ref fails closed' 'does not resolve' "$dangling_repo"
ambiguous_repo=$(new_repo)
git -C "$ambiguous_repo" config --add kandev.lintBaseRef origin/main
git -C "$ambiguous_repo" config --add kandev.lintBaseRef upstream/main
expect_fail 'multiple configured refs fail closed' 'ambiguous' "$ambiguous_repo"
missing_repo=$(new_repo)
git -C "$missing_repo" update-ref -d refs/remotes/origin/main
expect_fail 'missing default ref fails closed' 'does not resolve' "$missing_repo"
unsafe_repo=$(new_repo)
git -C "$unsafe_repo" config kandev.lintBaseRef 'origin/main --not-a-ref'
expect_fail 'unsafe configured ref is rejected' 'safe ref format' "$unsafe_repo"
history_repo=$(new_repo)
git -C "$history_repo" branch previous
git -C "$history_repo" switch -q previous
git -C "$history_repo" switch -q main
git -C "$history_repo" config kandev.lintBaseRef '@{-1}'
expect_fail 'checkout history is not a base ref' 'remote-tracking' "$history_repo"
full_ref_repo=$(new_repo)
expect_eq 'resolver emits fully qualified ref' 'refs/remotes/origin/main' "$(cd "$full_ref_repo" && "$RESOLVER")"
collision_repo=$(new_repo)
git -C "$collision_repo" tag origin/main
expect_eq 'tag collision still selects remote ref' 'refs/remotes/origin/main' "$(cd "$collision_repo" && "$RESOLVER")"
untrusted_repo=$(new_repo)
git -C "$untrusted_repo" remote add upstream https://example.test/not-kandev.git
git -C "$untrusted_repo" update-ref refs/remotes/upstream/main HEAD
expect_fail 'untrusted upstream fails closed' 'canonical' "$untrusted_repo"
namespace_repo=$(new_repo)
git -C "$namespace_repo" config kandev.lintBaseRef refs/heads/main
expect_fail 'configured local branch is rejected' 'remote-tracking' "$namespace_repo"
diverged_repo=$(new_repo)
git -C "$diverged_repo" branch feature
git -C "$diverged_repo" switch -q feature
git -C "$diverged_repo" commit --allow-empty -qm feature
git -C "$diverged_repo" switch -q main
git -C "$diverged_repo" commit --allow-empty -qm base-advance
diverged_base=$(git -C "$diverged_repo" rev-parse HEAD)
diverged_fork=$(git -C "$diverged_repo" rev-parse HEAD~1)
git -C "$diverged_repo" update-ref refs/remotes/origin/main "$diverged_base"
git -C "$diverged_repo" switch -q feature
expect_eq 'diverged history uses merge base' "$diverged_fork" "$(cd "$diverged_repo" && "$RESOLVER" --comparison-base)"
merge_repo=$(new_repo)
git -C "$merge_repo" branch feature
git -C "$merge_repo" switch -q feature
git -C "$merge_repo" commit --allow-empty -qm feature
git -C "$merge_repo" switch -q main
git -C "$merge_repo" commit --allow-empty -qm base-advance
merge_base_tip=$(git -C "$merge_repo" rev-parse HEAD)
git -C "$merge_repo" update-ref refs/remotes/origin/main "$merge_base_tip"
git -C "$merge_repo" switch -q feature
printf '%s\n' "$merge_base_tip" > "$merge_repo/.git/MERGE_HEAD"
expect_eq 'base merge keeps incoming tip' "$merge_base_tip" "$(cd "$merge_repo" && "$RESOLVER" --comparison-base)"
if [ "$status" -eq 0 ]; then
echo 'All Go lint base resolver checks passed.'
fi
exit "$status"