#!/bin/sh # Versioning guardrail — runs the structural checks from # docs/versioning.md that don't need git history. Designed to be # called from a CI job (once we have one) and/or as a pre-commit # step. Exits 0 if everything is in shape, non-zero on the first # failure with a precise message. # # What this DOES check: # * Migration filenames are sequential `0001_*.sql`, `0002_*.sql`, # ... starting from 0001 with no gaps and no duplicates. # * SDK_VERSION in shared::version parses as MAJOR.MINOR (numeric). # * Workspace product version in Cargo.toml parses as # MAJOR.MINOR.PATCH (numeric). # # What this does NOT check (deferred until we have CI + a CHANGELOG # file): # * Whether an SDK major bump was paired with a CHANGELOG entry. # * Whether commits that retype public fields carry a `BREAKING:` # annotation in the commit message. # # Usage: bash scripts/check-versioning.sh set -eu # Resolve repo root from this script's location so the checks run no # matter what working directory the caller is in. SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" fail() { printf 'check-versioning: FAIL — %s\n' "$1" >&2 exit 1 } # ---------------------------------------------------------------------- # 1. Migration filenames sequential # ---------------------------------------------------------------------- MIGRATIONS_DIR="$REPO_ROOT/crates/manager-core/migrations" [ -d "$MIGRATIONS_DIR" ] || fail "migrations dir not found at $MIGRATIONS_DIR" i=1 for file in "$MIGRATIONS_DIR"/*.sql; do [ -e "$file" ] || fail "no migration files found in $MIGRATIONS_DIR" base="$(basename "$file")" expected_prefix="$(printf '%04d_' "$i")" case "$base" in "$expected_prefix"*) ;; *) fail "migration $base is not next-in-sequence (expected ${expected_prefix}.sql); migrations must be added with strictly increasing 4-digit numbers" ;; esac i=$((i + 1)) done printf 'check-versioning: OK — %d migration(s) numbered sequentially\n' "$((i - 1))" # ---------------------------------------------------------------------- # 2. SDK_VERSION format # ---------------------------------------------------------------------- SDK_FILE="$REPO_ROOT/crates/shared/src/version.rs" [ -f "$SDK_FILE" ] || fail "shared::version not found at $SDK_FILE" SDK_VERSION="$( awk '/^pub const SDK_VERSION/ { match($0, /"[^"]+"/); print substr($0, RSTART+1, RLENGTH-2); exit }' "$SDK_FILE" )" [ -n "$SDK_VERSION" ] || fail "could not parse SDK_VERSION from $SDK_FILE" case "$SDK_VERSION" in [0-9]*"."[0-9]*) # Reject things like "1.2.3" or "v1.2" or empty parts. major="${SDK_VERSION%%.*}" minor="${SDK_VERSION#*.}" case "$major" in ''|*[!0-9]*) fail "SDK_VERSION '$SDK_VERSION' major is not numeric" ;; esac case "$minor" in ''|*[!0-9]*) fail "SDK_VERSION '$SDK_VERSION' minor is not numeric (extra components?)" ;; esac ;; *) fail "SDK_VERSION '$SDK_VERSION' is not MAJOR.MINOR (expected e.g. '1.1')" ;; esac printf 'check-versioning: OK — SDK_VERSION = %s\n' "$SDK_VERSION" # ---------------------------------------------------------------------- # 3. Workspace product version (semver MAJOR.MINOR.PATCH) # ---------------------------------------------------------------------- ROOT_CARGO="$REPO_ROOT/Cargo.toml" [ -f "$ROOT_CARGO" ] || fail "workspace Cargo.toml not found" PRODUCT_VERSION="$( awk ' /^\[workspace\.package\]/ { in_section = 1; next } /^\[/ { in_section = 0 } in_section && /^version *= */ { match($0, /"[^"]+"/) print substr($0, RSTART+1, RLENGTH-2) exit } ' "$ROOT_CARGO" )" [ -n "$PRODUCT_VERSION" ] || fail "could not parse [workspace.package].version from $ROOT_CARGO" case "$PRODUCT_VERSION" in [0-9]*"."[0-9]*"."[0-9]*) major="${PRODUCT_VERSION%%.*}" rest="${PRODUCT_VERSION#*.}" minor="${rest%%.*}" patch="${rest#*.}" for part_name in major minor patch; do eval "part=\$$part_name" case "$part" in ''|*[!0-9]*) fail "product version '$PRODUCT_VERSION' has non-numeric $part_name component" ;; esac done ;; *) fail "product version '$PRODUCT_VERSION' is not MAJOR.MINOR.PATCH" ;; esac printf 'check-versioning: OK — product version = %s\n' "$PRODUCT_VERSION" printf '\ncheck-versioning: all checks passed.\n'