Skip to main content

max / makenotwork

Fix git repo auto-create permission denied on push The makenotwork service user can't write to /opt/git/max/ (owned by git:git). git2::Repository::init_bare() failed with permission denied when auto-creating repos on first push. Fix: run git init --bare as the git user via sudo, matching how the rest of the git infrastructure operates. Fixed in both the API authorize endpoint and the embedded SSH handler. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-03 00:03 UTC
Commit: cf71082e6b7e01747b6fdd9c2f603739aae9e146
Parent: 1ccddc9
2 files changed, +40 insertions, -32 deletions
@@ -69,25 +69,29 @@ async fn exec_git_operation(
69 69
70 70 let git_root = std::env::var("GIT_REPOS_PATH")
71 71 .unwrap_or_else(|_| "/opt/git".to_string());
72 - let owner_dir = std::path::Path::new(&git_root).join(owner);
73 - let repo_dir = owner_dir.join(format!("{repo_name}.git"));
74 -
75 - std::fs::create_dir_all(&owner_dir)?;
76 - git2::Repository::init_bare(&repo_dir)?;
72 + let repo_dir = std::path::Path::new(&git_root)
73 + .join(owner)
74 + .join(format!("{repo_name}.git"));
75 +
76 + // Create the bare repo as the git user (the server process
77 + // doesn't have write permissions to the git:git owned directory).
78 + let status = std::process::Command::new("sudo")
79 + .args(["-u", "git", "git", "init", "--bare"])
80 + .arg(&repo_dir)
81 + .status()?;
82 + if !status.success() {
83 + anyhow::bail!("git init --bare failed for {}", repo_dir.display());
84 + }
77 85
78 86 // Install post-receive hook if build trigger token is configured
79 87 if let Ok(token) = std::env::var("BUILD_TRIGGER_TOKEN") {
80 88 let hook_content = crate::build_runner::post_receive_hook(&token);
81 89 install_hook_for_repo(&repo_dir, &hook_content)?;
82 - }
83 -
84 - // Fix ownership so the git user can write
85 - let status = std::process::Command::new("chown")
86 - .args(["-R", "git:git"])
87 - .arg(&repo_dir)
88 - .status()?;
89 - if !status.success() {
90 - anyhow::bail!("chown failed on {}", repo_dir.display());
90 + // Fix hook ownership
91 + let _ = std::process::Command::new("chown")
92 + .args(["git:git"])
93 + .arg(repo_dir.join("hooks/post-receive"))
94 + .status();
91 95 }
92 96
93 97 eprintln!("Auto-created repository {}/{}", owner, repo_name);
@@ -147,20 +147,30 @@ pub(super) async fn git_authorize(
147 147 return Err(AppError::NotFound);
148 148 }
149 149
150 - let owner_dir = std::path::Path::new(git_root).join(&req.owner);
151 - let repo_dir = owner_dir.join(format!("{}.git", req.repo_name));
150 + let repo_dir = std::path::Path::new(git_root)
151 + .join(&req.owner)
152 + .join(format!("{}.git", req.repo_name));
152 153 fn io_err(e: std::io::Error) -> AppError { AppError::Internal(e.into()) }
153 154
154 - std::fs::create_dir_all(&owner_dir).map_err(io_err)?;
155 - git2::Repository::init_bare(&repo_dir)
156 - .map_err(|e| AppError::Internal(e.into()))?;
155 + // Create the bare repo as the git user (the makenotwork service user
156 + // doesn't have write permissions to the git:git owned directory).
157 + let status = std::process::Command::new("sudo")
158 + .args(["-u", "git", "git", "init", "--bare"])
159 + .arg(&repo_dir)
160 + .status()
161 + .map_err(io_err)?;
162 + if !status.success() {
163 + return Err(AppError::Internal(anyhow::anyhow!(
164 + "git init --bare failed for {}",
165 + repo_dir.display()
166 + )));
167 + }
157 168
158 169 // Install post-receive hook if build trigger token is configured
159 170 if let Some(ref token) = state.config.build_trigger_token {
160 171 let hook_content = crate::build_runner::post_receive_hook(token);
161 - let hooks_dir = repo_dir.join("hooks");
162 - std::fs::create_dir_all(&hooks_dir).map_err(io_err)?;
163 - let hook_path = hooks_dir.join("post-receive");
172 + let hook_path = repo_dir.join("hooks/post-receive");
173 + // Write as root, then chown — hooks dir already exists from git init
164 174 std::fs::write(&hook_path, &hook_content).map_err(io_err)?;
165 175 #[cfg(unix)]
166 176 {
@@ -170,16 +180,10 @@ pub(super) async fn git_authorize(
170 180 std::fs::Permissions::from_mode(0o755),
171 181 );
172 182 }
173 - }
174 -
175 - // Fix ownership so the git user can write
176 - let status = std::process::Command::new("chown")
177 - .args(["-R", "git:git"])
178 - .arg(&repo_dir)
179 - .status()
180 - .map_err(io_err)?;
181 - if !status.success() {
182 - return Err(AppError::Internal(anyhow::anyhow!("chown failed on {}", repo_dir.display())));
183 + let _ = std::process::Command::new("chown")
184 + .args(["git:git"])
185 + .arg(&hook_path)
186 + .status();
183 187 }
184 188
185 189 tracing::info!(owner = %req.owner, repo = %req.repo_name, "auto-created bare repository");