infra/git-bridge/bin/import-github-branch.sh

312 lines
10 KiB
Bash
Executable File

#!/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"