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
|
GitHub remains a development workspace. Production deployment continues to
|
||||||
use only Gitea Main.
|
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