fix(release): sync root Cargo version in release pipeline (#449) (#461)

* fix(release): sync root Cargo version in bump flow (#449)

Add root Cargo.toml to release version bumping and introduce a
version-sync verifier for all four release version sources.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci(release): enforce version sync before tagging (#449)

Run release version consistency verification and include root
Cargo.toml in the release commit staging list.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(release): add local dmg preflight version dry-run (#449)

Add a local release dry-run script that builds frontend + release
sidecar, bundles app/dmg, and validates packaged core.version.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
oxoxDev
2026-04-09 10:44:46 -07:00
committed by GitHub
co-authored by Claude Opus 4.6
parent dc5e7adeb6
commit 4639913ddf
5 changed files with 196 additions and 13 deletions
+4 -1
View File
@@ -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
+1 -1
View File
@@ -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
+17 -11
View File
@@ -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 <patch|minor|major>
@@ -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`;
@@ -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"
+62
View File
@@ -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]}`);