Add tested GitHub branch import workflow #2
@ -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.
|
||||
|
||||
311
git-bridge/bin/import-github-branch.sh
Executable file
311
git-bridge/bin/import-github-branch.sh
Executable file
@ -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"
|
||||
Loading…
x
Reference in New Issue
Block a user