missions: fix repo checkout for retries + tokenize git.redclaw.dev clones
ci / gates (push) Successful in 7s
ci / frontend (push) Successful in 37s
ci / rust (push) Successful in 3m40s
ci / e2e (push) Skipped
ci / publish (push) Successful in 2m37s

- mission_orchestrator: run ensure_checkout BEFORE the team_id
  short-circuit. Previously, a re-launched or retried mission bailed
  out at the team_id=already-bound guard and skipped repo checkout
  entirely, so agents ran against an empty workspace.
- mission_workspace: inject GITEA_TOKEN into git.redclaw.dev URLs so
  clone auth works from the server container. Redact any token
  echoed back on failure.
- refresh buttons on MissionCanvas + MissionsList now spin the icon
  while loading so clicks are visibly acknowledged.
- refresh-spinner keyframe added to motion.css.

Requires operator on gw-04: sudo chown 65532:65532 /var/lib/clawmates-missions
(applied 2026-07-21 pre-commit).
This commit is contained in:
Omar Sobh
2026-07-21 20:33:33 -07:00
parent f1f3de4db0
commit 1e91a19707
5 changed files with 87 additions and 30 deletions
+43 -7
View File
@@ -13,10 +13,11 @@
//! to bring it in sync
//! - dir missing → `git clone --depth 1 <url> <path>`
//!
//! Auth: relies on the ambient git credential setup (SSH agent,
//! .netrc, or git-credential helper) in the process environment. We
//! deliberately don't embed tokens in URLs — footgun risk outweighs
//! the ergonomics, and prod already runs with a helper configured.
//! Auth: for `git.redclaw.dev` clones we inject the ambient
//! `GITEA_TOKEN` (already provisioned in the server container's env)
//! into the clone URL as basic-auth. For any other host we fall back
//! to the ambient credential setup (SSH agent, .netrc, git helper) —
//! prod hosts run with those configured. Tokens are never logged.
use std::path::PathBuf;
use tokio::process::Command;
@@ -64,14 +65,32 @@ pub async fn ensure_checkout(
.map_err(|e| format!("mkdir {}: {e}", parent.display()))?;
}
let auth_url = with_ambient_auth(clone_url);
if path.join(".git").exists() {
fetch_and_reset(&path, default_branch).await?;
} else {
clone(&path, clone_url).await?;
clone(&path, &auth_url).await?;
}
Ok(Some(path))
}
/// If the URL points at git.redclaw.dev AND GITEA_TOKEN is set in the
/// environment, rewrite it to include the token as basic-auth. Returns
/// the URL unchanged otherwise. The token is never logged (we only
/// pass the rewritten URL into `git clone` via argv).
fn with_ambient_auth(url: &str) -> String {
let Ok(token) = std::env::var("GITEA_TOKEN") else {
return url.to_string();
};
if token.is_empty() {
return url.to_string();
}
if let Some(rest) = url.strip_prefix("https://git.redclaw.dev/") {
return format!("https://oauth2:{token}@git.redclaw.dev/{rest}");
}
url.to_string()
}
async fn clone(path: &std::path::Path, url: &str) -> Result<(), String> {
let out = Command::new("git")
.args(["clone", "--depth", "1", url, &path.display().to_string()])
@@ -80,9 +99,9 @@ async fn clone(path: &std::path::Path, url: &str) -> Result<(), String> {
.map_err(|e| format!("spawn git clone: {e}"))?;
if !out.status.success() {
return Err(format!(
"git clone {url} → exit {}: {}",
"git clone → exit {}: {}",
out.status,
String::from_utf8_lossy(&out.stderr)
redact_token(&String::from_utf8_lossy(&out.stderr))
.chars()
.take(400)
.collect::<String>()
@@ -91,6 +110,23 @@ async fn clone(path: &std::path::Path, url: &str) -> Result<(), String> {
Ok(())
}
fn redact_token(s: &str) -> String {
// Strip any "oauth2:<token>@" segment that git may echo back on
// failures. Belt-and-braces: also nuke any raw token env value.
let mut out = s.to_string();
if let Some(pos) = out.find("oauth2:") {
if let Some(at) = out[pos..].find('@') {
out.replace_range(pos..pos + at, "oauth2:***");
}
}
if let Ok(t) = std::env::var("GITEA_TOKEN") {
if !t.is_empty() {
out = out.replace(&t, "***");
}
}
out
}
async fn fetch_and_reset(path: &std::path::Path, branch: &str) -> Result<(), String> {
let fetch = Command::new("git")
.args([