From: Alpha Chen Date: Mon, 27 Apr 2026 19:02:42 +0000 (-0700) Subject: Use HTTP Basic auth for mirror push to GitHub X-Git-Url: http://quire.kejadlen.dev/?a=commitdiff_plain;h=16ef4697ec540f44e7809cf9cacd2494077be655;p=quire.git Use HTTP Basic auth for mirror push to GitHub GitHub's git smart HTTP endpoint rejects `Authorization: Bearer ` even though the REST API accepts it. The 401 made git fall back to prompting for a username, which fails in a post-receive hook (no TTY). Assisted-by: Claude Opus 4.7 (1M context) via Claude Code --- diff --git a/Cargo.lock b/Cargo.lock index a5dd126..e6513d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1855,6 +1855,7 @@ version = "0.1.0" dependencies = [ "assert_cmd", "axum", + "base64", "clap", "clap_complete", "fs-err", diff --git a/Cargo.toml b/Cargo.toml index 9f18de7..ae0fbd6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ path = "src/bin/quire/main.rs" [dependencies] axum = "*" +base64 = "*" mlua = { version = "*", features = ["lua54", "serde", "vendored"] } regex = "*" serde = { version = "*", features = ["derive"] } diff --git a/src/quire.rs b/src/quire.rs index 0a19669..c1c363a 100644 --- a/src/quire.rs +++ b/src/quire.rs @@ -159,10 +159,7 @@ impl Repo { .git(&args) .env("GIT_CONFIG_COUNT", "1") .env("GIT_CONFIG_KEY_0", "http.extraHeader") - .env( - "GIT_CONFIG_VALUE_0", - format!("Authorization: Bearer {token}"), - ) + .env("GIT_CONFIG_VALUE_0", github_auth_header(token)) .stdout(std::process::Stdio::null()) .status() .map_err(crate::Error::Io)?; @@ -219,6 +216,21 @@ impl Repo { } } +/// Build the `Authorization` header value used to authenticate `git push` +/// to GitHub over HTTPS. +/// +/// GitHub's git smart HTTP endpoint (`/info/refs`, `git-receive-pack`) +/// rejects `Authorization: Bearer ` with 401, even though the REST +/// API accepts the same token via Bearer. Git then falls back to +/// prompting for a username, which fails inside a post-receive hook +/// because there's no TTY. HTTP Basic with any non-empty username and +/// the token as the password is the documented form for git push. +fn github_auth_header(token: &str) -> String { + use base64::{Engine, engine::general_purpose::STANDARD}; + let encoded = STANDARD.encode(format!("x-access-token:{token}")); + format!("Authorization: Basic {encoded}") +} + /// Application runtime context. /// /// Carries configuration and provides resolved paths to repositories. @@ -717,6 +729,24 @@ mod tests { .to_string() } + #[test] + fn github_auth_header_is_basic_with_x_access_token_username() { + use base64::{Engine, engine::general_purpose::STANDARD}; + + let header = super::github_auth_header("ghp_test"); + let encoded = header + .strip_prefix("Authorization: Basic ") + .unwrap_or_else(|| panic!("missing Basic prefix: {header}")); + // libcurl rejects header values containing newlines, so the encoded + // form must not wrap. + assert!( + !encoded.contains('\n'), + "encoded header value must not wrap: {encoded:?}" + ); + let decoded = STANDARD.decode(encoded).expect("valid base64"); + assert_eq!(decoded, b"x-access-token:ghp_test"); + } + #[test] fn push_to_mirror_pushes_main_to_file_mirror() { let dir = tempfile::tempdir().expect("tempdir");