From 6f7fb82496bd4623c19001b5885483e3f5b7de03 Mon Sep 17 00:00:00 2001 From: Christian Rodriguez Date: Thu, 4 Jun 2026 20:47:19 +0000 Subject: [PATCH] feat: route git push auth to the right token by GitHub org MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a global credential helper that picks a token based on the repo's GitHub org (Blueprint-Talent→BTG_GITHUB_TOKEN, confiador→CONFIADOR_ GITHUB_TOKEN, else GITHUB_TOKEN), read from env at push time and never stored. Wired into on-create so it survives rebuilds; documented in AUTH-PERSISTENCE.md and secrets.example. Co-Authored-By: Claude Opus 4.8 (1M context) --- .devcontainer/AUTH-PERSISTENCE.md | 25 +++++++++++++ .devcontainer/on-create.sh | 3 ++ .../on-create/setup-git-credentials.sh | 20 +++++++++++ .../scripts/git-credential-org-router.sh | 36 +++++++++++++++++++ .devcontainer/secrets.example | 11 ++++++ 5 files changed, 95 insertions(+) create mode 100644 .devcontainer/on-create/setup-git-credentials.sh create mode 100755 .devcontainer/scripts/git-credential-org-router.sh diff --git a/.devcontainer/AUTH-PERSISTENCE.md b/.devcontainer/AUTH-PERSISTENCE.md index a2f0692..db62fd7 100644 --- a/.devcontainer/AUTH-PERSISTENCE.md +++ b/.devcontainer/AUTH-PERSISTENCE.md @@ -127,6 +127,31 @@ provider names Octopus recognizes are `codex`, `gemini`, `opencode`, `copilot`, the list — it's the orchestrator.** Recognized aliases: `claude`/`anthropic`/ `sonnet`, `codex`/`openai`, `gemini`/`google`, `local`→`ollama`. +## GitHub push auth (credential routing by org) + +`git push` over HTTPS needs a token, and different GitHub orgs need different ones +(your personal `GITHUB_TOKEN` is read-only on org repos). Rather than configure each +repo, a **global credential helper routes by org**: + +- `scripts/git-credential-org-router.sh` — reads the repo's org from the push request + (`credential.useHttpPath=true` passes the path) and emits the matching token from the + environment. Tokens are read at push time and **never written to disk** (not in + `.git/config`, the remote URL, or output). +- `on-create/setup-git-credentials.sh` wires it into **global** git config. `~/.gitconfig` + isn't a persisted volume, so re-applying on each create is what makes it survive rebuilds. + +Routing (edit the `case` in the helper to change it): + +| Org | Token | +|---|---| +| `Blueprint-Talent/*` | `BTG_GITHUB_TOKEN` | +| `confiador/*` | `CONFIADOR_GITHUB_TOKEN` | +| everything else | `GITHUB_TOKEN` | + +**To add an org:** add one `case` line in the helper + put the matching `*_GITHUB_TOKEN` +in your host secrets (common or per-project). Nothing repo-specific to maintain — routing +is by the remote's org, and each repo's `/workspace` is its own bind mount. + ## Where we track keys / tokens - **`secrets.example`** is the registry of every key the dev container expects, diff --git a/.devcontainer/on-create.sh b/.devcontainer/on-create.sh index 499fa02..dac04c2 100755 --- a/.devcontainer/on-create.sh +++ b/.devcontainer/on-create.sh @@ -80,6 +80,9 @@ optional() { source "$1" || echo "⚠️ $(basename "$1") failed; continuing setup without it" } +# Configure GitHub credential routing (org → token), so `git push` just works per repo +optional /workspace/.devcontainer/on-create/setup-git-credentials.sh + # Install Biome optional /workspace/.devcontainer/on-create/setup-biome.sh diff --git a/.devcontainer/on-create/setup-git-credentials.sh b/.devcontainer/on-create/setup-git-credentials.sh new file mode 100644 index 0000000..599522c --- /dev/null +++ b/.devcontainer/on-create/setup-git-credentials.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -e + +# Route GitHub HTTPS auth to the right token per repo org (see +# scripts/git-credential-org-router.sh). Global config so it applies in every +# repo in the container; ~/.gitconfig isn't a persisted volume, so re-applying +# here on each create is what makes it survive rebuilds. + +echo "🔑 Configuring GitHub credential routing (org → token)..." + +helper="/workspace/.devcontainer/scripts/git-credential-org-router.sh" +chmod +x "$helper" 2>/dev/null || true + +# Pass the repo path (hence org) to the helper, and replace any prior helper for +# this host context with ours. +git config --global credential.https://github.com.useHttpPath true +git config --global --unset-all credential.https://github.com.helper 2>/dev/null || true +git config --global credential.https://github.com.helper "!$helper" + +echo "✅ GitHub pushes route by org: Blueprint-Talent→BTG_GITHUB_TOKEN, confiador→CONFIADOR_GITHUB_TOKEN, else→GITHUB_TOKEN" diff --git a/.devcontainer/scripts/git-credential-org-router.sh b/.devcontainer/scripts/git-credential-org-router.sh new file mode 100755 index 0000000..14edf48 --- /dev/null +++ b/.devcontainer/scripts/git-credential-org-router.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# git-credential-org-router — hand git the right GitHub token based on the repo's org. +# +# Wired up globally by .devcontainer/on-create/setup-git-credentials.sh: +# git config --global credential.https://github.com.useHttpPath true # so $path carries the org +# git config --global credential.https://github.com.helper "!" +# +# Git calls this with the operation ("get"/"store"/"erase") as $1 and the request +# fields (protocol/host/path/...) as key=value lines on stdin. We answer only "get", +# read the org from the first path segment, and emit the matching token from the env. +# Tokens are never stored on disk — they're read fresh from the environment each call. +# +# Add an org by adding a case below + the matching token to your devcontainer secrets. + +[ "${1:-}" = "get" ] || exit 0 + +host="" +path="" +while IFS='=' read -r key value; do + case "$key" in + host) host="$value" ;; + path) path="$value" ;; + esac +done + +[ "$host" = "github.com" ] || exit 0 +org="${path%%/*}" + +case "$org" in + Blueprint-Talent) token="${BTG_GITHUB_TOKEN:-}" ;; + confiador) token="${CONFIADOR_GITHUB_TOKEN:-}" ;; + *) token="${GITHUB_TOKEN:-}" ;; +esac + +[ -n "$token" ] || exit 0 +printf 'username=x-access-token\npassword=%s\n' "$token" diff --git a/.devcontainer/secrets.example b/.devcontainer/secrets.example index 7bed5f2..5a4cd3d 100644 --- a/.devcontainer/secrets.example +++ b/.devcontainer/secrets.example @@ -7,6 +7,17 @@ # The value in secrets.d/ wins when the same key appears in both files. # Both files use the same KEY=value format (no export, no quotes needed). +# ── GitHub push auth (credential routing by org) ───────────────────────────── +# git push routes to the right token based on the repo's GitHub org — see +# AUTH-PERSISTENCE.md "GitHub push auth". GITHUB_TOKEN is the fallback (and is also +# auto-passed from the host shell via devcontainer.json remoteEnv if set there). +# Add a token here for any org you push to, then add a case to +# scripts/git-credential-org-router.sh. + +# GITHUB_TOKEN= # default / fallback (personal; read-only on org repos) +# BTG_GITHUB_TOKEN= # Blueprint-Talent/* (push) +# CONFIADOR_GITHUB_TOKEN= # confiador/* (push) + # ── AI Tools ───────────────────────────────────────────────────────────────── # Context7 MCP server (https://context7.com)