build: derive package versions from Cargo

This commit is contained in:
Kayshen-X 2026-07-15 21:10:08 +08:00
parent 983b2a3573
commit d3fc4fed6d
7 changed files with 278 additions and 36 deletions

View file

@ -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)

View file

@ -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/<triple>/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

View file

@ -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

View file

@ -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

View file

@ -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" <<PLIST
<key>CFBundleIconFile</key><string>icon</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
<key>CFBundleShortVersionString</key><string>0.8.1</string>
<key>CFBundleShortVersionString</key><string>${APP_VERSION}</string>
<key>NSHighResolutionCapable</key><true/>
</dict>
</plist>

View file

@ -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</key><string>${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>' \
'version examples must use X.Y.Z or <version>'
reject_matches fixed "$example_file" '0.8.1' \
'version examples must use X.Y.Z or <version>, 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 <version>, 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"

View file

@ -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
<key>CFBundleShortVersionString</key><string>${APP_VERSION}</string>
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"