diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f9a017044..97e3f3f2d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -95,6 +95,9 @@ jobs: id: bump run: node scripts/release/bump-version.js "${{ inputs.release_type }}" + - name: Verify release version sync + run: node scripts/release/verify-version-sync.js "${{ steps.bump.outputs.version }}" + - name: Ensure tag does not already exist env: TAG: ${{ steps.bump.outputs.tag }} @@ -115,7 +118,7 @@ jobs: VERSION: ${{ steps.bump.outputs.version }} TAG: ${{ steps.bump.outputs.tag }} run: | - git add app/package.json app/src-tauri/tauri.conf.json app/src-tauri/Cargo.toml + git add app/package.json app/src-tauri/tauri.conf.json app/src-tauri/Cargo.toml Cargo.toml git commit -m "chore(release): v${VERSION}" git push origin main diff --git a/Cargo.toml b/Cargo.toml index af2197bd6..241197cb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openhuman" -version = "0.49.17" +version = "0.51.19" edition = "2021" description = "OpenHuman core business logic and RPC server" autobins = false diff --git a/scripts/release/bump-version.js b/scripts/release/bump-version.js index 0486b571c..681040ea1 100755 --- a/scripts/release/bump-version.js +++ b/scripts/release/bump-version.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -// Bump version in package.json, tauri.conf.json, and Cargo.toml. +// Bump version in package.json, tauri.conf.json, and both Cargo.toml manifests. // // Usage: // node scripts/release/bump-version.js @@ -25,7 +25,8 @@ if (!allowed.has(RELEASE_TYPE)) { const root = path.resolve(__dirname, '..', '..'); const packagePath = path.join(root, 'app/package.json'); const tauriPath = path.join(root, 'app/src-tauri/tauri.conf.json'); -const cargoPath = path.join(root, 'app/src-tauri/Cargo.toml'); +const tauriCargoPath = path.join(root, 'app/src-tauri/Cargo.toml'); +const coreCargoPath = path.join(root, 'Cargo.toml'); // ── Read current version ──────────────────────────────────────────────────── const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8')); @@ -57,16 +58,21 @@ const tauri = JSON.parse(fs.readFileSync(tauriPath, 'utf8')); tauri.version = nextVersion; fs.writeFileSync(tauriPath, `${JSON.stringify(tauri, null, 2)}\n`); -// ── Write Cargo.toml ──────────────────────────────────────────────────────── -const cargo = fs.readFileSync(cargoPath, 'utf8'); -const updatedCargo = cargo.replace( - /(\[package\][\s\S]*?^version\s*=\s*")([^"]+)(")/m, - `$1${nextVersion}$3`, -); -if (updatedCargo === cargo) { - throw new Error('Failed to update [package].version in app/src-tauri/Cargo.toml'); +function bumpCargoVersion(filePath, nextVersion) { + const cargo = fs.readFileSync(filePath, 'utf8'); + const updatedCargo = cargo.replace( + /(\[package\][\s\S]*?^version\s*=\s*")([^"]+)(")/m, + `$1${nextVersion}$3`, + ); + if (updatedCargo === cargo) { + throw new Error(`Failed to update [package].version in ${path.relative(root, filePath)}`); + } + fs.writeFileSync(filePath, updatedCargo); } -fs.writeFileSync(cargoPath, updatedCargo); + +// ── Write Cargo.toml files ────────────────────────────────────────────────── +bumpCargoVersion(tauriCargoPath, nextVersion); +bumpCargoVersion(coreCargoPath, nextVersion); // ── Output ────────────────────────────────────────────────────────────────── const lines = `version=${nextVersion}\ntag=v${nextVersion}\n`; diff --git a/scripts/release/local-dmg-version-dry-run.sh b/scripts/release/local-dmg-version-dry-run.sh new file mode 100644 index 000000000..a8199afa1 --- /dev/null +++ b/scripts/release/local-dmg-version-dry-run.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +APP_DIR="$REPO_ROOT/app" +APP_BUNDLE="$REPO_ROOT/app/src-tauri/target/release/bundle/macos/OpenHuman.app" +DMG_DIR="$REPO_ROOT/app/src-tauri/target/release/bundle/dmg" +TEMP_ENV_CREATED=0 +TMP_TAURI_CONF="" + +cleanup() { + if [[ "$TEMP_ENV_CREATED" -eq 1 ]]; then + rm -f "$REPO_ROOT/.env" + fi + if [[ -n "$TMP_TAURI_CONF" ]]; then + rm -f "$TMP_TAURI_CONF" + fi +} +trap cleanup EXIT + +echo "[dry-run] Verifying release version files are synced" +node "$REPO_ROOT/scripts/release/verify-version-sync.js" + +EXPECTED_VERSION="$( + node -e 'const fs=require("fs");const p=JSON.parse(fs.readFileSync(process.argv[1], "utf8"));process.stdout.write(p.version);' \ + "$APP_DIR/package.json" +)" +echo "[dry-run] Expected version: $EXPECTED_VERSION" + +if [[ ! -f "$REPO_ROOT/.env" ]]; then + if [[ -f "$REPO_ROOT/.env.example" ]]; then + cp "$REPO_ROOT/.env.example" "$REPO_ROOT/.env" + TEMP_ENV_CREATED=1 + echo "[dry-run] Created temporary .env from .env.example for local build" + else + echo "[dry-run] ERROR: missing .env and .env.example at repo root" >&2 + exit 1 + fi +fi + +HOST_TRIPLE="$(rustc -vV | awk '/^host:/ {print $2}')" + +echo "[dry-run] Building frontend bundle" +( + cd "$APP_DIR" + npm run build:app +) + +echo "[dry-run] Building release openhuman-core for $HOST_TRIPLE" +cargo build --release --manifest-path "$REPO_ROOT/Cargo.toml" --bin openhuman-core + +echo "[dry-run] Staging sidecar" +bash "$REPO_ROOT/scripts/release/stage-sidecar.sh" \ + "$HOST_TRIPLE" \ + "target/release" \ + "openhuman-core" \ + "openhuman-core" + +TMP_TAURI_CONF="$(mktemp "${TMPDIR:-/tmp}/openhuman-tauri-dry-run.XXXXXX").json" +node -e ' + const fs = require("fs"); + const input = process.argv[1]; + const output = process.argv[2]; + const config = JSON.parse(fs.readFileSync(input, "utf8")); + config.build = config.build || {}; + config.build.beforeBuildCommand = "echo \"[dry-run] beforeBuildCommand handled externally\""; + fs.writeFileSync(output, `${JSON.stringify(config, null, 2)}\n`); +' "$APP_DIR/src-tauri/tauri.conf.json" "$TMP_TAURI_CONF" + +echo "[dry-run] Building local DMG with staged sidecar" +( + cd "$APP_DIR" + source ../scripts/load-dotenv.sh + yarn tauri build --bundles app,dmg --config "$TMP_TAURI_CONF" +) + +if [[ ! -d "$APP_BUNDLE" ]]; then + echo "[dry-run] ERROR: app bundle not found at $APP_BUNDLE" >&2 + exit 1 +fi + +if [[ ! -d "$DMG_DIR" ]]; then + echo "[dry-run] ERROR: DMG directory not found at $DMG_DIR" >&2 + exit 1 +fi + +DMG_PATH="$(find "$DMG_DIR" -maxdepth 1 -type f -name '*.dmg' | sort | tail -n 1)" +if [[ -z "$DMG_PATH" ]]; then + echo "[dry-run] ERROR: no DMG artifact produced in $DMG_DIR" >&2 + exit 1 +fi + +CORE_BIN="$( + find "$APP_BUNDLE/Contents" -maxdepth 4 -type f -name 'openhuman-core*' ! -name '*.sig' | head -n 1 +)" +if [[ -z "$CORE_BIN" ]]; then + echo "[dry-run] ERROR: packaged openhuman-core binary not found in app bundle" >&2 + exit 1 +fi + +CORE_VERSION_OUTPUT="$("$CORE_BIN" call --method core.version)" +if ! grep -q "\"version\": \"$EXPECTED_VERSION\"" <<<"$CORE_VERSION_OUTPUT"; then + echo "[dry-run] ERROR: packaged core version does not match expected version" >&2 + echo "[dry-run] core output: $CORE_VERSION_OUTPUT" >&2 + exit 1 +fi + +echo "[dry-run] PASS" +echo "[dry-run] DMG: $DMG_PATH" +echo "[dry-run] Core binary: $CORE_BIN" +echo "[dry-run] Core version output: $CORE_VERSION_OUTPUT" diff --git a/scripts/release/verify-version-sync.js b/scripts/release/verify-version-sync.js new file mode 100644 index 000000000..640ca069b --- /dev/null +++ b/scripts/release/verify-version-sync.js @@ -0,0 +1,62 @@ +#!/usr/bin/env node +// Verify release version consistency across all authoritative files. +// +// Usage: +// node scripts/release/verify-version-sync.js [expected-version] +// +// If expected-version is provided, every source must match it. + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const root = path.resolve(__dirname, '..', '..'); +const expectedVersion = process.argv[2] || process.env.EXPECTED_VERSION || null; + +function readJsonVersion(filePath, field = 'version') { + const data = JSON.parse(fs.readFileSync(filePath, 'utf8')); + const value = data[field]; + if (!value || typeof value !== 'string') { + throw new Error(`Missing string "${field}" in ${path.relative(root, filePath)}`); + } + return value; +} + +function readCargoPackageVersion(filePath) { + const cargo = fs.readFileSync(filePath, 'utf8'); + const match = cargo.match(/^\[package\][\s\S]*?^version\s*=\s*"([^"]+)"/m); + if (!match) { + throw new Error(`Failed to read [package].version in ${path.relative(root, filePath)}`); + } + return match[1]; +} + +const versions = { + 'app/package.json': readJsonVersion(path.join(root, 'app/package.json')), + 'app/src-tauri/tauri.conf.json': readJsonVersion(path.join(root, 'app/src-tauri/tauri.conf.json')), + 'app/src-tauri/Cargo.toml': readCargoPackageVersion(path.join(root, 'app/src-tauri/Cargo.toml')), + 'Cargo.toml': readCargoPackageVersion(path.join(root, 'Cargo.toml')), +}; + +const values = Object.values(versions); +const unique = [...new Set(values)]; + +if (expectedVersion && values.some((value) => value !== expectedVersion)) { + console.error('[verify-version-sync] Expected version mismatch.'); + for (const [file, version] of Object.entries(versions)) { + console.error(` ${file}: ${version}`); + } + console.error(` expected: ${expectedVersion}`); + process.exit(1); +} + +if (unique.length !== 1) { + console.error('[verify-version-sync] Version mismatch detected.'); + for (const [file, version] of Object.entries(versions)) { + console.error(` ${file}: ${version}`); + } + process.exit(1); +} + +console.log(`[verify-version-sync] OK ${unique[0]}`);