apidiff: support trial builds of other projects

The intent is to check the real-world impact of an API change by
compiling controller-runtime with staging repos replaced by the
modified ones.

Such build checks must be requested explicitly with the new -b flag.
This commit is contained in:
Patrick Ohly 2024-10-02 09:50:52 +02:00
parent 22a30e7cbb
commit 42e12c3673

View File

@ -26,6 +26,11 @@ Usage: $0 [-r <revision>] [directory ...]"
Default is the current working tree instead of a revision.
-r <revision>: Report change in code added since this revision. Default is
the common base of origin/master and HEAD.
-b <directory> Build all packages in that directory after replacing
Kubernetes dependencies with the current content of the
staging repo. May be given more than once. Must be an
absolute path.
WARNING: this will modify the go.mod in that directory.
[directory]: Check one or more specific directory instead of everything.
EOF
exit 1
@ -40,7 +45,8 @@ source "${KUBE_ROOT}/hack/lib/init.sh"
base=
target=
while getopts "r:t:" o; do
builds=()
while getopts "r:t:b:" o; do
case "${o}" in
r)
base="${OPTARG}"
@ -58,6 +64,14 @@ while getopts "r:t:" o; do
usage
fi
;;
b)
if [ ! "${OPTARG}" ]; then
echo "ERROR: -${o} needs a non-empty parameter" >&2
echo >&2
usage
fi
builds+=("${OPTARG}")
;;
*)
usage
;;
@ -136,6 +150,8 @@ output_name () {
# run invokes apidiff once per target and stores the output
# file(s) in the given directory.
#
# shellcheck disable=SC2317 # "Command appears to be unreachable" - gets called indirectly.
run () {
out="$1"
mkdir -p "$out"
@ -144,31 +160,44 @@ run () {
done
}
# runWorktree checks out a specific revision, then invokes run there.
runWorktree () {
local out="$1"
local worktree="$2"
local rev="$3"
# inWorktree checks out a specific revision, then invokes the given
# command there.
#
# shellcheck disable=SC2317 # "Command appears to be unreachable" - gets called indirectly.
inWorktree () {
local worktree="$1"
shift
local rev="$1"
shift
# Create a copy of the repo with the specific revision checked out.
git worktree add -f -d "${worktree}" "${rev}"
# Clean up the copy on exit.
kube::util::trap_add "git worktree remove -f ${worktree}" EXIT
# Might already have been done before.
if ! [ -d "${worktree}" ]; then
git worktree add -f -d "${worktree}" "${rev}"
# Clean up the copy on exit.
kube::util::trap_add "git worktree remove -f ${worktree}" EXIT
fi
# Ready for apidiff.
(
cd "${worktree}"
run "${out}"
"$@"
)
}
# inTarget runs the given command in the target revision of Kubernetes,
# checking it out in a work tree if necessary.
inTarget () {
if [ -z "${target}" ]; then
"$@"
else
inWorktree "${KUBE_TEMP}/target" "${target}" "$@"
fi
}
# Dump old and new api state.
if [ -z "${target}" ]; then
run "${KUBE_TEMP}/after"
else
runWorktree "${KUBE_TEMP}/after" "${KUBE_TEMP}/target" "${target}"
fi
runWorktree "${KUBE_TEMP}/before" "${KUBE_TEMP}/base" "${base}"
inTarget run "${KUBE_TEMP}/after"
inWorktree "${KUBE_TEMP}/base" "${base}" run "${KUBE_TEMP}/before"
# Now produce a report. All changes get reported because exporting some API
# unnecessarily might also be good to know, but the final exit code will only
@ -199,4 +228,93 @@ for d in "${targets[@]}"; do
compare "${d}" "${KUBE_TEMP}/before/$(output_name "${d}")" "${KUBE_TEMP}/after/$(output_name "${d}")"
done
# tryBuild checks whether some other project builds with the staging repos
# of the current Kubernetes directory.
#
# shellcheck disable=SC2317 # "Command appears to be unreachable" - gets called indirectly.
tryBuild () {
local build="$1"
# Replace all staging repos, whether the project uses them or not (playing it safe...).
local repo
for repo in $(cd staging/src; find k8s.io -name go.mod); do
local path
repo=$(dirname "${repo}")
path="$(pwd)/staging/src/${repo}"
(
cd "$build"
go mod edit -replace "${repo}"="${path}"
)
done
# We only care about building. Breaking compilation of unit tests is also
# annoying, but does not affect downstream consumers.
(
cd "$build"
rm -rf vendor
go mod tidy
go build ./...
)
}
if [ $res -ne 0 ]; then
cat <<EOF
Some notes about API differences:
Changes in internal packages are usually okay.
However, remember that custom schedulers
and scheduler plugins depend on pkg/scheduler/framework.
API changes in staging repos are more critical.
Try to avoid them as much as possible.
But sometimes changing an API is the lesser evil
and/or the impact on downstream consumers is low.
Use common sense and code searches.
EOF
if [ ${#builds[@]} -gt 0 ]; then
cat <<EOF
To help with assessing the real-world impact of an
API change, $0 will now try to build code in
${builds[@]}.
EOF
if [[ "${builds[*]}" =~ controller-runtime ]]; then
cat <<EOF
controller-runtime is used because
- It tends to use advanced client-go functionality.
- Breaking it has additional impact on controller
built on top of it.
This doesn't mean that an API change isn't allowed
if it breaks controller runtime, it just needs additional
scrutiny.
https://github.com/kubernetes-sigs/controller-runtime?tab=readme-ov-file#compatibility
explicitly states that a controller-runtime
release cannot be expected to work with a newer
release of the Kubernetes Go packages.
EOF
fi
for build in "${builds[@]}"; do
echo
echo "vvvvvvvvvvvvvvvv ${build} vvvvvvvvvvvvvvvvvv"
if inTarget tryBuild "${build}"; then
echo "${build} builds without errors."
else
cat <<EOF
WARNING: Building ${build} failed. This may or may not be because of the API changes!
EOF
fi
echo "^^^^^^^^^^^^^^^^ ${build} ^^^^^^^^^^^^^^^^^^"
done
fi
fi
exit "$res"