feat: add phases 10-11, enhance phase 8 direct-check mode, and update Caddy migration
- Phase 10: local repo cutover (rename origin→github, add Gitea remote, push branches/tags) - Phase 11: custom runner infrastructure with toolchain-based naming (go-node-runner, jvm-android-runner) and repo variables via Gitea API - Add container_options support to manage_runner.sh for KVM passthrough - Phase 8: add --allow-direct-checks flag for LAN/split-DNS staging - Phase 7.5: add Cloudflare TLS block, retry logic for probes, multi-upstream support - Add toggle_dns.sh helper and update orchestration scripts for phases 10-11 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -96,6 +96,11 @@ LOCAL_REGISTRY= # Local registry prefix (e.g. registry.local:5
|
||||
# AUTO-POPULATED by phase3 scripts — do not fill manually:
|
||||
GITEA_RUNNER_REGISTRATION_TOKEN= # Retrieved from Gitea admin panel via API
|
||||
|
||||
# Custom runner image build contexts (phase 11)
|
||||
# Absolute paths to directories containing Dockerfiles for custom runner images.
|
||||
GO_NODE_RUNNER_CONTEXT= # Path to Go + Node toolchain Dockerfile (e.g. /path/to/augur/infra/runners)
|
||||
JVM_ANDROID_RUNNER_CONTEXT= # Path to JDK + Android SDK toolchain Dockerfile (e.g. /path/to/periodvault/infra/runners)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# REPOSITORIES
|
||||
|
||||
@@ -278,8 +278,8 @@ _ENV_CONDITIONAL_DB_NAMES=(GITEA_DB_PORT GITEA_DB_NAME GITEA_DB_USER GITEA_DB_PA
|
||||
_ENV_CONDITIONAL_DB_TYPES=(port nonempty nonempty password)
|
||||
|
||||
# Optional variables — validated only when non-empty (never required).
|
||||
_ENV_OPTIONAL_NAMES=(UNRAID_SSH_KEY FEDORA_SSH_KEY LOCAL_REGISTRY)
|
||||
_ENV_OPTIONAL_TYPES=(optional_path optional_path nonempty)
|
||||
_ENV_OPTIONAL_NAMES=(UNRAID_SSH_KEY FEDORA_SSH_KEY LOCAL_REGISTRY GO_NODE_RUNNER_CONTEXT JVM_ANDROID_RUNNER_CONTEXT)
|
||||
_ENV_OPTIONAL_TYPES=(optional_path optional_path nonempty optional_path optional_path)
|
||||
|
||||
# Human-readable format hints for error messages.
|
||||
_validator_hint() {
|
||||
|
||||
223
lib/phase10_common.sh
Normal file
223
lib/phase10_common.sh
Normal file
@@ -0,0 +1,223 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# lib/phase10_common.sh — Shared helpers for phase 10 local repo cutover
|
||||
# =============================================================================
|
||||
|
||||
# Shared discovery results (parallel arrays; bash 3.2 compatible).
|
||||
PHASE10_REPO_NAMES=()
|
||||
PHASE10_REPO_PATHS=()
|
||||
PHASE10_GITHUB_URLS=()
|
||||
PHASE10_DUPLICATES=()
|
||||
|
||||
# Parse common git remote URL formats into: host|owner|repo
|
||||
# Supports:
|
||||
# - https://host/owner/repo(.git)
|
||||
# - ssh://git@host/owner/repo(.git)
|
||||
# - git@host:owner/repo(.git)
|
||||
phase10_parse_git_url() {
|
||||
local url="$1"
|
||||
local rest host path owner repo
|
||||
|
||||
if [[ "$url" =~ ^[a-zA-Z][a-zA-Z0-9+.-]*:// ]]; then
|
||||
rest="${url#*://}"
|
||||
# Drop optional userinfo component.
|
||||
rest="${rest#*@}"
|
||||
host="${rest%%/*}"
|
||||
path="${rest#*/}"
|
||||
elif [[ "$url" == *@*:* ]]; then
|
||||
rest="${url#*@}"
|
||||
host="${rest%%:*}"
|
||||
path="${rest#*:}"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
|
||||
path="${path#/}"
|
||||
path="${path%.git}"
|
||||
owner="${path%%/*}"
|
||||
repo="${path#*/}"
|
||||
repo="${repo%%/*}"
|
||||
|
||||
if [[ -z "$host" ]] || [[ -z "$owner" ]] || [[ -z "$repo" ]] || [[ "$owner" == "$path" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s|%s|%s\n' "$host" "$owner" "$repo"
|
||||
}
|
||||
|
||||
phase10_host_matches() {
|
||||
local host="$1" expected="$2"
|
||||
[[ "$host" == "$expected" ]] || [[ "$host" == "${expected}:"* ]]
|
||||
}
|
||||
|
||||
# Return 0 when URL matches github.com/<owner>/<repo>.
|
||||
# If <repo> is omitted, only owner is checked.
|
||||
phase10_url_is_github_repo() {
|
||||
local url="$1" owner_expected="$2" repo_expected="${3:-}"
|
||||
local parsed host owner repo
|
||||
|
||||
parsed=$(phase10_parse_git_url "$url" 2>/dev/null) || return 1
|
||||
IFS='|' read -r host owner repo <<< "$parsed"
|
||||
|
||||
phase10_host_matches "$host" "github.com" || return 1
|
||||
[[ "$owner" == "$owner_expected" ]] || return 1
|
||||
if [[ -n "$repo_expected" ]] && [[ "$repo" != "$repo_expected" ]]; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
phase10_url_is_gitea_repo() {
|
||||
local url="$1" domain="$2" org="$3" repo_expected="$4"
|
||||
local parsed host owner repo
|
||||
|
||||
parsed=$(phase10_parse_git_url "$url" 2>/dev/null) || return 1
|
||||
IFS='|' read -r host owner repo <<< "$parsed"
|
||||
|
||||
phase10_host_matches "$host" "$domain" || return 1
|
||||
[[ "$owner" == "$org" ]] || return 1
|
||||
[[ "$repo" == "$repo_expected" ]] || return 1
|
||||
}
|
||||
|
||||
phase10_canonical_github_url() {
|
||||
local owner="$1" repo="$2"
|
||||
printf 'https://github.com/%s/%s.git' "$owner" "$repo"
|
||||
}
|
||||
|
||||
phase10_canonical_gitea_url() {
|
||||
local domain="$1" org="$2" repo="$3"
|
||||
printf 'https://%s/%s/%s.git' "$domain" "$org" "$repo"
|
||||
}
|
||||
|
||||
# Stable in-place sort by repo name (keeps arrays aligned).
|
||||
phase10_sort_repo_arrays() {
|
||||
local i j tmp
|
||||
for ((i = 0; i < ${#PHASE10_REPO_NAMES[@]}; i++)); do
|
||||
for ((j = i + 1; j < ${#PHASE10_REPO_NAMES[@]}; j++)); do
|
||||
if [[ "${PHASE10_REPO_NAMES[$i]}" > "${PHASE10_REPO_NAMES[$j]}" ]]; then
|
||||
tmp="${PHASE10_REPO_NAMES[$i]}"
|
||||
PHASE10_REPO_NAMES[i]="${PHASE10_REPO_NAMES[j]}"
|
||||
PHASE10_REPO_NAMES[j]="$tmp"
|
||||
|
||||
tmp="${PHASE10_REPO_PATHS[i]}"
|
||||
PHASE10_REPO_PATHS[i]="${PHASE10_REPO_PATHS[j]}"
|
||||
PHASE10_REPO_PATHS[j]="$tmp"
|
||||
|
||||
tmp="${PHASE10_GITHUB_URLS[i]}"
|
||||
PHASE10_GITHUB_URLS[i]="${PHASE10_GITHUB_URLS[j]}"
|
||||
PHASE10_GITHUB_URLS[j]="$tmp"
|
||||
fi
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
# Discover local repos under root that map to github.com/<github_owner>.
|
||||
# Discovery rules:
|
||||
# - Only direct children of root are considered.
|
||||
# - Excludes exclude_path (typically this toolkit repo).
|
||||
# - Accepts a repo if either "github" or "origin" points at GitHub owner.
|
||||
# - Deduplicates by repo slug, preferring directory basename == slug.
|
||||
#
|
||||
# Args:
|
||||
# $1 root dir (e.g., /Users/s/development)
|
||||
# $2 github owner (from GITHUB_USERNAME)
|
||||
# $3 exclude absolute path (optional; pass "" for none)
|
||||
# $4 expected count (0 = don't enforce)
|
||||
phase10_discover_local_repos() {
|
||||
local root="$1"
|
||||
local github_owner="$2"
|
||||
local exclude_path="${3:-}"
|
||||
local expected_count="${4:-0}"
|
||||
|
||||
PHASE10_REPO_NAMES=()
|
||||
PHASE10_REPO_PATHS=()
|
||||
PHASE10_GITHUB_URLS=()
|
||||
PHASE10_DUPLICATES=()
|
||||
|
||||
if [[ ! -d "$root" ]]; then
|
||||
log_error "Local repo root not found: ${root}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local dir top github_url parsed host owner repo canonical
|
||||
local i idx existing existing_base new_base duplicate
|
||||
for dir in "$root"/*; do
|
||||
[[ -d "$dir" ]] || continue
|
||||
if [[ -n "$exclude_path" ]] && [[ "$dir" == "$exclude_path" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! git -C "$dir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
continue
|
||||
fi
|
||||
|
||||
top=$(git -C "$dir" rev-parse --show-toplevel 2>/dev/null || true)
|
||||
[[ "$top" == "$dir" ]] || continue
|
||||
|
||||
github_url=""
|
||||
if github_url=$(git -C "$dir" remote get-url github 2>/dev/null); then
|
||||
if ! phase10_url_is_github_repo "$github_url" "$github_owner"; then
|
||||
github_url=""
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$github_url" ]] && github_url=$(git -C "$dir" remote get-url origin 2>/dev/null); then
|
||||
if ! phase10_url_is_github_repo "$github_url" "$github_owner"; then
|
||||
github_url=""
|
||||
fi
|
||||
fi
|
||||
|
||||
[[ -n "$github_url" ]] || continue
|
||||
|
||||
parsed=$(phase10_parse_git_url "$github_url" 2>/dev/null) || continue
|
||||
IFS='|' read -r host owner repo <<< "$parsed"
|
||||
canonical=$(phase10_canonical_github_url "$owner" "$repo")
|
||||
|
||||
idx=-1
|
||||
for i in "${!PHASE10_REPO_NAMES[@]}"; do
|
||||
if [[ "${PHASE10_REPO_NAMES[$i]}" == "$repo" ]]; then
|
||||
idx="$i"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$idx" -ge 0 ]]; then
|
||||
existing="${PHASE10_REPO_PATHS[$idx]}"
|
||||
existing_base="$(basename "$existing")"
|
||||
new_base="$(basename "$dir")"
|
||||
if [[ "$new_base" == "$repo" ]] && [[ "$existing_base" != "$repo" ]]; then
|
||||
PHASE10_REPO_PATHS[idx]="$dir"
|
||||
PHASE10_GITHUB_URLS[idx]="$canonical"
|
||||
PHASE10_DUPLICATES+=("${repo}: preferred ${dir} over ${existing}")
|
||||
else
|
||||
PHASE10_DUPLICATES+=("${repo}: ignored duplicate ${dir} (using ${existing})")
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
PHASE10_REPO_NAMES+=("$repo")
|
||||
PHASE10_REPO_PATHS+=("$dir")
|
||||
PHASE10_GITHUB_URLS+=("$canonical")
|
||||
done
|
||||
|
||||
phase10_sort_repo_arrays
|
||||
|
||||
for duplicate in "${PHASE10_DUPLICATES[@]}"; do
|
||||
log_info "$duplicate"
|
||||
done
|
||||
|
||||
if [[ "${#PHASE10_REPO_NAMES[@]}" -eq 0 ]]; then
|
||||
log_error "No local GitHub repos found under ${root} for owner '${github_owner}'"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ "$expected_count" -gt 0 ]] && [[ "${#PHASE10_REPO_NAMES[@]}" -ne "$expected_count" ]]; then
|
||||
log_error "Expected ${expected_count} local repos under ${root}; found ${#PHASE10_REPO_NAMES[@]}"
|
||||
for i in "${!PHASE10_REPO_NAMES[@]}"; do
|
||||
log_error " - ${PHASE10_REPO_NAMES[$i]} -> ${PHASE10_REPO_PATHS[$i]}"
|
||||
done
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
@@ -73,6 +73,9 @@ parse_runner_entry() {
|
||||
# "true" → /Library/LaunchDaemons/ (starts at boot, requires sudo)
|
||||
# "false" (default) → ~/Library/LaunchAgents/ (starts at login)
|
||||
RUNNER_BOOT=$(ini_get "$RUNNERS_CONF" "$target_name" "boot" "false")
|
||||
# container_options: extra Docker flags for act_runner job containers.
|
||||
# e.g. "--device=/dev/kvm" for KVM passthrough. Ignored for native runners.
|
||||
RUNNER_CONTAINER_OPTIONS=$(ini_get "$RUNNERS_CONF" "$target_name" "container_options" "")
|
||||
|
||||
# --- Host resolution ---
|
||||
# Also resolves RUNNER_COMPOSE_DIR: centralized compose dir on unraid/fedora,
|
||||
@@ -354,8 +357,9 @@ add_docker_runner() {
|
||||
# shellcheck disable=SC2090 # intentional — RUNNER_LABELS_YAML rendered via envsubst
|
||||
export RUNNER_LABELS_YAML
|
||||
export RUNNER_CAPACITY
|
||||
export RUNNER_CONTAINER_OPTIONS
|
||||
render_template "${SCRIPT_DIR}/templates/runner-config.yaml.tpl" "$tmpfile" \
|
||||
"\${RUNNER_NAME} \${RUNNER_LABELS_YAML} \${RUNNER_CAPACITY}"
|
||||
"\${RUNNER_NAME} \${RUNNER_LABELS_YAML} \${RUNNER_CAPACITY} \${RUNNER_CONTAINER_OPTIONS}"
|
||||
runner_scp "$tmpfile" "${RUNNER_DATA_PATH}/config.yaml"
|
||||
rm -f "$tmpfile"
|
||||
|
||||
@@ -422,9 +426,9 @@ add_native_runner() {
|
||||
local tmpfile
|
||||
tmpfile=$(mktemp)
|
||||
# shellcheck disable=SC2090 # intentional — RUNNER_LABELS_YAML rendered via envsubst
|
||||
export RUNNER_NAME RUNNER_DATA_PATH RUNNER_LABELS_YAML RUNNER_CAPACITY
|
||||
export RUNNER_NAME RUNNER_DATA_PATH RUNNER_LABELS_YAML RUNNER_CAPACITY RUNNER_CONTAINER_OPTIONS
|
||||
render_template "${SCRIPT_DIR}/templates/runner-config.yaml.tpl" "$tmpfile" \
|
||||
"\${RUNNER_NAME} \${RUNNER_LABELS_YAML} \${RUNNER_CAPACITY}"
|
||||
"\${RUNNER_NAME} \${RUNNER_LABELS_YAML} \${RUNNER_CAPACITY} \${RUNNER_CONTAINER_OPTIONS}"
|
||||
cp "$tmpfile" "${RUNNER_DATA_PATH}/config.yaml"
|
||||
rm -f "$tmpfile"
|
||||
|
||||
|
||||
511
phase10_local_repo_cutover.sh
Executable file
511
phase10_local_repo_cutover.sh
Executable file
@@ -0,0 +1,511 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# =============================================================================
|
||||
# phase10_local_repo_cutover.sh — Re-point local repos from GitHub to Gitea
|
||||
# Depends on: Phase 8 complete (Gitea publicly reachable) + Phase 4 migrated
|
||||
#
|
||||
# For each discovered local repo under /Users/s/development:
|
||||
# 1. Rename origin -> github (if needed)
|
||||
# 2. Ensure repo exists on Gitea (create if missing)
|
||||
# 3. Add/update origin to point at Gitea
|
||||
# 4. Push all branches and tags to Gitea origin
|
||||
# 5. Ensure every local branch tracks origin/<branch> (Gitea)
|
||||
#
|
||||
# Discovery is based on local git remotes:
|
||||
# - repo root is a direct child of PHASE10_LOCAL_ROOT (default /Users/s/development)
|
||||
# - repo has origin/github pointing to github.com/${GITHUB_USERNAME}/<repo>
|
||||
# - duplicate clones are deduped by repo slug
|
||||
# =============================================================================
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
source "${SCRIPT_DIR}/lib/common.sh"
|
||||
source "${SCRIPT_DIR}/lib/phase10_common.sh"
|
||||
|
||||
load_env
|
||||
require_vars GITEA_ADMIN_TOKEN GITEA_ADMIN_USER GITEA_ORG_NAME GITEA_DOMAIN GITEA_INTERNAL_URL GITHUB_USERNAME
|
||||
|
||||
phase_header 10 "Local Repo Remote Cutover"
|
||||
|
||||
LOCAL_REPO_ROOT="${PHASE10_LOCAL_ROOT:-/Users/s/development}"
|
||||
EXPECTED_REPO_COUNT="${PHASE10_EXPECTED_REPO_COUNT:-3}"
|
||||
DRY_RUN=false
|
||||
ASKPASS_SCRIPT=""
|
||||
PHASE10_GITEA_REPO_EXISTS=false
|
||||
PHASE10_REMOTE_BRANCHES=""
|
||||
PHASE10_REMOTE_TAGS=""
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--local-root=*) LOCAL_REPO_ROOT="${arg#*=}" ;;
|
||||
--expected-count=*) EXPECTED_REPO_COUNT="${arg#*=}" ;;
|
||||
--dry-run) DRY_RUN=true ;;
|
||||
--help|-h)
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [options]
|
||||
|
||||
Options:
|
||||
--local-root=PATH Root folder containing local repos (default: /Users/s/development)
|
||||
--expected-count=N Require exactly N discovered repos (default: 3, 0 disables)
|
||||
--dry-run Print planned actions only (no mutations)
|
||||
--help Show this help
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown argument: $arg"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if ! [[ "$EXPECTED_REPO_COUNT" =~ ^[0-9]+$ ]]; then
|
||||
log_error "--expected-count must be a non-negative integer"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "$ASKPASS_SCRIPT" ]]; then
|
||||
rm -f "$ASKPASS_SCRIPT"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
setup_git_auth() {
|
||||
ASKPASS_SCRIPT=$(mktemp)
|
||||
cat > "$ASKPASS_SCRIPT" <<'EOF'
|
||||
#!/usr/bin/env sh
|
||||
case "$1" in
|
||||
*sername*) printf '%s\n' "$GITEA_GIT_USERNAME" ;;
|
||||
*assword*) printf '%s\n' "$GITEA_GIT_TOKEN" ;;
|
||||
*) printf '\n' ;;
|
||||
esac
|
||||
EOF
|
||||
chmod 700 "$ASKPASS_SCRIPT"
|
||||
}
|
||||
|
||||
git_with_auth() {
|
||||
GIT_TERMINAL_PROMPT=0 \
|
||||
GIT_ASKPASS="$ASKPASS_SCRIPT" \
|
||||
GITEA_GIT_USERNAME="$GITEA_ADMIN_USER" \
|
||||
GITEA_GIT_TOKEN="$GITEA_ADMIN_TOKEN" \
|
||||
"$@"
|
||||
}
|
||||
|
||||
ensure_github_remote() {
|
||||
local repo_path="$1" repo_name="$2" github_url="$3"
|
||||
local existing origin_existing has_bad_github
|
||||
has_bad_github=false
|
||||
|
||||
if existing=$(git -C "$repo_path" remote get-url github 2>/dev/null); then
|
||||
if phase10_url_is_github_repo "$existing" "$GITHUB_USERNAME" "$repo_name"; then
|
||||
if [[ "$existing" != "$github_url" ]]; then
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
log_info "${repo_name}: would set github URL -> ${github_url}"
|
||||
else
|
||||
git -C "$repo_path" remote set-url github "$github_url"
|
||||
fi
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
has_bad_github=true
|
||||
fi
|
||||
|
||||
if origin_existing=$(git -C "$repo_path" remote get-url origin 2>/dev/null); then
|
||||
if phase10_url_is_github_repo "$origin_existing" "$GITHUB_USERNAME" "$repo_name"; then
|
||||
if [[ "$has_bad_github" == "true" ]]; then
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
log_warn "${repo_name}: would remove misconfigured 'github' remote and rebuild it from origin"
|
||||
else
|
||||
git -C "$repo_path" remote remove github
|
||||
log_warn "${repo_name}: removed misconfigured 'github' remote and rebuilt it from origin"
|
||||
fi
|
||||
fi
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
log_info "${repo_name}: would rename origin -> github"
|
||||
log_info "${repo_name}: would set github URL -> ${github_url}"
|
||||
else
|
||||
git -C "$repo_path" remote rename origin github
|
||||
git -C "$repo_path" remote set-url github "$github_url"
|
||||
log_success "${repo_name}: renamed origin -> github"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$has_bad_github" == "true" ]]; then
|
||||
log_error "${repo_name}: existing 'github' remote does not point to GitHub repo ${GITHUB_USERNAME}/${repo_name}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_error "${repo_name}: could not find GitHub remote in 'origin' or 'github'"
|
||||
return 1
|
||||
}
|
||||
|
||||
ensure_gitea_origin() {
|
||||
local repo_path="$1" repo_name="$2" gitea_url="$3"
|
||||
local existing
|
||||
|
||||
if existing=$(git -C "$repo_path" remote get-url origin 2>/dev/null); then
|
||||
if phase10_url_is_gitea_repo "$existing" "$GITEA_DOMAIN" "$GITEA_ORG_NAME" "$repo_name"; then
|
||||
if [[ "$existing" != "$gitea_url" ]]; then
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
log_info "${repo_name}: would normalize origin URL -> ${gitea_url}"
|
||||
else
|
||||
git -C "$repo_path" remote set-url origin "$gitea_url"
|
||||
fi
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
# origin exists but points somewhere else; force it to Gitea.
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
log_info "${repo_name}: would set origin URL -> ${gitea_url}"
|
||||
else
|
||||
git -C "$repo_path" remote set-url origin "$gitea_url"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
log_info "${repo_name}: would add origin -> ${gitea_url}"
|
||||
else
|
||||
git -C "$repo_path" remote add origin "$gitea_url"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
ensure_gitea_repo_exists() {
|
||||
local repo_name="$1"
|
||||
local create_payload http_code
|
||||
|
||||
get_gitea_repo_http_code() {
|
||||
local target_repo="$1"
|
||||
local tmpfile curl_code
|
||||
tmpfile=$(mktemp)
|
||||
curl_code=$(curl \
|
||||
-s \
|
||||
-o "$tmpfile" \
|
||||
-w "%{http_code}" \
|
||||
-H "Authorization: token ${GITEA_ADMIN_TOKEN}" \
|
||||
-H "Accept: application/json" \
|
||||
"${GITEA_INTERNAL_URL}/api/v1/repos/${GITEA_ORG_NAME}/${target_repo}") || {
|
||||
rm -f "$tmpfile"
|
||||
return 1
|
||||
}
|
||||
rm -f "$tmpfile"
|
||||
printf '%s' "$curl_code"
|
||||
}
|
||||
|
||||
PHASE10_GITEA_REPO_EXISTS=false
|
||||
if ! http_code="$(get_gitea_repo_http_code "$repo_name")"; then
|
||||
log_error "${repo_name}: failed to query Gitea API for repo existence"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ "$http_code" == "200" ]]; then
|
||||
PHASE10_GITEA_REPO_EXISTS=true
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
log_info "${repo_name}: Gitea repo already exists (${GITEA_ORG_NAME}/${repo_name})"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$http_code" != "404" ]]; then
|
||||
log_error "${repo_name}: unexpected Gitea API status while checking repo (${http_code})"
|
||||
return 1
|
||||
fi
|
||||
|
||||
create_payload=$(jq -n \
|
||||
--arg name "$repo_name" \
|
||||
'{name: $name, auto_init: false}')
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
log_info "${repo_name}: would create missing Gitea repo ${GITEA_ORG_NAME}/${repo_name}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if gitea_api POST "/orgs/${GITEA_ORG_NAME}/repos" "$create_payload" >/dev/null 2>&1; then
|
||||
log_success "${repo_name}: created missing Gitea repo ${GITEA_ORG_NAME}/${repo_name}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_error "${repo_name}: failed to create Gitea repo ${GITEA_ORG_NAME}/${repo_name}"
|
||||
return 1
|
||||
}
|
||||
|
||||
count_items() {
|
||||
local list="$1"
|
||||
if [[ -z "$list" ]]; then
|
||||
printf '0'
|
||||
return
|
||||
fi
|
||||
printf '%s\n' "$list" | sed '/^$/d' | wc -l | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
list_contains() {
|
||||
local list="$1" needle="$2"
|
||||
[[ -n "$list" ]] && printf '%s\n' "$list" | grep -Fxq "$needle"
|
||||
}
|
||||
|
||||
fetch_remote_refs() {
|
||||
local url="$1"
|
||||
local refs ref short
|
||||
|
||||
PHASE10_REMOTE_BRANCHES=""
|
||||
PHASE10_REMOTE_TAGS=""
|
||||
|
||||
refs=$(git_with_auth git ls-remote --heads --tags "$url" 2>/dev/null) || return 1
|
||||
[[ -n "$refs" ]] || return 0
|
||||
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
ref="${line#*[[:space:]]}"
|
||||
ref="${ref#"${ref%%[![:space:]]*}"}"
|
||||
[[ -n "$ref" ]] || continue
|
||||
case "$ref" in
|
||||
refs/heads/*)
|
||||
short="${ref#refs/heads/}"
|
||||
PHASE10_REMOTE_BRANCHES="${PHASE10_REMOTE_BRANCHES}${short}"$'\n'
|
||||
;;
|
||||
refs/tags/*)
|
||||
short="${ref#refs/tags/}"
|
||||
[[ "$short" == *"^{}" ]] && continue
|
||||
PHASE10_REMOTE_TAGS="${PHASE10_REMOTE_TAGS}${short}"$'\n'
|
||||
;;
|
||||
esac
|
||||
done <<< "$refs"
|
||||
|
||||
PHASE10_REMOTE_BRANCHES="$(printf '%s' "$PHASE10_REMOTE_BRANCHES" | sed '/^$/d' | LC_ALL=C sort -u)"
|
||||
PHASE10_REMOTE_TAGS="$(printf '%s' "$PHASE10_REMOTE_TAGS" | sed '/^$/d' | LC_ALL=C sort -u)"
|
||||
}
|
||||
|
||||
print_diff_summary() {
|
||||
local repo_name="$1" kind="$2" local_list="$3" remote_list="$4"
|
||||
local missing_count extra_count item
|
||||
local missing_preview="" extra_preview=""
|
||||
local preview_limit=5
|
||||
|
||||
missing_count=0
|
||||
while IFS= read -r item; do
|
||||
[[ -z "$item" ]] && continue
|
||||
if ! list_contains "$remote_list" "$item"; then
|
||||
missing_count=$((missing_count + 1))
|
||||
if [[ "$missing_count" -le "$preview_limit" ]]; then
|
||||
missing_preview="${missing_preview}${item}, "
|
||||
fi
|
||||
fi
|
||||
done <<< "$local_list"
|
||||
|
||||
extra_count=0
|
||||
while IFS= read -r item; do
|
||||
[[ -z "$item" ]] && continue
|
||||
if ! list_contains "$local_list" "$item"; then
|
||||
extra_count=$((extra_count + 1))
|
||||
if [[ "$extra_count" -le "$preview_limit" ]]; then
|
||||
extra_preview="${extra_preview}${item}, "
|
||||
fi
|
||||
fi
|
||||
done <<< "$remote_list"
|
||||
|
||||
if [[ "$missing_count" -eq 0 ]] && [[ "$extra_count" -eq 0 ]]; then
|
||||
log_success "${repo_name}: local ${kind}s match Gitea"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$missing_count" -gt 0 ]]; then
|
||||
missing_preview="${missing_preview%, }"
|
||||
log_info "${repo_name}: ${missing_count} ${kind}(s) missing on Gitea"
|
||||
if [[ -n "$missing_preview" ]]; then
|
||||
log_info " missing ${kind} sample: ${missing_preview}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$extra_count" -gt 0 ]]; then
|
||||
extra_preview="${extra_preview%, }"
|
||||
log_info "${repo_name}: ${extra_count} ${kind}(s) exist on Gitea but not locally"
|
||||
if [[ -n "$extra_preview" ]]; then
|
||||
log_info " remote-only ${kind} sample: ${extra_preview}"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
dry_run_compare_local_and_remote() {
|
||||
local repo_path="$1" repo_name="$2" gitea_url="$3"
|
||||
local local_branches local_tags
|
||||
local local_branch_count local_tag_count remote_branch_count remote_tag_count
|
||||
|
||||
local_branches="$(git -C "$repo_path" for-each-ref --format='%(refname:short)' refs/heads | LC_ALL=C sort -u)"
|
||||
local_tags="$(git -C "$repo_path" tag -l | LC_ALL=C sort -u)"
|
||||
local_branch_count="$(count_items "$local_branches")"
|
||||
local_tag_count="$(count_items "$local_tags")"
|
||||
|
||||
log_info "${repo_name}: local state = ${local_branch_count} branch(es), ${local_tag_count} tag(s)"
|
||||
|
||||
if [[ "$PHASE10_GITEA_REPO_EXISTS" != "true" ]]; then
|
||||
log_info "${repo_name}: remote state = repo missing (would be created)"
|
||||
if [[ "$local_branch_count" -gt 0 ]]; then
|
||||
log_info "${repo_name}: all local branches would be pushed to new Gitea repo"
|
||||
fi
|
||||
if [[ "$local_tag_count" -gt 0 ]]; then
|
||||
log_info "${repo_name}: all local tags would be pushed to new Gitea repo"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! fetch_remote_refs "$gitea_url"; then
|
||||
log_warn "${repo_name}: could not read Gitea refs via ls-remote; skipping diff"
|
||||
return 0
|
||||
fi
|
||||
|
||||
remote_branch_count="$(count_items "$PHASE10_REMOTE_BRANCHES")"
|
||||
remote_tag_count="$(count_items "$PHASE10_REMOTE_TAGS")"
|
||||
log_info "${repo_name}: remote Gitea state = ${remote_branch_count} branch(es), ${remote_tag_count} tag(s)"
|
||||
|
||||
print_diff_summary "$repo_name" "branch" "$local_branches" "$PHASE10_REMOTE_BRANCHES"
|
||||
print_diff_summary "$repo_name" "tag" "$local_tags" "$PHASE10_REMOTE_TAGS"
|
||||
}
|
||||
|
||||
push_all_refs_to_origin() {
|
||||
local repo_path="$1" repo_name="$2"
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
log_info "${repo_name}: would push all branches to origin"
|
||||
log_info "${repo_name}: would push all tags to origin"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! git_with_auth git -C "$repo_path" push --all origin >/dev/null; then
|
||||
log_error "${repo_name}: failed pushing branches to Gitea origin"
|
||||
return 1
|
||||
fi
|
||||
if ! git_with_auth git -C "$repo_path" push --tags origin >/dev/null; then
|
||||
log_error "${repo_name}: failed pushing tags to Gitea origin"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
retarget_tracking_to_origin() {
|
||||
local repo_path="$1" repo_name="$2"
|
||||
local branch upstream_remote upstream_short branch_count
|
||||
branch_count=0
|
||||
|
||||
while IFS= read -r branch; do
|
||||
[[ -z "$branch" ]] && continue
|
||||
branch_count=$((branch_count + 1))
|
||||
|
||||
if ! git -C "$repo_path" show-ref --verify --quiet "refs/remotes/origin/${branch}"; then
|
||||
# A local branch can exist without an origin ref if it never got pushed.
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
log_info "${repo_name}: would create origin/${branch} by pushing local ${branch}"
|
||||
else
|
||||
if ! git_with_auth git -C "$repo_path" push origin "refs/heads/${branch}:refs/heads/${branch}" >/dev/null; then
|
||||
log_error "${repo_name}: could not create origin/${branch} while setting tracking"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
log_info "${repo_name}: would set upstream ${branch} -> origin/${branch}"
|
||||
continue
|
||||
else
|
||||
if ! git -C "$repo_path" branch --set-upstream-to="origin/${branch}" "$branch" >/dev/null 2>&1; then
|
||||
log_error "${repo_name}: failed to set upstream for branch '${branch}' to origin/${branch}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
upstream_remote=$(git -C "$repo_path" for-each-ref --format='%(upstream:remotename)' "refs/heads/${branch}")
|
||||
upstream_short=$(git -C "$repo_path" for-each-ref --format='%(upstream:short)' "refs/heads/${branch}")
|
||||
if [[ "$upstream_remote" != "origin" ]] || [[ "$upstream_short" != "origin/${branch}" ]]; then
|
||||
log_error "${repo_name}: branch '${branch}' upstream is '${upstream_short:-<none>}' (expected origin/${branch})"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
done < <(git -C "$repo_path" for-each-ref --format='%(refname:short)' refs/heads)
|
||||
|
||||
if [[ "$branch_count" -eq 0 ]]; then
|
||||
log_warn "${repo_name}: no local branches found"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
if ! phase10_discover_local_repos "$LOCAL_REPO_ROOT" "$GITHUB_USERNAME" "$SCRIPT_DIR" "$EXPECTED_REPO_COUNT"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "Discovered ${#PHASE10_REPO_NAMES[@]} local repos in ${LOCAL_REPO_ROOT}"
|
||||
for i in "${!PHASE10_REPO_NAMES[@]}"; do
|
||||
log_info " - ${PHASE10_REPO_NAMES[$i]} -> ${PHASE10_REPO_PATHS[$i]}"
|
||||
done
|
||||
|
||||
setup_git_auth
|
||||
|
||||
SUCCESS=0
|
||||
FAILED=0
|
||||
|
||||
for i in "${!PHASE10_REPO_NAMES[@]}"; do
|
||||
repo_name="${PHASE10_REPO_NAMES[$i]}"
|
||||
repo_path="${PHASE10_REPO_PATHS[$i]}"
|
||||
github_url="${PHASE10_GITHUB_URLS[$i]}"
|
||||
gitea_url="$(phase10_canonical_gitea_url "$GITEA_DOMAIN" "$GITEA_ORG_NAME" "$repo_name")"
|
||||
|
||||
log_info "--- Processing repo: ${repo_name} (${repo_path}) ---"
|
||||
|
||||
if ! ensure_github_remote "$repo_path" "$repo_name" "$github_url"; then
|
||||
FAILED=$((FAILED + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! ensure_gitea_repo_exists "$repo_name"; then
|
||||
FAILED=$((FAILED + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
dry_run_compare_local_and_remote "$repo_path" "$repo_name" "$gitea_url"
|
||||
fi
|
||||
|
||||
if ! ensure_gitea_origin "$repo_path" "$repo_name" "$gitea_url"; then
|
||||
log_error "${repo_name}: failed to set origin to ${gitea_url}"
|
||||
FAILED=$((FAILED + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! push_all_refs_to_origin "$repo_path" "$repo_name"; then
|
||||
FAILED=$((FAILED + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! retarget_tracking_to_origin "$repo_path" "$repo_name"; then
|
||||
FAILED=$((FAILED + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
log_success "${repo_name}: dry-run plan complete"
|
||||
else
|
||||
log_success "${repo_name}: origin now points to Gitea and tracking updated"
|
||||
fi
|
||||
SUCCESS=$((SUCCESS + 1))
|
||||
done
|
||||
|
||||
printf '\n'
|
||||
TOTAL=${#PHASE10_REPO_NAMES[@]}
|
||||
log_info "Results: ${SUCCESS} succeeded, ${FAILED} failed (out of ${TOTAL})"
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
if [[ "$FAILED" -gt 0 ]]; then
|
||||
log_error "Phase 10 dry-run found ${FAILED} error(s); no changes were made"
|
||||
exit 1
|
||||
fi
|
||||
log_success "Phase 10 dry-run complete — no changes were made"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$FAILED" -gt 0 ]]; then
|
||||
log_error "Phase 10 failed for one or more repos"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_success "Phase 10 complete — local repos now push/track via Gitea origin"
|
||||
112
phase10_post_check.sh
Executable file
112
phase10_post_check.sh
Executable file
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# =============================================================================
|
||||
# phase10_post_check.sh — Verify local repo remote cutover to Gitea
|
||||
# Checks for each discovered local repo:
|
||||
# 1. origin points to Gitea org/repo
|
||||
# 2. github points to GitHub owner/repo
|
||||
# 3. every local branch tracks origin/<branch>
|
||||
# =============================================================================
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
source "${SCRIPT_DIR}/lib/common.sh"
|
||||
source "${SCRIPT_DIR}/lib/phase10_common.sh"
|
||||
|
||||
load_env
|
||||
require_vars GITEA_ORG_NAME GITEA_DOMAIN GITHUB_USERNAME
|
||||
|
||||
phase_header 10 "Local Repo Remote Cutover — Post-Check"
|
||||
|
||||
LOCAL_REPO_ROOT="${PHASE10_LOCAL_ROOT:-/Users/s/development}"
|
||||
EXPECTED_REPO_COUNT="${PHASE10_EXPECTED_REPO_COUNT:-3}"
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--local-root=*) LOCAL_REPO_ROOT="${arg#*=}" ;;
|
||||
--expected-count=*) EXPECTED_REPO_COUNT="${arg#*=}" ;;
|
||||
--help|-h)
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [options]
|
||||
|
||||
Options:
|
||||
--local-root=PATH Root folder containing local repos (default: /Users/s/development)
|
||||
--expected-count=N Require exactly N discovered repos (default: 3, 0 disables)
|
||||
--help Show this help
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown argument: $arg"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if ! [[ "$EXPECTED_REPO_COUNT" =~ ^[0-9]+$ ]]; then
|
||||
log_error "--expected-count must be a non-negative integer"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! phase10_discover_local_repos "$LOCAL_REPO_ROOT" "$GITHUB_USERNAME" "$SCRIPT_DIR" "$EXPECTED_REPO_COUNT"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
for i in "${!PHASE10_REPO_NAMES[@]}"; do
|
||||
repo_name="${PHASE10_REPO_NAMES[$i]}"
|
||||
repo_path="${PHASE10_REPO_PATHS[$i]}"
|
||||
github_url="${PHASE10_GITHUB_URLS[$i]}"
|
||||
gitea_url="$(phase10_canonical_gitea_url "$GITEA_DOMAIN" "$GITEA_ORG_NAME" "$repo_name")"
|
||||
|
||||
log_info "--- Checking repo: ${repo_name} (${repo_path}) ---"
|
||||
|
||||
origin_url="$(git -C "$repo_path" remote get-url origin 2>/dev/null || true)"
|
||||
if [[ -n "$origin_url" ]] && phase10_url_is_gitea_repo "$origin_url" "$GITEA_DOMAIN" "$GITEA_ORG_NAME" "$repo_name"; then
|
||||
log_success "origin points to Gitea (${gitea_url})"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
log_error "FAIL: origin does not point to ${gitea_url} (found: ${origin_url:-<missing>})"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
|
||||
github_remote_url="$(git -C "$repo_path" remote get-url github 2>/dev/null || true)"
|
||||
if [[ -n "$github_remote_url" ]] && phase10_url_is_github_repo "$github_remote_url" "$GITHUB_USERNAME" "$repo_name"; then
|
||||
log_success "github points to GitHub (${github_url})"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
log_error "FAIL: github does not point to ${github_url} (found: ${github_remote_url:-<missing>})"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
|
||||
branch_count=0
|
||||
while IFS= read -r branch; do
|
||||
[[ -z "$branch" ]] && continue
|
||||
branch_count=$((branch_count + 1))
|
||||
upstream_remote=$(git -C "$repo_path" for-each-ref --format='%(upstream:remotename)' "refs/heads/${branch}")
|
||||
upstream_short=$(git -C "$repo_path" for-each-ref --format='%(upstream:short)' "refs/heads/${branch}")
|
||||
if [[ "$upstream_remote" == "origin" ]] && [[ "$upstream_short" == "origin/${branch}" ]]; then
|
||||
log_success "branch ${branch} tracks origin/${branch}"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
log_error "FAIL: branch ${branch} tracks ${upstream_short:-<none>} (expected origin/${branch})"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
done < <(git -C "$repo_path" for-each-ref --format='%(refname:short)' refs/heads)
|
||||
|
||||
if [[ "$branch_count" -eq 0 ]]; then
|
||||
log_warn "No local branches found in ${repo_name}"
|
||||
fi
|
||||
done
|
||||
|
||||
printf '\n'
|
||||
log_info "Results: ${PASS} passed, ${FAIL} failed"
|
||||
|
||||
if [[ "$FAIL" -gt 0 ]]; then
|
||||
log_error "Phase 10 post-check FAILED"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_success "Phase 10 post-check PASSED — local repos track Gitea origin"
|
||||
172
phase10_teardown.sh
Executable file
172
phase10_teardown.sh
Executable file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# =============================================================================
|
||||
# phase10_teardown.sh — Reverse local repo remote cutover from phase 10
|
||||
# Reverts local repos so GitHub is origin again:
|
||||
# 1. Move Gitea origin -> gitea (if present)
|
||||
# 2. Move github -> origin
|
||||
# 3. Set local branch upstreams to origin/<branch> where available
|
||||
# =============================================================================
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
source "${SCRIPT_DIR}/lib/common.sh"
|
||||
source "${SCRIPT_DIR}/lib/phase10_common.sh"
|
||||
|
||||
load_env
|
||||
require_vars GITEA_ORG_NAME GITEA_DOMAIN GITHUB_USERNAME
|
||||
|
||||
phase_header 10 "Local Repo Remote Cutover — Teardown"
|
||||
|
||||
LOCAL_REPO_ROOT="${PHASE10_LOCAL_ROOT:-/Users/s/development}"
|
||||
EXPECTED_REPO_COUNT="${PHASE10_EXPECTED_REPO_COUNT:-3}"
|
||||
AUTO_YES=false
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--local-root=*) LOCAL_REPO_ROOT="${arg#*=}" ;;
|
||||
--expected-count=*) EXPECTED_REPO_COUNT="${arg#*=}" ;;
|
||||
--yes|-y) AUTO_YES=true ;;
|
||||
--help|-h)
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [options]
|
||||
|
||||
Options:
|
||||
--local-root=PATH Root folder containing local repos (default: /Users/s/development)
|
||||
--expected-count=N Require exactly N discovered repos (default: 3, 0 disables)
|
||||
--yes, -y Skip confirmation prompt
|
||||
--help Show this help
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown argument: $arg"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if ! [[ "$EXPECTED_REPO_COUNT" =~ ^[0-9]+$ ]]; then
|
||||
log_error "--expected-count must be a non-negative integer"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$AUTO_YES" != "true" ]]; then
|
||||
log_warn "This will revert local repo remotes so GitHub is origin again."
|
||||
printf 'Continue? [y/N] ' >&2
|
||||
read -r confirm
|
||||
if [[ "$confirm" != "y" ]] && [[ "$confirm" != "Y" ]]; then
|
||||
log_info "Teardown cancelled"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! phase10_discover_local_repos "$LOCAL_REPO_ROOT" "$GITHUB_USERNAME" "$SCRIPT_DIR" "$EXPECTED_REPO_COUNT"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set_tracking_to_origin_where_available() {
|
||||
local repo_path="$1" repo_name="$2"
|
||||
local branch branch_count
|
||||
branch_count=0
|
||||
|
||||
while IFS= read -r branch; do
|
||||
[[ -z "$branch" ]] && continue
|
||||
branch_count=$((branch_count + 1))
|
||||
|
||||
if git -C "$repo_path" show-ref --verify --quiet "refs/remotes/origin/${branch}"; then
|
||||
if git -C "$repo_path" branch --set-upstream-to="origin/${branch}" "$branch" >/dev/null 2>&1; then
|
||||
log_success "${repo_name}: branch ${branch} now tracks origin/${branch}"
|
||||
else
|
||||
log_warn "${repo_name}: could not set upstream for ${branch}"
|
||||
fi
|
||||
else
|
||||
log_warn "${repo_name}: origin/${branch} not found (upstream unchanged)"
|
||||
fi
|
||||
done < <(git -C "$repo_path" for-each-ref --format='%(refname:short)' refs/heads)
|
||||
|
||||
if [[ "$branch_count" -eq 0 ]]; then
|
||||
log_warn "${repo_name}: no local branches found"
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_origin_is_github() {
|
||||
local repo_path="$1" repo_name="$2" github_url="$3" gitea_url="$4"
|
||||
local origin_url github_url_existing gitea_url_existing
|
||||
|
||||
origin_url="$(git -C "$repo_path" remote get-url origin 2>/dev/null || true)"
|
||||
github_url_existing="$(git -C "$repo_path" remote get-url github 2>/dev/null || true)"
|
||||
gitea_url_existing="$(git -C "$repo_path" remote get-url gitea 2>/dev/null || true)"
|
||||
|
||||
if [[ -n "$origin_url" ]]; then
|
||||
if phase10_url_is_github_repo "$origin_url" "$GITHUB_USERNAME" "$repo_name"; then
|
||||
git -C "$repo_path" remote set-url origin "$github_url"
|
||||
if [[ -n "$github_url_existing" ]]; then
|
||||
git -C "$repo_path" remote remove github
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
if phase10_url_is_gitea_repo "$origin_url" "$GITEA_DOMAIN" "$GITEA_ORG_NAME" "$repo_name"; then
|
||||
if [[ -z "$gitea_url_existing" ]]; then
|
||||
git -C "$repo_path" remote rename origin gitea
|
||||
else
|
||||
git -C "$repo_path" remote set-url gitea "$gitea_url"
|
||||
git -C "$repo_path" remote remove origin
|
||||
fi
|
||||
else
|
||||
log_error "${repo_name}: origin remote is unexpected (${origin_url})"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if git -C "$repo_path" remote get-url origin >/dev/null 2>&1; then
|
||||
:
|
||||
elif [[ -n "$github_url_existing" ]]; then
|
||||
git -C "$repo_path" remote rename github origin
|
||||
else
|
||||
git -C "$repo_path" remote add origin "$github_url"
|
||||
fi
|
||||
|
||||
git -C "$repo_path" remote set-url origin "$github_url"
|
||||
|
||||
if git -C "$repo_path" remote get-url github >/dev/null 2>&1; then
|
||||
git -C "$repo_path" remote remove github
|
||||
fi
|
||||
|
||||
if git -C "$repo_path" remote get-url gitea >/dev/null 2>&1; then
|
||||
git -C "$repo_path" remote set-url gitea "$gitea_url"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
SUCCESS=0
|
||||
FAILED=0
|
||||
|
||||
for i in "${!PHASE10_REPO_NAMES[@]}"; do
|
||||
repo_name="${PHASE10_REPO_NAMES[$i]}"
|
||||
repo_path="${PHASE10_REPO_PATHS[$i]}"
|
||||
github_url="${PHASE10_GITHUB_URLS[$i]}"
|
||||
gitea_url="$(phase10_canonical_gitea_url "$GITEA_DOMAIN" "$GITEA_ORG_NAME" "$repo_name")"
|
||||
|
||||
log_info "--- Reverting repo: ${repo_name} (${repo_path}) ---"
|
||||
if ! ensure_origin_is_github "$repo_path" "$repo_name" "$github_url" "$gitea_url"; then
|
||||
FAILED=$((FAILED + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
set_tracking_to_origin_where_available "$repo_path" "$repo_name"
|
||||
SUCCESS=$((SUCCESS + 1))
|
||||
done
|
||||
|
||||
printf '\n'
|
||||
TOTAL=${#PHASE10_REPO_NAMES[@]}
|
||||
log_info "Results: ${SUCCESS} reverted, ${FAILED} failed (out of ${TOTAL})"
|
||||
|
||||
if [[ "$FAILED" -gt 0 ]]; then
|
||||
log_error "Phase 10 teardown completed with failures"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_success "Phase 10 teardown complete"
|
||||
288
phase11_custom_runners.sh
Executable file
288
phase11_custom_runners.sh
Executable file
@@ -0,0 +1,288 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# =============================================================================
|
||||
# phase11_custom_runners.sh — Deploy per-repo runner infrastructure & variables
|
||||
# Depends on: Phase 3 complete (runner infra), Phase 4 complete (repos on Gitea)
|
||||
#
|
||||
# Steps:
|
||||
# 1. Build custom toolchain images on Unraid (go-node-runner, jvm-android-runner)
|
||||
# 2. Consolidate macOS runners into a shared instance-level runner
|
||||
# 3. Deploy per-repo Docker runners via manage_runner.sh
|
||||
# 4. Set Gitea repository variables from repo_variables.conf
|
||||
#
|
||||
# Runner strategy:
|
||||
# - Linux runners: repo-scoped, separate toolchain images per repo
|
||||
# - Android emulator: shared (repos=all) — any repo can use it
|
||||
# - macOS runner: shared (repos=all) — any repo can use it
|
||||
#
|
||||
# Idempotent: skips images that already exist, runners already running,
|
||||
# and variables that already match.
|
||||
# =============================================================================
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
source "${SCRIPT_DIR}/lib/common.sh"
|
||||
|
||||
load_env
|
||||
require_vars GITEA_ADMIN_TOKEN GITEA_INTERNAL_URL GITEA_ORG_NAME \
|
||||
UNRAID_IP UNRAID_SSH_USER UNRAID_SSH_PORT \
|
||||
GO_NODE_RUNNER_CONTEXT JVM_ANDROID_RUNNER_CONTEXT \
|
||||
ACT_RUNNER_VERSION
|
||||
|
||||
phase_header 11 "Custom Runner Infrastructure"
|
||||
|
||||
REPO_VARS_CONF="${SCRIPT_DIR}/repo_variables.conf"
|
||||
REBUILD_IMAGES=false
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--rebuild-images) REBUILD_IMAGES=true ;;
|
||||
*) ;;
|
||||
esac
|
||||
done
|
||||
|
||||
SUCCESS=0
|
||||
FAILED=0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helper: rsync a build context directory to Unraid
|
||||
# ---------------------------------------------------------------------------
|
||||
rsync_to_unraid() {
|
||||
local src="$1" dest="$2"
|
||||
local ssh_key="${UNRAID_SSH_KEY:-}"
|
||||
local ssh_opts="ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new -p ${UNRAID_SSH_PORT}"
|
||||
if [[ -n "$ssh_key" ]]; then
|
||||
ssh_opts="${ssh_opts} -i ${ssh_key}"
|
||||
fi
|
||||
rsync -az --delete \
|
||||
--exclude='.env' \
|
||||
--exclude='.env.*' \
|
||||
--exclude='envs/' \
|
||||
--exclude='.git' \
|
||||
--exclude='.gitignore' \
|
||||
-e "$ssh_opts" \
|
||||
"${src}/" "${UNRAID_SSH_USER}@${UNRAID_IP}:${dest}/"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helper: check if a Docker image exists on Unraid
|
||||
# ---------------------------------------------------------------------------
|
||||
image_exists_on_unraid() {
|
||||
local tag="$1"
|
||||
ssh_exec "UNRAID" "docker image inspect '${tag}' >/dev/null 2>&1"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helper: list all keys in an INI section (for repo_variables.conf)
|
||||
# ---------------------------------------------------------------------------
|
||||
ini_list_keys() {
|
||||
local file="$1" section="$2"
|
||||
local in_section=false
|
||||
local line k
|
||||
while IFS= read -r line; do
|
||||
line="${line#"${line%%[![:space:]]*}"}"
|
||||
line="${line%"${line##*[![:space:]]}"}"
|
||||
[[ -z "$line" ]] && continue
|
||||
[[ "$line" == \#* ]] && continue
|
||||
if [[ "$line" =~ ^\[([^]]+)\] ]]; then
|
||||
if [[ "${BASH_REMATCH[1]}" == "$section" ]]; then
|
||||
in_section=true
|
||||
elif $in_section; then
|
||||
break
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
if $in_section && [[ "$line" =~ ^([^=]+)= ]]; then
|
||||
k="${BASH_REMATCH[1]}"
|
||||
k="${k#"${k%%[![:space:]]*}"}"
|
||||
k="${k%"${k##*[![:space:]]}"}"
|
||||
printf '%s\n' "$k"
|
||||
fi
|
||||
done < "$file"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helper: upsert a Gitea repo variable (create or update)
|
||||
# ---------------------------------------------------------------------------
|
||||
upsert_repo_variable() {
|
||||
local repo="$1" var_name="$2" var_value="$3"
|
||||
local owner="${GITEA_ORG_NAME}"
|
||||
|
||||
# Check if variable already exists with correct value
|
||||
local existing
|
||||
if existing=$(gitea_api GET "/repos/${owner}/${repo}/actions/variables/${var_name}" 2>/dev/null); then
|
||||
local current_value
|
||||
current_value=$(printf '%s' "$existing" | jq -r '.value // .data // empty' 2>/dev/null)
|
||||
if [[ "$current_value" == "$var_value" ]]; then
|
||||
log_info " ${var_name} already set correctly — skipping"
|
||||
return 0
|
||||
fi
|
||||
# Update existing variable
|
||||
if gitea_api PUT "/repos/${owner}/${repo}/actions/variables/${var_name}" \
|
||||
"$(jq -n --arg v "$var_value" '{value: $v}')" >/dev/null 2>&1; then
|
||||
log_success " Updated ${var_name}"
|
||||
return 0
|
||||
else
|
||||
log_error " Failed to update ${var_name}"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create new variable
|
||||
if gitea_api POST "/repos/${owner}/${repo}/actions/variables" \
|
||||
"$(jq -n --arg n "$var_name" --arg v "$var_value" '{name: $n, value: $v}')" >/dev/null 2>&1; then
|
||||
log_success " Created ${var_name}"
|
||||
return 0
|
||||
else
|
||||
log_error " Failed to create ${var_name}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# =========================================================================
|
||||
# Step 1: Build toolchain images on Unraid
|
||||
# =========================================================================
|
||||
log_step 1 "Building toolchain images on Unraid"
|
||||
|
||||
REMOTE_BUILD_BASE="/tmp/gitea-runner-builds"
|
||||
|
||||
# Image build definitions: TAG|LOCAL_CONTEXT|DOCKER_TARGET
|
||||
IMAGE_BUILDS=(
|
||||
"go-node-runner:latest|${GO_NODE_RUNNER_CONTEXT}|"
|
||||
"jvm-android-runner:slim|${JVM_ANDROID_RUNNER_CONTEXT}|slim"
|
||||
"jvm-android-runner:full|${JVM_ANDROID_RUNNER_CONTEXT}|full"
|
||||
)
|
||||
|
||||
for build_entry in "${IMAGE_BUILDS[@]}"; do
|
||||
IFS='|' read -r img_tag build_context docker_target <<< "$build_entry"
|
||||
|
||||
if [[ "$REBUILD_IMAGES" != "true" ]] && image_exists_on_unraid "$img_tag"; then
|
||||
log_info "Image ${img_tag} already exists on Unraid — skipping"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Derive a unique remote directory name from the image tag
|
||||
remote_dir="${REMOTE_BUILD_BASE}/${img_tag%%:*}"
|
||||
|
||||
log_info "Syncing build context for ${img_tag}..."
|
||||
ssh_exec "UNRAID" "mkdir -p '${remote_dir}'"
|
||||
rsync_to_unraid "$build_context" "$remote_dir"
|
||||
|
||||
log_info "Building ${img_tag} on Unraid (this may take a while)..."
|
||||
local_build_args=""
|
||||
if [[ -n "$docker_target" ]]; then
|
||||
local_build_args="--target ${docker_target}"
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
if ssh_exec "UNRAID" "cd '${remote_dir}' && docker build ${local_build_args} -t '${img_tag}' ."; then
|
||||
log_success "Built ${img_tag}"
|
||||
else
|
||||
log_error "Failed to build ${img_tag}"
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
# Clean up build contexts on Unraid
|
||||
ssh_exec "UNRAID" "rm -rf '${REMOTE_BUILD_BASE}'" 2>/dev/null || true
|
||||
|
||||
# =========================================================================
|
||||
# Step 2: Consolidate macOS runners into shared instance-level runner
|
||||
# =========================================================================
|
||||
log_step 2 "Consolidating macOS runners"
|
||||
|
||||
# Old per-repo macOS runners to remove
|
||||
OLD_MAC_RUNNERS=(
|
||||
macbook-runner-periodvault
|
||||
macbook-runner-intermittent-fasting-tracker
|
||||
)
|
||||
|
||||
for old_runner in "${OLD_MAC_RUNNERS[@]}"; do
|
||||
if ini_list_sections "${SCRIPT_DIR}/runners.conf" | grep -qx "$old_runner" 2>/dev/null; then
|
||||
log_info "Old runner section '${old_runner}' found — phase 11 runners.conf already has it removed"
|
||||
log_info " (If still registered in Gitea, run: manage_runner.sh remove --name ${old_runner})"
|
||||
fi
|
||||
# Remove from Gitea if still registered (launchd service)
|
||||
if launchctl list 2>/dev/null | grep -q "com.gitea.runner.${old_runner}"; then
|
||||
log_info "Removing old macOS runner '${old_runner}'..."
|
||||
"${SCRIPT_DIR}/manage_runner.sh" remove --name "$old_runner" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
|
||||
# Deploy the new shared macOS runner
|
||||
if launchctl list 2>/dev/null | grep -q "com.gitea.runner.macbook-runner"; then
|
||||
log_info "Shared macOS runner 'macbook-runner' already registered — skipping"
|
||||
else
|
||||
log_info "Deploying shared macOS runner 'macbook-runner'..."
|
||||
if "${SCRIPT_DIR}/manage_runner.sh" add --name macbook-runner; then
|
||||
log_success "Shared macOS runner deployed"
|
||||
else
|
||||
log_error "Failed to deploy shared macOS runner"
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
fi
|
||||
|
||||
# =========================================================================
|
||||
# Step 3: Deploy per-repo and shared Docker runners
|
||||
# =========================================================================
|
||||
log_step 3 "Deploying Docker runners"
|
||||
|
||||
# Phase 11 Docker runners (defined in runners.conf)
|
||||
PHASE11_DOCKER_RUNNERS=(
|
||||
unraid-go-node-1
|
||||
unraid-go-node-2
|
||||
unraid-go-node-3
|
||||
unraid-jvm-slim-1
|
||||
unraid-jvm-slim-2
|
||||
unraid-android-emulator
|
||||
)
|
||||
|
||||
for runner_name in "${PHASE11_DOCKER_RUNNERS[@]}"; do
|
||||
log_info "--- Deploying runner: ${runner_name} ---"
|
||||
if "${SCRIPT_DIR}/manage_runner.sh" add --name "$runner_name"; then
|
||||
SUCCESS=$((SUCCESS + 1))
|
||||
else
|
||||
log_error "Failed to deploy runner '${runner_name}'"
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
# =========================================================================
|
||||
# Step 4: Set repository variables from repo_variables.conf
|
||||
# =========================================================================
|
||||
log_step 4 "Setting Gitea repository variables"
|
||||
|
||||
if [[ ! -f "$REPO_VARS_CONF" ]]; then
|
||||
log_warn "repo_variables.conf not found — skipping variable setup"
|
||||
else
|
||||
# Iterate all sections (repos) in repo_variables.conf
|
||||
while IFS= read -r repo; do
|
||||
[[ -z "$repo" ]] && continue
|
||||
log_info "--- Setting variables for repo: ${repo} ---"
|
||||
|
||||
# Iterate all keys in this section
|
||||
while IFS= read -r var_name; do
|
||||
[[ -z "$var_name" ]] && continue
|
||||
var_value=$(ini_get "$REPO_VARS_CONF" "$repo" "$var_name" "")
|
||||
if [[ -z "$var_value" ]]; then
|
||||
log_warn " ${var_name} has empty value — skipping"
|
||||
continue
|
||||
fi
|
||||
upsert_repo_variable "$repo" "$var_name" "$var_value" || FAILED=$((FAILED + 1))
|
||||
done < <(ini_list_keys "$REPO_VARS_CONF" "$repo")
|
||||
|
||||
done < <(ini_list_sections "$REPO_VARS_CONF")
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Summary
|
||||
# ---------------------------------------------------------------------------
|
||||
printf '\n'
|
||||
log_info "Results: ${SUCCESS} runners deployed, ${FAILED} failures"
|
||||
|
||||
if [[ $FAILED -gt 0 ]]; then
|
||||
log_error "Some operations failed — check logs above"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_success "Phase 11 complete — custom runner infrastructure deployed"
|
||||
204
phase11_post_check.sh
Executable file
204
phase11_post_check.sh
Executable file
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# =============================================================================
|
||||
# phase11_post_check.sh — Verify custom runner infrastructure deployment
|
||||
# Checks:
|
||||
# 1. Toolchain images exist on Unraid
|
||||
# 2. All phase 11 runners registered and online in Gitea
|
||||
# 3. Shared macOS runner has correct labels
|
||||
# 4. Repository variables set correctly
|
||||
# 5. KVM available on Unraid (warning only)
|
||||
# =============================================================================
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
source "${SCRIPT_DIR}/lib/common.sh"
|
||||
|
||||
load_env
|
||||
require_vars GITEA_ADMIN_TOKEN GITEA_INTERNAL_URL GITEA_ORG_NAME \
|
||||
UNRAID_IP UNRAID_SSH_USER UNRAID_SSH_PORT
|
||||
|
||||
phase_header 11 "Custom Runners — Post-Check"
|
||||
|
||||
REPO_VARS_CONF="${SCRIPT_DIR}/repo_variables.conf"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
WARN=0
|
||||
|
||||
run_check() {
|
||||
local desc="$1"
|
||||
shift
|
||||
if "$@"; then
|
||||
log_success "$desc"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
log_error "FAIL: $desc"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
run_warn_check() {
|
||||
local desc="$1"
|
||||
shift
|
||||
if "$@"; then
|
||||
log_success "$desc"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
log_warn "WARN: $desc"
|
||||
WARN=$((WARN + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# =========================================================================
|
||||
# Check 1: Toolchain images exist on Unraid
|
||||
# =========================================================================
|
||||
log_info "--- Checking toolchain images ---"
|
||||
|
||||
check_image() {
|
||||
local tag="$1"
|
||||
ssh_exec "UNRAID" "docker image inspect '${tag}' >/dev/null 2>&1"
|
||||
}
|
||||
|
||||
run_check "Image go-node-runner:latest exists on Unraid" check_image "go-node-runner:latest"
|
||||
run_check "Image jvm-android-runner:slim exists on Unraid" check_image "jvm-android-runner:slim"
|
||||
run_check "Image jvm-android-runner:full exists on Unraid" check_image "jvm-android-runner:full"
|
||||
|
||||
# =========================================================================
|
||||
# Check 2: All phase 11 runners registered and online
|
||||
# =========================================================================
|
||||
log_info "--- Checking runner status ---"
|
||||
|
||||
# Fetch all runners from Gitea admin API (single call)
|
||||
ALL_RUNNERS=$(gitea_api GET "/admin/runners" 2>/dev/null || echo "[]")
|
||||
|
||||
check_runner_online() {
|
||||
local name="$1"
|
||||
local status
|
||||
status=$(printf '%s' "$ALL_RUNNERS" | jq -r --arg n "$name" \
|
||||
'[.[] | select(.name == $n)] | .[0].status // "not-found"' 2>/dev/null)
|
||||
if [[ "$status" == "not-found" ]] || [[ -z "$status" ]]; then
|
||||
log_error " Runner '${name}' not found in Gitea"
|
||||
return 1
|
||||
fi
|
||||
if [[ "$status" == "offline" ]] || [[ "$status" == "2" ]]; then
|
||||
log_error " Runner '${name}' is offline"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
PHASE11_RUNNERS=(
|
||||
macbook-runner
|
||||
unraid-go-node-1
|
||||
unraid-go-node-2
|
||||
unraid-go-node-3
|
||||
unraid-jvm-slim-1
|
||||
unraid-jvm-slim-2
|
||||
unraid-android-emulator
|
||||
)
|
||||
|
||||
for runner in "${PHASE11_RUNNERS[@]}"; do
|
||||
run_check "Runner '${runner}' registered and online" check_runner_online "$runner"
|
||||
done
|
||||
|
||||
# =========================================================================
|
||||
# Check 3: Shared macOS runner has correct labels
|
||||
# =========================================================================
|
||||
log_info "--- Checking macOS runner labels ---"
|
||||
|
||||
check_mac_labels() {
|
||||
local labels
|
||||
labels=$(printf '%s' "$ALL_RUNNERS" | jq -r \
|
||||
'[.[] | select(.name == "macbook-runner")] | .[0].labels // [] | .[].name' 2>/dev/null)
|
||||
local missing=0
|
||||
for expected in "self-hosted" "macOS" "ARM64"; do
|
||||
if ! printf '%s' "$labels" | grep -qx "$expected" 2>/dev/null; then
|
||||
log_error " macbook-runner missing label: ${expected}"
|
||||
missing=1
|
||||
fi
|
||||
done
|
||||
return "$missing"
|
||||
}
|
||||
|
||||
run_check "macbook-runner has labels: self-hosted, macOS, ARM64" check_mac_labels
|
||||
|
||||
# =========================================================================
|
||||
# Check 4: Repository variables set correctly
|
||||
# =========================================================================
|
||||
log_info "--- Checking repository variables ---"
|
||||
|
||||
check_repo_variable() {
|
||||
local repo="$1" var_name="$2" expected="$3"
|
||||
local owner="${GITEA_ORG_NAME}"
|
||||
local response
|
||||
if ! response=$(gitea_api GET "/repos/${owner}/${repo}/actions/variables/${var_name}" 2>/dev/null); then
|
||||
log_error " Variable ${var_name} not found on ${repo}"
|
||||
return 1
|
||||
fi
|
||||
local actual
|
||||
actual=$(printf '%s' "$response" | jq -r '.value // .data // empty' 2>/dev/null)
|
||||
if [[ "$actual" != "$expected" ]]; then
|
||||
log_error " Variable ${var_name} on ${repo}: expected '${expected}', got '${actual}'"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
if [[ -f "$REPO_VARS_CONF" ]]; then
|
||||
while IFS= read -r repo; do
|
||||
[[ -z "$repo" ]] && continue
|
||||
# Read all keys from the section using inline parsing
|
||||
local_in_section=false
|
||||
while IFS= read -r line; do
|
||||
line="${line#"${line%%[![:space:]]*}"}"
|
||||
line="${line%"${line##*[![:space:]]}"}"
|
||||
[[ -z "$line" ]] && continue
|
||||
[[ "$line" == \#* ]] && continue
|
||||
if [[ "$line" =~ ^\[([^]]+)\] ]]; then
|
||||
if [[ "${BASH_REMATCH[1]}" == "$repo" ]]; then
|
||||
local_in_section=true
|
||||
elif $local_in_section; then
|
||||
break
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
if $local_in_section && [[ "$line" =~ ^([^=]+)=(.*) ]]; then
|
||||
k="${BASH_REMATCH[1]}"
|
||||
v="${BASH_REMATCH[2]}"
|
||||
k="${k#"${k%%[![:space:]]*}"}"
|
||||
k="${k%"${k##*[![:space:]]}"}"
|
||||
v="${v#"${v%%[![:space:]]*}"}"
|
||||
v="${v%"${v##*[![:space:]]}"}"
|
||||
run_check "Variable ${k} on ${repo}" check_repo_variable "$repo" "$k" "$v"
|
||||
fi
|
||||
done < "$REPO_VARS_CONF"
|
||||
done < <(ini_list_sections "$REPO_VARS_CONF")
|
||||
else
|
||||
log_warn "repo_variables.conf not found — skipping variable checks"
|
||||
WARN=$((WARN + 1))
|
||||
fi
|
||||
|
||||
# =========================================================================
|
||||
# Check 5: KVM available on Unraid
|
||||
# =========================================================================
|
||||
log_info "--- Checking KVM availability ---"
|
||||
|
||||
check_kvm() {
|
||||
ssh_exec "UNRAID" "test -c /dev/kvm"
|
||||
}
|
||||
|
||||
run_warn_check "KVM device available on Unraid (/dev/kvm)" check_kvm
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Summary
|
||||
# ---------------------------------------------------------------------------
|
||||
printf '\n'
|
||||
TOTAL=$((PASS + FAIL + WARN))
|
||||
log_info "Results: ${PASS} passed, ${FAIL} failed, ${WARN} warnings (out of ${TOTAL})"
|
||||
|
||||
if [[ $FAIL -gt 0 ]]; then
|
||||
log_error "Some checks failed — review above"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_success "Phase 11 post-check complete"
|
||||
185
phase11_teardown.sh
Executable file
185
phase11_teardown.sh
Executable file
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# =============================================================================
|
||||
# phase11_teardown.sh — Remove custom runner infrastructure deployed by phase 11
|
||||
# Reverses:
|
||||
# 1. Repository variables
|
||||
# 2. Docker runners (per-repo + shared emulator)
|
||||
# 3. Shared macOS runner → restore original per-repo macOS runners
|
||||
# 4. Toolchain images on Unraid
|
||||
# =============================================================================
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
source "${SCRIPT_DIR}/lib/common.sh"
|
||||
|
||||
load_env
|
||||
require_vars GITEA_ADMIN_TOKEN GITEA_INTERNAL_URL GITEA_ORG_NAME
|
||||
|
||||
phase_header 11 "Custom Runners — Teardown"
|
||||
|
||||
REPO_VARS_CONF="${SCRIPT_DIR}/repo_variables.conf"
|
||||
AUTO_YES=false
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--yes|-y) AUTO_YES=true ;;
|
||||
*) ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$AUTO_YES" != "true" ]]; then
|
||||
log_warn "This will remove all phase 11 custom runners and repo variables."
|
||||
printf 'Continue? [y/N] ' >&2
|
||||
read -r confirm
|
||||
if [[ "$confirm" != "y" ]] && [[ "$confirm" != "Y" ]]; then
|
||||
log_info "Aborted"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
REMOVED=0
|
||||
FAILED=0
|
||||
|
||||
# =========================================================================
|
||||
# Step 1: Delete repository variables
|
||||
# =========================================================================
|
||||
log_step 1 "Removing repository variables"
|
||||
|
||||
if [[ -f "$REPO_VARS_CONF" ]]; then
|
||||
while IFS= read -r repo; do
|
||||
[[ -z "$repo" ]] && continue
|
||||
log_info "--- Removing variables for repo: ${repo} ---"
|
||||
|
||||
# Parse keys from section
|
||||
in_section=false
|
||||
while IFS= read -r line; do
|
||||
line="${line#"${line%%[![:space:]]*}"}"
|
||||
line="${line%"${line##*[![:space:]]}"}"
|
||||
[[ -z "$line" ]] && continue
|
||||
[[ "$line" == \#* ]] && continue
|
||||
if [[ "$line" =~ ^\[([^]]+)\] ]]; then
|
||||
if [[ "${BASH_REMATCH[1]}" == "$repo" ]]; then
|
||||
in_section=true
|
||||
elif $in_section; then
|
||||
break
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
if $in_section && [[ "$line" =~ ^([^=]+)= ]]; then
|
||||
k="${BASH_REMATCH[1]}"
|
||||
k="${k#"${k%%[![:space:]]*}"}"
|
||||
k="${k%"${k##*[![:space:]]}"}"
|
||||
if gitea_api DELETE "/repos/${GITEA_ORG_NAME}/${repo}/actions/variables/${k}" >/dev/null 2>&1; then
|
||||
log_success " Deleted ${k} from ${repo}"
|
||||
REMOVED=$((REMOVED + 1))
|
||||
else
|
||||
log_warn " Could not delete ${k} from ${repo} (may not exist)"
|
||||
fi
|
||||
fi
|
||||
done < "$REPO_VARS_CONF"
|
||||
|
||||
done < <(ini_list_sections "$REPO_VARS_CONF")
|
||||
else
|
||||
log_info "repo_variables.conf not found — skipping"
|
||||
fi
|
||||
|
||||
# =========================================================================
|
||||
# Step 2: Remove Docker runners
|
||||
# =========================================================================
|
||||
log_step 2 "Removing Docker runners"
|
||||
|
||||
PHASE11_DOCKER_RUNNERS=(
|
||||
unraid-go-node-1
|
||||
unraid-go-node-2
|
||||
unraid-go-node-3
|
||||
unraid-jvm-slim-1
|
||||
unraid-jvm-slim-2
|
||||
unraid-android-emulator
|
||||
)
|
||||
|
||||
for runner_name in "${PHASE11_DOCKER_RUNNERS[@]}"; do
|
||||
log_info "Removing runner '${runner_name}'..."
|
||||
if "${SCRIPT_DIR}/manage_runner.sh" remove --name "$runner_name" 2>/dev/null; then
|
||||
log_success "Removed ${runner_name}"
|
||||
REMOVED=$((REMOVED + 1))
|
||||
else
|
||||
log_warn "Could not remove ${runner_name} (may not exist)"
|
||||
fi
|
||||
done
|
||||
|
||||
# =========================================================================
|
||||
# Step 3: Remove shared macOS runner, restore original per-repo runners
|
||||
# =========================================================================
|
||||
log_step 3 "Restoring original macOS runner configuration"
|
||||
|
||||
# Remove shared runner
|
||||
if launchctl list 2>/dev/null | grep -q "com.gitea.runner.macbook-runner"; then
|
||||
log_info "Removing shared macOS runner 'macbook-runner'..."
|
||||
"${SCRIPT_DIR}/manage_runner.sh" remove --name macbook-runner 2>/dev/null || true
|
||||
REMOVED=$((REMOVED + 1))
|
||||
fi
|
||||
|
||||
# Note: original per-repo macOS runner sections were replaced in runners.conf
|
||||
# during phase 11. They need to be re-added manually or by re-running
|
||||
# configure_runners.sh. This teardown only cleans up deployed resources.
|
||||
log_info "Note: original macOS runner sections (macbook-runner-periodvault,"
|
||||
log_info " macbook-runner-intermittent-fasting-tracker) must be restored in"
|
||||
log_info " runners.conf manually or via git checkout."
|
||||
|
||||
# =========================================================================
|
||||
# Step 4: Remove toolchain images from Unraid
|
||||
# =========================================================================
|
||||
log_step 4 "Removing toolchain images from Unraid"
|
||||
|
||||
IMAGES_TO_REMOVE=(
|
||||
"go-node-runner:latest"
|
||||
"jvm-android-runner:slim"
|
||||
"jvm-android-runner:full"
|
||||
)
|
||||
|
||||
for img in "${IMAGES_TO_REMOVE[@]}"; do
|
||||
if ssh_exec "UNRAID" "docker rmi '${img}' 2>/dev/null"; then
|
||||
log_success "Removed image ${img}"
|
||||
REMOVED=$((REMOVED + 1))
|
||||
else
|
||||
log_warn "Could not remove image ${img} (may not exist or in use)"
|
||||
fi
|
||||
done
|
||||
|
||||
# =========================================================================
|
||||
# Step 5: Remove phase 11 runner sections from runners.conf
|
||||
# =========================================================================
|
||||
log_step 5 "Cleaning runners.conf"
|
||||
|
||||
RUNNERS_CONF="${SCRIPT_DIR}/runners.conf"
|
||||
PHASE11_SECTIONS=(
|
||||
unraid-go-node-1
|
||||
unraid-go-node-2
|
||||
unraid-go-node-3
|
||||
unraid-jvm-slim-1
|
||||
unraid-jvm-slim-2
|
||||
unraid-android-emulator
|
||||
macbook-runner
|
||||
)
|
||||
|
||||
for section in "${PHASE11_SECTIONS[@]}"; do
|
||||
if ini_list_sections "$RUNNERS_CONF" | grep -qx "$section" 2>/dev/null; then
|
||||
ini_remove_section "$RUNNERS_CONF" "$section"
|
||||
log_success "Removed [${section}] from runners.conf"
|
||||
REMOVED=$((REMOVED + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Summary
|
||||
# ---------------------------------------------------------------------------
|
||||
printf '\n'
|
||||
log_info "Results: ${REMOVED} items removed, ${FAILED} failures"
|
||||
|
||||
if [[ $FAILED -gt 0 ]]; then
|
||||
log_error "Some removals failed — check logs above"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_success "Phase 11 teardown complete"
|
||||
@@ -87,7 +87,7 @@ phase_header "7.5" "Nginx to Caddy Migration (Multi-domain)"
|
||||
|
||||
# host|upstream|streaming(true/false)|body_limit|insecure_skip_verify(true/false)
|
||||
FULL_HOST_MAP=(
|
||||
"ai.sintheus.com|http://192.168.1.82:8181|true|50MB|false"
|
||||
"ai.sintheus.com|http://192.168.1.82:8181 http://192.168.1.83:8181|true|50MB|false"
|
||||
"photos.sintheus.com|http://192.168.1.222:2283|false|50GB|false"
|
||||
"fin.sintheus.com|http://192.168.1.233:8096|true||false"
|
||||
"disk.sintheus.com|http://192.168.1.52:80|false|20GB|false"
|
||||
@@ -95,11 +95,11 @@ FULL_HOST_MAP=(
|
||||
"plex.sintheus.com|http://192.168.1.111:32400|true||false"
|
||||
"sync.sintheus.com|http://192.168.1.119:8384|false||false"
|
||||
"syno.sintheus.com|https://100.108.182.16:5001|false||true"
|
||||
"tower.sintheus.com|https://192.168.1.82:443|false||true"
|
||||
"tower.sintheus.com|https://192.168.1.82:443 https://192.168.1.83:443|false||true"
|
||||
)
|
||||
|
||||
CANARY_HOST_MAP=(
|
||||
"tower.sintheus.com|https://192.168.1.82:443|false||true"
|
||||
"tower.sintheus.com|https://192.168.1.82:443 https://192.168.1.83:443|false||true"
|
||||
)
|
||||
|
||||
GITEA_ENTRY="${GITEA_DOMAIN}|http://${UNRAID_GITEA_IP}:3000|false||false"
|
||||
@@ -175,7 +175,11 @@ emit_site_block_standalone() {
|
||||
|
||||
{
|
||||
echo "${host} {"
|
||||
if [[ "$TLS_MODE" == "existing" ]]; then
|
||||
if [[ "$TLS_MODE" == "cloudflare" ]]; then
|
||||
echo " tls {"
|
||||
echo " dns cloudflare {env.CF_API_TOKEN}"
|
||||
echo " }"
|
||||
elif [[ "$TLS_MODE" == "existing" ]]; then
|
||||
echo " tls ${SSL_CERT_PATH} ${SSL_KEY_PATH}"
|
||||
fi
|
||||
echo " encode zstd gzip"
|
||||
@@ -524,15 +528,19 @@ probe_http_code_ok() {
|
||||
|
||||
probe_host_via_caddy() {
|
||||
local host="$1" upstream="$2" role="$3"
|
||||
local max_attempts="${4:-5}" wait_secs="${5:-5}"
|
||||
local path="/"
|
||||
if [[ "$role" == "gitea_api" ]]; then
|
||||
path="/api/v1/version"
|
||||
fi
|
||||
|
||||
local tmp_body http_code
|
||||
local tmp_body http_code attempt
|
||||
tmp_body=$(mktemp)
|
||||
|
||||
for (( attempt=1; attempt<=max_attempts; attempt++ )); do
|
||||
http_code=$(curl -sk --resolve "${host}:443:${UNRAID_CADDY_IP}" \
|
||||
-o "$tmp_body" -w "%{http_code}" "https://${host}${path}" 2>/dev/null || echo "000")
|
||||
-o "$tmp_body" -w "%{http_code}" "https://${host}${path}" 2>/dev/null) || true
|
||||
[[ -z "$http_code" ]] && http_code="000"
|
||||
|
||||
if probe_http_code_ok "$http_code" "$role"; then
|
||||
log_success "Probe passed: ${host} (HTTP ${http_code})"
|
||||
@@ -540,7 +548,13 @@ probe_host_via_caddy() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_error "Probe failed: ${host} (HTTP ${http_code})"
|
||||
if [[ $attempt -lt $max_attempts ]]; then
|
||||
log_info "Probe attempt ${attempt}/${max_attempts} for ${host} (HTTP ${http_code}) — retrying in ${wait_secs}s..."
|
||||
sleep "$wait_secs"
|
||||
fi
|
||||
done
|
||||
|
||||
log_error "Probe failed: ${host} (HTTP ${http_code}) after ${max_attempts} attempts"
|
||||
if [[ "$http_code" == "502" || "$http_code" == "503" || "$http_code" == "504" || "$http_code" == "000" ]]; then
|
||||
local upstream_probe_raw upstream_code
|
||||
upstream_probe_raw=$(ssh_exec UNRAID "curl -sk -o /dev/null -w '%{http_code}' '${upstream}' || true" 2>/dev/null || true)
|
||||
|
||||
@@ -16,6 +16,31 @@ set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
source "${SCRIPT_DIR}/lib/common.sh"
|
||||
|
||||
ALLOW_DIRECT_CHECKS=false
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [options]
|
||||
|
||||
Options:
|
||||
--allow-direct-checks Allow fallback to direct Caddy-IP checks via --resolve
|
||||
(LAN/split-DNS staging mode; not a full public cutover)
|
||||
--help, -h Show this help
|
||||
EOF
|
||||
}
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--allow-direct-checks) ALLOW_DIRECT_CHECKS=true ;;
|
||||
--help|-h) usage; exit 0 ;;
|
||||
*)
|
||||
log_error "Unknown argument: $arg"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
load_env
|
||||
require_vars UNRAID_IP UNRAID_SSH_USER UNRAID_GITEA_IP UNRAID_CADDY_IP \
|
||||
UNRAID_COMPOSE_DIR \
|
||||
@@ -236,6 +261,16 @@ caddyfile_has_domain_block() {
|
||||
sub(/[[:space:]]+$/, "", s)
|
||||
return s
|
||||
}
|
||||
function matches_domain(label, dom, wild_suffix, dot_pos) {
|
||||
if (label == dom) return 1
|
||||
# Wildcard match: *.example.com covers sub.example.com
|
||||
if (substr(label, 1, 2) == "*.") {
|
||||
wild_suffix = substr(label, 2)
|
||||
dot_pos = index(dom, ".")
|
||||
if (dot_pos > 0 && substr(dom, dot_pos) == wild_suffix) return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
{
|
||||
line = $0
|
||||
if (line ~ /^[[:space:]]*#/) next
|
||||
@@ -248,7 +283,7 @@ caddyfile_has_domain_block() {
|
||||
gsub(/[[:space:]]+/, "", labels)
|
||||
n = split(labels, parts, ",")
|
||||
for (i = 1; i <= n; i++) {
|
||||
if (parts[i] == domain) {
|
||||
if (matches_domain(parts[i], domain)) {
|
||||
found = 1
|
||||
}
|
||||
}
|
||||
@@ -363,7 +398,6 @@ fi
|
||||
log_step 2 "Deploying Caddyfile..."
|
||||
GITEA_CONTAINER_IP="${UNRAID_GITEA_IP}"
|
||||
export GITEA_CONTAINER_IP GITEA_DOMAIN CADDY_DOMAIN
|
||||
CADDYFILE_UPDATED=0
|
||||
|
||||
# Build TLS block based on TLS_MODE
|
||||
if [[ "$TLS_MODE" == "cloudflare" ]]; then
|
||||
@@ -404,7 +438,6 @@ if ssh_exec UNRAID "test -f '${CADDY_DATA_PATH}/Caddyfile'" 2>/dev/null; then
|
||||
cat "$TMP_UPDATED" "$TMP_ROUTE_BLOCK" > "${TMP_UPDATED}.final"
|
||||
scp_to UNRAID "${TMP_UPDATED}.final" "${CADDY_DATA_PATH}/Caddyfile"
|
||||
log_success "Appended managed Gitea route to existing Caddyfile"
|
||||
CADDYFILE_UPDATED=1
|
||||
fi
|
||||
|
||||
rm -f "$TMP_EXISTING" "$TMP_UPDATED" "$TMP_ROUTE_BLOCK" "${TMP_UPDATED}.final"
|
||||
@@ -416,7 +449,6 @@ else
|
||||
scp_to UNRAID "$TMPFILE" "${CADDY_DATA_PATH}/Caddyfile"
|
||||
rm -f "$TMPFILE"
|
||||
log_success "Caddyfile deployed"
|
||||
CADDYFILE_UPDATED=1
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -505,12 +537,18 @@ fi
|
||||
# ---------------------------------------------------------------------------
|
||||
log_step 6 "Waiting for HTTPS (Caddy auto-provisions cert)..."
|
||||
check_unraid_gitea_backend
|
||||
if wait_for_https_public "${GITEA_DOMAIN}" 30; then
|
||||
if wait_for_https_public "${GITEA_DOMAIN}" 60; then
|
||||
log_success "HTTPS verified through current domain routing — https://${GITEA_DOMAIN} works"
|
||||
else
|
||||
log_warn "Public-domain routing to Caddy is not ready yet"
|
||||
if [[ "$ALLOW_DIRECT_CHECKS" == "true" ]]; then
|
||||
wait_for_https_via_resolve "${GITEA_DOMAIN}" "${UNRAID_CADDY_IP}" 300
|
||||
log_success "HTTPS verified via direct Caddy path; public routing can be completed later"
|
||||
log_warn "Proceeding with direct-only HTTPS validation (--allow-direct-checks)"
|
||||
else
|
||||
log_error "Refusing to continue cutover without public HTTPS reachability"
|
||||
log_error "Fix DNS/ingress routing and rerun Phase 8, or use --allow-direct-checks for staging only"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -15,6 +15,31 @@ set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
source "${SCRIPT_DIR}/lib/common.sh"
|
||||
|
||||
ALLOW_DIRECT_CHECKS=false
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [options]
|
||||
|
||||
Options:
|
||||
--allow-direct-checks Allow fallback to direct Caddy-IP checks via --resolve
|
||||
(LAN/split-DNS staging mode; not a full public cutover check)
|
||||
--help, -h Show this help
|
||||
EOF
|
||||
}
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--allow-direct-checks) ALLOW_DIRECT_CHECKS=true ;;
|
||||
--help|-h) usage; exit 0 ;;
|
||||
*)
|
||||
log_error "Unknown argument: $arg"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
load_env
|
||||
require_vars GITEA_DOMAIN UNRAID_CADDY_IP GITEA_ADMIN_TOKEN GITEA_ORG_NAME \
|
||||
GITHUB_USERNAME GITHUB_TOKEN \
|
||||
@@ -39,9 +64,15 @@ run_check() {
|
||||
|
||||
ACCESS_MODE="public"
|
||||
if ! curl -sf -o /dev/null "https://${GITEA_DOMAIN}/api/v1/version" 2>/dev/null; then
|
||||
ACCESS_MODE="direct"
|
||||
log_warn "Public routing to ${GITEA_DOMAIN} not reachable from control plane"
|
||||
if [[ "$ALLOW_DIRECT_CHECKS" == "true" ]]; then
|
||||
ACCESS_MODE="direct"
|
||||
log_warn "Using direct Caddy-IP checks via --resolve (${UNRAID_CADDY_IP})"
|
||||
else
|
||||
log_error "Public HTTPS check failed; this is not a complete Phase 8 validation"
|
||||
log_error "Fix DNS/ingress routing and rerun, or use --allow-direct-checks for staging-only checks"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
log_info "Using public-domain checks for ${GITEA_DOMAIN}"
|
||||
fi
|
||||
|
||||
20
repo_variables.conf.example
Normal file
20
repo_variables.conf.example
Normal file
@@ -0,0 +1,20 @@
|
||||
# =============================================================================
|
||||
# repo_variables.conf — Gitea Actions Repository Variables (INI format)
|
||||
# Copy to repo_variables.conf and edit.
|
||||
# Used by phase11_custom_runners.sh to set per-repo CI dispatch variables.
|
||||
# =============================================================================
|
||||
#
|
||||
# Each [section] = Gitea repository name (must exist in GITEA_ORG_NAME).
|
||||
# Keys = variable names. Values = literal string set via Gitea API.
|
||||
# Workflows access these as ${{ vars.VARIABLE_NAME }}.
|
||||
#
|
||||
# Common pattern: repos use fromJSON(vars.CI_RUNS_ON || '["ubuntu-latest"]')
|
||||
# in runs-on to dynamically select runners.
|
||||
|
||||
#[my-go-repo]
|
||||
#CI_RUNS_ON = ["self-hosted","Linux","X64"]
|
||||
|
||||
#[my-mobile-repo]
|
||||
#CI_RUNS_ON = ["self-hosted","Linux","X64"]
|
||||
#CI_RUNS_ON_MACOS = ["self-hosted","macOS","ARM64"]
|
||||
#CI_RUNS_ON_ANDROID = ["self-hosted","Linux","X64","android-emulator"]
|
||||
27
run_all.sh
27
run_all.sh
@@ -3,11 +3,11 @@ set -euo pipefail
|
||||
|
||||
# =============================================================================
|
||||
# run_all.sh — Orchestrate the full Gitea migration pipeline
|
||||
# Runs: setup → preflight → phase 1-9 (each with post-check) sequentially.
|
||||
# Runs: setup → preflight → phase 1-11 (each with post-check) sequentially.
|
||||
# Stops on first failure, prints summary of what completed.
|
||||
#
|
||||
# Usage:
|
||||
# ./run_all.sh # Full run: setup + preflight + phases 1-9
|
||||
# ./run_all.sh # Full run: setup + preflight + phases 1-11
|
||||
# ./run_all.sh --skip-setup # Skip setup scripts, start at preflight
|
||||
# ./run_all.sh --start-from=3 # Run preflight, then start at phase 3
|
||||
# ./run_all.sh --skip-setup --start-from=5
|
||||
@@ -28,10 +28,12 @@ require_local_os "Darwin" "run_all.sh must run from macOS (the control plane)"
|
||||
SKIP_SETUP=false
|
||||
START_FROM=0
|
||||
START_FROM_SET=false
|
||||
ALLOW_DIRECT_CHECKS=false
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--skip-setup) SKIP_SETUP=true ;;
|
||||
--allow-direct-checks) ALLOW_DIRECT_CHECKS=true ;;
|
||||
--dry-run)
|
||||
exec "${SCRIPT_DIR}/post-migration-check.sh"
|
||||
;;
|
||||
@@ -39,11 +41,11 @@ for arg in "$@"; do
|
||||
START_FROM="${arg#*=}"
|
||||
START_FROM_SET=true
|
||||
if ! [[ "$START_FROM" =~ ^[0-9]+$ ]]; then
|
||||
log_error "--start-from must be a number (1-9)"
|
||||
log_error "--start-from must be a number (1-11)"
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$START_FROM" -lt 1 ]] || [[ "$START_FROM" -gt 9 ]]; then
|
||||
log_error "--start-from must be between 1 and 9"
|
||||
if [[ "$START_FROM" -lt 1 ]] || [[ "$START_FROM" -gt 11 ]]; then
|
||||
log_error "--start-from must be between 1 and 11"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
@@ -54,13 +56,16 @@ Usage: $(basename "$0") [options]
|
||||
Options:
|
||||
--skip-setup Skip configure_env + machine setup, start at preflight
|
||||
--start-from=N Skip phases before N (still runs preflight)
|
||||
--allow-direct-checks Pass --allow-direct-checks to Phase 8 scripts
|
||||
(LAN/split-DNS staging mode)
|
||||
--dry-run Run read-only infrastructure check (no mutations)
|
||||
--help Show this help
|
||||
|
||||
Examples:
|
||||
$(basename "$0") Full run
|
||||
$(basename "$0") --skip-setup Skip setup, start at preflight
|
||||
$(basename "$0") --start-from=3 Run preflight, then phases 3-9
|
||||
$(basename "$0") --start-from=3 Run preflight, then phases 3-11
|
||||
$(basename "$0") --allow-direct-checks LAN mode: use direct Caddy-IP checks
|
||||
$(basename "$0") --dry-run Check current state without changing anything
|
||||
EOF
|
||||
exit 0 ;;
|
||||
@@ -157,7 +162,7 @@ else
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phases 1-9 — run sequentially, each followed by its post-check
|
||||
# Phases 1-11 — run sequentially, each followed by its post-check
|
||||
# The phase scripts are the "do" step, post-checks verify success.
|
||||
# ---------------------------------------------------------------------------
|
||||
PHASES=(
|
||||
@@ -170,6 +175,8 @@ PHASES=(
|
||||
"7|Phase 7: Branch Protection|phase7_branch_protection.sh|phase7_post_check.sh"
|
||||
"8|Phase 8: Cutover|phase8_cutover.sh|phase8_post_check.sh"
|
||||
"9|Phase 9: Security|phase9_security.sh|phase9_post_check.sh"
|
||||
"10|Phase 10: Local Repo Cutover|phase10_local_repo_cutover.sh|phase10_post_check.sh"
|
||||
"11|Phase 11: Custom Runners|phase11_custom_runners.sh|phase11_post_check.sh"
|
||||
)
|
||||
|
||||
for phase_entry in "${PHASES[@]}"; do
|
||||
@@ -181,8 +188,14 @@ for phase_entry in "${PHASES[@]}"; do
|
||||
continue
|
||||
fi
|
||||
|
||||
# Phase 8 scripts accept --allow-direct-checks for LAN/split-DNS setups.
|
||||
if [[ "$phase_num" -eq 8 ]] && [[ "$ALLOW_DIRECT_CHECKS" == "true" ]]; then
|
||||
run_step "$phase_name" "$phase_script" --allow-direct-checks
|
||||
run_step "${phase_name} — post-check" "$post_check" --allow-direct-checks
|
||||
else
|
||||
run_step "$phase_name" "$phase_script"
|
||||
run_step "${phase_name} — post-check" "$post_check"
|
||||
fi
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -55,6 +55,11 @@
|
||||
# (starts at login, no sudo needed).
|
||||
# Ignored for docker runners.
|
||||
#
|
||||
# container_options — Extra Docker flags for act_runner job containers.
|
||||
# Passed to the container.options field in act_runner config.
|
||||
# e.g. "--device=/dev/kvm" for KVM passthrough.
|
||||
# Empty = no extra flags. Ignored for native runners.
|
||||
#
|
||||
# STARTER ENTRIES (uncomment and edit):
|
||||
|
||||
#[unraid-runner]
|
||||
|
||||
@@ -3,11 +3,11 @@ set -euo pipefail
|
||||
|
||||
# =============================================================================
|
||||
# teardown_all.sh — Tear down migration in reverse order
|
||||
# Runs phase teardown scripts from phase 9 → phase 1 (or a subset).
|
||||
# Runs phase teardown scripts from phase 11 → phase 1 (or a subset).
|
||||
#
|
||||
# Usage:
|
||||
# ./teardown_all.sh # Tear down everything (phases 9 → 1)
|
||||
# ./teardown_all.sh --through=5 # Tear down phases 9 → 5 (leave 1-4)
|
||||
# ./teardown_all.sh # Tear down everything (phases 11 → 1)
|
||||
# ./teardown_all.sh --through=5 # Tear down phases 11 → 5 (leave 1-4)
|
||||
# ./teardown_all.sh --yes # Skip confirmation prompts
|
||||
# =============================================================================
|
||||
|
||||
@@ -25,8 +25,8 @@ for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--through=*)
|
||||
THROUGH="${arg#*=}"
|
||||
if ! [[ "$THROUGH" =~ ^[0-9]+$ ]] || [[ "$THROUGH" -lt 1 ]] || [[ "$THROUGH" -gt 9 ]]; then
|
||||
log_error "--through must be a number between 1 and 9"
|
||||
if ! [[ "$THROUGH" =~ ^[0-9]+$ ]] || [[ "$THROUGH" -lt 1 ]] || [[ "$THROUGH" -gt 11 ]]; then
|
||||
log_error "--through must be a number between 1 and 11"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
@@ -37,14 +37,14 @@ for arg in "$@"; do
|
||||
Usage: $(basename "$0") [options]
|
||||
|
||||
Options:
|
||||
--through=N Only tear down phases N through 9 (default: 1 = everything)
|
||||
--through=N Only tear down phases N through 11 (default: 1 = everything)
|
||||
--cleanup Also run setup/cleanup.sh to uninstall setup prerequisites
|
||||
--yes, -y Skip all confirmation prompts
|
||||
--help Show this help
|
||||
|
||||
Examples:
|
||||
$(basename "$0") Tear down everything
|
||||
$(basename "$0") --through=5 Tear down phases 5-9, leave 1-4
|
||||
$(basename "$0") --through=5 Tear down phases 11-5, leave 1-4
|
||||
$(basename "$0") --cleanup Full teardown + uninstall prerequisites
|
||||
$(basename "$0") --yes Non-interactive teardown
|
||||
EOF
|
||||
@@ -58,9 +58,9 @@ done
|
||||
# ---------------------------------------------------------------------------
|
||||
if [[ "$AUTO_YES" == "false" ]]; then
|
||||
if [[ "$THROUGH" -eq 1 ]]; then
|
||||
log_warn "This will tear down ALL phases (9 → 1)."
|
||||
log_warn "This will tear down ALL phases (11 → 1)."
|
||||
else
|
||||
log_warn "This will tear down phases 9 → ${THROUGH}."
|
||||
log_warn "This will tear down phases 11 → ${THROUGH}."
|
||||
fi
|
||||
printf 'Are you sure? [y/N] '
|
||||
read -r confirm
|
||||
@@ -70,9 +70,11 @@ if [[ "$AUTO_YES" == "false" ]]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Teardown scripts in reverse order (9 → 1)
|
||||
# Teardown scripts in reverse order (11 → 1)
|
||||
# Each entry: phase_num|script_path
|
||||
TEARDOWNS=(
|
||||
"11|phase11_teardown.sh"
|
||||
"10|phase10_teardown.sh"
|
||||
"9|phase9_teardown.sh"
|
||||
"8|phase8_teardown.sh"
|
||||
"7|phase7_teardown.sh"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# act_runner configuration — rendered by manage_runner.sh
|
||||
# Variables: RUNNER_NAME, RUNNER_LABELS_YAML, RUNNER_CAPACITY
|
||||
# Variables: RUNNER_NAME, RUNNER_LABELS_YAML, RUNNER_CAPACITY, RUNNER_CONTAINER_OPTIONS
|
||||
# Deployed alongside docker-compose.yml (docker) or act_runner binary (native).
|
||||
|
||||
log:
|
||||
@@ -22,7 +22,7 @@ cache:
|
||||
container:
|
||||
network: "" # Empty = use default Docker network.
|
||||
privileged: false # Never run job containers as privileged.
|
||||
options:
|
||||
options: ${RUNNER_CONTAINER_OPTIONS}
|
||||
workdir_parent:
|
||||
|
||||
host:
|
||||
|
||||
49
toggle_dns.sh
Executable file
49
toggle_dns.sh
Executable file
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Toggle DNS between Pi-hole and Cloudflare on all active network services.
|
||||
# Usage: ./toggle_dns.sh
|
||||
# Requires sudo for networksetup.
|
||||
|
||||
PIHOLE="pi.sintheus.com"
|
||||
CLOUDFLARE="1.1.1.1"
|
||||
|
||||
# Get all hardware network services (Wi-Fi, Ethernet, Thunderbolt, USB, etc.)
|
||||
services=()
|
||||
while IFS= read -r line; do
|
||||
[[ "$line" == *"*"* ]] && continue # skip disabled services
|
||||
services+=("$line")
|
||||
done < <(networksetup -listallnetworkservices 2>/dev/null | tail -n +2)
|
||||
|
||||
if [[ ${#services[@]} -eq 0 ]]; then
|
||||
echo "No network services found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Detect current mode from the first service that has a DNS set
|
||||
current_dns=""
|
||||
for svc in "${services[@]}"; do
|
||||
dns=$(networksetup -getdnsservers "$svc" 2>/dev/null | head -1)
|
||||
if [[ "$dns" != *"aren't any"* ]] && [[ -n "$dns" ]]; then
|
||||
current_dns="$dns"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$current_dns" == "$CLOUDFLARE" ]]; then
|
||||
target="$PIHOLE"
|
||||
label="Pi-hole"
|
||||
else
|
||||
target="$CLOUDFLARE"
|
||||
label="Cloudflare"
|
||||
fi
|
||||
|
||||
echo "Switching all services to ${label} (${target})..."
|
||||
for svc in "${services[@]}"; do
|
||||
sudo networksetup -setdnsservers "$svc" "$target"
|
||||
echo " ${svc} → ${target}"
|
||||
done
|
||||
|
||||
sudo dscacheutil -flushcache
|
||||
sudo killall -HUP mDNSResponder 2>/dev/null || true
|
||||
echo "DNS set to ${label} (${target})"
|
||||
Reference in New Issue
Block a user