# How Herdr Checks for New Versions: Inside the Rust Self-Update Mechanism

> Discover how Herdr checks for new versions using its Rust self-update mechanism. Learn about manifest fetching, semver comparison, and atomic binary replacement.

- Repository: [Can Celik/herdr](https://github.com/ogulcancelik/herdr)
- Tags: internals
- Published: 2026-05-31

---

**Herdr checks for new versions by fetching a JSON manifest from `https://herdr.dev/latest.json`, comparing semantic versions using the `semver` crate, and atomically replacing the running binary if a newer release exists.**

The self-update mechanism in the `ogulcancelik/herdr` repository is implemented entirely in Rust and provides a platform-aware way to keep the CLI tool current. Unlike package manager-dependent updates, Herdr implements an in-place binary replacement strategy that works across Linux, macOS, and other supported platforms. Understanding how this update mechanism checks for new versions helps developers debug deployment issues and ensures CI/CD pipelines remain up to date.

## How the Herdr Update Mechanism Works

The update logic resides in **[`src/update.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/update.rs)** and follows a sequential pipeline from manifest retrieval to atomic installation. Each stage is designed to be resilient, with clear error handling for network failures or malformed version strings.

### Define the Update Source

The updater begins by referencing a constant URL that always points to the current release manifest. According to the source code at **`src/update.rs#L20`**, this URL is hardcoded as `https://herdr.dev/latest.json`, ensuring every Herdr instance checks the same canonical source for version metadata.

### Fetch and Parse the Manifest

The function **`fetch_update_manifest()`** performs a blocking HTTP GET request to the manifest URL. As implemented around line 178 in [`src/update.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/update.rs), this function:

1. Downloads the raw JSON from [`herdr.dev/latest.json`](https://github.com/ogulcancelik/herdr/blob/main/herdr.dev/latest.json)
2. Deserializes the response into an **`UpdateManifest`** struct (defined at line 97)
3. Returns a structured object containing the latest version string, optional release notes, and a map of per-platform assets

The manifest format mirrors the `RemoteUpdateManifest` found in **[`src/remote.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/remote.rs)**, allowing both local and remote bootstrap instances to share identical version-checking logic.

### Compare Versions Semantically

Version comparison relies on strict semantic versioning. The mechanism:

- Parses the current binary's version using `env!("CARGO_PKG_VERSION")` at compile time
- Converts both the local version and the manifest's version string into `semver::Version` objects
- Performs a standard comparison at lines 225-226 in [`src/update.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/update.rs)

If the manifest version is **greater than** the running binary's version, the update proceeds. Otherwise, the process exits early with a "already up to date" message.

### Select the Platform-Specific Binary

Not every release contains identical binaries. The method **`UpdateManifest::download_url_for(os, arch)`** (line 138 in [`src/update.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/update.rs)) looks up the release assets using `std::env::consts` to detect the current operating system and architecture. This ensures an ARM64 macOS binary never attempts to download an x86_64 Linux replacement.

The manifest schema uses a nested structure where each version contains an `assets` map keyed by strings like `"linux-x86_64"` or `"macos-aarch64"`.

### Download and Atomic Installation

Once the correct asset URL is identified, **`download_update(&release)`** (line 333) handles the actual transfer. This function spawns a subprocess (typically `curl` or platform-native downloader) to stream the new executable to a temporary file while validating exit status codes.

Finally, **`install_downloaded_update(temp_file)`** at line 382 performs an **atomic file swap**. It locates the currently running executable path via `std::env::current_exe()` and replaces it with the downloaded binary while preserving file permissions. This atomicity prevents corruption if the process is interrupted mid-write.

## The Update Manifest Format

The manifest hosted at `https://herdr.dev/latest.json` (and checked into the repository as [`website/latest.json`](https://github.com/ogulcancelik/herdr/blob/main/website/latest.json)) follows a strict schema matching the `UpdateManifest` struct:

```json
{
  "version": "0.6.5",
  "notes": "### Added ...",

  "releases": {
    "0.6.5": {
      "assets": {
        "linux-x86_64": "https://github.com/ogulcancelik/herdr/releases/download/v0.6.5/herdr-linux-x86_64",
        "macos-aarch64": "https://github.com/ogulcancelik/herdr/releases/download/v0.6.5/herdr-macos-aarch64"
      },
      "download_url": "https://github.com/ogulcancelik/herdr/releases/download/v0.6.5/herdr-linux-x86_64"
    }
  }
}

```

The `download_url_for` method extracts the appropriate URL from the `assets` map based on runtime detection of the OS and architecture constants.

## Remote Bootstrap Updates

When Herdr initializes a remote instance (via `herdr --remote`), the same version-checking logic applies but through **[`src/remote.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/remote.rs)**. This file defines an identical `UPDATE_MANIFEST_URL` constant and a `RemoteUpdateManifest` struct with matching fields. The remote bootstrap path uses the same semantic comparison logic to decide whether to download a newer binary for headless server deployment, ensuring consistency between local CLI updates and remote server provisioning.

## Code Examples

### Checking for Updates via CLI

Run the built-in update command from any terminal to trigger the full check-and-replace workflow:

```bash

# Standard update check and automatic installation

herdr update

# Update with live handoff flag (recommended if a server is running)

herdr update --handoff

```

The `--handoff` flag signals Herdr to coordinate with a running server process to minimize downtime during the binary swap.

### Programmatic Version Check in Rust

Integrate the update mechanism into your own Rust applications using Herdr's internal modules:

```rust
use herdr::update::{fetch_update_manifest, CURRENT_VERSION};
use semver::Version;

async fn check_for_herdr_update() -> Result<(), String> {
    // Fetch the latest manifest from herdr.dev/latest.json
    let manifest = fetch_update_manifest().await
        .map_err(|e| format!("Failed to fetch manifest: {e}"))?;

    // Parse versions using semver
    let current = Version::parse(CURRENT_VERSION)
        .map_err(|e| format!("Invalid local version: {e}"))?;
    let latest = Version::parse(&manifest.version)
        .map_err(|e| format!("Invalid manifest version: {e}"))?;

    // Compare and proceed if newer version exists
    if latest > current {
        let os = std::env::consts::OS;
        let arch = std::env::consts::ARCH;
        
        let url = manifest.download_url_for(os, arch)
            .ok_or_else(|| format!("No binary available for {}-{}", os, arch))?;
        
        println!("Update available: {} -> {}", current, latest);
        println!("Download URL: {}", url);
        // Proceed with download_update() and install_downloaded_update()...
    } else {
        println!("Herdr is up to date ({})", current);
    }
    
    Ok(())
}

```

### Debugging the Manifest Fetch

To verify connectivity or inspect the raw manifest without triggering an installation:

```rust
use herdr::update::fetch_update_manifest;

#[tokio::main]
async fn main() {
    match fetch_update_manifest().await {
        Ok(manifest) => {
            println!("Latest version: {}", manifest.version);
            println!("Release notes: {}", manifest.notes.unwrap_or_default());
        }
        Err(e) => eprintln!("Manifest fetch failed: {}", e),
    }
}

```

This pattern is useful for CI environments where you need to validate that `https://herdr.dev/latest.json` is reachable before attempting an upgrade.

## Summary

- **Herdr checks for updates** by downloading a JSON manifest from `https://herdr.dev/latest.json`, defined as a constant in `src/update.rs#L20`.
- **Semantic comparison** occurs using the `semver` crate to compare `env!("CARGO_PKG_VERSION")` against the manifest's version string at lines 225-226.
- **Platform detection** via `download_url_for(os, arch)` ensures the correct binary is selected for the current OS and architecture.
- **Atomic installation** prevents corruption by using `install_downloaded_update` to swap executables only after successful download validation.
- **Remote bootstrap** shares identical logic in [`src/remote.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/remote.rs), enabling version-aware deployment for headless servers.

## Frequently Asked Questions

### Where does Herdr look for version information?

Herdr queries the HTTPS endpoint `https://herdr.dev/latest.json` to retrieve the canonical version manifest. This URL is hardcoded as `UPDATE_MANIFEST_URL` in [`src/update.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/update.rs) and [`src/remote.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/remote.rs). The JSON file contains the latest semantic version string and a map of downloadable assets for each supported platform.

### How does Herdr handle different operating systems and architectures?

The `UpdateManifest::download_url_for` method inspects `std::env::consts::OS` and `std::env::consts::ARCH` at runtime to construct a platform key like `"linux-x86_64"` or `"macos-aarch64"`. It then looks up this key in the manifest's `assets` map to return the correct binary URL, ensuring architecture-specific updates are downloaded even when the manifest contains multiple release artifacts.

### What happens if the update download is interrupted?

The `download_update` function streams data to a **temporary file** first and validates the exit status of the download command before returning. Only after successful completion does `install_downloaded_update` perform the atomic replacement of the running binary. If the download fails or is interrupted, the temporary file is discarded and the original binary remains untouched at its current path returned by `std::env::current_exe()`.

### Can I run Herdr update while a server is active?

Yes, but you should use the `--handoff` flag. According to the user-facing messages in [`src/update.rs`](https://github.com/ogulcancelik/herdr/blob/main/src/update.rs) around lines 700-800, running `herdr update --handoff` signals the updater to coordinate with a running Herdr server process. This allows for a live handoff where the server can gracefully transition to the new binary version without dropping active connections or requiring a manual restart.