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
20 changes: 11 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,20 @@ FROM rust:1-trixie AS builder

WORKDIR /app

# Build-time git metadata for the footer. The build context excludes
# `.git` (see .dockerignore), so `build.rs` can't shell out to git here.
# Production only ever deploys `main`, so default the branch to `main`
# (override with `--build-arg GIT_BRANCH=...` for a one-off branch build).
# The commit comes from Coolify's auto-injected `SOURCE_COMMIT`; set
# `GIT_HASH` explicitly to override it.
# Build-time git metadata for the footer, used only as a fallback. The
# build context excludes `.git` (see .dockerignore) so `build.rs` can't
# shell out to git here, and since Coolify v450 `SOURCE_COMMIT` is no
# longer injected as a build arg by default (it busts the layer cache).
# The server therefore resolves the branch and commit from the *runtime*
# environment first (Coolify exposes `SOURCE_COMMIT` and `COOLIFY_BRANCH`
# to the container), falling back to these baked-in values. Production
# only ever deploys `main`, so default the branch to `main`. Pass
# `--build-arg GIT_HASH=...` (and/or `GIT_BRANCH=...`) to bake values in
# for a one-off build outside Coolify.
ARG GIT_BRANCH=main
ARG GIT_HASH=
ARG SOURCE_COMMIT=
ENV GIT_BRANCH=${GIT_BRANCH} \
GIT_HASH=${GIT_HASH} \
SOURCE_COMMIT=${SOURCE_COMMIT}
GIT_HASH=${GIT_HASH}

# Cache dependencies separately from source. Copy just the manifests
# first, build a dummy main so cargo downloads + compiles deps, then
Expand Down
80 changes: 66 additions & 14 deletions src/bin/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,66 @@ use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use sqlx::{Row, Sqlite, SqlitePool, migrate::MigrateDatabase, sqlite::SqlitePoolOptions};
use std::env;
use std::sync::Arc;
use std::sync::{Arc, LazyLock};
use tower_http::services::ServeDir;
use ulid::Ulid;

/// Version of the course, sourced from the crate version in `Cargo.toml`.
/// Surfaced in the site footer so users can tell which release they're on.
const COURSE_VERSION: &str = env!("CARGO_PKG_VERSION");

/// Git branch and short commit the running server was built from, injected by
/// `build.rs` via `cargo:rustc-env`. `option_env!` keeps the build working when
/// `.git` isn't available (e.g. some Docker builds); we fall back to "unknown".
/// Surfaced next to `COURSE_VERSION` in the footer so we can tell which branch
/// and commit a given deploy is on during the restructure.
const GIT_BRANCH: &str = match option_env!("GIT_BRANCH") {
Some(b) => b,
None => "unknown",
};
const GIT_HASH: &str = match option_env!("GIT_HASH") {
Some(h) => h,
None => "unknown",
};
/// Build-time git metadata baked in by `build.rs` via `cargo:rustc-env`.
/// `option_env!` keeps the build working when `.git` isn't available (the
/// Docker build context excludes it), in which case these are `None`.
const GIT_BRANCH_BUILD: Option<&str> = option_env!("GIT_BRANCH");
const GIT_HASH_BUILD: Option<&str> = option_env!("GIT_HASH");

/// Git branch and short commit shown in the footer, resolved once at startup.
///
/// We prefer a runtime environment variable over the build-time value. The
/// production deploy (Coolify) builds the image with `.git` excluded and, since
/// Coolify v450, no longer injects `SOURCE_COMMIT` as a build arg by default
/// (it would bust the Docker layer cache), so `build.rs` bakes in "unknown".
/// Coolify still exposes the commit and branch to the running container as the
/// env vars `SOURCE_COMMIT` and `COOLIFY_BRANCH`, so we read those at runtime.
/// Precedence: explicit runtime override, then the build-time value, then
/// "unknown".
static GIT_BRANCH: LazyLock<String> = LazyLock::new(|| {
git_env("GIT_BRANCH")
.or_else(|| git_env("COOLIFY_BRANCH"))
.or_else(|| usable(GIT_BRANCH_BUILD.map(str::to_owned)))
.unwrap_or_else(|| "unknown".into())
});

static GIT_HASH: LazyLock<String> = LazyLock::new(|| {
git_env("GIT_HASH")
.or_else(|| git_env("SOURCE_COMMIT").map(|s| s.chars().take(7).collect()))
.or_else(|| usable(GIT_HASH_BUILD.map(str::to_owned)))
.unwrap_or_else(|| "unknown".into())
});

/// A runtime environment variable, normalised: trimmed, and treated as absent
/// when empty or the literal "unknown".
fn git_env(key: &str) -> Option<String> {
usable(std::env::var(key).ok())
}

/// Drop values that carry no information (empty or "unknown"), trim the rest.
fn usable(value: Option<String>) -> Option<String> {
value
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty() && s != "unknown")
}

/// Footer accessors used by `base.html`. Askama can't render a `LazyLock`
/// directly, so hand it a plain `&str`.
fn git_branch() -> &'static str {
GIT_BRANCH.as_str()
}

fn git_hash() -> &'static str {
GIT_HASH.as_str()
}

/// Application state shared across all routes
#[derive(Clone)]
Expand Down Expand Up @@ -3007,4 +3046,17 @@ mod tests {
fn bucket_participants_handles_empty_input() {
assert!(bucket_participants_by_team(Vec::new()).is_empty());
}

#[test]
fn usable_filters_out_uninformative_git_values() {
// Real values pass through, trimmed.
assert_eq!(usable(Some("main".into())), Some("main".into()));
assert_eq!(usable(Some(" abc1234 ".into())), Some("abc1234".into()));
// Absent, empty, whitespace-only, and the "unknown" sentinel all
// collapse to None so the next source in the chain is tried.
assert_eq!(usable(None), None);
assert_eq!(usable(Some(String::new())), None);
assert_eq!(usable(Some(" ".into())), None);
assert_eq!(usable(Some("unknown".into())), None);
}
}
2 changes: 1 addition & 1 deletion templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -2366,7 +2366,7 @@ <h2 id="cheatsheet-modal-title">Cheatsheet</h2>
rel="noopener"
>v{{ crate::COURSE_VERSION }}</a
>
&middot; {{ crate::GIT_BRANCH }} @ {{ crate::GIT_HASH }}
&middot; {{ crate::git_branch() }} @ {{ crate::git_hash() }}
</p>
</div>

Expand Down
Loading