Created
August 21, 2024 04:45
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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