From f31a0329983b9815b04b77e29e13f5834585fa1a Mon Sep 17 00:00:00 2001 From: Jeroen Rinzema Date: Mon, 21 Sep 2026 12:14:31 +0200 Subject: [PATCH] fix: derive the reported version from the release tag brainpod --version printed 0.1.0 for every published release because Cargo.toml hardcoded that version and releases are tagged only in git, so the tag never reached the crate. The release workflow now derives the version from the tag, writes it to VERSION, and flake.nix passes it to the build as BRAINPOD_VERSION, which build.rs re-exports for clap. VERSION sits outside the crane source filter, so stamping a release does not invalidate cargoArtifacts and only the crate itself rebuilds. A malformed or empty tag fails the release, and both workflows run the binary they just built and fail if its reported version disagrees. --- .github/workflows/build.yml | 15 +++++++++ .github/workflows/release.yml | 34 +++++++++++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 18 ++++++++++ VERSION | 1 + build.rs | 63 +++++++++++++++++++++++++++++++++++ flake.nix | 8 ++++- src/describe.rs | 2 +- src/main.rs | 2 +- 10 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 VERSION diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3bfeab3..773c695 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -59,6 +59,21 @@ jobs: - name: Build run: nix build ".#packages.${{ matrix.system }}.${{ matrix.package }}" + - name: Verify reported version + if: matrix.package == 'default' + run: | + set -euo pipefail + expected="brainpod $(tr -d '[:space:]' < VERSION)" + if ! reported="$(./result/bin/brainpod --version 2>&1)"; then + echo "::error::could not run the ${{ matrix.system }} binary on ${{ matrix.runner }}: $reported" + exit 1 + fi + if [ "$reported" != "$expected" ]; then + echo "::error::binary reports '$reported' but VERSION expects '$expected'" + exit 1 + fi + echo "$reported" + - name: Package Unix binary if: matrix.package == 'default' run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c5f171b..7bf1c07 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,6 +45,24 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Resolve version from release tag + id: version + env: + TAG: ${{ github.event.release.tag_name }} + run: | + set -euo pipefail + if [ -z "${TAG:-}" ]; then + echo "::error::release tag_name is empty, refusing to build an unversioned release" + exit 1 + fi + version="${TAG#v}" + if ! printf '%s' "$version" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$'; then + echo "::error::release tag '$TAG' is not a semver version, refusing to ship a wrong version" + exit 1 + fi + printf '%s\n' "$version" > VERSION + echo "version=$version" >> "$GITHUB_OUTPUT" + - uses: cachix/install-nix-action@v31 with: github_access_token: ${{ secrets.GITHUB_TOKEN }} @@ -59,6 +77,22 @@ jobs: - name: Build run: nix build ".#packages.${{ matrix.system }}.${{ matrix.package }}" + - name: Verify reported version + if: matrix.package == 'default' + env: + EXPECTED: brainpod ${{ steps.version.outputs.version }} + run: | + set -euo pipefail + if ! reported="$(./result/bin/brainpod --version 2>&1)"; then + echo "::error::could not run the ${{ matrix.system }} binary on ${{ matrix.runner }}: $reported" + exit 1 + fi + if [ "$reported" != "$EXPECTED" ]; then + echo "::error::binary reports '$reported' but the release tag expects '$EXPECTED'" + exit 1 + fi + echo "$reported" + - name: Package Unix binary if: matrix.package == 'default' run: | diff --git a/Cargo.lock b/Cargo.lock index faf7c8c..90ae9ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -246,7 +246,7 @@ dependencies = [ [[package]] name = "brainpod-cli" -version = "0.1.0" +version = "0.0.0" dependencies = [ "anyhow", "axum 0.8.9", diff --git a/Cargo.toml b/Cargo.toml index 2ea6afb..66e8108 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "brainpod-cli" -version = "0.1.0" +version = "0.0.0" edition = "2024" [[bin]] diff --git a/README.md b/README.md index 0565de0..2422155 100644 --- a/README.md +++ b/README.md @@ -373,3 +373,21 @@ Create and deploy: brainpod resource create --file resources.json --json brainpod deploy --summary "Configure application resources" --json ``` + +## Versioning + +`brainpod --version` reports the release the binary was built from. The release workflow derives that version from the published git tag, so no version field has to be bumped by hand: + +```sh +brainpod --version +brainpod 0.0.6 +``` + +The tag is written to `VERSION`, read by `flake.nix`, and passed to the build as `BRAINPOD_VERSION`. Builds that the release workflow did not stamp report `0.0.0-dev`, suffixed with the commit they were built from when git metadata is available: + +```sh +brainpod --version +brainpod 0.0.0-dev+d4ac667a1b2c +``` + +`Cargo.toml` deliberately stays at `0.0.0`; it is not the release version. Both the build and release workflows run the binary they just built and fail if its reported version disagrees. diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..cb676de --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.0.0-dev diff --git a/build.rs b/build.rs index 4714397..9697787 100644 --- a/build.rs +++ b/build.rs @@ -1,4 +1,18 @@ +use std::fs; +use std::path::Path; +use std::process::Command; + fn main() -> Result<(), Box> { + println!("cargo:rerun-if-changed=proto"); + println!("cargo:rerun-if-env-changed=BRAINPOD_VERSION"); + + let version = std::env::var("BRAINPOD_VERSION") + .ok() + .map(|value| value.trim().to_owned()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(development_version); + println!("cargo:rustc-env=BRAINPOD_VERSION={version}"); + let descriptors = protox::compile( [ "proto/brainpod/tunnel/v1/broker.proto", @@ -14,3 +28,52 @@ fn main() -> Result<(), Box> { Ok(()) } + +/// Version for builds the release workflow did not stamp, carrying the commit +/// it was built from when git metadata is reachable, so a local build is never +/// mistaken for a published release. +fn development_version() -> String { + let base = format!( + "{}-dev", + std::env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "0.0.0".to_owned()) + ); + + match commit_sha() { + Some(sha) => format!("{base}+{sha}"), + None => base, + } +} + +fn commit_sha() -> Option { + watch_git_head(Path::new(".git")); + + let output = Command::new("git") + .args(["rev-parse", "--short=12", "HEAD"]) + .output() + .ok()?; + if !output.status.success() { + return None; + } + + let sha = String::from_utf8(output.stdout).ok()?.trim().to_owned(); + (!sha.is_empty()).then_some(sha) +} + +fn watch_git_head(git_dir: &Path) { + let head = git_dir.join("HEAD"); + if !head.exists() { + return; + } + + println!("cargo:rerun-if-changed={}", head.display()); + + let Ok(contents) = fs::read_to_string(&head) else { + return; + }; + if let Some(reference) = contents.strip_prefix("ref: ") { + println!( + "cargo:rerun-if-changed={}", + git_dir.join(reference.trim()).display() + ); + } +} diff --git a/flake.nix b/flake.nix index 6af1e20..3019e10 100644 --- a/flake.nix +++ b/flake.nix @@ -24,6 +24,11 @@ overlays = [ (import rust-overlay) ]; eachSystem = nixpkgs.lib.genAttrs supportedSystems; + # The release workflow rewrites ./VERSION from the release tag before + # `nix build`. It is deliberately outside the crane source filter below, + # so stamping a release does not invalidate cargoArtifacts. + version = nixpkgs.lib.head (nixpkgs.lib.splitString "\n" (builtins.readFile ./VERSION)); + rustTargetFor = pkgs: if pkgs.stdenv.hostPlatform.isWindows then @@ -108,7 +113,8 @@ craneLib.buildPackage ( craneArgs // { - inherit cargoArtifacts; + inherit cargoArtifacts version; + BRAINPOD_VERSION = version; meta.mainProgram = if pkgs.stdenv.hostPlatform.isWindows then "brainpod.exe" else "brainpod"; } // pkgs.lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux { diff --git a/src/describe.rs b/src/describe.rs index bb6796c..2ed66c2 100644 --- a/src/describe.rs +++ b/src/describe.rs @@ -28,7 +28,7 @@ pub fn generate(mut root: Command, path: &[String]) -> Result { Ok(json!({ "schemaVersion": 1, - "cliVersion": root.get_version().unwrap_or(env!("CARGO_PKG_VERSION")), + "cliVersion": root.get_version().unwrap_or(env!("BRAINPOD_VERSION")), "command": describe_command(command, &resolved_path), "globalArguments": global_arguments, "outputContract": { diff --git a/src/main.rs b/src/main.rs index 630be05..87f42b8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,7 +29,7 @@ const UPGRADE_URL: &str = "https://console.brainpod.io/onboarding?upgrade=1"; #[derive(Debug, Parser)] #[command( name = "brainpod", - version, + version = env!("BRAINPOD_VERSION"), about = "Manage Brainpod deployments, images, resources, and database tunnels", after_help = "For machine-readable command metadata, run `brainpod describe --json`." )]