From 1ca9c7d117107ecc4798be134e8c2ce9f677c970 Mon Sep 17 00:00:00 2001 From: John Myers <9696606+johntmyers@users.noreply.github.com> Date: Sat, 14 Mar 2026 13:59:09 -0700 Subject: [PATCH] chore: derive build version from git tags for all components Compute version strings from git describe in openshell-core's build.rs using the guess-next-dev scheme (e.g. 0.0.4-dev.6+g2bf9969). All binary crates and the TUI splash screen now use the shared openshell_core::VERSION constant instead of CARGO_PKG_VERSION. In Docker/CI builds where .git is absent, falls back to CARGO_PKG_VERSION which is already set correctly by the sed-patch pipeline. Also adds OPENSHELL_CARGO_VERSION support to the cluster image and fast-deploy supervisor builds for parity with the gateway. --- crates/openshell-cli/src/main.rs | 2 +- crates/openshell-core/build.rs | 58 +++++++++++++++++++++++++++ crates/openshell-core/src/lib.rs | 12 ++++++ crates/openshell-sandbox/src/main.rs | 1 + crates/openshell-server/src/auth.rs | 2 +- crates/openshell-server/src/grpc.rs | 2 +- crates/openshell-server/src/http.rs | 2 +- crates/openshell-server/src/main.rs | 1 + crates/openshell-tui/src/ui/splash.rs | 37 +++++++---------- deploy/docker/Dockerfile.cluster | 4 ++ tasks/scripts/cluster-deploy-fast.sh | 12 ++++++ tasks/scripts/docker-build-cluster.sh | 12 ++++++ 12 files changed, 119 insertions(+), 26 deletions(-) diff --git a/crates/openshell-cli/src/main.rs b/crates/openshell-cli/src/main.rs index f21ad759..b88dd992 100644 --- a/crates/openshell-cli/src/main.rs +++ b/crates/openshell-cli/src/main.rs @@ -298,7 +298,7 @@ const DOCTOR_HELP: &str = "\x1b[1mALIAS\x1b[0m /// `OpenShell` CLI - agent execution and management. #[derive(Parser, Debug)] #[command(name = "openshell")] -#[command(author, version, about, long_about = None)] +#[command(author, version = openshell_core::VERSION, about, long_about = None)] #[command(propagate_version = true)] #[command(help_template = HELP_TEMPLATE)] #[command(disable_help_subcommand = true)] diff --git a/crates/openshell-core/build.rs b/crates/openshell-core/build.rs index 3bda443e..c06702c5 100644 --- a/crates/openshell-core/build.rs +++ b/crates/openshell-core/build.rs @@ -4,6 +4,19 @@ use std::env; fn main() -> Result<(), Box> { + // --- Git-derived version --- + // Compute a version from `git describe` for local builds. In Docker/CI + // builds where .git is absent, this silently does nothing and the binary + // falls back to CARGO_PKG_VERSION (which is already sed-patched by the + // build pipeline). + println!("cargo:rerun-if-changed=../../.git/HEAD"); + println!("cargo:rerun-if-changed=../../.git/refs/tags"); + + if let Some(version) = git_version() { + println!("cargo:rustc-env=OPENSHELL_GIT_VERSION={version}"); + } + + // --- Protobuf compilation --- // Use bundled protoc from protobuf-src // SAFETY: This is run at build time in a single-threaded build script context. // No other threads are reading environment variables concurrently. @@ -33,3 +46,48 @@ fn main() -> Result<(), Box> { Ok(()) } + +/// Derive a version string from `git describe --tags`. +/// +/// Implements the "guess-next-dev" convention used by the release pipeline +/// (`setuptools-scm`): when there are commits past the last tag, the patch +/// version is bumped and `-dev.+g` is appended. +/// +/// Examples: +/// on tag v0.0.3 → "0.0.3" +/// 3 commits past v0.0.3 → "0.0.4-dev.3+g2bf9969" +/// +/// Returns `None` when git is unavailable or the repo has no matching tags. +fn git_version() -> Option { + let output = std::process::Command::new("git") + .args(["describe", "--tags", "--long", "--match", "v*"]) + .output() + .ok()?; + + if !output.status.success() { + return None; + } + + let desc = String::from_utf8(output.stdout).ok()?; + let desc = desc.trim(); + let desc = desc.strip_prefix('v').unwrap_or(desc); + + // `git describe --long` format: --g + // Split from the right to handle tags that contain hyphens. + let (rest, sha) = desc.rsplit_once('-')?; + let (tag, commits_str) = rest.rsplit_once('-')?; + let commits: u32 = commits_str.parse().ok()?; + + if commits == 0 { + // Exactly on a tag — use the tag version as-is. + return Some(tag.to_string()); + } + + // Bump patch version (guess-next-dev scheme). + let mut parts = tag.splitn(3, '.'); + let major = parts.next()?; + let minor = parts.next()?; + let patch: u32 = parts.next()?.parse().ok()?; + + Some(format!("{major}.{minor}.{}-dev.{commits}+{sha}", patch + 1)) +} diff --git a/crates/openshell-core/src/lib.rs b/crates/openshell-core/src/lib.rs index a84c586b..4c91a91d 100644 --- a/crates/openshell-core/src/lib.rs +++ b/crates/openshell-core/src/lib.rs @@ -7,6 +7,7 @@ //! - Protocol buffer definitions and generated code //! - Configuration management //! - Common error types +//! - Build version metadata pub mod config; pub mod error; @@ -16,3 +17,14 @@ pub mod proto; pub use config::{Config, TlsConfig}; pub use error::{Error, Result}; + +/// Build version string derived from git metadata. +/// +/// For local builds this is computed by `build.rs` via `git describe` using +/// the guess-next-dev scheme (e.g. `0.0.4-dev.6+g2bf9969`). In Docker/CI +/// builds where `.git` is absent, falls back to `CARGO_PKG_VERSION` which +/// is already set correctly by the build pipeline's sed patch. +pub const VERSION: &str = match option_env!("OPENSHELL_GIT_VERSION") { + Some(v) => v, + None => env!("CARGO_PKG_VERSION"), +}; diff --git a/crates/openshell-sandbox/src/main.rs b/crates/openshell-sandbox/src/main.rs index b3f15a31..7e02459e 100644 --- a/crates/openshell-sandbox/src/main.rs +++ b/crates/openshell-sandbox/src/main.rs @@ -14,6 +14,7 @@ use openshell_sandbox::run_sandbox; /// OpenShell Sandbox - process isolation and monitoring. #[derive(Parser, Debug)] #[command(name = "openshell-sandbox")] +#[command(version = openshell_core::VERSION)] #[command(about = "Process sandbox and monitor", long_about = None)] struct Args { /// Command to execute in the sandbox. diff --git a/crates/openshell-server/src/auth.rs b/crates/openshell-server/src/auth.rs index 12e4665e..5a3229ff 100644 --- a/crates/openshell-server/src/auth.rs +++ b/crates/openshell-server/src/auth.rs @@ -121,7 +121,7 @@ fn render_connect_page( .replace('<', "\\x3c") .replace('>', "\\x3e"); - let version = env!("CARGO_PKG_VERSION"); + let version = openshell_core::VERSION; format!( r#" diff --git a/crates/openshell-server/src/grpc.rs b/crates/openshell-server/src/grpc.rs index 61531882..422b6463 100644 --- a/crates/openshell-server/src/grpc.rs +++ b/crates/openshell-server/src/grpc.rs @@ -134,7 +134,7 @@ impl OpenShell for OpenShellService { ) -> Result, Status> { Ok(Response::new(HealthResponse { status: ServiceStatus::Healthy.into(), - version: env!("CARGO_PKG_VERSION").to_string(), + version: openshell_core::VERSION.to_string(), })) } diff --git a/crates/openshell-server/src/http.rs b/crates/openshell-server/src/http.rs index c0156df6..afe7edc1 100644 --- a/crates/openshell-server/src/http.rs +++ b/crates/openshell-server/src/http.rs @@ -31,7 +31,7 @@ async fn healthz() -> impl IntoResponse { async fn readyz() -> impl IntoResponse { let response = HealthResponse { status: "healthy", - version: env!("CARGO_PKG_VERSION"), + version: openshell_core::VERSION, }; (StatusCode::OK, Json(response)) diff --git a/crates/openshell-server/src/main.rs b/crates/openshell-server/src/main.rs index 67388378..50fe81dd 100644 --- a/crates/openshell-server/src/main.rs +++ b/crates/openshell-server/src/main.rs @@ -15,6 +15,7 @@ use openshell_server::{run_server, tracing_bus::TracingLogBus}; /// `OpenShell` Server - gRPC and HTTP server with protocol multiplexing. #[derive(Parser, Debug)] #[command(name = "openshell-server")] +#[command(version = openshell_core::VERSION)] #[command(about = "OpenShell gRPC/HTTP server", long_about = None)] struct Args { /// Port to bind the server to (all interfaces). diff --git a/crates/openshell-tui/src/ui/splash.rs b/crates/openshell-tui/src/ui/splash.rs index 34fae673..1a5595d1 100644 --- a/crates/openshell-tui/src/ui/splash.rs +++ b/crates/openshell-tui/src/ui/splash.rs @@ -45,8 +45,8 @@ const ART_WIDTH: u16 = 40; /// Total content lines: 6 (OPEN) + 6 (SHELL) + 1 (blank) + 1 (tagline) = 14. const CONTENT_LINES: u16 = 14; -// Border (2) + top/bottom inner padding (2) + content + blank before footer (1) + footer (1). -const MODAL_HEIGHT: u16 = CONTENT_LINES + 6; +// Border (2) + top/bottom inner padding (2) + content + blank before footer (1) + footer (2). +const MODAL_HEIGHT: u16 = CONTENT_LINES + 7; // Art width + left/right padding (3+3) + borders (2). const MODAL_WIDTH: u16 = ART_WIDTH + 8; @@ -73,13 +73,13 @@ pub fn draw(frame: &mut Frame<'_>, area: Rect, theme: &crate::theme::Theme) { let inner = block.inner(popup); frame.render_widget(block, popup); - // Split inner area: art content + spacer + footer. + // Split inner area: art content + spacer + footer (2 lines). let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Length(CONTENT_LINES), // OPEN + SHELL + blank + tagline Constraint::Min(0), // spacer - Constraint::Length(1), // footer + Constraint::Length(2), // footer (version + prompt) ]) .split(inner); @@ -102,27 +102,20 @@ pub fn draw(frame: &mut Frame<'_>, area: Rect, theme: &crate::theme::Theme) { frame.render_widget(Paragraph::new(content_lines), chunks[0]); - // -- Footer: version + ALPHA badge (left) + prompt (right) -- - let version = format!("v{}", env!("CARGO_PKG_VERSION")); - let spacer = " "; + // -- Footer: version + ALPHA badge on line 1, prompt on line 2 -- + let version = format!("v{}", openshell_core::VERSION); let alpha_badge = "ALPHA"; - let prompt_text = "press any key"; - - // Pad between left group and prompt to fill the line. - let used = version.len() + spacer.len() + alpha_badge.len() + prompt_text.len() + 2; // +2 for ░ and space - let footer_width = chunks[2].width as usize; - let gap = footer_width.saturating_sub(used); - - let footer = Line::from(vec![ - Span::styled(version, t.accent), - Span::styled(spacer, t.muted), - Span::styled(alpha_badge, t.title_bar), - Span::styled(" ".repeat(gap), t.muted), - Span::styled(prompt_text, t.muted), - Span::styled(" ░", t.muted), + + let footer = Paragraph::new(vec![ + Line::from(vec![ + Span::styled(version, t.accent), + Span::styled(" ", t.muted), + Span::styled(alpha_badge, t.title_bar), + ]), + Line::from(Span::styled("press any key ░", t.muted)), ]); - frame.render_widget(Paragraph::new(footer), chunks[2]); + frame.render_widget(footer, chunks[2]); } fn centered_rect(width: u16, height: u16, area: Rect) -> Rect { diff --git a/deploy/docker/Dockerfile.cluster b/deploy/docker/Dockerfile.cluster index 643aa0b8..49e29a98 100644 --- a/deploy/docker/Dockerfile.cluster +++ b/deploy/docker/Dockerfile.cluster @@ -76,6 +76,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certifi FROM --platform=$BUILDPLATFORM rust:1.88-slim AS supervisor-builder ARG TARGETARCH ARG BUILDARCH +ARG OPENSHELL_CARGO_VERSION ARG CARGO_TARGET_CACHE_SCOPE=default ARG SCCACHE_MEMCACHED_ENDPOINT @@ -135,6 +136,9 @@ RUN --mount=type=cache,id=cargo-registry-supervisor-${TARGETARCH},sharing=locked --mount=type=cache,id=cargo-target-supervisor-${TARGETARCH}-${CARGO_TARGET_CACHE_SCOPE},sharing=locked,target=/build/target \ --mount=type=cache,id=sccache-supervisor-${TARGETARCH},sharing=locked,target=/tmp/sccache \ . cross-build.sh && \ + if [ -n "${OPENSHELL_CARGO_VERSION:-}" ]; then \ + sed -i -E '/^\[workspace\.package\]/,/^\[/{s/^version[[:space:]]*=[[:space:]]*".*"/version = "'"${OPENSHELL_CARGO_VERSION}"'"/}' Cargo.toml; \ + fi && \ cargo_cross_build --release -p openshell-sandbox && \ mkdir -p /build/out && \ cp "$(cross_output_dir release)/openshell-sandbox" /build/out/ diff --git a/tasks/scripts/cluster-deploy-fast.sh b/tasks/scripts/cluster-deploy-fast.sh index 348640d5..c9eba82d 100755 --- a/tasks/scripts/cluster-deploy-fast.sh +++ b/tasks/scripts/cluster-deploy-fast.sh @@ -321,11 +321,23 @@ if [[ "${build_supervisor}" == "1" ]]; then SUPERVISOR_BUILD_DIR=$(mktemp -d) trap 'rm -rf "${SUPERVISOR_BUILD_DIR}"' EXIT + # Compute cargo version from git tags for the supervisor binary. + SUPERVISOR_VERSION_ARGS=() + if [[ -n "${OPENSHELL_CARGO_VERSION:-}" ]]; then + SUPERVISOR_VERSION_ARGS=(--build-arg "OPENSHELL_CARGO_VERSION=${OPENSHELL_CARGO_VERSION}") + else + _cargo_version=$(uv run python tasks/scripts/release.py get-version --cargo 2>/dev/null || true) + if [[ -n "${_cargo_version}" ]]; then + SUPERVISOR_VERSION_ARGS=(--build-arg "OPENSHELL_CARGO_VERSION=${_cargo_version}") + fi + fi + docker buildx build \ --file deploy/docker/Dockerfile.cluster \ --target supervisor-builder \ --build-arg "BUILDARCH=$(docker version --format '{{.Server.Arch}}')" \ --build-arg "TARGETARCH=${CLUSTER_ARCH}" \ + ${SUPERVISOR_VERSION_ARGS[@]+"${SUPERVISOR_VERSION_ARGS[@]}"} \ --output "type=local,dest=${SUPERVISOR_BUILD_DIR}" \ --platform "linux/${CLUSTER_ARCH}" \ . diff --git a/tasks/scripts/docker-build-cluster.sh b/tasks/scripts/docker-build-cluster.sh index 9f52f54c..80dc2a48 100755 --- a/tasks/scripts/docker-build-cluster.sh +++ b/tasks/scripts/docker-build-cluster.sh @@ -65,10 +65,22 @@ elif [[ "${DOCKER_PLATFORM:-}" == *","* ]]; then OUTPUT_FLAG="--push" fi +# Compute cargo version from git tags (same scheme as docker-build-component.sh). +VERSION_ARGS=() +if [[ -n "${OPENSHELL_CARGO_VERSION:-}" ]]; then + VERSION_ARGS=(--build-arg "OPENSHELL_CARGO_VERSION=${OPENSHELL_CARGO_VERSION}") +else + CARGO_VERSION=$(uv run python tasks/scripts/release.py get-version --cargo 2>/dev/null || true) + if [[ -n "${CARGO_VERSION}" ]]; then + VERSION_ARGS=(--build-arg "OPENSHELL_CARGO_VERSION=${CARGO_VERSION}") + fi +fi + docker buildx build \ ${BUILDER_ARGS[@]+"${BUILDER_ARGS[@]}"} \ ${DOCKER_PLATFORM:+--platform ${DOCKER_PLATFORM}} \ ${CACHE_ARGS[@]+"${CACHE_ARGS[@]}"} \ + ${VERSION_ARGS[@]+"${VERSION_ARGS[@]}"} \ -f deploy/docker/Dockerfile.cluster \ -t ${IMAGE_NAME}:${IMAGE_TAG} \ ${K3S_VERSION:+--build-arg K3S_VERSION=${K3S_VERSION}} \