Shared GitHub Actions composite actions for Rust repositories in the bitcoincore-dev org. The goal is one place to define how we install Rust, normalize CI for crates.io builds, and operate safely on self-hosted runners (disk and Cargo caches).
This repository has no crates—only reusable workflow building blocks. Workflows reference actions with:
uses: bitcoincore-dev/rust-ci/<action-name>@main
(or a pinned SHA for reproducibility).
Wraps dtolnay/rust-toolchain with an org default pinned toolchain when you do not use a repo rust-toolchain.toml.
| Input | Role |
|---|---|
toolchain |
Version string (default 1.88.0). Ignored if toolchain-file is set. |
toolchain-file |
Path to rust-toolchain.toml or rust-toolchain; channel comes from that file. |
components |
Extra components (rustfmt, clippy, …). With toolchain-file, comma-separated for dtolnay; otherwise passed to rustup component add after the pinned install. |
Use toolchain-file when the repo owns the channel; use the default pin when you want org-wide consistency without per-repo files.
Self-hosted runners (shared disk, concurrent jobs): If you see rustc: Text file busy (exit 126 / ETXTBSY) during install-rust-toolchain, rustup is usually replacing the ~/.cargo/bin proxies while another job is executing them. This action exports RUSTUP_PERMIT_COPY_RENAME and RUSTUP_NO_SELF_UPDATE for all steps so file updates use a safer pattern and rustup does not self-update mid-job.
Multiple listeners, one Unix user (e.g. Zeus zeus-workstation + zeus-workstation-2): Both services often run as github-runner with the same HOME=/var/lib/github-runner, so concurrent jobs on different listeners still collide on ~/.cargo/bin/cargo. Fix at the host:
packaging/github-runner/runner-toolchain-isolation.conf— systemd drop-in per extra listener (HOME/CARGO_HOME/RUSTUP_HOMEunder e.g./var/lib/github-runner-2).packaging/github-runner/bootstrap-isolated-runner-toolchain.sh— one-time seed of rustup +cargo-auditfor that home.- Workflows:
isolate-cargo-envper job + invokecargo-audit(notcargo audit) when auditing; pre-install audit on the runner so jobs skipcargo install.
If flakes persist after host isolation, reduce parallelism (workflow concurrency) or use fully job-local RUSTUP_HOME (slower: reinstalls toolchain per job).
Removes [patch.crates-io] sections from Cargo.toml and .cargo/config.toml under a chosen directory so CI resolves dependencies from crates.io instead of local path overrides (typical monorepo / path-dev setup).
| Input | Role |
|---|---|
working-directory |
Root to search (default .). |
Run this before cargo fetch / cargo build in workflows that must behave like downstream crates.io consumers.
If root filesystem (/) or the filesystem containing cache-root is above a threshold, prunes old cache-root subtrees (default age 7 days, maxdepth 2) so long-lived runners do not fill the disk. That includes the common case where /tmp is a small tmpfs, cache-root defaults to /tmp/runner-cache, and / still has free space—previously the guard only looked at / and would never prune.
| Input | Role |
|---|---|
cache-root |
Directory to prune when over threshold. |
threshold-percent |
Trigger cleanup when either measured use exceeds this percent (default 80). |
show-df |
If true, log df -h before/after when cleanup runs. |
Sets CARGO_HOME to ${RUNNER_TEMP}/<subdir> (default cargo) and prepends $CARGO_HOME/bin to PATH. Symlinks registry and git to the runner user's shared store (default $HOME/.cargo/...) so per-job metadata stays isolated without duplicating downloaded crates.
Use early in the job, before install-rust-toolchain / cargo install, on workflows that run on shared self-hosted users. Pairs with runner-cargo-cache (restore still populates the shared registry; isolated CARGO_HOME picks it up via symlink).
| Input | Role |
|---|---|
cargo-home-subdir |
Subdir under RUNNER_TEMP (default cargo). |
shared-registry |
Override registry symlink target (default $HOME/.cargo/registry). |
shared-git |
Override git symlink target (default $HOME/.cargo/git). |
Manages persistent Cargo/registry/git and optional target caches on the runner: bind environment variables from a cache key, restore before builds, save after builds, or prune old cache directories.
| Input | Role |
|---|---|
operation |
bind-env | restore | save | prune (required). |
cache-key |
Segment used to isolate cache trees (required for bind-env). |
cache-root |
Root for cache dirs (default /tmp/runner-cache). |
include-target |
For bind-env, whether to set TARGET_CACHE_DIR. |
save-target-only |
For save, only sync ./target (no registry/git). |
Typical sequence: bind-env → restore → build → save (and occasional prune in maintenance jobs).
Clones the blvm-spec (Orange Paper) repository for spec-lock verification, mdBook includes, or any job that needs spec markdown on disk.
| Input | Role |
|---|---|
working-directory |
Repo root for path resolution (default .). |
target |
Empty (default): clone to ../blvm-spec (sibling of the checked-out repo), matching cargo-spec-lock verify --spec-path ../blvm-spec/.... Set to a path relative to the repo (e.g. modules/blvm-spec) for in-tree checkouts. |
repository |
Git URL (default official blvm-spec repo). |
depth |
Shallow clone depth (default 1). |
GitHub-hosted: install-rust-toolchain and often strip-patch-crates-io are enough.
Self-hosted: add runner-disk-guard early in the job, and use runner-cargo-cache around the compile steps so repeated builds stay fast without unbounded disk growth.
Default cache-root is /tmp/runner-cache. On many Linux setups /tmp is tmpfs (RAM). Cargo/registry + target caches can grow to tens of GiB and fill /tmp entirely, breaking anything that writes there (e.g. rustup’s rustup-init download). / may still have plenty of space.
Do one of the following (recommended: A).
Pick a directory on a large persistent volume (example: /mnt/data/github-runner-cache). Workflows that keep the default /tmp/runner-cache will follow the symlink and use disk.
-
Find and stop the runner service(s) (units look like
actions.runner.<org>-<repo>.<hostname>.service). Stop every enabled runner on this machine:systemctl list-unit-files --no-legend \ | awk '/^actions\.runner\..*\.service/ && $2 == "enabled" {print $1}'
systemctl list-unit-files --no-legend \ | awk '/^actions\.runner\..*\.service/ && $2 == "enabled" {print $1}' \ | xargs -r sudo systemctl stop
Wait until jobs drain; optionally confirm nothing is writing the cache:
sudo lsof +D /tmp/runner-cache 2>/dev/null | head(empty is ideal). -
Find the Unix user (and group) the runner runs as (needed for
chownand the write test). Use any enabledactions.runnerunit (after stop,list-unitsmay omit idle services, so use unit files):UNIT=$(systemctl list-unit-files --no-legend \ | awk '/^actions\.runner\..*\.service/ && $2 == "enabled" {print $1; exit}') systemctl cat "$UNIT" | grep -E '^User=|^Group='
If
Group=is missing, use the same name asUser=forRUNNER_GROUP, orid -gn RUNNER_USER. Put those in place ofRUNNER_USER/RUNNER_GROUPin step 3 and 6. -
Create the disk directory, migrate cache, replace
/tmp/runner-cachewith a symlinkIf
/tmp/runner-cacheis already a symlink,rm -rf /tmp/runner-cacheonly removes the link (not the target). If it is a real directory, this deletes/recreates the path under/tmp.DISK_CACHE=/mnt/data/github-runner-cache # change to your volume sudo mkdir -p "$DISK_CACHE" if [ -d /tmp/runner-cache ] && [ ! -L /tmp/runner-cache ]; then sudo rsync -a /tmp/runner-cache/ "$DISK_CACHE/" elif [ -L /tmp/runner-cache ]; then echo "Existing symlink: $(readlink -f /tmp/runner-cache) — copy from there if you still want migration." fi sudo rm -rf /tmp/runner-cache sudo ln -s "$DISK_CACHE" /tmp/runner-cache sudo chown -R RUNNER_USER:RUNNER_GROUP "$DISK_CACHE"
Replace
RUNNER_USER:RUNNER_GROUPwith the values from step 2 (e.g.josh:joshorrunner:runner). -
Optional — SELinux (enforcing): if the new path is not on a typical home/data label, you may need a context the runner can write, e.g. after policy review:
sudo semanage fcontext -a -t var_lib_t "/mnt/data/github-runner-cache(/.*)?" 2>/dev/null || true sudo restorecon -RFv /mnt/data/github-runner-cache
Use the same path as
DISK_CACHEin step 3. Adjust the SELinux type to match your distribution if the journal still shows denials. -
Start the runner(s) again
Prefer starting the same unit names you stopped in step 1. If you did not record them, start every enabled runner service:
systemctl list-unit-files --no-legend \ | awk '/^actions\.runner\..*\.service/ && $2 == "enabled" {print $1}' \ | xargs -r sudo systemctl start
-
Verify (replace
RUNNER_USER— same as step 2)readlink -f /tmp/runner-cache # should print your DISK_CACHE path df -P /tmp/runner-cache # should show the disk filesystem, not tmpfs sudo -u RUNNER_USER sh -c 'touch /tmp/runner-cache/.write-test && rm /tmp/runner-cache/.write-test'
Notes
- several runners on one host — they usually share the same default
/tmp/runner-cache; one symlink updates all of them. - No systemd (you only run
./run.sh/runsvc.sh) — stop those processes, perform steps 3–4 as the same user that runs the runner (skipsystemctl; usechown -Ronly if you created the tree as root), then start the listener again.
Use the same directory in runner-cargo-cache and runner-disk-guard inputs (org Variables help). No symlink; every workflow must pass the path (or inherit from a reusable workflow).
- Cron or a nightly job can run
runner-cargo-cachewithoperation: prune(samecache-root). TMPDIRon disk: for systemd runners, add e.g.Environment=TMPDIR=/var/tmpviasystemctl edit <actions.runner.unit>so rustup and other tools avoid a full tmpfs. Create/var/tmp(or your choice) with normal permissions if needed.
Changes here affect every workflow that pins @main (or your tag). Prefer small, backward-compatible input defaults; document new inputs in each action’s action.yml and update this README when behavior or defaults change.