chore: versioning guardrail script for the structural checks
scripts/check-versioning.sh — POSIX sh, no dependencies, runs in
under a second. Three structural checks that don't need git
history (the parts that do need it stay deferred until we have CI
and a CHANGELOG file):
1. Migration filenames are sequential 0001_*.sql, 0002_*.sql, ...
with no gaps or duplicates. Catches "added migration with
the wrong number" before it reaches review.
2. SDK_VERSION in shared::version parses as MAJOR.MINOR
(numeric, no extra components). Catches accidental
PATCH-style bumps like "1.1.0" that the SemVer-for-SDKs
rule in docs/versioning.md forbids.
3. [workspace.package].version parses as MAJOR.MINOR.PATCH
(numeric). Catches typos in the product version bump
that would silently downgrade everywhere.
Each check prints a precise FAIL message identifying the
offending file/value when it trips. Verified by deliberately
breaking each one and confirming exit=1.
Run manually as `bash scripts/check-versioning.sh` for now; wires
into CI as soon as we have one. Docs/versioning.md updated to
reflect that items (3) and (4) are now in place and (5) is partly
implemented.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -93,12 +93,14 @@ A versioning scheme without enforcement decays in months. Five cheap mechanical
|
|||||||
2. **Runtime self-report.** `GET /version` returns every surface version. Dashboards, monitoring, inter-service handshakes, and humans all read from one source. `/healthz` stays a plain `"ok"` string for k8s probes — version negotiation is a separate concern.
|
2. **Runtime self-report.** `GET /version` returns every surface version. Dashboards, monitoring, inter-service handshakes, and humans all read from one source. `/healthz` stays a plain `"ok"` string for k8s probes — version negotiation is a separate concern.
|
||||||
3. **Golden SDK contract tests.** `tests/sdk_contract/` Rhai scripts exercise every SDK surface and must pass on every commit. The contract is the test.
|
3. **Golden SDK contract tests.** `tests/sdk_contract/` Rhai scripts exercise every SDK surface and must pass on every commit. The contract is the test.
|
||||||
4. **Migration replay test.** An integration test that boots a fresh Postgres, applies every migration in order, and asserts the resulting schema. Catches the most common mistake (edited-not-added migration).
|
4. **Migration replay test.** An integration test that boots a fresh Postgres, applies every migration in order, and asserts the resulting schema. Catches the most common mistake (edited-not-added migration).
|
||||||
5. **CI guardrail script.** A small diff-aware check that:
|
5. **CI guardrail script.** [`scripts/check-versioning.sh`](../scripts/check-versioning.sh) — runs the structural checks that don't need git history:
|
||||||
- Fails if `SDK_VERSION`'s major changed without a `CHANGELOG.md` breaking-change entry
|
- Migration files are numbered sequentially from `0001_*.sql` with no gaps.
|
||||||
- Fails if a new file appeared in `migrations/` that isn't the next sequential number
|
- `SDK_VERSION` parses as `MAJOR.MINOR` (numeric, no extra components).
|
||||||
- Fails if a route handler removed or retyped a public field without a `BREAKING:` line in the commit message
|
- `[workspace.package].version` parses as `MAJOR.MINOR.PATCH`.
|
||||||
|
|
||||||
(3) through (5) are wired in over the next few PRs; (1) and (2) land in the same commit as this document.
|
Run manually as `bash scripts/check-versioning.sh`. Wires into CI when CI exists. Deferred to the same future PR that introduces CI: SDK-major-bump-needs-CHANGELOG and `BREAKING:` commit-message annotation (both need git history + a CHANGELOG file that doesn't exist yet).
|
||||||
|
|
||||||
|
(3) and (4) are now in place: [`crates/executor-core/tests/sdk_contract.rs`](../crates/executor-core/tests/sdk_contract.rs) holds the SDK contract suite; [`crates/manager-core/tests/schema_snapshot.rs`](../crates/manager-core/tests/schema_snapshot.rs) holds the schema snapshot guard.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
126
scripts/check-versioning.sh
Executable file
126
scripts/check-versioning.sh
Executable file
@@ -0,0 +1,126 @@
|
|||||||
|
#!/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}<name>.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'
|
||||||
Reference in New Issue
Block a user