diff --git a/builder/build.sh b/builder/build.sh index a779d11..bc5e79f 100755 --- a/builder/build.sh +++ b/builder/build.sh @@ -5,6 +5,9 @@ # MEMORY_LIMIT (e.g. "512m"), CPU_LIMIT (e.g. "0.5") set -euo pipefail +# Never prompt for git credentials — fail immediately if auth is missing. +export GIT_TERMINAL_PROMPT=0 + # Defaults — overridden by per-app settings stored in the control plane. MEMORY_LIMIT="${MEMORY_LIMIT:-512m}" CPU_LIMIT="${CPU_LIMIT:-0.5}" @@ -18,17 +21,33 @@ log "Branch: $BRANCH" log "Build dir: $BUILD_DIR" # ── 1. Clone or pull ─────────────────────────────────────────────────────────── +# Build an authenticated URL when a git token is set (private repos). +# GIT_TOKEN is passed by hiy-server and never echoed here. +CLONE_URL="$REPO_URL" +if [ -n "${GIT_TOKEN:-}" ]; then + case "$REPO_URL" in + https://*) + CLONE_URL="https://x-token-auth:${GIT_TOKEN}@${REPO_URL#https://}" + ;; + esac +fi + mkdir -p "$BUILD_DIR" cd "$BUILD_DIR" if [ -d ".git" ]; then log "Updating existing clone…" + git remote set-url origin "$CLONE_URL" git fetch origin "$BRANCH" --depth=50 git checkout "$BRANCH" git reset --hard "origin/$BRANCH" + # Strip credentials from the stored remote so they don't sit in .git/config. + git remote set-url origin "$REPO_URL" else log "Cloning repository…" - git clone --depth=50 --branch "$BRANCH" "$REPO_URL" . + git clone --depth=50 --branch "$BRANCH" "$CLONE_URL" . + # Strip credentials from the stored remote so they don't sit in .git/config. + git remote set-url origin "$REPO_URL" fi ACTUAL_SHA=$(git rev-parse HEAD) diff --git a/server/src/builder.rs b/server/src/builder.rs index ea1c25c..32a95a6 100644 --- a/server/src/builder.rs +++ b/server/src/builder.rs @@ -133,11 +133,22 @@ async fn run_build(state: &AppState, deploy_id: &str) -> anyhow::Result<()> { let domain_suffix = std::env::var("DOMAIN_SUFFIX").unwrap_or_else(|_| "localhost".into()); let caddy_api_url = std::env::var("CADDY_API_URL").unwrap_or_else(|_| "http://localhost:2019".into()); - let mut child = Command::new("bash") - .arg(&build_script) + let mut cmd = Command::new("bash"); + cmd.arg(&build_script) .env("APP_ID", &app.id) .env("APP_NAME", &app.name) - .env("REPO_URL", &repo_url) + .env("REPO_URL", &repo_url); + + // Decrypt the git token (if any) and pass it separately so build.sh can + // inject it into the clone URL without it appearing in REPO_URL or logs. + if let Some(enc) = &app.git_token { + match crate::crypto::decrypt(enc) { + Ok(tok) => { cmd.env("GIT_TOKEN", tok); } + Err(e) => tracing::warn!("Could not decrypt git_token for {}: {}", app.id, e), + } + } + + let mut child = cmd .env("BRANCH", &app.branch) .env("PORT", app.port.to_string()) .env("ENV_FILE", &env_file) diff --git a/server/src/db.rs b/server/src/db.rs index 7d4ce46..0366a6a 100644 --- a/server/src/db.rs +++ b/server/src/db.rs @@ -106,6 +106,8 @@ pub async fn migrate(pool: &DbPool) -> anyhow::Result<()> { .execute(pool).await; let _ = sqlx::query("ALTER TABLE apps ADD COLUMN cpu_limit TEXT NOT NULL DEFAULT '0.5'") .execute(pool).await; + let _ = sqlx::query("ALTER TABLE apps ADD COLUMN git_token TEXT") + .execute(pool).await; sqlx::query( r#"CREATE TABLE IF NOT EXISTS databases ( diff --git a/server/src/models.rs b/server/src/models.rs index 39b9efe..c6de907 100644 --- a/server/src/models.rs +++ b/server/src/models.rs @@ -12,6 +12,9 @@ pub struct App { pub cpu_limit: String, pub created_at: String, pub updated_at: String, + /// Encrypted git token for cloning private repos. Never serialised to API responses. + #[serde(skip_serializing)] + pub git_token: Option, } #[derive(Debug, Deserialize)] @@ -22,6 +25,7 @@ pub struct CreateApp { pub port: i64, pub memory_limit: Option, pub cpu_limit: Option, + pub git_token: Option, } #[derive(Debug, Deserialize)] @@ -31,6 +35,7 @@ pub struct UpdateApp { pub port: Option, pub memory_limit: Option, pub cpu_limit: Option, + pub git_token: Option, } #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] diff --git a/server/src/routes/apps.rs b/server/src/routes/apps.rs index 3c161ca..f354498 100644 --- a/server/src/routes/apps.rs +++ b/server/src/routes/apps.rs @@ -31,10 +31,16 @@ pub async fn create( let secret = Uuid::new_v4().to_string().replace('-', ""); let memory_limit = payload.memory_limit.unwrap_or_else(|| "512m".into()); let cpu_limit = payload.cpu_limit.unwrap_or_else(|| "0.5".into()); + let git_token_enc = payload.git_token + .as_deref() + .filter(|t| !t.is_empty()) + .map(crate::crypto::encrypt) + .transpose() + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; sqlx::query( - "INSERT INTO apps (id, name, repo_url, branch, port, webhook_secret, memory_limit, cpu_limit, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "INSERT INTO apps (id, name, repo_url, branch, port, webhook_secret, memory_limit, cpu_limit, git_token, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", ) .bind(&id) .bind(&payload.name) @@ -44,6 +50,7 @@ pub async fn create( .bind(&secret) .bind(&memory_limit) .bind(&cpu_limit) + .bind(&git_token_enc) .bind(&now) .bind(&now) .execute(&s.db) @@ -105,6 +112,21 @@ pub async fn update( .execute(&s.db).await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; } + if let Some(v) = payload.git_token { + if v.is_empty() { + sqlx::query("UPDATE apps SET git_token = NULL, updated_at = ? WHERE id = ?") + .bind(&now).bind(&id) + .execute(&s.db).await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + } else { + let enc = crate::crypto::encrypt(&v) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + sqlx::query("UPDATE apps SET git_token = ?, updated_at = ? WHERE id = ?") + .bind(enc).bind(&now).bind(&id) + .execute(&s.db).await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + } + } fetch_app(&s, &id).await.map(Json) }