ci: gate the API deploy on what the API is running, not on the last push
deploy / build (push) Successful in 8m3s
deploy / deploy-web (push) Successful in 7s
deploy / deploy-api (push) Successful in 20s

The site is currently serving a frontend that asks for `window=3600` from an API
that only knows `six_hours`, and every step behaved as designed:

  run 51  ef367d5  rust + web   BUILD FAILED — nothing deployed
  run 52  2594190  web only     web deployed, deploy-api skipped
  run 53  49f50bd  docs only    nothing deployed

Run 52 diffed against `github.event.before`, which is run 51's commit — a commit
whose Rust half never shipped. It correctly saw only `web/src/lib/routes.ts` and
correctly skipped the API. The frontend went out alone.

The gate was asking the wrong question. "What changed since the previous push"
is only the same as "what changed since what is deployed" while every push
deploys, and a failed run breaks that quietly — the workflow's own comment warns
about "a binary on bob that does not match this commit, which nothing would
report", and this is that case arriving by a route the path list cannot see.

So ask the API. `/v1/healthz` now carries the commit it was built from, stamped
via `GIT_SHA` at build time, and the gate diffs from there. `build.rs` exists
only to make cargo notice the value changed, since `option_env!` is read at
compile time and a warm cache would otherwise keep a stale stamp.

Best effort throughout: an unreachable API, a binary too old to carry a stamp,
or a commit this clone does not have all fall back to `github.event.before`,
which is exactly today's behaviour. The gate only ever widens — a false positive
costs a slow deploy, and that trade is already written down beside it.

This commit touches `.gitea/` and `crates/`, so it deploys the API and unbreaks
the site as it lands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5
This commit is contained in:
2026-09-10 14:56:07 +03:00
co-authored by Claude Opus 5
parent 49f50bd416
commit d12299f242
3 changed files with 44 additions and 0 deletions
+26
View File
@@ -91,6 +91,29 @@ jobs:
exit 0
fi
# Prefer the commit the API is *actually running* over the previous
# push. They are usually the same and the difference only shows up
# when it matters: if a run fails, its changes never ship, and the
# next push diffs against a commit that was never deployed. That is
# how a frontend asking for `window=3600` came to be served by an API
# that only knew `six_hours` — the run that would have taught it
# failed, and the next push was frontend-only, so nothing rebuilt the
# API and nothing said so.
#
# Best effort: an unreachable API, an old binary with no stamp, or a
# commit this clone does not have all fall back to `event.before`,
# which is no worse than before. The gate only ever widens.
live=$(curl -sS --max-time 10 "http://${API_HOST}:${API_PORT}/v1/healthz" 2>/dev/null \
| python3 -c 'import sys,json; print(json.load(sys.stdin).get("commit") or "")' 2>/dev/null || true)
if [ -n "$live" ] && git cat-file -e "${live}^{commit}" 2>/dev/null; then
if [ "$live" != "$base" ]; then
echo "diffing against the running API at ${live}, not the previous push ${base}"
fi
base="$live"
else
echo "the running API did not name a commit we have; falling back to ${base}"
fi
changed=$(git diff --name-only "$base" "${{ github.sha }}")
echo "changed since ${base}:"
echo "$changed" | sed 's/^/ /'
@@ -127,6 +150,9 @@ jobs:
# simpler when the runner matches the target fleet — it does not here:
# `runner-rust` is Fedora 44 and the targets are Fedora 43, so a
# glibc-linked binary built here would not load there.
# Stamped so the API can answer "which commit am I?" — see the
# deploy gate above, which asks exactly that.
GIT_SHA="${{ github.sha }}" \
cargo build --release --target x86_64-unknown-linux-musl \
-p blackbeard-api -p blackbeard-cli
file target/x86_64-unknown-linux-musl/release/blackbeard-api
+9
View File
@@ -0,0 +1,9 @@
//! Stamp the commit into the binary, so the deploy gate can ask what is running.
//!
//! `option_env!("GIT_SHA")` is read at compile time, and cargo has no way to
//! know that value changed unless it is told — without this, a rebuild in a
//! warm cache would keep the previous commit's stamp and the gate would trust
//! a stale answer.
fn main() {
println!("cargo::rerun-if-env-changed=GIT_SHA");
}
+9
View File
@@ -106,6 +106,14 @@ struct HealthResponse {
/// alongside, for a human to read.
healthy: bool,
version: &'static str,
/// The commit this binary was built from, or `null` for a local build.
///
/// The deploy gate reads it. Without it, "is the API on bob this commit?"
/// could only be guessed from the previous push — and when a run fails, the
/// next push diffs against a commit that never shipped and concludes
/// nothing needs deploying. That is how a frontend expecting `window=3600`
/// came to be served by an API that only knew `six_hours`.
commit: Option<&'static str>,
uptime_seconds: i64,
database: bool,
chains: Vec<ChainInfo>,
@@ -116,6 +124,7 @@ async fn healthz(State(state): State<AppState>) -> Response {
let body = HealthResponse {
healthy: database,
version: env!("CARGO_PKG_VERSION"),
commit: option_env!("GIT_SHA"),
uptime_seconds: (chrono::Utc::now() - state.started).num_seconds(),
database,
chains: state.chain_infos(),