blob: ce5bb42bac1050fbc59e7ec4babcc13edbacebd2 [file] [edit]
#!/usr/bin/env bash
# Copyright 2024 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# apidiff.sh: Compare public API changes between revisions or directories using Git worktrees.
set -euo pipefail
# Usage Information
usage() {
echo "Usage: $0 [-r <revision>] [-t <revision>] [directory ...]"
echo " -t <revision>: Report changes in code up to and including this revision."
echo " Default is the current working tree instead of a revision."
echo " -r <revision>: Report changes in code added since this revision."
echo " Default is the common base of master and HEAD."
exit 1
}
# Default Values
TARGET_REVISION="" # -t: Target revision
REFERENCE_REVISION="" # -r: Reference revision
TARGET_DIR="." # Default directory to compare is current working directory
API_DIFF_TOOL="apidiff"
REF_API_SNAPSHOT="ref.api"
TGT_API_SNAPSHOT="target.api"
WORKTREES=() # Track created worktrees for cleanup
# Parse Command-Line Arguments
while getopts ":t:r:" opt; do
case ${opt} in
t) TARGET_REVISION="$OPTARG" ;;
r) REFERENCE_REVISION="$OPTARG" ;;
\?) echo "Error: Invalid option -$OPTARG" >&2; usage ;;
:) echo "Error: Option -$OPTARG requires an argument." >&2; usage ;;
esac
done
shift $((OPTIND - 1))
# Remaining arguments are directories
if [ "$#" -ge 1 ]; then
TARGET_DIR="$1"
fi
# Debug print to see all traps
trap -p
# Step 1: Create a temporary directory structure under _output
mkdir -p "_output"
TMP_DIR=$(mktemp -d "_output/apidiff.XXXXXX")
TMP_DIR=$(cd "${TMP_DIR}" && pwd) # Convert to absolute path
TEMP_GOBIN="${TMP_DIR}/gobin"
TEMP_WORKTREES="${TMP_DIR}/worktrees"
mkdir -p "${TEMP_GOBIN}" "${TEMP_WORKTREES}"
# Single trap for cleanup
trap 'cleanup' EXIT
# shellcheck disable=SC2317
cleanup() {
# Remove all created worktrees
for worktree in "${WORKTREES[@]}"; do
git worktree remove --force "$worktree"
done
# Remove temporary directory with all contents
rm -rf "${TMP_DIR}"
}
# Update GOBIN to use temporary location
if ! command -v "${API_DIFF_TOOL}" &> /dev/null; then
echo "Installing apidiff into ${TEMP_GOBIN}"
GOBIN="${TEMP_GOBIN}" go install golang.org/x/exp/cmd/apidiff@latest
# Add GOBIN to PATH
export PATH=$PATH:${TEMP_GOBIN}
fi
# Set target revision: PULL_PULL_SHA > target > HEAD
if [ -z "${TARGET_REVISION}" ] && [ -n "${PULL_PULL_SHA:-}" ]; then
TARGET_REVISION="${PULL_PULL_SHA}"
elif [ -z "${TARGET_REVISION}" ]; then
TARGET_REVISION="HEAD"
fi
# Verify target commit exists
TARGET_REVISION="$(git rev-parse --verify "${TARGET_REVISION}")"
# Try to determine base revision if not explicitly set
if [ -z "${REFERENCE_REVISION}" ]; then
if [ -n "${PULL_BASE_SHA:-}" ]; then
# Use PULL_BASE_SHA directly as the base
REFERENCE_REVISION="${PULL_BASE_SHA}"
else
# Fall back to merge-base with origin/master
if ! REFERENCE_REVISION="$(git merge-base origin/master "${TARGET_REVISION}")"; then
echo "Error: Could not determine base revision. Please configure git remote 'origin' or use -r explicitly." >&2
exit 1
fi
fi
fi
# Verify base commit exists
REFERENCE_REVISION="$(git rev-parse --verify "${REFERENCE_REVISION}")"
# Step 2: Export API snapshot for the reference revision
REF_WORKTREE="${TEMP_WORKTREES}/ref"
echo "Creating Git worktree for reference revision: ${REFERENCE_REVISION}"
git worktree add "${REF_WORKTREE}" "${REFERENCE_REVISION}" --quiet
WORKTREES+=("${REF_WORKTREE}")
echo "Exporting API snapshot for reference revision..."
pushd "${REF_WORKTREE}" > /dev/null
"${API_DIFF_TOOL}" -m -w "${TMP_DIR}/${REF_API_SNAPSHOT}" "${TARGET_DIR}"
popd > /dev/null
# Step 3: Export API snapshot for the target revision
TGT_WORKTREE="${TEMP_WORKTREES}/target"
if [ -n "${TARGET_REVISION}" ]; then
echo "Creating Git worktree for target revision: ${TARGET_REVISION}"
git worktree add "${TGT_WORKTREE}" "${TARGET_REVISION}" --quiet
WORKTREES+=("${TGT_WORKTREE}")
TGT_PATH="${TGT_WORKTREE}"
else
# If no target revision specified, compare with current working tree
TGT_PATH="${TARGET_DIR}"
fi
echo "Exporting API snapshot for target revision..."
pushd "${TGT_PATH}" > /dev/null
"${API_DIFF_TOOL}" -m -w "${TMP_DIR}/${TGT_API_SNAPSHOT}" "${TARGET_DIR}"
popd > /dev/null
# Step 4: Compare the two API snapshots for incompatible changes
# Step 4: Compare the two API snapshots for changes
echo "Checking for API changes..."
# All changes
all_changes=$("${API_DIFF_TOOL}" -m "${TMP_DIR}/${REF_API_SNAPSHOT}" "${TMP_DIR}/${TGT_API_SNAPSHOT}" 2>&1 | grep -v -e "^Ignoring internal package" || true)
# Incompatible changes
incompatible_changes=$("${API_DIFF_TOOL}" -incompatible -m "${TMP_DIR}/${REF_API_SNAPSHOT}" "${TMP_DIR}/${TGT_API_SNAPSHOT}" 2>&1 | grep -v -e "^Ignoring internal package" || true)
# Print out results
echo
echo "API compatibility check completed."
res=0
if [ -n "$incompatible_changes" ]; then
res=1
echo "Incompatible API changes found!"
else
echo "No incompatible API changes found."
fi
if [ -z "$all_changes" ]; then
echo "No API changes found."
else
echo "All API changes:"
echo "$all_changes"
echo
fi
exit ${res}