feat(git-auth): fleet-wide non-interactive git auth
Add setup-git-auth.sh: idempotent, fail-silent script that primes the git credential store from the vault Gitea token, scoped per-repo by the actual origin host. Only seizes the helper from the prompting GCM `manager` (leaves Mac osxkeychain alone); fast-path no-op once set. Wire it into a backgrounded SessionStart hook and set GIT_TERMINAL_PROMPT=0 / GCM_INTERACTIVE=Never in settings.json env so no session on any machine can hang on a credential prompt. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
102
.claude/scripts/setup-git-auth.sh
Normal file
102
.claude/scripts/setup-git-auth.sh
Normal file
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env bash
|
||||
# setup-git-auth.sh — make git push/fetch fully non-interactive on this machine.
|
||||
#
|
||||
# Mike's requirement: git must NEVER sit at an interactive credential prompt
|
||||
# (Git Credential Manager popups hang automation/background pushes). This script
|
||||
# primes the git "store" credential helper with the shared azcomputerguru Gitea
|
||||
# API token (from the SOPS vault), scoped to each repo's actual remote host.
|
||||
#
|
||||
# Properties:
|
||||
# - Idempotent + fast-path: if every managed repo already has a stored
|
||||
# credential for its remote host, it exits WITHOUT touching the vault.
|
||||
# - Conservative: only switches a repo to the `store` helper when the current
|
||||
# helper is empty or the prompting GCM `manager` (so a Mac osxkeychain setup
|
||||
# that already works silently is left untouched).
|
||||
# - Fail-silent: always exits 0; never blocks a session.
|
||||
#
|
||||
# Runs from the SessionStart hook (backgrounded) and from onboarding.
|
||||
# See: .claude/memory/feedback_git_noninteractive_auth.md
|
||||
|
||||
set -u
|
||||
|
||||
# --- locate repo root + identity ------------------------------------------------
|
||||
CT_ROOT="${CLAUDE_PROJECT_DIR:-}"
|
||||
if [ -z "$CT_ROOT" ]; then
|
||||
# two levels up from this script: .claude/scripts/ -> repo root
|
||||
CT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." 2>/dev/null && pwd)"
|
||||
fi
|
||||
IDENTITY="$CT_ROOT/.claude/identity.json"
|
||||
VAULT="$CT_ROOT/.claude/scripts/vault.sh"
|
||||
CRED_FILE="$HOME/.git-credentials"
|
||||
GIT_USER="azcomputerguru"
|
||||
|
||||
# Extract a flat string field from identity.json without requiring jq.
|
||||
json_field() { grep -oE "\"$1\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" "$IDENTITY" 2>/dev/null | head -1 | sed -E 's/.*:[[:space:]]*"([^"]*)"/\1/'; }
|
||||
|
||||
VAULT_PATH="$(json_field vault_path)"
|
||||
|
||||
# Candidate repos to make non-interactive: this repo + the vault repo.
|
||||
REPOS=("$CT_ROOT")
|
||||
[ -n "$VAULT_PATH" ] && [ -d "$VAULT_PATH/.git" ] && REPOS+=("$VAULT_PATH")
|
||||
|
||||
# --- derive scheme + host (authority) from a remote URL -------------------------
|
||||
remote_authority() { # echoes "scheme host[:port]" or nothing
|
||||
local url="$1" scheme rest auth host
|
||||
case "$url" in
|
||||
http://*|https://*) scheme="${url%%://*}";;
|
||||
*) return 0;; # ssh/git@ remotes don't use the credential store
|
||||
esac
|
||||
rest="${url#*://}"
|
||||
auth="${rest%%/*}" # strip path
|
||||
host="${auth##*@}" # strip any userinfo
|
||||
[ -n "$host" ] && printf '%s %s' "$scheme" "$host"
|
||||
}
|
||||
|
||||
# Does the cred file already have an entry for this scheme://user@host ?
|
||||
have_cred() { # $1=scheme $2=host
|
||||
[ -f "$CRED_FILE" ] || return 1
|
||||
grep -qE "^$1://$GIT_USER:[^@]*@$2$" "$CRED_FILE" 2>/dev/null
|
||||
}
|
||||
|
||||
# --- fast path: everything already configured? ---------------------------------
|
||||
needs_priming=0
|
||||
for repo in "${REPOS[@]}"; do
|
||||
url="$(git -C "$repo" remote get-url origin 2>/dev/null)" || continue
|
||||
read -r scheme host <<<"$(remote_authority "$url")"
|
||||
[ -n "${host:-}" ] || continue
|
||||
have_cred "$scheme" "$host" || needs_priming=1
|
||||
done
|
||||
|
||||
# --- fetch token only if needed ------------------------------------------------
|
||||
TOKEN=""
|
||||
if [ "$needs_priming" -eq 1 ] && [ -f "$VAULT" ]; then
|
||||
TOKEN="$(bash "$VAULT" get-field services/gitea.sops.yaml credentials.api.api-token 2>/dev/null | tr -d '\r\n ')"
|
||||
# Fallback for machines missing PyYAML/yq: parse the full decrypted entry.
|
||||
if ! printf '%s' "$TOKEN" | grep -qE '^[0-9a-f]{40}$'; then
|
||||
TOKEN="$(bash "$VAULT" get services/gitea.sops.yaml 2>/dev/null | grep -oE 'api-token:[[:space:]]*[0-9a-f]{40}' | grep -oE '[0-9a-f]{40}' | head -1)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- configure each repo -------------------------------------------------------
|
||||
touch "$CRED_FILE" 2>/dev/null && chmod 600 "$CRED_FILE" 2>/dev/null || true
|
||||
for repo in "${REPOS[@]}"; do
|
||||
url="$(git -C "$repo" remote get-url origin 2>/dev/null)" || continue
|
||||
read -r scheme host <<<"$(remote_authority "$url")"
|
||||
[ -n "${host:-}" ] || continue
|
||||
|
||||
# Prime the store entry if missing and we have a token.
|
||||
if ! have_cred "$scheme" "$host" && [ -n "$TOKEN" ]; then
|
||||
printf '%s://%s:%s@%s\n' "$scheme" "$GIT_USER" "$TOKEN" "$host" >>"$CRED_FILE"
|
||||
fi
|
||||
|
||||
# Only seize the helper away from the prompting GCM (or an unset helper).
|
||||
helper="$(git -C "$repo" config --get credential.helper 2>/dev/null)"
|
||||
case "$helper" in
|
||||
""|*manager*)
|
||||
git -C "$repo" config --local --unset-all credential.helper 2>/dev/null || true
|
||||
git -C "$repo" config --local credential.helper store 2>/dev/null || true
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
exit 0
|
||||
Reference in New Issue
Block a user