ci: skip the Rust build and API deploy on frontend-only pushes
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:
@@ -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.
|
||||
|
||||
13
CLAUDE.md
13
CLAUDE.md
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user