feat: private repo support via encrypted git token
- db.rs: add nullable git_token column (idempotent ALTER TABLE ADD COLUMN)
- models.rs: git_token on App (#[serde(skip_serializing)]), CreateApp, UpdateApp
- routes/apps.rs: encrypt token on create/update; empty string clears it
- builder.rs: decrypt token, pass as GIT_TOKEN env var to build script
- build.sh: GIT_TERMINAL_PROMPT=0 (fail fast, not hang); when GIT_TOKEN is
set, inject it into the HTTPS clone URL as x-token-auth; strip credentials
from .git/config after clone/fetch so the token is never persisted to disk
Token usage: PATCH /api/apps/:id with {"git_token": "ghp_..."}
Clear token: PATCH /api/apps/:id with {"git_token": ""}
https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
This commit is contained in:
parent
73ea7320fd
commit
0b3cbf8734
5 changed files with 65 additions and 6 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
@ -22,6 +25,7 @@ pub struct CreateApp {
|
|||
pub port: i64,
|
||||
pub memory_limit: Option<String>,
|
||||
pub cpu_limit: Option<String>,
|
||||
pub git_token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
@ -31,6 +35,7 @@ pub struct UpdateApp {
|
|||
pub port: Option<i64>,
|
||||
pub memory_limit: Option<String>,
|
||||
pub cpu_limit: Option<String>,
|
||||
pub git_token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue