mirror of
https://github.com/rangermix/TwitchDropsMiner.git
synced 2026-05-30 17:09:36 +00:00
feat: enhance version validation with semver range support and test suite
- Add dual-mode version extraction supporting branch and file validation - Implement comprehensive semver range validation (caret, tilde, wildcards) - Extract release creation logic to dedicated script for reusability - Add test suite with comprehensive coverage for validation scripts - Update workflow to validate new version is greater than current - Bump version to 1.1.0 in pyproject.toml to match version.py 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
259
.github/scripts/validate_semver.sh
vendored
259
.github/scripts/validate_semver.sh
vendored
@@ -13,17 +13,255 @@ NC='\033[0m' # No Color
|
||||
|
||||
# Display usage information
|
||||
usage() {
|
||||
echo "Usage: $0 <version>"
|
||||
echo "Usage: $0 <version> [range]"
|
||||
echo ""
|
||||
echo "Validates a version string against SemVer specification."
|
||||
echo "Optionally validates that the version satisfies a semver range."
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 1.2.3"
|
||||
echo " $0 2.0.0-rc.1"
|
||||
echo " $0 1.0.0-beta.2+build.123"
|
||||
echo ""
|
||||
echo "Range validation examples:"
|
||||
echo " $0 1.2.3 '>=1.0.0' # Greater than or equal"
|
||||
echo " $0 2.5.0 '^2.0.0' # Caret range (compatible with 2.x.x)"
|
||||
echo " $0 1.2.5 '~1.2.0' # Tilde range (compatible with 1.2.x)"
|
||||
echo " $0 3.1.4 '>=1.0.0 <4.0.0' # Compound range"
|
||||
echo " $0 1.5.2 '1.x' # Wildcard range"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse version string into components
|
||||
# Usage: parse_version <version>
|
||||
# Outputs: major minor patch prerelease
|
||||
parse_version() {
|
||||
local version="$1"
|
||||
# Remove build metadata
|
||||
version="${version%%+*}"
|
||||
|
||||
# Extract prerelease if present
|
||||
local prerelease=""
|
||||
if [[ "$version" =~ - ]]; then
|
||||
prerelease="${version#*-}"
|
||||
version="${version%%-*}"
|
||||
fi
|
||||
|
||||
# Parse major.minor.patch
|
||||
local major="${version%%.*}"
|
||||
local rest="${version#*.}"
|
||||
local minor="${rest%%.*}"
|
||||
local patch="${rest#*.}"
|
||||
|
||||
echo "$major $minor $patch $prerelease"
|
||||
}
|
||||
|
||||
# Compare two versions
|
||||
# Returns: 0 if v1 == v2, 1 if v1 > v2, -1 if v1 < v2
|
||||
compare_versions() {
|
||||
local v1="$1"
|
||||
local v2="$2"
|
||||
|
||||
read -r maj1 min1 pat1 pre1 <<< "$(parse_version "$v1")"
|
||||
read -r maj2 min2 pat2 pre2 <<< "$(parse_version "$v2")"
|
||||
|
||||
# Compare major
|
||||
if [ "$maj1" -gt "$maj2" ]; then echo 1; return; fi
|
||||
if [ "$maj1" -lt "$maj2" ]; then echo -1; return; fi
|
||||
|
||||
# Compare minor
|
||||
if [ "$min1" -gt "$min2" ]; then echo 1; return; fi
|
||||
if [ "$min1" -lt "$min2" ]; then echo -1; return; fi
|
||||
|
||||
# Compare patch
|
||||
if [ "$pat1" -gt "$pat2" ]; then echo 1; return; fi
|
||||
if [ "$pat1" -lt "$pat2" ]; then echo -1; return; fi
|
||||
|
||||
# Compare prerelease (version with prerelease < version without)
|
||||
if [ -z "$pre1" ] && [ -n "$pre2" ]; then echo 1; return; fi
|
||||
if [ -n "$pre1" ] && [ -z "$pre2" ]; then echo -1; return; fi
|
||||
|
||||
# Both have prerelease or both don't - lexicographic comparison
|
||||
if [ "$pre1" \> "$pre2" ]; then echo 1; return; fi
|
||||
if [ "$pre1" \< "$pre2" ]; then echo -1; return; fi
|
||||
|
||||
echo 0
|
||||
}
|
||||
|
||||
# Check if version satisfies a single range constraint
|
||||
# Usage: check_constraint <version> <operator> <target>
|
||||
check_constraint() {
|
||||
local version="$1"
|
||||
local operator="$2"
|
||||
local target="$3"
|
||||
local cmp
|
||||
|
||||
cmp=$(compare_versions "$version" "$target")
|
||||
|
||||
case "$operator" in
|
||||
"="|"")
|
||||
[ "$cmp" -eq 0 ]
|
||||
;;
|
||||
">")
|
||||
[ "$cmp" -eq 1 ]
|
||||
;;
|
||||
">=")
|
||||
[ "$cmp" -eq 1 ] || [ "$cmp" -eq 0 ]
|
||||
;;
|
||||
"<")
|
||||
[ "$cmp" -eq -1 ]
|
||||
;;
|
||||
"<=")
|
||||
[ "$cmp" -eq -1 ] || [ "$cmp" -eq 0 ]
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Check if version satisfies caret range (^)
|
||||
# ^1.2.3 := >=1.2.3 <2.0.0
|
||||
# ^0.2.3 := >=0.2.3 <0.3.0
|
||||
# ^0.0.3 := >=0.0.3 <0.0.4
|
||||
check_caret_range() {
|
||||
local version="$1"
|
||||
local target="$2"
|
||||
|
||||
read -r maj min pat pre <<< "$(parse_version "$target")"
|
||||
|
||||
# Must be >= target
|
||||
if ! check_constraint "$version" ">=" "$target"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Determine upper bound based on leftmost non-zero component
|
||||
local upper
|
||||
if [ "$maj" != "0" ]; then
|
||||
upper="$((maj + 1)).0.0"
|
||||
elif [ "$min" != "0" ]; then
|
||||
upper="0.$((min + 1)).0"
|
||||
else
|
||||
upper="0.0.$((pat + 1))"
|
||||
fi
|
||||
|
||||
check_constraint "$version" "<" "$upper"
|
||||
}
|
||||
|
||||
# Check if version satisfies tilde range (~)
|
||||
# ~1.2.3 := >=1.2.3 <1.3.0
|
||||
# ~1.2 := >=1.2.0 <1.3.0
|
||||
check_tilde_range() {
|
||||
local version="$1"
|
||||
local target="$2"
|
||||
|
||||
read -r maj min pat pre <<< "$(parse_version "$target")"
|
||||
|
||||
# Must be >= target
|
||||
if ! check_constraint "$version" ">=" "$target"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Upper bound is next minor version
|
||||
local upper="$maj.$((min + 1)).0"
|
||||
|
||||
check_constraint "$version" "<" "$upper"
|
||||
}
|
||||
|
||||
# Check if version matches wildcard pattern
|
||||
# 1.x, 1.X, 1.*, 1.2.x, etc.
|
||||
check_wildcard() {
|
||||
local version="$1"
|
||||
local pattern="$2"
|
||||
|
||||
read -r v_maj v_min v_pat v_pre <<< "$(parse_version "$version")"
|
||||
|
||||
# Normalize shorthand patterns: "1.x" -> "1.x.x", "1" -> "1.x.x"
|
||||
local dot_count
|
||||
dot_count=$(echo "$pattern" | tr -cd '.' | wc -c)
|
||||
|
||||
if [ "$dot_count" -eq 0 ]; then
|
||||
# Just "1" or "1x" -> "1.x.x"
|
||||
pattern="${pattern}.x.x"
|
||||
elif [ "$dot_count" -eq 1 ]; then
|
||||
# "1.2" or "1.x" -> "1.2.x" or "1.x.x"
|
||||
pattern="${pattern}.x"
|
||||
fi
|
||||
|
||||
# Replace wildcards with a placeholder that won't conflict
|
||||
pattern="${pattern//x/__WILDCARD__}"
|
||||
pattern="${pattern//X/__WILDCARD__}"
|
||||
pattern="${pattern//\*/__WILDCARD__}"
|
||||
|
||||
# Escape dots
|
||||
pattern="${pattern//./\\.}"
|
||||
|
||||
# Replace placeholders with regex pattern for any number
|
||||
pattern="${pattern//__WILDCARD__/[0-9]+}"
|
||||
|
||||
# Add start/end anchors and optional prerelease/build metadata
|
||||
pattern="^${pattern}(-.*)?(\+.*)?$"
|
||||
|
||||
echo "$version" | grep -Pq "$pattern"
|
||||
}
|
||||
|
||||
# Check if version satisfies a range expression
|
||||
satisfies_range() {
|
||||
local version="$1"
|
||||
local range="$2"
|
||||
|
||||
# Handle caret range
|
||||
if [[ "$range" =~ ^\^(.+)$ ]]; then
|
||||
check_caret_range "$version" "${BASH_REMATCH[1]}"
|
||||
return $?
|
||||
fi
|
||||
|
||||
# Handle tilde range
|
||||
if [[ "$range" =~ ^~(.+)$ ]]; then
|
||||
check_tilde_range "$version" "${BASH_REMATCH[1]}"
|
||||
return $?
|
||||
fi
|
||||
|
||||
# Handle wildcard
|
||||
if [[ "$range" =~ [xX*] ]]; then
|
||||
check_wildcard "$version" "$range"
|
||||
return $?
|
||||
fi
|
||||
|
||||
# Handle compound ranges (space-separated)
|
||||
if [[ "$range" =~ [[:space:]] ]]; then
|
||||
local all_satisfied=true
|
||||
local constraint
|
||||
|
||||
# Split on spaces and process each constraint
|
||||
while read -r constraint; do
|
||||
[ -z "$constraint" ] && continue
|
||||
|
||||
if ! satisfies_range "$version" "$constraint"; then
|
||||
all_satisfied=false
|
||||
break
|
||||
fi
|
||||
done <<< "$(echo "$range" | tr ' ' '\n')"
|
||||
|
||||
[ "$all_satisfied" = true ]
|
||||
return $?
|
||||
fi
|
||||
|
||||
# Handle simple operator ranges (>=, >, <=, <, =)
|
||||
if [[ "$range" =~ ^(>=|>|<=|<|=)?(.+)$ ]]; then
|
||||
local operator="${BASH_REMATCH[1]}"
|
||||
local target="${BASH_REMATCH[2]}"
|
||||
|
||||
# Default to = if no operator
|
||||
[ -z "$operator" ] && operator="="
|
||||
|
||||
check_constraint "$version" "$operator" "$target"
|
||||
return $?
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Check if version argument is provided
|
||||
if [ $# -eq 0 ]; then
|
||||
echo -e "${RED}Error: No version specified${NC}"
|
||||
@@ -31,6 +269,7 @@ if [ $# -eq 0 ]; then
|
||||
fi
|
||||
|
||||
VERSION="$1"
|
||||
RANGE="${2:-}"
|
||||
|
||||
# SemVer regex from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
|
||||
SEMVER_REGEX='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?$'
|
||||
@@ -48,6 +287,19 @@ if ! echo "$VERSION" | grep -Pq "$SEMVER_REGEX"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate range if provided
|
||||
if [ -n "$RANGE" ]; then
|
||||
if satisfies_range "$VERSION" "$RANGE"; then
|
||||
echo -e "${GREEN}✓ Version '$VERSION' satisfies range '$RANGE'${NC}"
|
||||
RANGE_SATISFIED=true
|
||||
else
|
||||
echo -e "${RED}Error: Version '$VERSION' does not satisfy range '$RANGE'${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
RANGE_SATISFIED=false
|
||||
fi
|
||||
|
||||
# Determine release type
|
||||
if echo "$VERSION" | grep -q '-'; then
|
||||
echo -e "${YELLOW}✓ Detected pre-release version: $VERSION${NC}"
|
||||
@@ -66,6 +318,11 @@ if [ -n "${GITHUB_OUTPUT:-}" ]; then
|
||||
else
|
||||
echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# Output range validation result if range was provided
|
||||
if [ -n "$RANGE" ]; then
|
||||
echo "range_satisfied=$RANGE_SATISFIED" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
||||
Reference in New Issue
Block a user