runner follow-ups: XDG config path + composite action + docs
Build with clawstor cache / Cargo build (clawstor-cached) (pull_request) Successful in 10s

Three fixes surfaced by the 2026-07-13 Gitea Actions wire-up.

## XDG config path

Before: `client_config::user_config_path` only looked at
`~/.claw-cargo/config.toml`. My runner-integration doc initially
told operators to install at `~/.config/claw-cargo/config.toml`
(XDG-style). Config wasn't loaded.

Now: three-way lookup, first hit wins.
  1. `$XDG_CONFIG_HOME/claw-cargo/config.toml`
  2. `$HOME/.config/claw-cargo/config.toml`
  3. `$HOME/.claw-cargo/config.toml` (legacy, still honoured)

+3 tests: XDG env wins when the file exists, .config wins over
legacy dotfile when both present, legacy dotfile returned as
error-message fallback when none exist.

## Composite action

Before: composite action silently no-op'd. Log showed the script
lines echoed but only the `if command -v claw-cargo` fail branch
ran. Root cause: Gitea Actions composite steps run with a stripped
PATH that omits `/usr/local/bin`.

Now: composite step exports PATH defensively:

    export PATH="/usr/local/bin:$HOME/.cargo/bin:$PATH"

And it looks for the config at BOTH the XDG-style path and the
legacy dotfile (same order as client_config).

Workflow file back to using the composite action.

## Docs

Runner-integration doc now:
- Explicitly warns that `ubuntu-latest` routes to container mode
  even with `:host` suffix on runner labels (field-observed).
- Documents the dedicated `clawstor-cache` label pattern that
  works.
- Sample workflow uses `runs-on: clawstor-cache` instead of
  `ubuntu-latest`.

+3 tests, 262 total (baseline unchanged).
This commit is contained in:
Omar Sobh
2026-07-13 06:21:42 -07:00
parent 3628322859
commit bfcc11de82
4 changed files with 194 additions and 95 deletions
+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]