runner follow-ups: XDG config path + composite action + docs #38
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user