Repository navigation
Expand file tree
/
Copy pathaffected-go-packages.sh
More file actions
executable file
·155 lines (140 loc) · 7 KB
/
Copy pathaffected-go-packages.sh
File metadata and controls
executable file
·155 lines (140 loc) · 7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
#!/usr/bin/env bash
# Affected-package Go test selection for v2-tests.yml pull requests (#12454).
#
# Usage (from the Go module root, src/):
# affected-go-packages.sh <base-rev> <head-rev> <out-file>
#
# Writes <out-file> as EITHER the single line `all` (run the full suite) OR the
# sorted import paths of the packages the diff can affect, one per line. An
# empty file is a valid answer: no Go package is affected (for example a
# changelog.d-only diff), and every shard reports success without running.
#
# Selection is deliberately conservative. A package is selected when:
# 1. a changed .go file lives in its directory, or
# 2. it, its test variant or its test binary transitively imports a package
# with a changed non-test .go file (reverse dependencies over the Deps of
# `go list -test`; _test.go files are never compiled into importers), or
# 3. one of its _test.go files names a changed file or directory as a string
# literal (tests that read other packages' sources as text, e.g.
# "../../cmd/hive/main.go"), or
# 4. it is in ALWAYS_PKGS below (tests that walk the whole source tree).
#
# Anything this script cannot reason about falls back to `all`: a change
# outside src/ (workflows, CI scripts, bin/, config/, Justfile, Makefile,
# dashboard/openapi.json — all read or executed by tests or shards), go.mod,
# go.sum, go.work or the nested Crux module's crux/go.mod and crux/go.sum,
# src/scripts/, the shared test harness under src/internal/, any
# non-Go file under src/ (embedded static assets, docs, policies, fixtures —
# several are read across packages), any testdata/ file, a .go file whose
# directory is not a package in the module, an empty diff, or any git/go
# failure. Push and scheduled runs never call this script: they always run the
# full suite, which is the safety net for anything this misses.
#
# Never fails the job on an analysis problem: every error path writes `all`.
set -uo pipefail
base=${1:?base revision}
head=${2:?head revision}
out=${3:?output file}
# Packages whose tests scan the whole source tree as text rather than through
# imports, so any .go change anywhere can move their verdict. KEEP IN SYNC when
# adding such a test (the push/schedule full runs catch a stale entry).
# internal/... sleep/dashboard-import ratchets and settings parity
# pkg/config env-vars.md parity walks all of src/
# pkg/rotation walks src/ for credential-handling call sites
ALWAYS_PATTERNS=(./internal/... ./pkg/config ./pkg/rotation)
full() {
echo "affected-go-packages: running the full suite: $*" >&2
echo all > "$out"
exit 0
}
repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || full "not inside a git checkout"
module_root=$(pwd -P)
case "$module_root/" in
"$repo_root/src/") ;;
*) full "must run from $repo_root/src (ran from $module_root)" ;;
esac
git rev-parse -q --verify "${base}^{commit}" >/dev/null || full "base revision $base is not available"
git rev-parse -q --verify "${head}^{commit}" >/dev/null || full "head revision $head is not available"
# --no-renames lists both sides of a rename, so a moved file selects (or
# invalidates) both its old and new package.
changed=$(git diff --no-renames --name-only "$base" "$head") || full "git diff $base $head failed"
[ -n "$changed" ] || full "empty changed-file list"
# src/go.work puts the go tool in workspace mode: `go list -m` prints every
# workspace module and `./...` covers the root module only, so the nested
# Crux module (src/crux, #13536) is addressed explicitly as ./crux/... .
mod_path=$(GOWORK=off go list -m 2>/dev/null) || full "go list -m failed"
# Directory -> import path for every package in the module.
dir_map=$(go list -f '{{.Dir}}|{{.ImportPath}}' ./... ./crux/... 2>/dev/null) || full "go list ./... ./crux/... failed"
[ -n "$dir_map" ] || full "go list ./... ./crux/... returned no packages"
seeds=""
rdep_seeds=""
literals=""
while IFS= read -r f; do
[ -n "$f" ] || continue
case "$f" in
changelog.d/*|CHANGELOG.md|docs/*) continue ;; # never read by Go tests
src/*) ;;
*) full "$f is outside src/" ;;
esac
rel=${f#src/}
case "$rel" in
go.mod|go.sum|go.work|go.work.sum|crux/go.mod|crux/go.sum) full "$f changes the module graph" ;;
scripts/*) full "$f is a repository script" ;;
internal/*) full "$f is part of the shared test harness" ;;
testdata/*|*/testdata/*) full "$f is a test fixture (fixtures are read across packages)" ;;
*.go) ;;
*) full "$f is not a Go source file (assets, docs and fixtures can be read by any package's tests)" ;;
esac
dir="$repo_root/$(dirname "$f")"
pkg=$(awk -F'|' -v d="$dir" '$1 == d { print $2; exit }' <<<"$dir_map")
[ -n "$pkg" ] || full "$f is not in a package of $mod_path (deleted or build-constrained package)"
seeds="$seeds$pkg"$'\n'
# _test.go files are never compiled into importers, so only a non-test
# change has to be expanded to reverse dependencies.
case "$f" in
*_test.go) ;;
*) rdep_seeds="$rdep_seeds$pkg"$'\n' ;;
esac
# Literal forms a test uses to read this file or its package directory.
literals="$literals$(basename "$f")\""$'\n'"$(dirname "$rel")\""$'\n'
done <<<"$changed"
selected="$out.unsorted"
: > "$selected"
trap 'rm -f "$selected"' EXIT
if [ -n "$seeds" ]; then
# Every package, test variant ("p [p.test]", "p_test [p.test]") and test
# binary ("p.test") with its transitive deps; a line's owning package is its
# import path with the variant/binary decoration stripped.
graph=$(go list -test -f '{{.ImportPath}}|{{join .Deps "|"}}' ./... ./crux/... 2>/dev/null) || full "go list -test ./... ./crux/... failed"
printf '%s' "$seeds" >> "$selected"
awk -F'|' '
NR == FNR { if ($0 != "") seed[$0] = 1; next }
{
owner = $1
sub(/ \[.*$/, "", owner)
sub(/\.test$/, "", owner)
sub(/_test$/, "", owner)
hit = (owner in seed)
for (i = 2; !hit && i <= NF; i++) {
d = $i
sub(/ \[.*$/, "", d)
if (d in seed) hit = 1
}
if (hit) print owner
}
' <(printf '%s' "$rdep_seeds") <(printf '%s\n' "$graph") >> "$selected" || full "reverse-dependency expansion failed"
# Text references from _test.go files (see rule 3 in the header).
refs=$(printf '%s' "$literals" | LC_ALL=C sort -u | grep -vx '\."' || true)
if [ -n "$refs" ]; then
while IFS= read -r tf; do
d="$repo_root/src/$(dirname "${tf#./}")"
awk -F'|' -v d="$d" '$1 == d { print $2; exit }' <<<"$dir_map" >> "$selected"
done < <(grep -rlF --include='*_test.go' -f <(printf '%s\n' "$refs") . 2>/dev/null || true)
fi
go list "${ALWAYS_PATTERNS[@]}" >> "$selected" 2>/dev/null || full "go list ${ALWAYS_PATTERNS[*]} failed"
fi
# Bound the answer by the package set the shards actually test.
universe=$(go list ./pkg/... ./cmd/... ./internal/... ./crux/... 2>/dev/null) || full "go list of the tested package set failed"
LC_ALL=C sort -u "$selected" | grep -Fx -f <(printf '%s\n' "$universe") > "$out" || true
echo "affected-go-packages: $(grep -c . "$out" || true) of $(printf '%s\n' "$universe" | grep -c .) tested packages selected" >&2
sed 's/^/ /' "$out" >&2