ci: skip the Rust build and API deploy on frontend-only pushes
All checks were successful
deploy / build (push) Successful in 7m25s
deploy / deploy-web (push) Successful in 6s
deploy / deploy-api (push) Successful in 15s

The musl release build plus the gate is most of the wall clock, and a commit
that touches only `web/` cannot change a byte of it. A `what changed` step
diffs against the previous head and sets one output; the gate, the build, the
ts-rs drift check and the whole `deploy-api` job hang off it. Not
`on.push.paths`, which would skip the entire workflow — the site still has to
build and ship.

Anything unrecognised counts as Rust. A false positive costs a slow deploy; a
false negative leaves a binary on bob that does not match the commit, and
nothing would report it. Two entries in the path list are less obvious than
they look: `asset/`, because deploy-api ships the systemd units, the firewalld
service and the rendered config from it; and `web/src/api/generated/`, because
the drift gate only runs once `cargo test` has regenerated those files, so a
hand-edit of them must not be able to arrive labelled frontend-only — that is
exactly the change the gate exists to catch.

Checked against this session's five commits: the two pure-UI ones classify as
frontend, the three touching crates or .sqlx classify as Rust, and a
hand-edited generated type classifies as Rust.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5
This commit is contained in:
2026-09-09 17:23:31 +03:00
parent e330d2b78a
commit 1410d3dda7
2 changed files with 82 additions and 0 deletions

View File

@@ -42,10 +42,69 @@ jobs:
# `runner-fedora-44`, so it carries node + pnpm too and one job can build
# both halves.
runs-on: rust
outputs:
rust: ${{ steps.changes.outputs.rust }}
steps:
- uses: actions/checkout@v4
with:
# The change detection below diffs against the previous head, which a
# depth-1 clone does not contain.
fetch-depth: 0
- name: what changed
id: changes
# The Rust half is the expensive half — a cold musl release build plus
# the gate is most of the wall clock — and a frontend commit cannot
# change a byte of it. Deciding here rather than with `on.push.paths`
# because that filter skips the *whole* workflow, and the site still
# has to build and ship.
#
# Anything that is not clearly frontend-only counts as Rust. The cost of
# a false positive is a slow deploy; the cost of a false negative is a
# binary on bob that does not match this commit, which nothing would
# report. Two entries are less obvious than they look:
#
# - `asset/` because deploy-api ships the systemd units, the firewalld
# service and the rendered config template from it.
# - `web/src/api/generated/` because the drift gate below only runs
# when `cargo test` has regenerated those files. A hand-edit of them
# is exactly what that gate exists to catch, so it must not be able
# to arrive as a "frontend-only" change.
#
# `.gitea/` is in the list so that editing this file exercises the whole
# path it describes.
run: |
set -euo pipefail
base="${{ github.event.before }}"
reason=""
if [ "${{ github.event_name }}" != "push" ]; then
reason="not a push"
elif [ -z "$base" ] || [ "$base" = "0000000000000000000000000000000000000000" ]; then
reason="no previous head — new branch or force push"
elif ! git cat-file -e "${base}^{commit}" 2>/dev/null; then
reason="previous head ${base} is not in this clone"
fi
if [ -n "$reason" ]; then
echo "building everything: ${reason}"
echo "rust=true" >> "$GITHUB_OUTPUT"
exit 0
fi
changed=$(git diff --name-only "$base" "${{ github.sha }}")
echo "changed since ${base}:"
echo "$changed" | sed 's/^/ /'
if echo "$changed" | grep -qE '^(crates/|asset/|script/|\.gitea/|\.cargo/|\.sqlx/|web/src/api/generated/|Cargo\.(toml|lock)$|rust-toolchain\.toml$|rustfmt\.toml$)'; then
echo "rust=true" >> "$GITHUB_OUTPUT"
echo "→ building and deploying the API"
else
echo "rust=false" >> "$GITHUB_OUTPUT"
echo "→ frontend only; skipping the Rust build and the API deploy"
fi
- name: rust gate
if: steps.changes.outputs.rust == 'true'
# Format, lint-as-error and the full test suite before anything is
# built, so a broken commit never reaches a host. SQLX_OFFLINE makes the
# compile-time-checked queries read the committed .sqlx cache instead of
@@ -59,6 +118,7 @@ jobs:
cargo test --all
- name: build api and cli
if: steps.changes.outputs.rust == 'true'
env:
SQLX_OFFLINE: "true"
run: |
@@ -72,6 +132,7 @@ jobs:
file target/x86_64-unknown-linux-musl/release/blackbeard-api
- name: generated types are current
if: steps.changes.outputs.rust == 'true'
# ts-rs writes web/src/api/generated/ from the Rust entities crate.
# `cargo test` above regenerates them; a diff here means someone edited
# a DTO and committed the Rust without the TypeScript, which would
@@ -102,6 +163,10 @@ jobs:
- uses: actions/upload-artifact@v3
with:
name: blackbeard
# The binaries are absent on a frontend-only run. `warn` rather than
# `error` for exactly that case — `deploy-api` is skipped alongside,
# so nothing downstream goes looking for them.
if-no-files-found: warn
path: |
target/x86_64-unknown-linux-musl/release/blackbeard-api
target/x86_64-unknown-linux-musl/release/blackbeard
@@ -110,6 +175,10 @@ jobs:
deploy-api:
needs: build
# Skipped when nothing that reaches bob changed. The binary already there is
# still the right one — this deploy is idempotent and its absence changes
# nothing about what is running.
if: needs.build.outputs.rust == 'true'
# `infra`, not `fedora-43`: the targets are mesh-only .internal names and
# the fedora runners have no route to the WireGuard mesh. Same reason as
# lair/quantus and lair/mail.

View File

@@ -308,6 +308,19 @@ OPNsense LAN interface (`reverse-proxies.md` §2). Verify the vhost with
`--resolve blackbeard.observer:443:127.0.0.1`, which still exercises the `:443`
stream router, SNI, the vhost, the cross-site hop and the API.
**A frontend-only push skips the Rust build and the API deploy.** The `what
changed` step in `deploy.yaml` diffs against `github.event.before` and sets one
output; the gate, the musl build, the ts-rs drift check and the whole
`deploy-api` job hang off it. Two entries in that path list are not obvious and
must not be trimmed: `asset/`, because `deploy-api` ships the systemd units, the
firewalld service and the rendered config from it; and
`web/src/api/generated/`, because the drift gate only runs when `cargo test` has
regenerated those files, so a hand-edit of them must not be able to arrive
labelled "frontend only" — which is precisely the change that gate exists to
catch. Anything unrecognised counts as Rust: a false positive costs a slow
deploy, a false negative leaves a binary on bob that does not match the commit
and nothing reports it.
**Runner labels.** `fedora-*` images have **no cargo** — Rust builds go on
`rust`, which is based on `runner-fedora-44` and so carries node + pnpm too.
Never `corepack enable`; pnpm is already on PATH (`gitea-runners.md` §4).