mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
* feat: add WASM extension versioning with WIT compat checks and CI enforcement Phase 1 — WIT Versioning & Compatibility Checks: - Version WIT packages as `package near:[email protected];` - Add `semver` crate for version parsing and comparison - Add `WIT_TOOL_VERSION` / `WIT_CHANNEL_VERSION` host constants - Add `version` and `wit_version` fields to capabilities schemas - Add `wit_version` column to `wasm_tools` DB table (both backends) - Add load-time `check_wit_version_compat()` with semver rules - Add `IncompatibleWitVersion` error variants for tools and channels - Enhance instantiation errors with WIT version mismatch hints - Update all 14 capabilities JSON and 14 registry JSON files Phase 2 — Upgrade-in-Place & Channel DB Storage: - Change tool store to DELETE-before-INSERT (one version per extension) - Create `wasm_channels` table (PostgreSQL migration + libSQL schema) - Add `WasmChannelStore` trait with PostgreSQL and libSQL backends - Add `extension_info` tool showing version, WIT version, and status - Wire `ExtensionInfoTool` into tool registry (7 extension tools) Phase 3 — CI Version-Bump Enforcement: - Add `scripts/check-version-bumps.sh` checking WIT/tool/channel versions - Add `version-check` CI job (PR-only) to `.github/workflows/test.yml` - Support `[skip-version-check]` label/commit message bypass Includes 7 regression tests for WIT version compatibility checking and 2 integration tests for WIT version annotation verification. [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: address PR review feedback for WASM extension versioning - Wrap PostgreSQL DELETE+INSERT in transactions for both tool and channel store() methods to prevent data loss on partial failure (Gemini, Copilot) - Rename StoredWasmChannelWithBinary.tool → .channel (copy-paste fix) - Remove unused WasmError::IncompatibleWitVersion variant (dead code) - Map channel loader WIT mismatch to IncompatibleWitVersion instead of generic Config error, simplify variant to single String message - Fix extension_info description to match actual returned fields - Add schema test for ExtensionInfoTool matching existing test pattern - Fix CI script to fail fast on git errors instead of silent bypass [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
252 lines
8.0 KiB
Bash
Executable File
252 lines
8.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# CI script: check that version bumps accompany WIT or extension source changes.
|
|
# Exit 0 if all checks pass, exit 1 if any version wasn't bumped.
|
|
|
|
ERRORS=0
|
|
|
|
# --- Skip mechanism -----------------------------------------------------------
|
|
|
|
if [[ "${PR_LABELS:-}" == *"skip-version-check"* ]]; then
|
|
echo "skip-version-check label detected — skipping all version checks."
|
|
exit 0
|
|
fi
|
|
|
|
# Check commit messages for [skip-version-check]
|
|
if git log "origin/${GITHUB_BASE_REF:-main}...HEAD" --pretty=format:"%s %b" 2>/dev/null \
|
|
| grep -qF '[skip-version-check]'; then
|
|
echo "[skip-version-check] found in commit message — skipping all version checks."
|
|
exit 0
|
|
fi
|
|
|
|
# --- Determine base branch and changed files ----------------------------------
|
|
|
|
BASE_BRANCH="${GITHUB_BASE_REF:-main}"
|
|
echo "Base branch: $BASE_BRANCH"
|
|
|
|
# Ensure the base branch ref is available
|
|
if ! git rev-parse "origin/${BASE_BRANCH}" >/dev/null 2>&1; then
|
|
echo "Fetching origin/${BASE_BRANCH}..."
|
|
git fetch origin "$BASE_BRANCH" --depth=1
|
|
fi
|
|
|
|
CHANGED_FILES=$(git diff --name-only "origin/${BASE_BRANCH}...HEAD")
|
|
|
|
if [[ -z "$CHANGED_FILES" ]]; then
|
|
echo "No changed files detected. Nothing to check."
|
|
exit 0
|
|
fi
|
|
|
|
# --- Helper functions ---------------------------------------------------------
|
|
|
|
# Extract the version from a WIT package line like: package near:[email protected];
|
|
extract_wit_version() {
|
|
local file="$1"
|
|
if [[ ! -f "$file" ]]; then
|
|
echo ""
|
|
return
|
|
fi
|
|
sed -n 's/^[[:space:]]*package[[:space:]]\+[^@]*@\([0-9][0-9.]*[0-9]\)[[:space:]]*;.*/\1/p' "$file" \
|
|
| head -n1
|
|
}
|
|
|
|
# Extract version from the base branch copy of a file
|
|
extract_wit_version_base() {
|
|
local file="$1"
|
|
git show "origin/${BASE_BRANCH}:${file}" 2>/dev/null \
|
|
| sed -n 's/^[[:space:]]*package[[:space:]]\+[^@]*@\([0-9][0-9.]*[0-9]\)[[:space:]]*;.*/\1/p' \
|
|
| head -n1 || true
|
|
}
|
|
|
|
# Extract a Rust string constant value: pub const NAME: &str = "value";
|
|
extract_rust_const() {
|
|
local file="$1"
|
|
local const_name="$2"
|
|
if [[ ! -f "$file" ]]; then
|
|
echo ""
|
|
return
|
|
fi
|
|
sed -n "s/^.*${const_name}[[:space:]]*:[[:space:]]*&str[[:space:]]*=[[:space:]]*\"\([^\"]*\)\".*/\1/p" "$file" \
|
|
| head -n1
|
|
}
|
|
|
|
# Extract JSON "version" field using jq
|
|
extract_json_version() {
|
|
local file="$1"
|
|
if [[ ! -f "$file" ]]; then
|
|
echo ""
|
|
return
|
|
fi
|
|
jq -r '.version // empty' "$file" 2>/dev/null || true
|
|
}
|
|
|
|
# Extract JSON "version" from the base branch copy of a file
|
|
extract_json_version_base() {
|
|
local file="$1"
|
|
git show "origin/${BASE_BRANCH}:${file}" 2>/dev/null | jq -r '.version // empty' 2>/dev/null || true
|
|
}
|
|
|
|
# Return 0 if $1 (new) is strictly greater than $2 (old) via sort -V, or old is empty.
|
|
version_was_bumped() {
|
|
local new="$1"
|
|
local old="$2"
|
|
if [[ -z "$old" ]]; then
|
|
# No prior version — treat as new, no bump required
|
|
return 0
|
|
fi
|
|
if [[ -z "$new" ]]; then
|
|
# Version was removed — that's a problem
|
|
return 1
|
|
fi
|
|
if [[ "$new" == "$old" ]]; then
|
|
return 1
|
|
fi
|
|
# Check new > old via sort -V
|
|
local highest
|
|
highest=$(printf '%s\n%s\n' "$new" "$old" | sort -V | tail -n1)
|
|
[[ "$highest" == "$new" ]]
|
|
}
|
|
|
|
# --- 1. WIT changes ----------------------------------------------------------
|
|
|
|
WIT_TOOL_CHANGED=false
|
|
WIT_CHANNEL_CHANGED=false
|
|
|
|
if echo "$CHANGED_FILES" | grep -qx 'wit/tool\.wit'; then
|
|
WIT_TOOL_CHANGED=true
|
|
fi
|
|
if echo "$CHANGED_FILES" | grep -qx 'wit/channel\.wit'; then
|
|
WIT_CHANNEL_CHANGED=true
|
|
fi
|
|
|
|
if $WIT_TOOL_CHANGED; then
|
|
echo ""
|
|
echo "=== wit/tool.wit changed ==="
|
|
|
|
NEW_VER=$(extract_wit_version "wit/tool.wit")
|
|
OLD_VER=$(extract_wit_version_base "wit/tool.wit")
|
|
echo " WIT package version: ${OLD_VER:-<none>} -> ${NEW_VER:-<missing>}"
|
|
|
|
if ! version_was_bumped "${NEW_VER}" "${OLD_VER}"; then
|
|
echo " ERROR: wit/tool.wit package version was not bumped (${OLD_VER} -> ${NEW_VER:-<missing>})."
|
|
ERRORS=$((ERRORS + 1))
|
|
else
|
|
echo " OK: WIT package version bumped."
|
|
fi
|
|
|
|
# Check WIT_TOOL_VERSION constant matches
|
|
CONST_VER=$(extract_rust_const "src/tools/wasm/mod.rs" "WIT_TOOL_VERSION")
|
|
if [[ -n "$NEW_VER" && "$CONST_VER" != "$NEW_VER" ]]; then
|
|
echo " ERROR: WIT_TOOL_VERSION in src/tools/wasm/mod.rs is '${CONST_VER}' but wit/tool.wit has '${NEW_VER}'. They must match."
|
|
ERRORS=$((ERRORS + 1))
|
|
elif [[ -n "$NEW_VER" ]]; then
|
|
echo " OK: WIT_TOOL_VERSION matches wit/tool.wit."
|
|
fi
|
|
fi
|
|
|
|
if $WIT_CHANNEL_CHANGED; then
|
|
echo ""
|
|
echo "=== wit/channel.wit changed ==="
|
|
|
|
NEW_VER=$(extract_wit_version "wit/channel.wit")
|
|
OLD_VER=$(extract_wit_version_base "wit/channel.wit")
|
|
echo " WIT package version: ${OLD_VER:-<none>} -> ${NEW_VER:-<missing>}"
|
|
|
|
if ! version_was_bumped "${NEW_VER}" "${OLD_VER}"; then
|
|
echo " ERROR: wit/channel.wit package version was not bumped (${OLD_VER} -> ${NEW_VER:-<missing>})."
|
|
ERRORS=$((ERRORS + 1))
|
|
else
|
|
echo " OK: WIT package version bumped."
|
|
fi
|
|
|
|
# Check WIT_CHANNEL_VERSION constant matches
|
|
CONST_VER=$(extract_rust_const "src/tools/wasm/mod.rs" "WIT_CHANNEL_VERSION")
|
|
if [[ -n "$NEW_VER" && "$CONST_VER" != "$NEW_VER" ]]; then
|
|
echo " ERROR: WIT_CHANNEL_VERSION in src/tools/wasm/mod.rs is '${CONST_VER}' but wit/channel.wit has '${NEW_VER}'. They must match."
|
|
ERRORS=$((ERRORS + 1))
|
|
elif [[ -n "$NEW_VER" ]]; then
|
|
echo " OK: WIT_CHANNEL_VERSION matches wit/channel.wit."
|
|
fi
|
|
fi
|
|
|
|
if $WIT_TOOL_CHANGED || $WIT_CHANNEL_CHANGED; then
|
|
echo ""
|
|
echo " WARNING: WIT interface changed. All published registry extensions should bump their versions for compatibility."
|
|
fi
|
|
|
|
# --- 2. Tool source changes ---------------------------------------------------
|
|
|
|
TOOL_NAMES=$(echo "$CHANGED_FILES" | sed -n 's|^tools-src/\([^/]*\)/.*|\1|p' | sort -u)
|
|
|
|
if [[ -n "$TOOL_NAMES" ]]; then
|
|
echo ""
|
|
echo "=== Tool source changes ==="
|
|
fi
|
|
|
|
for tool in $TOOL_NAMES; do
|
|
REGISTRY_FILE="registry/tools/${tool}.json"
|
|
echo ""
|
|
echo " --- tools-src/${tool}/ changed ---"
|
|
|
|
if [[ ! -f "$REGISTRY_FILE" ]]; then
|
|
echo " SKIP: ${REGISTRY_FILE} does not exist yet (new extension?)."
|
|
continue
|
|
fi
|
|
|
|
NEW_VER=$(extract_json_version "$REGISTRY_FILE")
|
|
OLD_VER=$(extract_json_version_base "$REGISTRY_FILE")
|
|
|
|
echo " Registry version: ${OLD_VER:-<none>} -> ${NEW_VER:-<missing>}"
|
|
|
|
if ! version_was_bumped "${NEW_VER}" "${OLD_VER}"; then
|
|
echo " ERROR: ${REGISTRY_FILE} version was not bumped (${OLD_VER} -> ${NEW_VER:-<missing>}). Bump the version when changing tools-src/${tool}/."
|
|
ERRORS=$((ERRORS + 1))
|
|
else
|
|
echo " OK: version bumped."
|
|
fi
|
|
done
|
|
|
|
# --- 3. Channel source changes ------------------------------------------------
|
|
|
|
CHANNEL_NAMES=$(echo "$CHANGED_FILES" | sed -n 's|^channels-src/\([^/]*\)/.*|\1|p' | sort -u)
|
|
|
|
if [[ -n "$CHANNEL_NAMES" ]]; then
|
|
echo ""
|
|
echo "=== Channel source changes ==="
|
|
fi
|
|
|
|
for channel in $CHANNEL_NAMES; do
|
|
REGISTRY_FILE="registry/channels/${channel}.json"
|
|
echo ""
|
|
echo " --- channels-src/${channel}/ changed ---"
|
|
|
|
if [[ ! -f "$REGISTRY_FILE" ]]; then
|
|
echo " SKIP: ${REGISTRY_FILE} does not exist yet (new extension?)."
|
|
continue
|
|
fi
|
|
|
|
NEW_VER=$(extract_json_version "$REGISTRY_FILE")
|
|
OLD_VER=$(extract_json_version_base "$REGISTRY_FILE")
|
|
|
|
echo " Registry version: ${OLD_VER:-<none>} -> ${NEW_VER:-<missing>}"
|
|
|
|
if ! version_was_bumped "${NEW_VER}" "${OLD_VER}"; then
|
|
echo " ERROR: ${REGISTRY_FILE} version was not bumped (${OLD_VER} -> ${NEW_VER:-<missing>}). Bump the version when changing channels-src/${channel}/."
|
|
ERRORS=$((ERRORS + 1))
|
|
else
|
|
echo " OK: version bumped."
|
|
fi
|
|
done
|
|
|
|
# --- Summary ------------------------------------------------------------------
|
|
|
|
echo ""
|
|
if [[ $ERRORS -gt 0 ]]; then
|
|
echo "FAILED: ${ERRORS} version check(s) did not pass. See errors above."
|
|
exit 1
|
|
else
|
|
echo "All version checks passed."
|
|
exit 0
|
|
fi
|