diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 2bbc399ca..19c0d0e53 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -155,14 +155,15 @@ jobs: - name: Compute release version shell: bash run: | - # Tag pushes (vX.Y.Z) drive the artifact version; workflow_dispatch - # falls back to [workspace.package].version in the root Cargo.toml. + cargo_version="$(scripts/workspace-version.sh)" if [[ "$GITHUB_REF" == refs/tags/v* ]]; then - echo "OP_VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV" - else - ver="$(awk -F'"' '/^\[workspace\.package\]/{f=1} f && /^version/{print $2; exit}' Cargo.toml)" - echo "OP_VERSION=${ver:-0.0.0}" >> "$GITHUB_ENV" + tag_version="${GITHUB_REF_NAME#v}" + if [[ "$tag_version" != "$cargo_version" ]]; then + echo "::error::release tag version $tag_version does not match Cargo workspace version $cargo_version" + exit 1 + fi fi + echo "OP_VERSION=$cargo_version" >> "$GITHUB_ENV" - name: Build (host) run: cargo build -p op-host-desktop -p op-cli --target ${{ matrix.target }} --release - name: Package archives (unix) diff --git a/scripts/bundle-macos.sh b/scripts/bundle-macos.sh index df96650f5..a2e0427c8 100755 --- a/scripts/bundle-macos.sh +++ b/scripts/bundle-macos.sh @@ -29,7 +29,8 @@ # first). # # CI / cross-build controls (all optional, default = local behavior): -# OPENPENCIL_VERSION CFBundleShortVersionString (default 0.8.1) +# OPENPENCIL_VERSION CFBundleShortVersionString (default: root Cargo +# workspace version; overrides must match it) # OPENPENCIL_TARGET cargo target triple (e.g. x86_64-apple-darwin); # builds + bundles for that triple and reads the # bundle from target//release/bundle/osx @@ -49,7 +50,13 @@ set -euo pipefail WS_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -APP_VERSION="${OPENPENCIL_VERSION:-0.8.1}" +CANONICAL_VERSION="$("$WS_ROOT/scripts/workspace-version.sh")" +APP_VERSION="${OPENPENCIL_VERSION:-$CANONICAL_VERSION}" +if [[ "$APP_VERSION" != "$CANONICAL_VERSION" ]]; then + printf 'bundle-macos: error: OPENPENCIL_VERSION (%s) must match Cargo workspace version (%s)\n' \ + "$APP_VERSION" "$CANONICAL_VERSION" >&2 + exit 1 +fi TARGET_TRIPLE="${OPENPENCIL_TARGET:-}" # Locate cargo-bundle. Tries PATH first, then a workspace-local diff --git a/scripts/install-op.sh b/scripts/install-op.sh index 14928e5f6..4ae760769 100755 --- a/scripts/install-op.sh +++ b/scripts/install-op.sh @@ -7,7 +7,7 @@ # # Usage: # ./install-op.sh # install the latest stable release -# OP_VERSION=0.8.1 ./install-op.sh # pin a specific version +# OP_VERSION=X.Y.Z ./install-op.sh # pin a specific version # OP_PRERELEASE=1 ./install-op.sh # allow the newest pre-release # INSTALL_DIR=$HOME/.local/bin ./install-op.sh # @@ -68,7 +68,7 @@ resolve_version() { tag="$(curl -fsSL "$api" | grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -n1 | sed 's/.*"\(v\{0,1\}[^"]*\)"$/\1/')" if [ -z "$tag" ]; then echo "error: could not resolve the latest release tag from GitHub" >&2 - echo " set OP_VERSION explicitly, e.g. OP_VERSION=0.8.1 $0" >&2 + echo " set OP_VERSION explicitly, e.g. OP_VERSION=X.Y.Z $0" >&2 echo " or set OP_PRERELEASE=1 to allow pre-release tags" >&2 exit 1 fi diff --git a/scripts/package-windows.nsi b/scripts/package-windows.nsi index bcd952881..dc29b1686 100644 --- a/scripts/package-windows.nsi +++ b/scripts/package-windows.nsi @@ -25,10 +25,10 @@ ; ; Compile (relative paths resolve against this script's directory, so the ; workflow passes absolute /D defines): -; makensis "/DVERSION=0.8.1" "/DARCH=x64" ^ +; makensis "/DVERSION=X.Y.Z" "/DARCH=x64" ^ ; "/DBIN_DIR=D:\w\target\x86_64-pc-windows-msvc\release" ^ ; "/DICON_FILE=D:\w\crates\op-host-desktop\assets\icon.ico" ^ -; "/DOUT_FILE=D:\w\OpenPencil-0.8.1-x64-win-setup.exe" ^ +; "/DOUT_FILE=D:\w\OpenPencil-X.Y.Z-x64-win-setup.exe" ^ ; scripts\package-windows.nsi ; ; NOT compiled locally (no makensis on the macOS dev machine) — first real diff --git a/tools/bundle-macos.sh b/tools/bundle-macos.sh index b4f6a6904..3355cbb21 100755 --- a/tools/bundle-macos.sh +++ b/tools/bundle-macos.sh @@ -24,6 +24,13 @@ set -e ROOT="$(cd "$(dirname "$0")/.." && pwd)" +CANONICAL_VERSION="$("$ROOT/scripts/workspace-version.sh")" +APP_VERSION="${OPENPENCIL_VERSION:-$CANONICAL_VERSION}" +if [ "$APP_VERSION" != "$CANONICAL_VERSION" ]; then + printf 'bundle-macos: error: OPENPENCIL_VERSION (%s) must match Cargo workspace version (%s)\n' \ + "$APP_VERSION" "$CANONICAL_VERSION" >&2 + exit 1 +fi BIN="$ROOT/target/release/openpencil-desktop" ICON="$ROOT/crates/op-host-desktop/assets/icon.icns" ENTITLEMENTS="$ROOT/crates/op-host-desktop/entitlements.plist" @@ -75,7 +82,7 @@ cat > "$APP/Contents/Info.plist" <CFBundleIconFileicon CFBundlePackageTypeAPPL CFBundleInfoDictionaryVersion6.0 - CFBundleShortVersionString0.8.1 + CFBundleShortVersionString${APP_VERSION} NSHighResolutionCapable diff --git a/tools/check-version-fixtures.sh b/tools/check-version-fixtures.sh index 873c3eebb..f0198370b 100755 --- a/tools/check-version-fixtures.sh +++ b/tools/check-version-fixtures.sh @@ -7,38 +7,155 @@ repo_root=$(CDPATH= cd "$script_dir/.." && pwd) fixture_version=1.0.0 current_version=$(bash "$repo_root/scripts/workspace-version.sh") +cd "$repo_root" + +errors=0 +fixture_scan_skipped=0 + +report_missing() { + file=$1 + message=$2 + printf '%s:1: error: %s\n' "$file" "$message" >&2 + errors=1 +} + +require_fixed() { + file=$1 + needle=$2 + message=$3 + if [[ ! -f "$file" ]] || ! rg --fixed-strings --quiet -- "$needle" "$file"; then + report_missing "$file" "$message" + fi +} + +require_regex() { + file=$1 + pattern=$2 + message=$3 + if [[ ! -f "$file" ]] || ! rg --quiet -- "$pattern" "$file"; then + report_missing "$file" "$message" + fi +} + +reject_matches() { + mode=$1 + file=$2 + pattern=$3 + message=$4 + rg_status=0 + if [[ "$mode" == fixed ]]; then + matches=$(rg --fixed-strings --line-number --with-filename --color never -- \ + "$pattern" "$file") || rg_status=$? + else + matches=$(rg --line-number --with-filename --color never -- \ + "$pattern" "$file") || rg_status=$? + fi + + if [[ "$rg_status" -gt 1 ]]; then + printf '%s:1: error: failed to scan version policy (rg status %s)\n' \ + "$file" "$rg_status" >&2 + errors=1 + return + fi + + if [[ -n "$matches" ]]; then + while IFS=: read -r match_file match_line _; do + printf '%s:%s: error: %s\n' "$match_file" "$match_line" "$message" >&2 + done <<< "$matches" + errors=1 + fi +} + 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' \ "$current_version" "$fixture_version" - exit 0 + fixture_scan_skipped=1 +else + rg_status=0 + matches=$(rg \ + --fixed-strings \ + --line-number \ + --with-filename \ + --color never \ + --glob '*.rs' \ + --glob '!**/op-host-desktop/src/update_check.rs' \ + "$current_version" \ + 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 + exit "$rg_status" + fi + + if [[ -n "$matches" ]]; then + printf 'version-fixtures: 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' \ + "$fixture_version" >&2 + errors=1 + fi fi -cd "$repo_root" +for macos_script in scripts/bundle-macos.sh tools/bundle-macos.sh; do + if [[ "$macos_script" == scripts/bundle-macos.sh ]]; then + require_fixed "$macos_script" \ + 'CANONICAL_VERSION="$("$WS_ROOT/scripts/workspace-version.sh")"' \ + 'macOS packaging must assign CANONICAL_VERSION from scripts/workspace-version.sh' + require_fixed "$macos_script" 'CFBundleShortVersionString $APP_VERSION' \ + 'CFBundleShortVersionString must use APP_VERSION' + else + require_fixed "$macos_script" \ + 'CANONICAL_VERSION="$("$ROOT/scripts/workspace-version.sh")"' \ + 'macOS packaging must assign CANONICAL_VERSION from scripts/workspace-version.sh' + require_fixed "$macos_script" \ + 'CFBundleShortVersionString${APP_VERSION}' \ + 'CFBundleShortVersionString must use APP_VERSION' + fi + require_fixed "$macos_script" 'APP_VERSION="${OPENPENCIL_VERSION:-$CANONICAL_VERSION}"' \ + 'OPENPENCIL_VERSION must default to the Cargo workspace version' + require_fixed "$macos_script" '"$APP_VERSION" != "$CANONICAL_VERSION"' \ + 'OPENPENCIL_VERSION overrides must be rejected when they differ from Cargo' + reject_matches regex "$macos_script" \ + 'OPENPENCIL_VERSION:-[0-9]+[.][0-9]+[.][0-9]+' \ + 'OPENPENCIL_VERSION must fall back to the Cargo workspace version' + reject_matches regex "$macos_script" \ + 'CFBundleShortVersionString[^[:cntrl:]]*[0-9]+[.][0-9]+[.][0-9]+' \ + 'CFBundleShortVersionString must use the resolved Cargo workspace version' +done -rg_status=0 -matches=$(rg \ - --fixed-strings \ - --line-number \ - --with-filename \ - --color never \ - --glob '*.rs' \ - --glob '!**/op-host-desktop/src/update_check.rs' \ - "$current_version" \ - crates) || rg_status=$? +for example_file in scripts/package-windows.nsi scripts/install-op.sh; do + require_regex "$example_file" 'X[.]Y[.]Z|' \ + 'version examples must use X.Y.Z or ' + reject_matches fixed "$example_file" '0.8.1' \ + 'version examples must use X.Y.Z or , not a product release' + if [[ "$current_version" != 0.8.1 ]]; then + reject_matches fixed "$example_file" "$current_version" \ + 'version examples must use X.Y.Z or , not the current Cargo version' + fi +done -if [[ "$rg_status" -gt 1 ]]; then - printf 'version-fixtures: failed to scan Rust sources with rg (status %s)\n' "$rg_status" >&2 - exit "$rg_status" -fi +release_workflow=.github/workflows/rust-release.yml +require_fixed "$release_workflow" 'cargo_version="$(scripts/workspace-version.sh)"' \ + 'release version computation must invoke scripts/workspace-version.sh' +require_fixed "$release_workflow" 'tag_version="${GITHUB_REF_NAME#v}"' \ + 'release version computation must derive the version from v* tags' +require_fixed "$release_workflow" '"$tag_version" != "$cargo_version"' \ + 'release tags must be compared with the Cargo workspace version' +require_fixed "$release_workflow" 'echo "OP_VERSION=$cargo_version" >> "$GITHUB_ENV"' \ + 'OP_VERSION must always be written from the Cargo workspace version' +reject_matches fixed "$release_workflow" 'echo "OP_VERSION=${GITHUB_REF_NAME#v}"' \ + 'OP_VERSION must not be written directly from the release tag' +reject_matches fixed "$release_workflow" 'echo "OP_VERSION=${ver:-0.0.0}"' \ + 'OP_VERSION must not use an independent manifest parser or fallback' -if [[ -n "$matches" ]]; then - printf 'version-fixtures: 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' \ - "$fixture_version" >&2 +if [[ "$errors" -ne 0 ]]; then exit 1 fi -printf 'version-fixtures: no ordinary Rust fixtures copy current product version %s\n' \ +if [[ "$fixture_scan_skipped" -eq 0 ]]; then + printf 'version-fixtures: 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' \ "$current_version" diff --git a/tools/check-version-fixtures.test.sh b/tools/check-version-fixtures.test.sh index 83168b3fb..7973d0079 100755 --- a/tools/check-version-fixtures.test.sh +++ b/tools/check-version-fixtures.test.sh @@ -67,6 +67,7 @@ new_repo() { repo="$temp_root/$name" mkdir -p \ + "$repo/.github/workflows" \ "$repo/tools" \ "$repo/scripts" \ "$repo/crates/example/src" \ @@ -83,6 +84,51 @@ new_repo() { "version = \"$version\"" \ 'edition = "2024"' > "$repo/Cargo.toml" + cat > "$repo/scripts/bundle-macos.sh" <<'SCRIPT' +#!/usr/bin/env bash +WS_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +CANONICAL_VERSION="$("$WS_ROOT/scripts/workspace-version.sh")" +APP_VERSION="${OPENPENCIL_VERSION:-$CANONICAL_VERSION}" +if [[ "$APP_VERSION" != "$CANONICAL_VERSION" ]]; then + exit 1 +fi +/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $APP_VERSION" "$PLIST" +SCRIPT + + cat > "$repo/tools/bundle-macos.sh" <<'SCRIPT' +#!/bin/sh +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +CANONICAL_VERSION="$("$ROOT/scripts/workspace-version.sh")" +APP_VERSION="${OPENPENCIL_VERSION:-$CANONICAL_VERSION}" +if [ "$APP_VERSION" != "$CANONICAL_VERSION" ]; then + exit 1 +fi +CFBundleShortVersionString${APP_VERSION} +SCRIPT + + cat > "$repo/scripts/package-windows.nsi" <<'SCRIPT' +; makensis "/DVERSION=X.Y.Z" "/DOUT_FILE=OpenPencil-X.Y.Z-x64-win-setup.exe" +SCRIPT + + cat > "$repo/scripts/install-op.sh" <<'SCRIPT' +# OP_VERSION=X.Y.Z ./install-op.sh +# set OP_VERSION explicitly, e.g. OP_VERSION=X.Y.Z ./install-op.sh +SCRIPT + + cat > "$repo/.github/workflows/rust-release.yml" <<'SCRIPT' +- name: Compute release version + shell: bash + run: | + cargo_version="$(scripts/workspace-version.sh)" + if [[ "$GITHUB_REF" == refs/tags/v* ]]; then + tag_version="${GITHUB_REF_NAME#v}" + if [[ "$tag_version" != "$cargo_version" ]]; then + exit 1 + fi + fi + echo "OP_VERSION=$cargo_version" >> "$GITHUB_ENV" +SCRIPT + printf '%s\n' "$repo" } @@ -146,4 +192,68 @@ assert_not_contains 'no ordinary Rust fixtures copy current product version' \ 'fixture-version collision' pass 'fixture-version collision is documented and skipped' +repo=$(new_repo hardcoded_macos_version 0.8.1) +printf '%s\n' 'APP_VERSION="${OPENPENCIL_VERSION:-0.8.1}"' \ + >> "$repo/scripts/bundle-macos.sh" +run_guard "$repo" +assert_status 1 'hardcoded macOS version' +assert_contains 'scripts/bundle-macos.sh:' 'hardcoded macOS version' +assert_contains 'error: OPENPENCIL_VERSION must fall back to the Cargo workspace version' \ + 'hardcoded macOS version' +assert_no_success_output 'hardcoded macOS version' +pass 'hardcoded macOS package version is rejected with file and line guidance' + +repo=$(new_repo macos_reader_comment_only 0.8.1) +cat > "$repo/scripts/bundle-macos.sh" <<'SCRIPT' +#!/usr/bin/env bash +# scripts/workspace-version.sh +APP_VERSION="${OPENPENCIL_VERSION:-$CANONICAL_VERSION}" +if [[ "$APP_VERSION" != "$CANONICAL_VERSION" ]]; then + exit 1 +fi +/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $OTHER_VERSION" "$PLIST" +SCRIPT +run_guard "$repo" +assert_status 1 'macOS reader comment only' +assert_contains 'scripts/bundle-macos.sh:1:' 'macOS reader comment only' +assert_contains 'error: macOS packaging must assign CANONICAL_VERSION from scripts/workspace-version.sh' \ + 'macOS reader comment only' +assert_contains 'error: CFBundleShortVersionString must use APP_VERSION' \ + 'macOS reader comment only' +assert_no_success_output 'macOS reader comment only' +pass 'macOS packaging must invoke the reader and use the resolved version' + +repo=$(new_repo release_missing_tag_equality 0.8.1) +cat > "$repo/.github/workflows/rust-release.yml" <<'SCRIPT' +- name: Compute release version + shell: bash + run: | + cargo_version="$(scripts/workspace-version.sh)" + if [[ "$GITHUB_REF" == refs/tags/v* ]]; then + tag_version="${GITHUB_REF_NAME#v}" + fi + echo "OP_VERSION=$cargo_version" >> "$GITHUB_ENV" +SCRIPT +run_guard "$repo" +assert_status 1 'release missing tag equality' +assert_contains '.github/workflows/rust-release.yml:1:' 'release missing tag equality' +assert_contains 'error: release tags must be compared with the Cargo workspace version' \ + 'release missing tag equality' +assert_no_success_output 'release missing tag equality' +pass 'release workflow must reject tags that differ from Cargo' + +repo=$(new_repo collision_still_checks_packaging 1.0.0) +printf '%s\n' 'APP_VERSION="${OPENPENCIL_VERSION:-0.8.1}"' \ + >> "$repo/tools/bundle-macos.sh" +run_guard "$repo" +assert_status 1 'fixture-version collision packaging check' +assert_contains 'skipping literal fixture drift scan' \ + 'fixture-version collision packaging check' +assert_contains 'tools/bundle-macos.sh:' 'fixture-version collision packaging check' +assert_contains 'error: OPENPENCIL_VERSION must fall back to the Cargo workspace version' \ + 'fixture-version collision packaging check' +assert_not_contains 'no ordinary Rust fixtures copy current product version' \ + 'fixture-version collision packaging check' +pass 'fixture-version collision still runs packaging checks' + printf '1..%s\n' "$tests_run"