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