Skip to content

Instantly share code, notes, and snippets.

@nevercast
Created August 21, 2024 04:45
Show Gist options
  • Save nevercast/af661b80ae912d393c60fcd34f6672d0 to your computer and use it in GitHub Desktop.
Save nevercast/af661b80ae912d393c60fcd34f6672d0 to your computer and use it in GitHub Desktop.
Find the last npm package that worked / find npm package that breaks build
#!/usr/bin/env bash
set -euo pipefail
usage() {
echo "Usage: $0 <[email protected]@minimum_version> <build_command> [search_mode]"
echo "Example: $0 '@hey-api/[email protected]..@hey-api/[email protected]' 'npm run build' [binary|scan]"
echo "Search mode is optional. Default is 'binary'."
exit 1
}
# Check if correct number of arguments are provided
if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then
usage
fi
VERSION_RANGE="$1"
BUILD_COMMAND="$2"
SEARCH_MODE="${3:-binary}"
if [ "$SEARCH_MODE" != "binary" ] && [ "$SEARCH_MODE" != "scan" ]; then
echo "Error: Invalid search mode. Use 'binary' or 'scan'."
usage
fi
# Extract package name, start version, and minimum version
PACKAGE=$(echo "$VERSION_RANGE" | sed -E 's/(.+)@[^@]+\.\..+/\1/')
START_VERSION=$(echo "$VERSION_RANGE" | sed -E 's/.+@([^@]+)\.\..+/\1/')
MIN_VERSION=$(echo "$VERSION_RANGE" | sed -E 's/.+\.\.@?[^@]+@(.+)/\1/')
# compare versions
version_lt() {
test "$(echo "$@" | tr " " "\n" | sort -rV | head -n 1)" != "$1"
}
fetch_and_filter_versions() {
local package="$1"
local start_version="$2"
local min_version="$3"
echo "Fetching available versions for $package..."
local versions
versions=$(npm view "$package" versions --json 2>/dev/null | jq -r '.[]' 2>/dev/null | sort -rV) || {
echo "Error: Failed to fetch versions for $package" >&2
return 1
}
echo -n "Versions within specified range: "
local filtered_versions=""
while read -r version; do
if version_lt "$version" "$start_version" || [ "$version" = "$start_version" ]; then
if version_lt "$min_version" "$version" || [ "$version" = "$min_version" ]; then
filtered_versions+="$version "
echo "$version" >> /tmp/filtered_versions.txt
fi
fi
done <<< "$versions"
echo "${filtered_versions% }" # Remove trailing space
}
install_package() {
local package="$1"
local version="$2"
echo "Installing $package@$version"
if ! npm install "$package@$version"; then
echo "Failed to install $package@$version" >&2
return 1
fi
return 0
}
test_build() {
local version="$1"
echo "Testing build with version $version"
if eval "$BUILD_COMMAND"; then
echo "Build successful with $PACKAGE version $version"
return 0
else
echo "Build failed with $PACKAGE version $version"
return 1
fi
}
binary_search() {
local versions=( $(cat /tmp/filtered_versions.txt) )
local count=${#versions[@]}
# Check the highest version (should fail)
if ! install_package "$PACKAGE" "${versions[0]}"; then
echo "Failed to install the highest version ${versions[0]}. Exiting." >&2
exit 1
fi
if test_build "${versions[0]}"; then
echo "The highest version ${versions[0]} unexpectedly succeeded. No downgrade needed."
exit 0
fi
# Check the lowest version (should succeed)
if ! install_package "$PACKAGE" "${versions[-1]}"; then
echo "Failed to install the lowest version ${versions[-1]}. Exiting." >&2
exit 1
fi
if ! test_build "${versions[-1]}"; then
echo "The lowest version ${versions[-1]} failed. No working version found in the specified range."
exit 1
fi
local low=0
local high=$((count - 1))
local mid
local last_working_version="${versions[-1]}"
local first_failing_version="${versions[0]}"
while [ $((low + 1)) -lt $high ]; do
mid=$(( (low + high) / 2 ))
version="${versions[mid]}"
if ! install_package "$PACKAGE" "$version"; then
echo "Failed to install $PACKAGE@$version. Skipping this version." >&2
low=$((mid + 1))
continue
fi
if test_build "$version"; then
last_working_version="$version"
high=$mid
else
first_failing_version="$version"
low=$mid
fi
done
echo "Final successful version: $last_working_version"
echo "First failing version: $first_failing_version"
}
scan_search() {
local versions=( $(cat /tmp/filtered_versions.txt) )
local current_version="${versions[0]}"
local previous_version=""
echo "Starting version: $current_version"
for version in "${versions[@]}"; do
echo "Current version: $version"
if ! install_package "$PACKAGE" "$version"; then
echo "Failed to install $PACKAGE@$version. Skipping this version." >&2
continue
fi
if test_build "$version"; then
if [ -n "$previous_version" ]; then
echo "Build successful with $PACKAGE version $version, version $previous_version breaks the build."
else
echo "Build successful with $PACKAGE version $version (highest version tested)."
fi
echo "Final successful version: $version"
if [ -n "$previous_version" ]; then
echo "First failing version: $previous_version"
fi
break
else
previous_version="$version"
fi
done
if [ -z "$previous_version" ]; then
echo "All versions failed. No working version found in the specified range."
exit 1
fi
}
# main starts here
if ! fetch_and_filter_versions "$PACKAGE" "$START_VERSION" "$MIN_VERSION"; then
echo "Failed to fetch versions. Exiting." >&2
exit 1
fi
if [ "$SEARCH_MODE" = "binary" ]; then
echo "Performing binary search..."
binary_search
elif [ "$SEARCH_MODE" = "scan" ]; then
echo "Performing scan search..."
scan_search
fi
# Clean up
rm -f /tmp/filtered_versions.txt
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment