build: add version synchronization gate

This commit is contained in:
Kayshen-X 2026-07-15 21:10:09 +08:00
parent 68579c4c86
commit e5b7425f3a
22 changed files with 682 additions and 74 deletions

View file

@ -1,30 +1,14 @@
#!/bin/sh
#
# Pre-commit hook: read version from branch name (e.g. v0.5.0)
# and sync to all package.json files
#
set -eu
BRANCH=$(git rev-parse --abbrev-ref HEAD)
script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd)
repo_root=$(CDPATH= cd "$script_dir/.." && pwd)
cd "$repo_root"
# Extract version from branch name like v0.5.0, v1.2.3, release/v2.0.0
VERSION=$(echo "$BRANCH" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || true)
if [ -n "$VERSION" ]; then
CHANGED=0
for f in package.json apps/*/package.json packages/*/package.json; do
if [ -f "$f" ]; then
CURRENT=$(jq -r '.version' "$f")
if [ "$CURRENT" != "$VERSION" ]; then
jq --arg v "$VERSION" '.version=$v' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
git add "$f"
CHANGED=1
fi
fi
done
if [ "$CHANGED" = "1" ]; then
echo "[version-sync] branch $BRANCH → $VERSION"
fi
echo "[pre-commit] checking version consistency"
if ! tools/check-version-sync.sh; then
echo "[pre-commit] version consistency check failed. Run: scripts/sync-version.sh"
exit 1
fi
# --- Rust format + lint gates (same checks as CI) ---

102
.github/workflows/version-sync.yml vendored Normal file
View file

@ -0,0 +1,102 @@
name: Version consistency
on:
pull_request:
paths:
- "Cargo.toml"
- "Cargo.lock"
- "crates/**/Cargo.toml"
- "crates/op-cli/assets/skill-bundle.json"
- "crates/op-cli/src/app_control_cli.rs"
- "crates/op-editor-core/src/host_support.rs"
- "crates/op-editor-core/src/state.rs"
- "packages/package.json"
- "packages/bun.lock"
- "packages/op-web-sdk/package.json"
- "packages/op-web-sdk/src/index.ts"
- "packages/op-web-sdk-react/package.json"
- "packages/op-web-sdk-react/src/index.ts"
- "packages/op-web-sdk-vue/package.json"
- "packages/op-web-sdk-vue/src/index.ts"
- "packages/scripts/sync-version.mjs"
- "packages/scripts/sync-version.test.mjs"
- "scripts/workspace-version.sh"
- "scripts/workspace-version.test.sh"
- "scripts/sync-version.sh"
- "scripts/sync-version.test.sh"
- "scripts/bundle-macos.sh"
- "scripts/package-windows.nsi"
- "scripts/install-op.sh"
- "tools/check-version-sync.sh"
- "tools/check-version-sync.test.sh"
- "tools/bundle-macos.sh"
- ".githooks/pre-commit"
- ".github/workflows/rust-release.yml"
- ".github/workflows/version-sync.yml"
push:
paths:
- "Cargo.toml"
- "Cargo.lock"
- "crates/**/Cargo.toml"
- "crates/op-cli/assets/skill-bundle.json"
- "crates/op-cli/src/app_control_cli.rs"
- "crates/op-editor-core/src/host_support.rs"
- "crates/op-editor-core/src/state.rs"
- "packages/package.json"
- "packages/bun.lock"
- "packages/op-web-sdk/package.json"
- "packages/op-web-sdk/src/index.ts"
- "packages/op-web-sdk-react/package.json"
- "packages/op-web-sdk-react/src/index.ts"
- "packages/op-web-sdk-vue/package.json"
- "packages/op-web-sdk-vue/src/index.ts"
- "packages/scripts/sync-version.mjs"
- "packages/scripts/sync-version.test.mjs"
- "scripts/workspace-version.sh"
- "scripts/workspace-version.test.sh"
- "scripts/sync-version.sh"
- "scripts/sync-version.test.sh"
- "scripts/bundle-macos.sh"
- "scripts/package-windows.nsi"
- "scripts/install-op.sh"
- "tools/check-version-sync.sh"
- "tools/check-version-sync.test.sh"
- "tools/bundle-macos.sh"
- ".githooks/pre-commit"
- ".github/workflows/rust-release.yml"
- ".github/workflows/version-sync.yml"
permissions:
contents: read
jobs:
version-sync:
name: Verify synchronized versions
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install Bun
uses: oven-sh/setup-bun@v2
- name: Install SDK dependencies
working-directory: packages
run: bun install --frozen-lockfile
- name: Test canonical version reader
run: scripts/workspace-version.test.sh
- name: Test version synchronization entrypoint
run: scripts/sync-version.test.sh
- name: Test repository version guard
run: tools/check-version-sync.test.sh
- name: Check repository version consistency
run: tools/check-version-sync.sh

View file

@ -23,7 +23,7 @@ Tooling is **Cargo** (Rust — the product). The root has **no `package.json`**;
- **CLI:** `cargo build -p op-cli` → binary `op`
- **MCP server:** built into the desktop/web host (`--mcp <path>`); crate `op-mcp`
- **Iconify catalog (Rust assets):** from `packages/`: `bun run generate-iconify-catalog`
- **Sync SDK versions:** from `packages/`: `bun run sync-version` (reads the canonical version from root `Cargo.toml`); verify with `bun run sync-version:check`
- **Sync all managed versions:** `scripts/sync-version.sh` reads the canonical version from root `Cargo.toml`; verify without writing via `tools/check-version-sync.sh`
## Architecture

View file

@ -378,8 +378,8 @@ cargo run -p op-cli -- <args> # CLI (binary: op)
# Web SDK / JS tooling (run from packages/)
cd packages && bun run lint # Lint the web SDK (oxlint); also: bun run format
cd packages && bun run generate-iconify-catalog # Regenerate the Rust icon catalog assets
cd packages && bun run sync-version # Sync SDK versions from root Cargo.toml
cd packages && bun run sync-version:check # Verify SDK versions match root Cargo.toml
scripts/sync-version.sh # Sync all managed versions from root Cargo.toml
tools/check-version-sync.sh # Verify all managed versions match root Cargo.toml
```
## Mitwirken

View file

@ -341,7 +341,7 @@ openpencil/
│ ├── jian/ Framework de UI GPU-Skia — widgets/render/eventos
│ ├── casement/ Fork de winit
│ └── agent/ Runtime de agente Rust multi-producto (agent-rs)
└── .githooks/ Sincronización de versión pre-commit desde nombre de rama
└── .githooks/ Comprobación de coherencia de versiones en pre-commit
```
## Atajos de Teclado
@ -378,8 +378,8 @@ cargo run -p op-cli -- <args> # CLI (binary: op)
# Web SDK / JS tooling (run from packages/)
cd packages && bun run lint # Lint the web SDK (oxlint); also: bun run format
cd packages && bun run generate-iconify-catalog # Regenerate the Rust icon catalog assets
cd packages && bun run sync-version # Sync SDK versions from root Cargo.toml
cd packages && bun run sync-version:check # Verify SDK versions match root Cargo.toml
scripts/sync-version.sh # Sync all managed versions from root Cargo.toml
tools/check-version-sync.sh # Verify all managed versions match root Cargo.toml
```
## Contribuir

View file

@ -341,7 +341,7 @@ openpencil/
│ ├── jian/ Framework UI GPU-Skia — widgets/rendu/événements
│ ├── casement/ Fork de winit
│ └── agent/ Runtime d'agent Rust transversal (agent-rs)
└── .githooks/ Synchronisation de version pre-commit depuis le nom de branche
└── .githooks/ Vérification de cohérence des versions en pre-commit
```
## Raccourcis clavier
@ -378,8 +378,8 @@ cargo run -p op-cli -- <args> # CLI (binary: op)
# Web SDK / JS tooling (run from packages/)
cd packages && bun run lint # Lint the web SDK (oxlint); also: bun run format
cd packages && bun run generate-iconify-catalog # Regenerate the Rust icon catalog assets
cd packages && bun run sync-version # Sync SDK versions from root Cargo.toml
cd packages && bun run sync-version:check # Verify SDK versions match root Cargo.toml
scripts/sync-version.sh # Sync all managed versions from root Cargo.toml
tools/check-version-sync.sh # Verify all managed versions match root Cargo.toml
```
## Contribuer

View file

@ -378,8 +378,8 @@ cargo run -p op-cli -- <args> # CLI (binary: op)
# Web SDK / JS tooling (run from packages/)
cd packages && bun run lint # Lint the web SDK (oxlint); also: bun run format
cd packages && bun run generate-iconify-catalog # Regenerate the Rust icon catalog assets
cd packages && bun run sync-version # Sync SDK versions from root Cargo.toml
cd packages && bun run sync-version:check # Verify SDK versions match root Cargo.toml
scripts/sync-version.sh # Sync all managed versions from root Cargo.toml
tools/check-version-sync.sh # Verify all managed versions match root Cargo.toml
```
## योगदान

View file

@ -378,8 +378,8 @@ cargo run -p op-cli -- <args> # CLI (binary: op)
# Web SDK / JS tooling (run from packages/)
cd packages && bun run lint # Lint the web SDK (oxlint); also: bun run format
cd packages && bun run generate-iconify-catalog # Regenerate the Rust icon catalog assets
cd packages && bun run sync-version # Sync SDK versions from root Cargo.toml
cd packages && bun run sync-version:check # Verify SDK versions match root Cargo.toml
scripts/sync-version.sh # Sync all managed versions from root Cargo.toml
tools/check-version-sync.sh # Verify all managed versions match root Cargo.toml
```
## Berkontribusi

View file

@ -378,8 +378,8 @@ cargo run -p op-cli -- <args> # CLI (binary: op)
# Web SDK / JS tooling (run from packages/)
cd packages && bun run lint # Lint the web SDK (oxlint); also: bun run format
cd packages && bun run generate-iconify-catalog # Regenerate the Rust icon catalog assets
cd packages && bun run sync-version # Sync SDK versions from root Cargo.toml
cd packages && bun run sync-version:check # Verify SDK versions match root Cargo.toml
scripts/sync-version.sh # Sync all managed versions from root Cargo.toml
tools/check-version-sync.sh # Verify all managed versions match root Cargo.toml
```
## コントリビュート

View file

@ -341,7 +341,7 @@ openpencil/
│ ├── jian/ GPU-Skia UI 프레임워크 — 위젯/렌더/이벤트
│ ├── casement/ winit 포크
│ └── agent/ 제품 공용 Rust 에이전트 런타임 (agent-rs)
└── .githooks/ 브랜치 이름에서 버전 동기화를 위한 pre-commit
└── .githooks/ pre-commit 버전 일관성 검사
```
## 키보드 단축키
@ -378,8 +378,8 @@ cargo run -p op-cli -- <args> # CLI (binary: op)
# Web SDK / JS tooling (run from packages/)
cd packages && bun run lint # Lint the web SDK (oxlint); also: bun run format
cd packages && bun run generate-iconify-catalog # Regenerate the Rust icon catalog assets
cd packages && bun run sync-version # Sync SDK versions from root Cargo.toml
cd packages && bun run sync-version:check # Verify SDK versions match root Cargo.toml
scripts/sync-version.sh # Sync all managed versions from root Cargo.toml
tools/check-version-sync.sh # Verify all managed versions match root Cargo.toml
```
## 기여하기

View file

@ -479,7 +479,7 @@ openpencil/
│ ├── jian/ GPU-Skia UI framework — widgets/render/events
│ ├── casement/ winit fork
│ └── agent/ Cross-product Rust agent runtime (agent-rs)
└── .githooks/ Pre-commit version sync from branch name
└── .githooks/ Pre-commit version consistency check
```
## Keyboard Shortcuts
@ -516,8 +516,8 @@ cargo run -p op-cli -- <args> # CLI (binary: op)
# Web SDK / JS tooling (run from packages/)
cd packages && bun run lint # Lint the web SDK (oxlint); also: bun run format
cd packages && bun run generate-iconify-catalog # Regenerate the Rust icon catalog assets
cd packages && bun run sync-version # Sync SDK versions from root Cargo.toml
cd packages && bun run sync-version:check # Verify SDK versions match root Cargo.toml
scripts/sync-version.sh # Sync all managed versions from root Cargo.toml
tools/check-version-sync.sh # Verify all managed versions match root Cargo.toml
```
### Rust workspace details

View file

@ -341,7 +341,7 @@ openpencil/
│ ├── jian/ Framework de UI GPU-Skia — widgets/renderização/eventos
│ ├── casement/ Fork do winit
│ └── agent/ Runtime de agente Rust compartilhado entre produtos (agent-rs)
└── .githooks/ Sincronização de versão no pre-commit a partir do nome da branch
└── .githooks/ Verificação de consistência de versão no pre-commit
```
## Atalhos de Teclado
@ -378,8 +378,8 @@ cargo run -p op-cli -- <args> # CLI (binary: op)
# Web SDK / JS tooling (run from packages/)
cd packages && bun run lint # Lint the web SDK (oxlint); also: bun run format
cd packages && bun run generate-iconify-catalog # Regenerate the Rust icon catalog assets
cd packages && bun run sync-version # Sync SDK versions from root Cargo.toml
cd packages && bun run sync-version:check # Verify SDK versions match root Cargo.toml
scripts/sync-version.sh # Sync all managed versions from root Cargo.toml
tools/check-version-sync.sh # Verify all managed versions match root Cargo.toml
```
## Contribuindo

View file

@ -378,8 +378,8 @@ cargo run -p op-cli -- <args> # CLI (binary: op)
# Web SDK / JS tooling (run from packages/)
cd packages && bun run lint # Lint the web SDK (oxlint); also: bun run format
cd packages && bun run generate-iconify-catalog # Regenerate the Rust icon catalog assets
cd packages && bun run sync-version # Sync SDK versions from root Cargo.toml
cd packages && bun run sync-version:check # Verify SDK versions match root Cargo.toml
scripts/sync-version.sh # Sync all managed versions from root Cargo.toml
tools/check-version-sync.sh # Verify all managed versions match root Cargo.toml
```
## Участие в разработке

View file

@ -341,7 +341,7 @@ openpencil/
│ ├── jian/ เฟรมเวิร์ก UI แบบ GPU-Skia — widget/render/event
│ ├── casement/ winit fork
│ └── agent/ Rust agent runtime ข้ามผลิตภัณฑ์ (agent-rs)
└── .githooks/ Pre-commit version sync จาก branch name
└── .githooks/ การตรวจสอบความสอดคล้องของเวอร์ชันก่อน commit
```
## คีย์ลัด
@ -378,8 +378,8 @@ cargo run -p op-cli -- <args> # CLI (binary: op)
# Web SDK / JS tooling (run from packages/)
cd packages && bun run lint # Lint the web SDK (oxlint); also: bun run format
cd packages && bun run generate-iconify-catalog # Regenerate the Rust icon catalog assets
cd packages && bun run sync-version # Sync SDK versions from root Cargo.toml
cd packages && bun run sync-version:check # Verify SDK versions match root Cargo.toml
scripts/sync-version.sh # Sync all managed versions from root Cargo.toml
tools/check-version-sync.sh # Verify all managed versions match root Cargo.toml
```
## การมีส่วนร่วม

View file

@ -378,8 +378,8 @@ cargo run -p op-cli -- <args> # CLI (binary: op)
# Web SDK / JS tooling (run from packages/)
cd packages && bun run lint # Lint the web SDK (oxlint); also: bun run format
cd packages && bun run generate-iconify-catalog # Regenerate the Rust icon catalog assets
cd packages && bun run sync-version # Sync SDK versions from root Cargo.toml
cd packages && bun run sync-version:check # Verify SDK versions match root Cargo.toml
scripts/sync-version.sh # Sync all managed versions from root Cargo.toml
tools/check-version-sync.sh # Verify all managed versions match root Cargo.toml
```
## Katkıda Bulunma

View file

@ -378,8 +378,8 @@ cargo run -p op-cli -- <args> # CLI (binary: op)
# Web SDK / JS tooling (run from packages/)
cd packages && bun run lint # Lint the web SDK (oxlint); also: bun run format
cd packages && bun run generate-iconify-catalog # Regenerate the Rust icon catalog assets
cd packages && bun run sync-version # Sync SDK versions from root Cargo.toml
cd packages && bun run sync-version:check # Verify SDK versions match root Cargo.toml
scripts/sync-version.sh # Sync all managed versions from root Cargo.toml
tools/check-version-sync.sh # Verify all managed versions match root Cargo.toml
```
## Đóng góp

View file

@ -378,8 +378,8 @@ cargo run -p op-cli -- <args> # CLI (binary: op)
# Web SDK / JS tooling (run from packages/)
cd packages && bun run lint # Lint the web SDK (oxlint); also: bun run format
cd packages && bun run generate-iconify-catalog # Regenerate the Rust icon catalog assets
cd packages && bun run sync-version # Sync SDK versions from root Cargo.toml
cd packages && bun run sync-version:check # Verify SDK versions match root Cargo.toml
scripts/sync-version.sh # Sync all managed versions from root Cargo.toml
tools/check-version-sync.sh # Verify all managed versions match root Cargo.toml
```
## 參與貢獻

View file

@ -378,8 +378,8 @@ cargo run -p op-cli -- <args> # CLI (binary: op)
# Web SDK / JS tooling (run from packages/)
cd packages && bun run lint # Lint the web SDK (oxlint); also: bun run format
cd packages && bun run generate-iconify-catalog # Regenerate the Rust icon catalog assets
cd packages && bun run sync-version # Sync SDK versions from root Cargo.toml
cd packages && bun run sync-version:check # Verify SDK versions match root Cargo.toml
scripts/sync-version.sh # Sync all managed versions from root Cargo.toml
tools/check-version-sync.sh # Verify all managed versions match root Cargo.toml
```
## 参与贡献

36
scripts/sync-version.sh Executable file
View file

@ -0,0 +1,36 @@
#!/bin/sh
set -eu
if [ "$#" -ne 0 ]; then
printf 'usage: scripts/sync-version.sh\n' >&2
printf 'sync-version: edit [workspace.package].version in Cargo.toml, then run this command without arguments\n' >&2
exit 2
fi
script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd)
repo_root=$(CDPATH= cd "$script_dir/.." && pwd)
if ! canonical_version=$("$repo_root/scripts/workspace-version.sh"); then
printf 'sync-version: failed to read the canonical Cargo workspace version\n' >&2
exit 1
fi
printf 'sync-version: canonical Cargo workspace version is %s\n' "$canonical_version"
if ! (cd "$repo_root" && cargo metadata --no-deps --format-version 1 >/dev/null); then
printf 'sync-version: Cargo lock refresh failed\n' >&2
exit 1
fi
if ! (cd "$repo_root/packages" && bun run sync-version); then
printf 'sync-version: package version synchronization failed\n' >&2
exit 1
fi
if ! (cd "$repo_root" && "$repo_root/tools/check-version-sync.sh"); then
printf 'sync-version: version consistency check failed\n' >&2
exit 1
fi
printf 'sync-version: all managed versions are synchronized to %s\n' "$canonical_version"

209
scripts/sync-version.test.sh Executable file
View file

@ -0,0 +1,209 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd)
repo_root=$(CDPATH= cd "$script_dir/.." && pwd)
sync_source="$script_dir/sync-version.sh"
reader_source="$script_dir/workspace-version.sh"
tmp_base=${TMPDIR:-/tmp}
tmp_dir=$(mktemp -d "${tmp_base%/}/sync-version.XXXXXX")
trap 'rm -rf "$tmp_dir"' 0 HUP INT TERM
test_count=0
case_status=0
case_output=
fail() {
printf 'not ok %s - %s\n' "$test_count" "$1" >&2
exit 1
}
pass() {
test_count=$((test_count + 1))
printf 'ok %s - %s\n' "$test_count" "$1"
}
assert_status() {
expected=$1
label=$2
if [ "$case_status" -ne "$expected" ]; then
printf '%s\n' "$case_output" >&2
fail "$label: expected status $expected, got $case_status"
fi
}
assert_contains() {
needle=$1
label=$2
case "$case_output" in
*"$needle"*) ;;
*)
printf '%s\n' "$case_output" >&2
fail "$label: missing output: $needle"
;;
esac
}
assert_file_contains() {
file=$1
needle=$2
label=$3
if ! grep -Fq -- "$needle" "$file"; then
fail "$label: $file does not contain: $needle"
fi
}
assert_file_not_matches() {
file=$1
pattern=$2
label=$3
if grep -Eq -- "$pattern" "$file"; then
fail "$label: $file unexpectedly matches: $pattern"
fi
}
cargo() {
printf 'cargo:%s:%s\n' "$PWD" "$*" >> "$SYNC_TEST_LOG"
if [ -e "$PWD/.fail-cargo" ]; then
printf 'fake cargo failure\n' >&2
return 31
fi
version=$(sed -n 's/^version = "\([^"]*\)"/\1/p' "$PWD/Cargo.toml")
printf 'lock-version=%s\n' "$version" > "$PWD/Cargo.lock"
printf '%s\n' '{"packages":[]}'
}
bun() {
printf 'bun:%s:%s\n' "$PWD" "$*" >> "$SYNC_TEST_LOG"
repo_root=$(CDPATH= cd "$(dirname "$PWD")" && pwd)
if [ -e "$repo_root/.fail-bun" ]; then
printf 'fake bun failure\n' >&2
return 32
fi
version=$(sed -n 's/^version = "\([^"]*\)"/\1/p' "$repo_root/Cargo.toml")
printf '{"version":"%s"}\n' "$version" > "$PWD/package.json"
}
export -f cargo bun
new_repo() {
name=$1
repo="$tmp_dir/$name"
mkdir -p "$repo/scripts" "$repo/tools" "$repo/packages" "$repo/outside"
ln -s "$sync_source" "$repo/scripts/sync-version.sh"
ln -s "$reader_source" "$repo/scripts/workspace-version.sh"
printf '%s\n' \
'[workspace]' \
'members = []' \
'' \
'[workspace.package]' \
'version = "2.3.4"' > "$repo/Cargo.toml"
printf '%s\n' 'lock-version=1.0.0' > "$repo/Cargo.lock"
printf '%s\n' '{"version":"1.0.0"}' > "$repo/packages/package.json"
cat > "$repo/tools/check-version-sync.sh" <<'SCRIPT'
#!/bin/sh
set -eu
repo_root=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
printf 'guard:%s\n' "$PWD" >> "$SYNC_TEST_LOG"
if [ -e "$repo_root/.fail-guard" ]; then
printf 'fake guard failure\n' >&2
exit 33
fi
SCRIPT
chmod +x "$repo/tools/check-version-sync.sh"
printf '%s\n' "$repo"
}
run_sync() {
repo=$1
shift
case_status=0
case_output=$(cd "$repo/outside" && env SYNC_TEST_LOG="$repo/calls.log" \
bash "$repo/scripts/sync-version.sh" "$@" 2>&1) || case_status=$?
}
case_status=0
case_output=$("$sync_source" 2.3.4 2>&1) || case_status=$?
assert_status 2 'argument rejection'
assert_contains 'usage: scripts/sync-version.sh' 'argument rejection'
pass 'sync entrypoint rejects positional versions with actionable usage'
repo=$(new_repo external_cwd)
run_sync "$repo"
assert_status 0 'external cwd'
assert_file_contains "$repo/calls.log" "cargo:$repo:metadata --no-deps --format-version 1" \
'external cwd'
assert_file_contains "$repo/calls.log" "bun:$repo/packages:run sync-version" 'external cwd'
assert_file_contains "$repo/calls.log" "guard:$repo" 'external cwd'
assert_file_contains "$repo/Cargo.lock" 'lock-version=2.3.4' 'external cwd'
assert_file_contains "$repo/packages/package.json" '"version":"2.3.4"' 'external cwd'
pass 'sync entrypoint locates the repository and runs every stage from an external cwd'
first_hash=$(shasum "$repo/Cargo.lock" "$repo/packages/package.json")
run_sync "$repo"
assert_status 0 'idempotent rerun'
second_hash=$(shasum "$repo/Cargo.lock" "$repo/packages/package.json")
if [ "$first_hash" != "$second_hash" ]; then
fail 'idempotent rerun changed managed file hashes'
fi
pass 'running synchronization twice leaves managed file hashes unchanged'
repo=$(new_repo cargo_failure)
touch "$repo/.fail-cargo"
run_sync "$repo"
assert_status 1 'Cargo stage failure'
assert_contains 'sync-version: Cargo lock refresh failed' 'Cargo stage failure'
pass 'Cargo refresh failures identify the failing synchronization stage'
repo=$(new_repo bun_failure)
touch "$repo/.fail-bun"
run_sync "$repo"
assert_status 1 'package stage failure'
assert_contains 'sync-version: package version synchronization failed' 'package stage failure'
pass 'package synchronization failures identify the failing stage'
repo=$(new_repo guard_failure)
touch "$repo/.fail-guard"
run_sync "$repo"
assert_status 1 'guard stage failure'
assert_contains 'sync-version: version consistency check failed' 'guard stage failure'
pass 'guard failures identify the final verification stage'
hook="$repo_root/.githooks/pre-commit"
assert_file_contains "$hook" 'tools/check-version-sync.sh' 'pre-commit guard'
assert_file_not_matches "$hook" 'rev-parse --abbrev-ref|grep -oE.*[0-9]|git add|jq --arg|\.tmp.*mv' \
'pre-commit detect-only policy'
guard_line=$(grep -n 'tools/check-version-sync.sh' "$hook" | head -n 1 | cut -d: -f1)
fmt_line=$(grep -n 'cargo fmt' "$hook" | head -n 1 | cut -d: -f1)
if [ -z "$guard_line" ] || [ -z "$fmt_line" ] || [ "$guard_line" -ge "$fmt_line" ]; then
fail 'pre-commit guard must run before Rust format and lint checks'
fi
pass 'pre-commit is detect-only and runs the version guard before Rust checks'
workflow="$repo_root/.github/workflows/version-sync.yml"
assert_file_contains "$workflow" 'pull_request:' 'version sync workflow trigger'
assert_file_contains "$workflow" 'push:' 'version sync workflow trigger'
assert_file_contains "$workflow" 'submodules: recursive' 'version sync checkout'
assert_file_contains "$workflow" 'dtolnay/rust-toolchain@stable' 'version sync Rust setup'
assert_file_contains "$workflow" 'oven-sh/setup-bun@v2' 'version sync Bun setup'
assert_file_contains "$workflow" 'bun install --frozen-lockfile' 'version sync dependency install'
assert_file_contains "$workflow" 'packages/bun.lock' 'version sync managed paths'
assert_file_contains "$workflow" 'packages/scripts/sync-version.mjs' 'version sync implementation paths'
assert_file_not_matches "$workflow" 'packages/\*\*' 'version sync focused path filters'
assert_file_contains "$workflow" 'scripts/workspace-version.test.sh' 'version reader tests'
assert_file_contains "$workflow" 'scripts/sync-version.test.sh' 'version sync tests'
assert_file_contains "$workflow" 'tools/check-version-sync.test.sh' 'version guard tests'
assert_file_contains "$workflow" 'tools/check-version-sync.sh' 'version guard'
pass 'dedicated CI installs prerequisites and runs the focused version checks'
old_guard_name='check-version-'"fixtures"
if rg -n "$old_guard_name" "$repo_root" --hidden \
--glob '!.git/**' --glob '!vendor/**' > "$tmp_dir/old-paths"; then
sed 's/^/ /' "$tmp_dir/old-paths" >&2
fail 'old version guard paths remain in the repository'
fi
pass 'repository no longer references the retired fixture-only guard name'
printf '1..%s\n' "$test_count"

View file

@ -5,6 +5,14 @@ set -euo pipefail
script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd)
repo_root=$(CDPATH= cd "$script_dir/.." && pwd)
fixture_version=1.0.0
for required_command in cargo jq bun rg; do
if ! command -v "$required_command" >/dev/null 2>&1; then
printf 'version-sync: required command not found: %s\n' "$required_command" >&2
exit 1
fi
done
current_version=$(bash "$repo_root/scripts/workspace-version.sh")
cd "$repo_root"
@ -170,8 +178,110 @@ reject_matches() {
fi
}
validate_workspace_package_versions() {
if ! metadata=$(cargo metadata --no-deps --format-version 1 --locked); then
report_missing Cargo.lock \
'cargo metadata --locked failed; run scripts/sync-version.sh to refresh the lockfile'
return
fi
mismatch_status=0
mismatches=$(printf '%s\n' "$metadata" | jq -r \
--arg crates_prefix "$repo_root/crates/" \
--arg expected "$current_version" '
.packages[]?
| select(.name | startswith("op-"))
| select(.manifest_path | startswith($crates_prefix))
| select(.version != $expected)
| [.manifest_path, .name, .version]
| @tsv
' 2>&1) || mismatch_status=$?
if [[ "$mismatch_status" -ne 0 ]]; then
printf '%s\n' "$mismatches" >&2
report_missing Cargo.toml 'failed to inspect cargo metadata with jq'
return
fi
if [[ -n "$mismatches" ]]; then
while IFS=$'\t' read -r manifest package package_version; do
relative_manifest=${manifest#"$repo_root"/}
printf '%s:1: error: workspace package %s has version %s; expected %s\n' \
"$relative_manifest" "$package" "$package_version" "$current_version" >&2
errors=1
done <<< "$mismatches"
fi
}
validate_package_versions() {
package_status=0
package_output=$(cd packages && bun run sync-version:check 2>&1) || package_status=$?
if [[ "$package_status" -ne 0 ]]; then
printf '%s\n' "$package_output" >&2
report_missing packages \
'bun run sync-version:check failed; run scripts/sync-version.sh to repair package drift'
fi
}
validate_release_tag() {
tag_name=
if [[ "${GITHUB_REF:-}" == refs/tags/v* ]]; then
tag_name=${GITHUB_REF#refs/tags/}
elif [[ -z "${GITHUB_REF:-}" && "${GITHUB_REF_NAME:-}" == v* ]]; then
tag_name=$GITHUB_REF_NAME
fi
if [[ -n "$tag_name" && "${tag_name#v}" != "$current_version" ]]; then
report_missing environment \
"release tag ${tag_name} does not match Cargo workspace version ${current_version}"
fi
}
validate_cli_bundle_version_template() {
bundle=crates/op-cli/assets/skill-bundle.json
sentinel=__OPENPENCIL_VERSION__
sentinel_count=$(rg --fixed-strings --count-matches -- "$sentinel" "$bundle" || true)
if [[ "${sentinel_count:-0}" != 6 ]]; then
report_missing "$bundle" \
"expected exactly 6 version sentinels ${sentinel} (found ${sentinel_count:-0})"
fi
reject_matches fixed "$bundle" "$current_version" \
'embedded CLI bundle must not contain the canonical version literal'
}
validate_rust_product_version_producers() {
require_statement crates/op-editor-core/src/state.rs \
'version:[[:space:]]*env!\("CARGO_PKG_VERSION"\)[.]to_owned\(\),' \
'empty documents must derive their version from CARGO_PKG_VERSION'
host_support=crates/op-editor-core/src/host_support.rs
host_version_count=$(rg --fixed-strings --count-matches -- \
'src.replace("__OPENPENCIL_VERSION__", env!("CARGO_PKG_VERSION"))' \
"$host_support" || true)
if [[ "$host_version_count" != 3 ]]; then
report_missing "$host_support" \
"expected 3 document templates to derive from CARGO_PKG_VERSION (found ${host_version_count:-0})"
fi
cli_source=crates/op-cli/src/app_control_cli.rs
require_regex "$cli_source" 'env!\("CARGO_PKG_VERSION"\)' \
'CLI starter documents must derive their version from CARGO_PKG_VERSION'
reject_matches regex "$cli_source" \
'"version"[^[:cntrl:]]*"[0-9]+[.][0-9]+[.][0-9]+' \
'CLI starter documents must not hard-code a product version'
reject_matches regex crates/op-host-desktop/Cargo.toml \
'^[[:space:]]*op-host-native[[:space:]]*=.*path[[:space:]]*=.*version[[:space:]]*=' \
'local op-host-native dependency must not duplicate the product version'
}
validate_workspace_package_versions
validate_package_versions
validate_release_tag
validate_cli_bundle_version_template
validate_rust_product_version_producers
if [[ "$current_version" == "$fixture_version" ]]; then
printf 'version-fixtures: current product version %s equals stable fixture version %s; skipping literal fixture drift scan because stable fixtures and product-version literals are indistinguishable\n' \
printf 'version-sync: current product version %s equals stable fixture version %s; skipping literal fixture drift scan because stable fixtures and product-version literals are indistinguishable\n' \
"$current_version" "$fixture_version"
fixture_scan_skipped=1
else
@ -187,15 +297,15 @@ else
crates) || rg_status=$?
if [[ "$rg_status" -gt 1 ]]; then
printf 'version-fixtures: failed to scan Rust sources with rg (status %s)\n' "$rg_status" >&2
printf 'version-sync: failed to scan Rust sources with rg (status %s)\n' "$rg_status" >&2
exit "$rg_status"
fi
if [[ -n "$matches" ]]; then
printf 'version-fixtures: ordinary Rust fixtures copy current product version %s:\n' \
printf 'version-sync: ordinary Rust fixtures copy current product version %s:\n' \
"$current_version" >&2
printf '%s\n' "$matches" >&2
printf 'version-fixtures: use stable %s test data unless a test explicitly covers compatibility, migration, or updates\n' \
printf 'version-sync: use stable %s test data unless a test explicitly covers compatibility, migration, or updates\n' \
"$fixture_version" >&2
errors=1
fi
@ -319,8 +429,8 @@ if [[ "$errors" -ne 0 ]]; then
fi
if [[ "$fixture_scan_skipped" -eq 0 ]]; then
printf 'version-fixtures: no ordinary Rust fixtures copy current product version %s\n' \
printf 'version-sync: no ordinary Rust fixtures copy current product version %s\n' \
"$current_version"
fi
printf 'version-fixtures: packaging and release versions derive from Cargo workspace version %s\n' \
printf 'version-sync: all managed versions derive from Cargo workspace version %s\n' \
"$current_version"

View file

@ -3,9 +3,9 @@
set -euo pipefail
script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd)
guard_source="$script_dir/check-version-fixtures.sh"
guard_source="$script_dir/check-version-sync.sh"
reader_source="$script_dir/../scripts/workspace-version.sh"
temp_root=$(mktemp -d "${TMPDIR:-/tmp}/check-version-fixtures.XXXXXX")
temp_root=$(mktemp -d "${TMPDIR:-/tmp}/check-version-sync.XXXXXX")
trap 'rm -rf "$temp_root"' EXIT HUP INT TERM
tests_run=0
@ -61,6 +61,38 @@ assert_no_success_output() {
assert_not_contains 'skipping literal fixture drift scan' "$label"
}
cargo() {
if [[ "$*" != 'metadata --no-deps --format-version 1 --locked' ]]; then
printf 'unexpected cargo arguments: %s\n' "$*" >&2
return 41
fi
repo_root=$PWD
canonical=$("$repo_root/scripts/workspace-version.sh")
package_version=$canonical
if [[ -f "$repo_root/.fake-cargo-version" ]]; then
package_version=$(sed -n '1p' "$repo_root/.fake-cargo-version")
fi
if [[ -e "$repo_root/.fake-cargo-warning" ]]; then
printf 'warning: fake Cargo metadata warning\n' >&2
fi
printf '{"packages":[{"name":"op-example","version":"%s","manifest_path":"%s/crates/example/Cargo.toml"}]}\n' \
"$package_version" "$repo_root"
}
bun() {
repo_root=$(CDPATH= cd "$PWD/.." && pwd)
if [[ "$PWD" != "$repo_root/packages" || "$*" != 'run sync-version:check' ]]; then
printf 'unexpected bun invocation: cwd=%s args=%s\n' "$PWD" "$*" >&2
return 42
fi
if [[ -e "$repo_root/.fake-bun-fail" ]]; then
printf 'package versions are stale\n' >&2
return 43
fi
}
export -f cargo bun
write_workflow_fixture() {
repo=$1
dependency_mode=$2
@ -158,14 +190,19 @@ new_repo() {
mkdir -p \
"$repo/.github/workflows" \
"$repo/packages" \
"$repo/tools" \
"$repo/scripts" \
"$repo/crates/op-cli/assets" \
"$repo/crates/op-cli/src" \
"$repo/crates/op-editor-core/src" \
"$repo/crates/op-host-desktop" \
"$repo/crates/example/src" \
"$repo/crates/op-host-desktop/src"
git -C "$repo" init -q
cp "$guard_source" "$repo/tools/check-version-fixtures.sh"
cp "$guard_source" "$repo/tools/check-version-sync.sh"
cp "$reader_source" "$repo/scripts/workspace-version.sh"
chmod +x "$repo/tools/check-version-fixtures.sh" "$repo/scripts/workspace-version.sh"
chmod +x "$repo/tools/check-version-sync.sh" "$repo/scripts/workspace-version.sh"
printf '%s\n' \
'[workspace]' \
'members = []' \
@ -173,6 +210,26 @@ new_repo() {
'[workspace.package]' \
"version = \"$version\"" \
'edition = "2024"' > "$repo/Cargo.toml"
printf '%s\n' 'version = 4' > "$repo/Cargo.lock"
printf '%s\n' '{"name":"fixture-packages"}' > "$repo/packages/package.json"
cat > "$repo/crates/op-cli/assets/skill-bundle.json" <<'JSON'
{"one":"__OPENPENCIL_VERSION__","two":"__OPENPENCIL_VERSION__","three":"__OPENPENCIL_VERSION__","four":"__OPENPENCIL_VERSION__","five":"__OPENPENCIL_VERSION__","six":"__OPENPENCIL_VERSION__"}
JSON
cat > "$repo/crates/op-editor-core/src/state.rs" <<'RUST'
version: env!("CARGO_PKG_VERSION").to_owned(),
RUST
cat > "$repo/crates/op-editor-core/src/host_support.rs" <<'RUST'
let src = src.replace("__OPENPENCIL_VERSION__", env!("CARGO_PKG_VERSION"));
let src = src.replace("__OPENPENCIL_VERSION__", env!("CARGO_PKG_VERSION"));
let src = src.replace("__OPENPENCIL_VERSION__", env!("CARGO_PKG_VERSION"));
RUST
cat > "$repo/crates/op-cli/src/app_control_cli.rs" <<'RUST'
const MINIMAL_DOCUMENT: &str = concat!(env!("CARGO_PKG_VERSION"));
RUST
cat > "$repo/crates/op-host-desktop/Cargo.toml" <<'TOML'
op-host-native = { path = "../op-host-native", features = ["gl-host"] }
TOML
cat > "$repo/scripts/bundle-macos.sh" <<'SCRIPT'
#!/usr/bin/env bash
@ -229,8 +286,18 @@ SCRIPT
run_guard() {
repo=$1
shift
case_status=0
case_output=$(cd "$repo" && bash tools/check-version-fixtures.sh 2>&1) || case_status=$?
case_output=$(cd "$repo" && env "$@" \
bash tools/check-version-sync.sh 2>&1) || case_status=$?
}
repo_snapshot() {
repo=$1
find "$repo" -type f ! -path "$repo/.git/*" -exec shasum {} + |
LC_ALL=C sort |
shasum |
awk '{print $1}'
}
repo=$(new_repo v_prefixed_stale_version 0.8.1)
@ -470,14 +537,14 @@ printf '%s\n' '# identifier build0.8.2candidate is not a version token' \
>> "$repo/scripts/install-op.sh"
run_guard "$repo"
assert_status 0 'embedded version-like substring'
assert_contains 'packaging and release versions derive from Cargo workspace version 0.8.1' \
assert_contains 'all managed versions derive from Cargo workspace version 0.8.1' \
'embedded version-like substring'
pass 'version-like substrings inside larger identifiers are allowed'
repo=$(new_repo nsis_defensive_fallback 0.8.1)
run_guard "$repo"
assert_status 0 'NSIS defensive fallback'
assert_contains 'packaging and release versions derive from Cargo workspace version 0.8.1' \
assert_contains 'all managed versions derive from Cargo workspace version 0.8.1' \
'NSIS defensive fallback'
pass 'NSIS 0.0.0 defensive fallback remains allowed'
@ -518,4 +585,104 @@ assert_not_contains 'no ordinary Rust fixtures copy current product version' \
'fixture-version collision packaging check'
pass 'fixture-version collision still runs packaging checks'
repo=$(new_repo cargo_metadata_version_mismatch 0.8.1)
printf '%s\n' '0.8.0' > "$repo/.fake-cargo-version"
run_guard "$repo"
assert_status 1 'Cargo metadata version mismatch'
assert_contains 'crates/example/Cargo.toml:1: error: workspace package op-example has version 0.8.0; expected 0.8.1' \
'Cargo metadata version mismatch'
assert_no_success_output 'Cargo metadata version mismatch'
pass 'workspace op-* packages under crates must match the canonical Cargo version'
repo=$(new_repo cargo_metadata_warning 0.8.1)
touch "$repo/.fake-cargo-warning"
run_guard "$repo"
assert_status 0 'Cargo metadata warning'
assert_contains 'warning: fake Cargo metadata warning' 'Cargo metadata warning'
pass 'Cargo warnings do not corrupt the metadata JSON passed to jq'
repo=$(new_repo package_version_drift 0.8.1)
touch "$repo/.fake-bun-fail"
run_guard "$repo"
assert_status 1 'package version drift'
assert_contains 'packages:1: error: bun run sync-version:check failed' 'package version drift'
assert_no_success_output 'package version drift'
pass 'web SDK package drift is reported by the read-only guard'
repo=$(new_repo matching_release_tag 0.8.1)
run_guard "$repo" GITHUB_REF=refs/tags/v0.8.1 GITHUB_REF_NAME=v0.8.1
assert_status 0 'matching release tag'
pass 'matching v* release tags pass the repository guard'
repo=$(new_repo mismatched_release_tag 0.8.1)
run_guard "$repo" GITHUB_REF=refs/tags/v0.8.2 GITHUB_REF_NAME=v0.8.2
assert_status 1 'mismatched release tag'
assert_contains 'environment:1: error: release tag v0.8.2 does not match Cargo workspace version 0.8.1' \
'mismatched release tag'
assert_no_success_output 'mismatched release tag'
pass 'mismatched v* release tags are rejected outside the release workflow too'
repo=$(new_repo cli_bundle_wrong_sentinel_count 0.8.1)
sed -i.bak 's/,"six":"__OPENPENCIL_VERSION__"//' \
"$repo/crates/op-cli/assets/skill-bundle.json"
rm "$repo/crates/op-cli/assets/skill-bundle.json.bak"
run_guard "$repo"
assert_status 1 'CLI bundle sentinel count'
assert_contains 'crates/op-cli/assets/skill-bundle.json:1: error: expected exactly 6 version sentinels' \
'CLI bundle sentinel count'
assert_no_success_output 'CLI bundle sentinel count'
pass 'embedded CLI bundle retains exactly six version sentinels'
repo=$(new_repo cli_bundle_without_sentinels 0.8.1)
sed -i.bak 's/__OPENPENCIL_VERSION__/__MISSING_VERSION__/g' \
"$repo/crates/op-cli/assets/skill-bundle.json"
rm "$repo/crates/op-cli/assets/skill-bundle.json.bak"
run_guard "$repo"
assert_status 1 'CLI bundle missing sentinels'
assert_contains 'crates/op-cli/assets/skill-bundle.json:1: error: expected exactly 6 version sentinels' \
'CLI bundle missing sentinels'
assert_contains '(found 0)' 'CLI bundle missing sentinels'
assert_no_success_output 'CLI bundle missing sentinels'
pass 'embedded CLI bundle reports zero missing version sentinels actionably'
repo=$(new_repo cli_bundle_numeric_product_version 0.8.1)
printf '%s\n' '{"stale":"0.8.1"}' >> "$repo/crates/op-cli/assets/skill-bundle.json"
run_guard "$repo"
assert_status 1 'CLI bundle numeric product version'
assert_contains 'crates/op-cli/assets/skill-bundle.json:' 'CLI bundle numeric product version'
assert_contains 'error: embedded CLI bundle must not contain the canonical version literal' \
'CLI bundle numeric product version'
assert_no_success_output 'CLI bundle numeric product version'
pass 'embedded CLI bundle cannot duplicate the numeric product version'
repo=$(new_repo hardcoded_rust_product_version 0.8.1)
printf '%s\n' 'version: "0.8.1".to_owned(),' > "$repo/crates/op-editor-core/src/state.rs"
run_guard "$repo"
assert_status 1 'hardcoded Rust product version'
assert_contains 'crates/op-editor-core/src/state.rs:1: error: empty documents must derive their version from CARGO_PKG_VERSION' \
'hardcoded Rust product version'
assert_no_success_output 'hardcoded Rust product version'
pass 'Rust product-version producers remain derived from Cargo metadata'
repo=$(new_repo versioned_local_product_dependency 0.8.1)
printf '%s\n' \
'op-host-native = { path = "../op-host-native", version = "0.8.1", features = ["gl-host"] }' \
> "$repo/crates/op-host-desktop/Cargo.toml"
run_guard "$repo"
assert_status 1 'versioned local product dependency'
assert_contains 'crates/op-host-desktop/Cargo.toml:1: error: local op-host-native dependency must not duplicate the product version' \
'versioned local product dependency'
assert_no_success_output 'versioned local product dependency'
pass 'local product dependencies do not repeat the workspace version'
repo=$(new_repo guard_is_read_only 0.8.1)
before_snapshot=$(repo_snapshot "$repo")
run_guard "$repo"
assert_status 0 'read-only guard'
after_snapshot=$(repo_snapshot "$repo")
if [[ "$before_snapshot" != "$after_snapshot" ]]; then
fail 'read-only guard changed repository file contents'
fi
pass 'repository-wide version guard does not write files'
printf '1..%s\n' "$tests_run"