From f15aa44e9f24d576930f5b2a330c9d958d310ec2 Mon Sep 17 00:00:00 2001 From: git-bridge Date: Fri, 17 Jul 2026 02:33:13 +0000 Subject: [PATCH] add tested GitHub branch import workflow --- git-bridge/README.md | 16 ++ git-bridge/bin/import-github-branch.sh | 311 +++++++++++++++++++++++++ 2 files changed, 327 insertions(+) create mode 100755 git-bridge/bin/import-github-branch.sh diff --git a/git-bridge/README.md b/git-bridge/README.md index 298b149..490580b 100644 --- a/git-bridge/README.md +++ b/git-bridge/README.md @@ -11,3 +11,19 @@ docker compose --profile git-bridge run --rm --no-deps git-bridge GitHub remains a development workspace. Production deployment continues to use only Gitea Main. + +## GitHub branch import + +Import one reviewed cloud-development branch at a time: + +```bash +sudo env GITHUB_BRANCH=codex/example \ + /volume2/docker/infra/git-bridge/bin/import-github-branch.sh +``` + +Accepted prefixes are `codex/`, `grok/`, and `cloud/`. Before a normal push to +the same Gitea branch and creation of a Gitea pull request, the importer checks +that both Main branches match, the candidate descends from Main, an existing +Gitea branch can fast-forward, the canonical Compose renders, shell scripts +parse, and no known secret or runtime-data path is tracked. It never merges, +deploys, force-pushes, deletes a branch, or writes Main. diff --git a/git-bridge/bin/import-github-branch.sh b/git-bridge/bin/import-github-branch.sh new file mode 100755 index 0000000..eeb4ab5 --- /dev/null +++ b/git-bridge/bin/import-github-branch.sh @@ -0,0 +1,311 @@ +#!/usr/bin/env sh +set -eu +umask 077 + +branch=${GITHUB_BRANCH:-} +infra_dir=${INFRA_DIR:-/volume2/docker/infra} +proxy_url=${GITHUB_PROXY_URL:-http://192.168.0.4:7890} +compose_file=$infra_dir/docker-compose.yaml +state_dir=$infra_dir/git-bridge/state +state_repo=$state_dir/import.git +candidate=$state_dir/import-candidate +gitea_token_file=$infra_dir/git-bridge/secrets/gitea_token +github_token_file=$infra_dir/git-bridge/secrets/github_token +api_base=http://127.0.0.1:13000/api/v1 +temp_dir= + +if [ "$(id -u)" -ne 0 ]; then + echo "refusing import: run with sudo" >&2 + exit 2 +fi +case "$branch" in + codex/*|grok/*|cloud/*) ;; + *) + echo "refusing import: GITHUB_BRANCH must use codex/, grok/, or cloud/ prefix" >&2 + exit 2 + ;; +esac +case "$branch" in + *[!A-Za-z0-9._/-]*|*..*|*/.|/*|*/|*//*|main) + echo "refusing import: unsafe branch name" >&2 + exit 2 + ;; +esac + +for command_name in docker curl python3; do + command -v "$command_name" >/dev/null 2>&1 || { + echo "$command_name is required" >&2 + exit 2 + } +done +docker info >/dev/null +[ -f "$compose_file" ] || { + echo "canonical Infra Compose is missing" >&2 + exit 2 +} +[ ! -e "$infra_dir/compose.yaml" ] || { + echo "legacy compose.yaml must not exist" >&2 + exit 2 +} +for secret_file in "$gitea_token_file" "$github_token_file"; do + [ -s "$secret_file" ] || { + echo "bridge secret is missing: $secret_file" >&2 + exit 2 + } + [ "$(stat -c '%a' "$secret_file")" = "600" ] || { + echo "bridge secret permissions must be 0600: $secret_file" >&2 + exit 2 + } +done +[ ! -e "$candidate" ] || { + echo "stale candidate worktree exists: $candidate" >&2 + exit 2 +} + +temp_dir=$(mktemp -d /tmp/infra-branch-import.XXXXXX) +askpass_file=$temp_dir/askpass.sh +curl_config=$temp_dir/gitea-curl.conf +response_file=$temp_dir/response.json +request_file=$temp_dir/request.json + +cleanup() { + status=$? + if [ -d "$candidate" ]; then + git_run --git-dir=/infra/git-bridge/state/import.git worktree remove --force \ + /infra/git-bridge/state/import-candidate >/dev/null 2>&1 || true + fi + if [ -n "$temp_dir" ]; then + rm -f "$askpass_file" "$curl_config" "$response_file" "$request_file" \ + "$temp_dir/tracked.zlist" + rmdir "$temp_dir" 2>/dev/null || true + fi + exit "$status" +} +trap cleanup EXIT +trap 'exit 130' INT TERM + +cat >"$askpass_file" <<'ASKPASS' +#!/bin/sh +case "$1" in + *Username*github.com*|*github.com*Username*) printf '%s\n' 'RX-Penguin' ;; + *github.com*) cat /run/secrets/github_token ;; + *Username*gitea*|*gitea*Username*) printf '%s\n' 'git-bridge' ;; + *gitea*) cat /run/secrets/gitea_token ;; + *) exit 1 ;; +esac +ASKPASS +chmod 0500 "$askpass_file" + +gitea_token=$(cat "$gitea_token_file") +printf 'header = "Authorization: token %s"\n' "$gitea_token" >"$curl_config" +chmod 0600 "$curl_config" +gitea_token= + +gitea_image=$(docker container inspect --format '{{.Config.Image}}' gitea) +git_run() { + docker run --rm \ + --user 0:0 \ + --network git_forge \ + --entrypoint git \ + -e GIT_TERMINAL_PROMPT=0 \ + -e GIT_ASKPASS=/run/secrets/askpass.sh \ + -e HTTP_PROXY="$proxy_url" \ + -e HTTPS_PROXY="$proxy_url" \ + -e ALL_PROXY="$proxy_url" \ + -e NO_PROXY=localhost,127.0.0.1,::1,gitea,postgres,litellm \ + -v "$infra_dir:/infra" \ + -v "$gitea_token_file:/run/secrets/gitea_token:ro" \ + -v "$github_token_file:/run/secrets/github_token:ro" \ + -v "$askpass_file:/run/secrets/askpass.sh:ro" \ + "$gitea_image" \ + -c safe.directory='*' "$@" +} + +git_run check-ref-format --branch "$branch" >/dev/null +install -d -m 0700 "$state_dir" +if [ ! -d "$state_repo" ]; then + git_run init --bare /infra/git-bridge/state/import.git >/dev/null +fi + +if git_run --git-dir=/infra/git-bridge/state/import.git remote get-url gitea >/dev/null 2>&1; then + [ "$(git_run --git-dir=/infra/git-bridge/state/import.git remote get-url gitea)" = \ + "http://gitea:3000/slh/infra.git" ] || { + echo "gitea remote mismatch" >&2 + exit 2 + } +else + git_run --git-dir=/infra/git-bridge/state/import.git remote add gitea \ + http://gitea:3000/slh/infra.git +fi +if git_run --git-dir=/infra/git-bridge/state/import.git remote get-url github >/dev/null 2>&1; then + [ "$(git_run --git-dir=/infra/git-bridge/state/import.git remote get-url github)" = \ + "https://github.com/RX-Penguin/infra.git" ] || { + echo "github remote mismatch" >&2 + exit 2 + } +else + git_run --git-dir=/infra/git-bridge/state/import.git remote add github \ + https://github.com/RX-Penguin/infra.git +fi + +git_run --git-dir=/infra/git-bridge/state/import.git fetch --no-tags gitea \ + '+refs/heads/main:refs/remotes/gitea/main' +git_run --git-dir=/infra/git-bridge/state/import.git fetch --no-tags github \ + "+refs/heads/main:refs/remotes/github/main" \ + "+refs/heads/$branch:refs/remotes/github/$branch" +gitea_main=$(git_run --git-dir=/infra/git-bridge/state/import.git rev-parse refs/remotes/gitea/main) +github_main=$(git_run --git-dir=/infra/git-bridge/state/import.git rev-parse refs/remotes/github/main) +candidate_sha=$(git_run --git-dir=/infra/git-bridge/state/import.git rev-parse "refs/remotes/github/$branch") + +[ "$gitea_main" = "$github_main" ] || { + echo "import refused: GitHub Main differs from Gitea Main; run forward sync first" >&2 + exit 42 +} +git_run --git-dir=/infra/git-bridge/state/import.git merge-base --is-ancestor \ + "$gitea_main" "$candidate_sha" || { + echo "import refused: candidate branch does not descend from current Gitea Main" >&2 + exit 42 +} +[ "$candidate_sha" != "$gitea_main" ] || { + echo "import refused: candidate branch contains no change from Gitea Main" >&2 + exit 42 +} + +if git_run --git-dir=/infra/git-bridge/state/import.git ls-remote --exit-code \ + --heads gitea "refs/heads/$branch" >/dev/null 2>&1; then + git_run --git-dir=/infra/git-bridge/state/import.git fetch --no-tags gitea \ + "+refs/heads/$branch:refs/remotes/gitea/$branch" + current_sha=$(git_run --git-dir=/infra/git-bridge/state/import.git \ + rev-parse "refs/remotes/gitea/$branch") + git_run --git-dir=/infra/git-bridge/state/import.git merge-base --is-ancestor \ + "$current_sha" "$candidate_sha" || { + echo "import refused: existing Gitea branch cannot fast-forward" >&2 + exit 42 + } +fi + +git_run --git-dir=/infra/git-bridge/state/import.git worktree add --detach \ + /infra/git-bridge/state/import-candidate "$candidate_sha" >/dev/null + +[ -f "$candidate/docker-compose.yaml" ] || { + echo "candidate test failed: docker-compose.yaml is missing" >&2 + exit 1 +} +[ ! -e "$candidate/compose.yaml" ] || { + echo "candidate test failed: a second Compose entrypoint exists" >&2 + exit 1 +} +docker compose -f "$candidate/docker-compose.yaml" config -q +find "$candidate" -type f -name '*.sh' -exec sh -n '{}' ';' + +git_run -C /infra/git-bridge/state/import-candidate ls-files -z >"$temp_dir/tracked.zlist" +python3 - "$temp_dir/tracked.zlist" <<'PY' +import pathlib +import sys + +items = pathlib.Path(sys.argv[1]).read_bytes().split(b"\0") +for raw in items: + if not raw: + continue + path = raw.decode("utf-8", "strict") + parts = path.split("/") + unsafe = ( + path == ".env" + or path.endswith("/.env") + or "secrets" in parts + or path.endswith("auth.json") + or path.startswith("git-bridge/state/") + or path.startswith("gitea/") + or path.startswith("postgres/data/") + or path.startswith("litellm/chatgpt/") + or path.startswith("litellm/xai_oauth/") + ) + if unsafe: + raise SystemExit(f"candidate test failed: runtime or secret path is tracked: {path}") +PY + +# This is deliberately a normal push. Existing branches must fast-forward; +# Git rejects divergence and the protected Main branch is never a destination. +git_run --git-dir=/infra/git-bridge/state/import.git push gitea \ + "refs/remotes/github/$branch:refs/heads/$branch" +verified_sha=$(git_run --git-dir=/infra/git-bridge/state/import.git ls-remote gitea \ + "refs/heads/$branch" | awk 'NR == 1 { print $1 }') +[ "$verified_sha" = "$candidate_sha" ] || { + echo "import verification failed" >&2 + exit 1 +} + +status=$(curl -sS --config "$curl_config" -o "$response_file" -w '%{http_code}' \ + --get \ + --data-urlencode 'state=open' \ + --data-urlencode 'base=main' \ + --data-urlencode "head=$branch" \ + "$api_base/repos/slh/infra/pulls") +[ "$status" = "200" ] || { + echo "failed to inspect existing Gitea pull requests (HTTP $status)" >&2 + exit 1 +} +pr_url=$(python3 - "$response_file" "$branch" <<'PY' +import json +import sys + +for item in json.load(open(sys.argv[1], encoding="utf-8")): + head = item.get("head") or {} + if head.get("ref") == sys.argv[2]: + print(item.get("html_url") or "") + break +PY +) + +if [ -z "$pr_url" ]; then + python3 - "$request_file" "$branch" <<'PY' +import json +import sys + +branch = sys.argv[2] +payload = { + "base": "main", + "head": branch, + "title": f"Import GitHub branch: {branch}", + "body": ( + "Imported by git-bridge after branch ancestry, canonical Compose, " + "Compose render, shell syntax, and tracked-secret boundary checks. " + "No automatic merge or deployment was performed." + ), +} +with open(sys.argv[1], "w", encoding="utf-8") as handle: + json.dump(payload, handle) +PY + status=$(curl -sS --config "$curl_config" -o "$response_file" -w '%{http_code}' \ + -H 'Content-Type: application/json' \ + -X POST "$api_base/repos/slh/infra/pulls" \ + --data-binary "@$request_file") + [ "$status" = "201" ] || { + echo "branch imported but Gitea PR creation failed (HTTP $status)" >&2 + exit 1 + } + pr_url=$(python3 -c \ + 'import json,sys; print(json.load(open(sys.argv[1])).get("html_url", ""))' \ + "$response_file") +fi +[ -n "$pr_url" ] || { + echo "Gitea PR URL is unavailable" >&2 + exit 1 +} + +git_run --git-dir=/infra/git-bridge/state/import.git worktree remove --force \ + /infra/git-bridge/state/import-candidate >/dev/null +rm -f "$askpass_file" "$curl_config" "$response_file" "$request_file" \ + "$temp_dir/tracked.zlist" +rmdir "$temp_dir" +temp_dir= +trap - EXIT INT TERM + +echo "GitHub branch import completed" +echo "branch=$branch" +echo "commit=$candidate_sha" +echo "gitea_pull_request=$pr_url" +echo "tests=passed" +echo "force_push=false" +echo "main_modified=false" +echo "production_started=false"