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
109 changes: 82 additions & 27 deletions .github/workflows/build-daemon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ on:
workflow_call:
workflow_dispatch:

# Consecutive pushes to a Rust PR would otherwise stack 4-leg cross-compile
# matrices that nothing cancels — the most expensive PR-triggered workflow in
# the repo, started once per push and left running. Superseding is only ever
# right on `pull_request`: the `workflow_call` legs ARE the release's binaries,
# and a cancelled one is a release that ships without its daemon.
concurrency:
group: build-daemon-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
# The Rust workspace does not exist on every ref this workflow can run
# against: `main` carries no `Cargo.toml` until the daemon lands, and the
Expand All @@ -32,6 +41,7 @@ jobs:
# no daemon at all.
detect:
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
has_crates: ${{ steps.check.outputs.has_crates }}
steps:
Expand Down Expand Up @@ -87,6 +97,13 @@ jobs:
os: macos-14
platform: darwin-arm64
runs-on: ${{ matrix.os }}
# Nothing in this job has a bounded runtime of its own, and GitHub's default
# is six hours. On 2026-08-19 the linux-x64 leg sat in `apt-get update`
# against a stalled Azure mirror through three runner re-dispatches with a
# release blocked behind it, while the arm64 leg ran the same step in
# seconds. A cross compile that has not finished in 30 minutes is not going
# to, and a bounded failure is re-runnable where a hang is not.
timeout-minutes: 30
steps:
- uses: actions/checkout@v7.0.1
with:
Expand All @@ -103,27 +120,75 @@ jobs:
# without musl-tools the leg fails at link time with
# `linker 'musl-gcc' not found`. Both Linux runners are native to their
# own target, so the distro package is the right linker for the triple.
# It is not droppable either: `-p failproofaid` reaches `rusqlite` with
# `bundled` (compiles sqlite3.c) and `ring` through rustls, so the `cc`
# crate resolves `musl-gcc` on both musl legs.
#
# Retried and bounded, because the failure mode here is a HANG rather than
# an error, and apt's own acquire timeouts do not reliably catch it: on
# 2026-08-19 every `azure.archive.ubuntu.com` line came back `Ign`, apt
# fell back to `archive.ubuntu.com`, fetched the InRelease files, and then
# sat for three and a half minutes emitting nothing.
#
# `sudo timeout`, NOT `nick-fields/retry` — that was the first attempt and
# CI rejected it. The action bounds a step by killing its process tree
# from the runner user, and apt runs as root: the timeout fired correctly
# at four minutes and the action then died with `kill EPERM` instead of
# retrying, turning a recoverable stall into a failed leg. Putting
# `timeout` INSIDE the sudo is what makes the killer root too.
#
# The fast path skips the network altogether. `apt-get update` is the step
# that stalls, and it is only needed when the runner image's package lists
# cannot satisfy the install — so try the install first and refresh only
# on failure. `-qq` is gone on purpose as well: it suppressed the
# per-mirror progress that says WHICH endpoint stalled.
- name: Install the musl toolchain
if: contains(matrix.target, 'musl')
run: sudo apt-get update -qq && sudo apt-get install -y -qq musl-tools
run: |
install_musl() {
sudo timeout -k 10 90 apt-get install -y --no-install-recommends musl-tools
}
if install_musl; then
musl-gcc --version | head -1
exit 0
fi
for attempt in 1 2 3; do
echo "::group::apt-get update (attempt $attempt)"
sudo timeout -k 10 120 apt-get update \
-o Acquire::Retries=3 \
-o Acquire::http::Timeout=15 \
-o Acquire::https::Timeout=15 \
-o Acquire::Languages=none || echo "attempt $attempt timed out or failed"
echo "::endgroup::"
if install_musl; then
musl-gcc --version | head -1
exit 0
fi
sleep 5
done
echo "::error::musl-tools could not be installed after 3 attempts — see the apt output above for the stalling mirror"
exit 1

# Split restore/save rather than `actions/cache@v6`, which does both.
# This job runs on `pull_request` AND on the release path (via
# `workflow_call` from publish.yml, where `github.event_name` is the
# caller's `release`/`workflow_dispatch`), and the cache key is shared
# between them: a PR branch could otherwise write a poisoned `target/`
# entry that a later release run restores straight into a published
# binary. PR runs read the cache; only release / manual runs write it.
- uses: actions/cache/restore@v6
id: cargo-cache
# `save-if` rather than a plain cache, for the reason the split
# restore/save here used to carry: this job runs on `pull_request` AND on
# the release path (via `workflow_call` from publish.yml, where
# `github.event_name` is the caller's `release`/`workflow_dispatch`), and
# the cache key is shared between them. A PR branch could otherwise write
# a poisoned `target/` entry that a later release run restores straight
# into a published binary. PR runs read the cache; only release / manual
# runs write it.
#
# rust-cache rather than the hand-rolled `actions/cache` pair, because the
# naive `path: target` it replaced is what made ci.yml's sibling entry
# 5.7 GB — 57% of the repo's whole 10 GB cache quota in one key, taking
# 127s to restore against the 74s of compilation it saved. rust-cache
# prunes `target/` to dependency artifacts and drops the workspace's own
# output, which is the difference between caching a build and caching a
# build directory. `key` keeps the four legs from sharing an entry.
- uses: Swatinem/rust-cache@v2
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
target
key: cargo-build-${{ matrix.target }}-${{ hashFiles('rust-toolchain.toml', 'Cargo.lock') }}
restore-keys: cargo-build-${{ matrix.target }}-
key: ${{ matrix.target }}
save-if: ${{ github.event_name != 'pull_request' }}

# --locked: this job's output is the binary users install, so it must be
# built from the dependency versions committed in Cargo.lock. Without it
Expand All @@ -133,16 +198,6 @@ jobs:
- name: cargo build --release
run: cargo build --locked --release --target ${{ matrix.target }} -p failproofaid

- uses: actions/cache/save@v6
if: github.event_name != 'pull_request' && steps.cargo-cache.outputs.cache-hit != 'true'
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
target
key: cargo-build-${{ matrix.target }}-${{ hashFiles('rust-toolchain.toml', 'Cargo.lock') }}

# Plain gzip, not tar: the CLI decompresses this with `node:zlib` and no
# dependency, and a single-file stream needs no archive format. It also
# sidesteps `upload-artifact` dropping the executable bit — a compressed
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/build-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,18 @@ on:
default: true
required: false

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

permissions:
contents: read
packages: write

jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down
Loading
Loading