runner follow-ups: XDG config path + composite action + docs #38

Merged
osobh merged 1 commits from runner-followups into main 2026-07-13 13:21:48 +00:00
4 changed files with 194 additions and 95 deletions
+36 -34
View File
@@ -3,9 +3,9 @@ description: >
Wraps `cargo build` with a peer-cache lookup: HIT restores the
target dir from the runner-local clawstor daemon (skipping dep
compile); MISS runs cargo build then captures + uploads the target
dir to the daemon. Config comes from
`~/.config/claw-cargo/config.toml` on the runner host — provisioned
by the fleet-admin, not from the workflow.
dir to the daemon. Config comes from the runner user's home
(~/.config/claw-cargo/config.toml or the legacy ~/.claw-cargo/) —
provisioned by the fleet-admin, not from the workflow.
inputs:
workspace:
@@ -46,24 +46,6 @@ outputs:
runs:
using: 'composite'
steps:
- name: Preflight check (claw-cargo + config)
shell: bash
run: |
set -euo pipefail
if ! command -v claw-cargo >/dev/null 2>&1; then
echo "::error::claw-cargo not on PATH — fleet admin needs to install /usr/local/bin/claw-cargo on this runner"
exit 1
fi
claw-cargo --version
# The daemon config is per-runner; the action expects it to
# exist under $HOME/.config/claw-cargo/config.toml. We do not
# ship secrets from the workflow.
cfg="${HOME}/.config/claw-cargo/config.toml"
if [ ! -s "$cfg" ]; then
echo "::error::missing $cfg — fleet admin needs to provision peer + tls_dir for this runner"
exit 1
fi
- name: Build with clawstor cache
id: build
shell: bash
@@ -74,33 +56,53 @@ runs:
PAR_RESTORE: ${{ inputs.parallel-restore }}
run: |
set -euo pipefail
# Field finding 2026-07-13: Gitea Actions composite steps run
# with a stripped PATH that omits /usr/local/bin. Force it +
# the runner user's cargo bin, so the CLI is found regardless
# of how the workflow was invoked.
export PATH="/usr/local/bin:$HOME/.cargo/bin:$PATH"
if ! command -v claw-cargo >/dev/null 2>&1; then
echo "::error::claw-cargo not on PATH - fleet admin needs to install /usr/local/bin/claw-cargo on this runner"
exit 1
fi
claw-cargo --version
# Accept both the XDG-style path and the legacy dotfile
# location. client_config resolves them in the same order.
cfg=""
for cand in \
"${XDG_CONFIG_HOME:-$HOME/.config}/claw-cargo/config.toml" \
"$HOME/.claw-cargo/config.toml"; do
if [ -s "$cand" ]; then
cfg="$cand"
break
fi
done
if [ -z "$cfg" ]; then
echo "::error::no claw-cargo config found under \$HOME/.config/claw-cargo/ or \$HOME/.claw-cargo/ - fleet admin needs to provision peer + tls_dir for this runner"
exit 1
fi
args=(build --workspace "$WS" --profile "$PROFILE" --parallel-restore "$PAR_RESTORE")
if [ "$NO_UPLOAD" = "true" ]; then
args+=(--no-upload)
fi
started=$SECONDS
# Tee the summary so we can grep the outcome out for the output.
set +e
claw-cargo "${args[@]}" 2>&1 | tee /tmp/clawstor-build.out
rc=${PIPESTATUS[0]}
set -e
elapsed=$((SECONDS - started))
# Parse `cache:` line. Shapes we handle:
# cache: HIT (... bytes downloaded)
# cache: MISS
# cache: MISS -> populated (... bytes uploaded)
# cache: MISS (no upload performed)
outcome="SKIPPED"
outcome=SKIPPED
if grep -q 'cache: *HIT' /tmp/clawstor-build.out; then
outcome="HIT"
elif grep -q 'cache: *MISS -> populated' /tmp/clawstor-build.out \
|| grep -q 'cache: *MISS → populated' /tmp/clawstor-build.out; then
outcome="POPULATED"
outcome=HIT
elif grep -qE 'cache: *MISS.*populated' /tmp/clawstor-build.out; then
outcome=POPULATED
elif grep -q 'cache: *MISS' /tmp/clawstor-build.out; then
outcome="MISS"
outcome=MISS
fi
fingerprint="$(grep -oE 'fingerprint: [0-9a-f]+' /tmp/clawstor-build.out | head -1 | awk '{print $2}')"
fingerprint="$(grep -oE 'fingerprint: *[0-9a-f]+' /tmp/clawstor-build.out | head -1 | awk '{print $2}')"
echo "cache-outcome=$outcome" >> "$GITHUB_OUTPUT"
echo "fingerprint=${fingerprint:-unknown}" >> "$GITHUB_OUTPUT"
echo "elapsed-seconds=$elapsed" >> "$GITHUB_OUTPUT"
+9 -44
View File
@@ -14,50 +14,15 @@ jobs:
- uses: actions/checkout@v4
- name: Build with clawstor cache
id: build
env:
NO_UPLOAD: ${{ github.event_name == 'pull_request' && 'true' || 'false' }}
run: |
set -euo pipefail
echo "user=$(whoami) home=$HOME"
export PATH="$HOME/.cargo/bin:/usr/local/bin:$PATH"
echo "PATH=$PATH"
for bin in rustc cargo claw-cargo; do
if ! command -v $bin >/dev/null 2>&1; then
echo "missing $bin"; exit 1
fi
$bin --version | head -1
done
cfg="$HOME/.claw-cargo/config.toml"
test -s "$cfg" || { echo "no $cfg"; exit 1; }
args=(build --workspace . --profile dev --parallel-restore 1)
if [ "${NO_UPLOAD:-false}" = "true" ]; then
args+=(--no-upload)
fi
started=$SECONDS
set +e
claw-cargo "${args[@]}" 2>&1 | tee /tmp/clawstor-build.out
rc=${PIPESTATUS[0]}
set -e
elapsed=$((SECONDS - started))
outcome=SKIPPED
if grep -q 'cache: *HIT' /tmp/clawstor-build.out; then outcome=HIT
elif grep -q 'cache: *MISS.*populated' /tmp/clawstor-build.out; then outcome=POPULATED
elif grep -q 'cache: *MISS' /tmp/clawstor-build.out; then outcome=MISS
fi
fp=$(grep -oE 'fingerprint: [0-9a-f]+' /tmp/clawstor-build.out | head -1 | awk '{print $2}')
echo "cache-outcome=$outcome" >> "$GITHUB_OUTPUT"
echo "fingerprint=${fp:-unknown}" >> "$GITHUB_OUTPUT"
echo "elapsed-seconds=$elapsed" >> "$GITHUB_OUTPUT"
echo "::notice::clawstor $outcome fp=${fp:-unknown} elapsed=${elapsed}s"
exit "$rc"
id: cache
uses: ./.gitea/actions/cargo-cache
with:
workspace: '.'
profile: 'dev'
no-upload: ${{ github.event_name == 'pull_request' && 'true' || 'false' }}
- name: Announce outcome
run: |
echo "cache: ${{ steps.build.outputs.cache-outcome }}"
echo "fp: ${{ steps.build.outputs.fingerprint }}"
echo "time: ${{ steps.build.outputs.elapsed-seconds }}s"
echo "cache: ${{ steps.cache.outputs.cache-outcome }}"
echo "fp: ${{ steps.cache.outputs.fingerprint }}"
echo "time: ${{ steps.cache.outputs.elapsed-seconds }}s"
+126 -10
View File
@@ -3,7 +3,12 @@
//!
//! Layered defaults:
//! 1. Built-in defaults (empty struct).
//! 2. `~/.claw-cargo/config.toml` if present.
//! 2. XDG-style user config, first hit wins:
//! * `$XDG_CONFIG_HOME/claw-cargo/config.toml`
//! * `$HOME/.config/claw-cargo/config.toml`
//! * `$HOME/.claw-cargo/config.toml` (legacy — from before XDG
//! support landed on 2026-07-13; still honoured for existing
//! runner installs).
//! 3. `<workspace>/.claw-cargo.toml` if present.
//! 4. CLI overrides.
//!
@@ -16,11 +21,43 @@ use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
/// Location of a per-user config file: `<home>/.claw-cargo/config.toml`.
/// Ordered list of candidate user-config paths, first-match wins.
///
/// Field finding 2026-07-13 (Gitea runner deploy): the runner
/// integration doc initially told operators to install the config
/// under `$HOME/.config/claw-cargo/` (XDG-style), but the code only
/// looked at `$HOME/.claw-cargo/`. Both are now honoured so old and
/// new installs both work.
pub fn user_config_candidates() -> Vec<PathBuf> {
let mut out = Vec::with_capacity(3);
// Path 1: explicit XDG override.
if let Some(xdg) = std::env::var_os("XDG_CONFIG_HOME") {
let xdg = PathBuf::from(xdg);
if !xdg.as_os_str().is_empty() {
out.push(xdg.join("claw-cargo").join("config.toml"));
}
}
// Paths 2 + 3: derived from $HOME.
if let Some(home) = std::env::var_os("HOME") {
let home = PathBuf::from(home);
out.push(home.join(".config").join("claw-cargo").join("config.toml"));
out.push(home.join(".claw-cargo").join("config.toml"));
}
out
}
/// Location of a per-user config file. Returns the first candidate
/// that exists on disk, or the *last* candidate (legacy dotfile) when
/// none exists — so error messages point at a stable path for
/// operators to create.
pub fn user_config_path() -> Option<PathBuf> {
std::env::var_os("HOME")
.map(PathBuf::from)
.map(|h| h.join(".claw-cargo").join("config.toml"))
let candidates = user_config_candidates();
for c in &candidates {
if c.is_file() {
return Some(c.clone());
}
}
candidates.into_iter().last()
}
/// Location of a workspace-level config file: `<workspace>/.claw-cargo.toml`.
@@ -464,15 +501,94 @@ profile = "release"
}
#[test]
fn user_config_path_uses_home() {
let saved = std::env::var_os("HOME");
std::env::set_var("HOME", "/tmp/test-home");
fn user_config_path_falls_back_to_legacy_dotfile_when_none_exist() {
// When neither XDG-style path nor the legacy dotfile exists,
// `user_config_path` returns the LAST candidate so error
// messages point at a stable path. Legacy dotfile is last.
let saved_home = std::env::var_os("HOME");
let saved_xdg = std::env::var_os("XDG_CONFIG_HOME");
std::env::set_var("HOME", "/tmp/test-home-no-config");
std::env::remove_var("XDG_CONFIG_HOME");
let path = user_config_path().unwrap();
assert_eq!(path, PathBuf::from("/tmp/test-home/.claw-cargo/config.toml"));
match saved {
assert_eq!(
path,
PathBuf::from("/tmp/test-home-no-config/.claw-cargo/config.toml")
);
match saved_home {
Some(v) => std::env::set_var("HOME", v),
None => std::env::remove_var("HOME"),
}
match saved_xdg {
Some(v) => std::env::set_var("XDG_CONFIG_HOME", v),
None => std::env::remove_var("XDG_CONFIG_HOME"),
}
}
#[test]
fn user_config_path_prefers_xdg_when_that_file_exists() {
// Field finding 2026-07-13: runner install placed the config
// at the XDG path but the code only looked at the legacy
// dotfile. Now XDG is checked FIRST if the file is really there.
let saved_home = std::env::var_os("HOME");
let saved_xdg = std::env::var_os("XDG_CONFIG_HOME");
let tmp = tempfile::TempDir::new().unwrap();
let xdg = tmp.path().join("xdg");
let home = tmp.path().join("home");
std::fs::create_dir_all(xdg.join("claw-cargo")).unwrap();
std::fs::write(xdg.join("claw-cargo").join("config.toml"), b"# xdg\n").unwrap();
std::env::set_var("HOME", &home);
std::env::set_var("XDG_CONFIG_HOME", &xdg);
let path = user_config_path().unwrap();
assert_eq!(path, xdg.join("claw-cargo").join("config.toml"));
match saved_home {
Some(v) => std::env::set_var("HOME", v),
None => std::env::remove_var("HOME"),
}
match saved_xdg {
Some(v) => std::env::set_var("XDG_CONFIG_HOME", v),
None => std::env::remove_var("XDG_CONFIG_HOME"),
}
}
#[test]
fn user_config_path_prefers_home_dot_config_over_legacy_dotfile() {
// Between the two `$HOME`-relative paths, `.config/claw-cargo/`
// wins over `.claw-cargo/` — matches what most runner installs
// will look like going forward.
let saved_home = std::env::var_os("HOME");
let saved_xdg = std::env::var_os("XDG_CONFIG_HOME");
std::env::remove_var("XDG_CONFIG_HOME");
let tmp = tempfile::TempDir::new().unwrap();
let home = tmp.path();
// Create BOTH files; the XDG-style .config path should win.
std::fs::create_dir_all(home.join(".config").join("claw-cargo")).unwrap();
std::fs::write(
home.join(".config").join("claw-cargo").join("config.toml"),
b"# xdg-style\n",
)
.unwrap();
std::fs::create_dir_all(home.join(".claw-cargo")).unwrap();
std::fs::write(home.join(".claw-cargo").join("config.toml"), b"# legacy\n").unwrap();
std::env::set_var("HOME", home);
let path = user_config_path().unwrap();
assert_eq!(
path,
home.join(".config").join("claw-cargo").join("config.toml")
);
match saved_home {
Some(v) => std::env::set_var("HOME", v),
None => std::env::remove_var("HOME"),
}
match saved_xdg {
Some(v) => std::env::set_var("XDG_CONFIG_HOME", v),
None => std::env::remove_var("XDG_CONFIG_HOME"),
}
}
#[test]
+23 -7
View File
@@ -63,24 +63,40 @@ sudo -u act_runner claw-cargo peer-metrics \
--tls-dir /var/lib/act_runner/clawstor-tls
```
## Runner label
Every provisioned runner gets a dedicated label so workflows only
land on hosts that actually have `claw-cargo` installed. Add to
`/etc/act_runner/config.yaml`:
```yaml
runner:
labels:
- "clawstor-cache:host" # add this line
- "ubuntu-latest:host" # keep any existing labels
```
Then `sudo systemctl restart act_runner.service` so the label
re-registers.
## Using the action in a workflow
Field finding: `ubuntu-latest` on Gitea Actions routes to
container mode by default (even with the `:host` suffix on the
runner label). Container mode hides `/usr/local/bin/claw-cargo` and
the per-runner tls_dir. Use the dedicated label instead:
```yaml
jobs:
build:
runs-on: ubuntu-latest # host runner
runs-on: clawstor-cache # dedicated host runner
steps:
- uses: actions/checkout@v4
- name: Rust toolchain
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --profile minimal
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- uses: ./.gitea/actions/cargo-cache
with:
workspace: .
# Read-only from forks so a malicious PR can't poison the cache.
no-upload: ${{ github.event_name == 'pull_request' }}
no-upload: ${{ github.event_name == 'pull_request' && 'true' || 'false' }}
```
Outputs available on the step: `cache-outcome` (HIT | MISS |