Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/release-builds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,14 @@ jobs:
portable-path: src-tauri/target/release/gridfpv-desktop

steps:
# fetch-tags is load-bearing for the build stamp (#513): a build names itself by its
# release version only when HEAD sits exactly on a clean v* tag, so the tags must be
# present in the checkout. Dispatch this workflow FROM the tagged commit to produce a
# release-named build; a dispatch from an untagged commit correctly produces the
# 0.4.0-dev-<short hash> naming (the dev-* prerelease portables).
- uses: actions/checkout@v7
with:
fetch-tags: true

# Linux-only: Tauri's webkit2gtk/GTK system deps (Windows uses the bundled
# WebView2 runtime and needs none of these). Mirror src-tauri/README.md.
Expand Down
2 changes: 1 addition & 1 deletion crates/app/src/logging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ fn open_sink() -> Option<Sink> {
.unwrap_or_else(|_| "<unknown>".to_string());
sink.write_line(&format!(
"==== GridFPV {} — session start (pid {}, {} {}, exe {exe}) ====",
env!("CARGO_PKG_VERSION"),
gridfpv_server::BUILD_VERSION,
std::process::id(),
std::env::consts::OS,
std::env::consts::ARCH,
Expand Down
7 changes: 5 additions & 2 deletions crates/app/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,10 @@ fn print_startup(
bound.to_string()
};

println!("GridFPV Director {} — serving", env!("CARGO_PKG_VERSION"));
println!(
"GridFPV Director {} — serving",
gridfpv_server::BUILD_VERSION
);
println!(" listening on : http://{bound}");
println!(" console URL : http://{url_host}/");
match rd_token {
Expand Down Expand Up @@ -182,7 +185,7 @@ async fn shutdown_signal() {
fn run_demo() -> Result<(), Box<dyn std::error::Error>> {
println!(
"GridFPV {} — walking-skeleton demo\n",
env!("CARGO_PKG_VERSION")
gridfpv_server::BUILD_VERSION
);

let events = synthetic_session(
Expand Down
85 changes: 85 additions & 0 deletions crates/server/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//! Stamps `GRIDFPV_BUILD_VERSION` — the version the running build *reports* (#513), read back
//! as [`gridfpv_server::BUILD_VERSION`] and served on `/about` (the console's brand stamp, the
//! Director banner, the log session header).
//!
//! The scheme, per the field ruling (#513): **a release names itself, everything else names its
//! commit.** Before this every build — the `dev-*` prerelease portables included — reported the
//! static workspace version (`0.4.0-alpha.1`), so "which build is this?" was unanswerable on a
//! support call.
//!
//! Resolution order:
//! 1. `GRIDFPV_RELEASE_VERSION` env (leading `v` stripped) — the explicit override for a build
//! pipeline that knows exactly what it is shipping.
//! 2. HEAD exactly on a **clean** `v*` tag ⇒ the tag minus the `v` — the standard
//! alpha/beta/full naming (`0.4.0-alpha.1`, `0.4.0`). The `--match "v*"` filter is
//! load-bearing: the `dev-2026-*` prerelease tags are exactly the builds that must NOT name
//! themselves like releases.
//! 3. Otherwise ⇒ `<workspace base>-dev-<short hash>`, `-dirty` appended when the tree has
//! uncommitted changes (a dirty build is not its hash).
//! 4. No usable git at all (a source tarball) ⇒ the workspace version verbatim.

use std::process::Command;

fn main() {
// Re-stamp when HEAD moves (commit / checkout / tag) or the index changes (staging is the
// cheapest observable signal that the dirty state may have flipped). Both relative to the
// workspace root's .git, two levels up from this crate.
println!("cargo:rerun-if-changed=../../.git/HEAD");
println!("cargo:rerun-if-changed=../../.git/index");
println!("cargo:rerun-if-env-changed=GRIDFPV_RELEASE_VERSION");

let pkg = std::env::var("CARGO_PKG_VERSION").expect("cargo sets CARGO_PKG_VERSION");
println!(
"cargo:rustc-env=GRIDFPV_BUILD_VERSION={}",
build_version(&pkg)
);
}

fn build_version(pkg: &str) -> String {
if let Ok(v) = std::env::var("GRIDFPV_RELEASE_VERSION") {
let v = v.trim();
if !v.is_empty() {
return v.trim_start_matches('v').to_string();
}
}
// `git()` yields None for empty output, so Some(porcelain) == a dirty tree.
let dirty = git(&["status", "--porcelain"]).is_some();
if !dirty {
if let Some(tag) = git(&[
"describe",
"--exact-match",
"--tags",
"--match",
"v*",
"HEAD",
]) {
return tag.trim_start_matches('v').to_string();
}
}
let Some(hash) = git(&["rev-parse", "--short=7", "HEAD"]) else {
// No git (a source tarball / a stripped checkout): the workspace version verbatim is
// the most honest answer left.
return pkg.to_string();
};
// The dev base is the workspace version's MAJOR.MINOR.PATCH — the milestone being worked
// toward — with the `-alpha.N`-style tail dropped; the commit hash is the real identity.
let base = pkg.split('-').next().unwrap_or(pkg);
let dirty = if dirty { "-dirty" } else { "" };
format!("{base}-dev-{hash}{dirty}")
}

/// Run `git` in the crate directory (git walks up to the workspace root on its own); `None` on
/// a failed/missing git or empty output, so callers read `Some` as a real answer.
fn git(args: &[&str]) -> Option<String> {
let out = Command::new("git").args(args).output().ok()?;
if !out.status.success() {
return None;
}
let s = String::from_utf8(out.stdout).ok()?;
let s = s.trim();
if s.is_empty() {
None
} else {
Some(s.to_string())
}
}
8 changes: 5 additions & 3 deletions crates/server/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -383,14 +383,16 @@ pub fn router(registry: EventRegistry) -> Router {
.route("/health", get(|| async { "ok" }))
// The product identity (alpha field-support): WHICH build is this rig running? The
// console footer reads it, and a bug report from the field should quote it. The
// version is the ONE workspace version (v0.4.0-alpha.1 scheme, `cargo xtask version`);
// the contract version is the independent wire-compat integer.
// version is the BUILD's version (#513): a release names itself (the v0.4.0-alpha.1
// scheme, `cargo xtask version`), any other build names its commit
// (`0.4.0-dev-<short hash>`); the contract version is the independent wire-compat
// integer.
.route(
"/about",
get(|| async {
Json(serde_json::json!({
"name": "GridFPV",
"version": env!("CARGO_PKG_VERSION"),
"version": crate::BUILD_VERSION,
"contract_version": crate::CONTRACT_VERSION,
}))
}),
Expand Down
24 changes: 24 additions & 0 deletions crates/server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,14 @@ impl ContractVersion {
/// The contract version this server build speaks. The first wire contract is `1`.
pub const CONTRACT_VERSION: ContractVersion = ContractVersion::new(1);

/// The **product version this build reports** (#513), stamped by `build.rs`: a release build
/// (HEAD on a clean `v*` tag, or an explicit `GRIDFPV_RELEASE_VERSION`) names itself by its
/// standard alpha/beta/full version, and every other build names its commit —
/// `0.4.0-dev-<short hash>` (`-dirty` when the tree had uncommitted changes). This is what
/// `/about` serves and the console's brand stamp shows; use it in place of
/// `CARGO_PKG_VERSION` anywhere a human reads a version off a running build.
pub const BUILD_VERSION: &str = env!("GRIDFPV_BUILD_VERSION");

/// The **oldest** contract version this server still serves (protocol.html §7, §9.7) —
/// the bottom of the supported band `MIN_SUPPORTED_CONTRACT_VERSION..=CONTRACT_VERSION`.
///
Expand Down Expand Up @@ -254,4 +262,20 @@ mod tests {
let back: ServerHello = serde_json::from_str(&json).unwrap();
assert_eq!(reply, back);
}

#[test]
fn build_version_is_semver_shaped_whatever_named_this_build() {
// The stamp (#513) resolves differently per build (a release tag, a dev commit hash, a
// tarball fallback), but every spelling must satisfy the shape the /about contract test
// pins: x.y.z with an optional -prerelease tail. This is the compile-time proof the
// build.rs emitted SOMETHING sane for the build running these tests.
let v = crate::BUILD_VERSION;
let (core, _tail) = v.split_once('-').unwrap_or((v, ""));
assert_eq!(core.split('.').count(), 3, "MAJOR.MINOR.PATCH core: {v:?}");
assert!(
core.split('.')
.all(|p| !p.is_empty() && p.chars().all(|c| c.is_ascii_digit())),
"numeric core: {v:?}"
);
}
}
5 changes: 3 additions & 2 deletions frontend/apps/rd-console/src/lib/buildVersion.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ let version = $state<string | undefined>(undefined);
let asked = false;

/**
* The Director's version string (`"0.4.0-alpha.1"`), or `undefined` until the read lands — or for
* good, if it never does. Reading this the first time starts the fetch.
* The Director's version string — a release's standard name (`"0.4.0-alpha.1"`) or a
* non-release build's commit stamp (`"0.4.0-dev-<short hash>"`, #513) — or `undefined` until
* the read lands, or for good, if it never does. Reading this the first time starts the fetch.
*/
export function directorVersion(): string | undefined {
if (!asked) {
Expand Down
4 changes: 3 additions & 1 deletion frontend/contract/events.contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1720,7 +1720,9 @@ describe('seam: /about (the build stamp)', () => {
expect(res.ok).toBe(true);
const about = await res.json();
expect(about.name).toBe('GridFPV');
// x.y.z with an optional -prerelease tail (the v0.4.0-alpha.1 scheme).
// x.y.z with an optional -prerelease tail: a release's standard naming
// (0.4.0-alpha.1), or a non-release build's commit stamp (0.4.0-dev-<short hash>,
// -dirty appended for an uncommitted tree) — #513.
expect(about.version).toMatch(/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/);
expect(typeof about.contract_version).toBe('number');
});
Expand Down