--- description: 交互式批量删除 Gitea Runners 脚本 agent: general --- # Delete Runner Script 本文档包含用于交互式批量删除 Gitea Actions Runner 的完整 Bash 脚本。 ## 功能特点 - **多选支持**:支持输入多个序号(如 `1,3` 或 `1 3`)或 `all` 进行批量删除。 - **双重清理**:同时从 Gitea 服务器注销 Runner 和删除本地配置/容器。 - **智能识别**:自动关联远程 Runner 状态与本地 Runner 目录。 - **安全检查**:删除前强制二次确认,防止误删。 ## 脚本文件 你可以将以下内容保存为 `delete_runner.sh` 并赋予执行权限 (`chmod +x delete_runner.sh`)。 ```bash #!/bin/bash # Gitea Runner Deletion Script # Generated by OpenCode Skill set -e # ========================================== # 1. Setup & Config # ========================================== # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color # Config CONFIG_FILE="$HOME/.config/gitea/config.env" RUNNERS_BASE_DIR="$HOME/.config/gitea/runners" if [ ! -f "$CONFIG_FILE" ]; then echo -e "${RED}❌ 配置文件不存在: $CONFIG_FILE${NC}" exit 1 fi source "$CONFIG_FILE" if [ -z "$GITEA_URL" ] || [ -z "$GITEA_TOKEN" ]; then echo -e "${RED}❌ 配置无效: 缺少 URL 或 Token${NC}" exit 1 fi # Check requirements if ! command -v jq &> /dev/null; then echo -e "${RED}❌ 需要安装 jq 工具来解析 JSON${NC}" exit 1 fi echo "正在获取 Runner 列表..." # ========================================== # 2. Data Collection # ========================================== # Temporary files REMOTE_LIST=$(mktemp) LOCAL_MAP=$(mktemp) FINAL_LIST=$(mktemp) # 2.1 Fetch Remote Runners (Try Admin first, then Org) # Note: Admin endpoint /api/v1/admin/runners lists all runners HTTP_CODE=$(curl -s -w "%{http_code}" -o "$REMOTE_LIST" \ -H "Authorization: token $GITEA_TOKEN" \ "${GITEA_URL}/api/v1/admin/runners?page=1&limit=100") if [ "$HTTP_CODE" != "200" ]; then # Fallback to Org level if defined if [ -n "$GITEA_DEFAULT_ORG" ]; then HTTP_CODE=$(curl -s -w "%{http_code}" -o "$REMOTE_LIST" \ -H "Authorization: token $GITEA_TOKEN" \ "${GITEA_URL}/api/v1/orgs/${GITEA_DEFAULT_ORG}/actions/runners?page=1&limit=100") fi fi if [ "$HTTP_CODE" != "200" ]; then echo -e "${RED}❌ 无法获取 Runner 列表 (HTTP $HTTP_CODE)${NC}" cat "$REMOTE_LIST" rm "$REMOTE_LIST" "$LOCAL_MAP" "$FINAL_LIST" exit 1 fi # 2.2 Scan Local Directories to map UUID -> Path # We need to find which local directory corresponds to which runner ID/UUID echo "{}" > "$LOCAL_MAP" if [ -d "$RUNNERS_BASE_DIR" ]; then for d in "$RUNNERS_BASE_DIR"/*; do if [ -d "$d" ]; then # Check Host mode .runner if [ -f "$d/.runner" ]; then uuid=$(jq -r '.uuid' "$d/.runner" 2>/dev/null) if [ -n "$uuid" ] && [ "$uuid" != "null" ]; then # Add to JSON map tmp=$(mktemp) jq --arg uuid "$uuid" --arg path "$d" '.[$uuid] = $path' "$LOCAL_MAP" > "$tmp" && mv "$tmp" "$LOCAL_MAP" fi fi # Check Docker mode data/.runner if [ -f "$d/data/.runner" ]; then uuid=$(jq -r '.uuid' "$d/data/.runner" 2>/dev/null) if [ -n "$uuid" ] && [ "$uuid" != "null" ]; then tmp=$(mktemp) jq --arg uuid "$uuid" --arg path "$d" '.[$uuid] = $path' "$LOCAL_MAP" > "$tmp" && mv "$tmp" "$LOCAL_MAP" fi fi fi done fi # ========================================== # 3. Display Interface # ========================================== # Combine Remote and Local info # Output format: index | id | name | status | local_path jq -r --slurpfile local "$LOCAL_MAP" ' .runners[] | [.id, .uuid, .name, .status, ($local[0][.uuid] // "")] | @tsv ' "$REMOTE_LIST" > "$FINAL_LIST" count=$(wc -l < "$FINAL_LIST" | tr -d ' ') if [ "$count" -eq 0 ]; then echo "没有发现任何 Runners。" rm "$REMOTE_LIST" "$LOCAL_MAP" "$FINAL_LIST" exit 0 fi echo "" echo -e "${YELLOW}Gitea Runners 列表 (共 $count 个)${NC}" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" printf "%-4s | %-8s | %-20s | %-10s | %-30s\n" "序号" "ID" "名称" "状态" "本地目录" echo "----------------------------------------------------------------------------" i=1 declare -a runner_ids declare -a runner_names declare -a runner_paths while IFS=$'\t' read -r id uuid name status local_path; do status_icon="🔴" if [ "$status" = "online" ] || [ "$status" = "idle" ] || [ "$status" = "active" ]; then status_icon="🟢" fi local_mark="" if [ -n "$local_path" ]; then local_mark="$(basename "$local_path")" else local_mark="-" fi printf "%-4d | %-8s | %-20s | %s %-8s | %-30s\n" "$i" "$id" "${name:0:20}" "$status_icon" "$status" "$local_mark" runner_ids[$i]=$id runner_names[$i]=$name runner_paths[$i]=$local_path i=$((i+1)) done < "$FINAL_LIST" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" # ========================================== # 4. User Selection # ========================================== echo "请输入要删除的序号:" echo " - 单个: 1" echo " - 多选: 1,3,5 或 1 3 5" echo " - 全部: all" echo " - 退出: q" echo "" read -p "选择 > " selection if [[ "$selection" =~ ^[qQ] ]]; then echo "已取消。" rm "$REMOTE_LIST" "$LOCAL_MAP" "$FINAL_LIST" exit 0 fi target_indices=() if [ "$selection" = "all" ]; then for ((j=1; j<=count; j++)); do target_indices+=($j) done else # Replace commas with spaces and iterate for idx in ${selection//,/ }; do # Validate number if [[ "$idx" =~ ^[0-9]+$ ]] && [ "$idx" -ge 1 ] && [ "$idx" -le "$count" ]; then target_indices+=($idx) else echo -e "${YELLOW}⚠️ 忽略无效序号: $idx${NC}" fi done fi if [ ${#target_indices[@]} -eq 0 ]; then echo -e "${RED}未选择任何有效 Runner。${NC}" rm "$REMOTE_LIST" "$LOCAL_MAP" "$FINAL_LIST" exit 1 fi # ========================================== # 5. Confirmation # ========================================== echo "" echo -e "${RED}⚠️ 警告: 即将删除以下 ${#target_indices[@]} 个 Runner:${NC}" for idx in "${target_indices[@]}"; do echo " - [${runner_ids[$idx]}] ${runner_names[$idx]}" if [ -n "${runner_paths[$idx]}" ]; then echo " └─ 本地目录: ${runner_paths[$idx]}" fi done echo "" echo "此操作将从服务器注销 Runner 并删除本地文件/容器。" read -p "确认删除? (输入 yes 继续): " confirm if [ "$confirm" != "yes" ]; then echo "操作已取消。" rm "$REMOTE_LIST" "$LOCAL_MAP" "$FINAL_LIST" exit 0 fi # ========================================== # 6. Execution # ========================================== echo "" echo "开始执行删除..." for idx in "${target_indices[@]}"; do r_id="${runner_ids[$idx]}" r_name="${runner_names[$idx]}" r_path="${runner_paths[$idx]}" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "正在处理: $r_name (ID: $r_id)" # 6.1 Delete from Server echo -n " 1. 从服务器注销... " del_code=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \ -H "Authorization: token $GITEA_TOKEN" \ "${GITEA_URL}/api/v1/admin/actions/runners/${r_id}") if [ "$del_code" = "204" ] || [ "$del_code" = "404" ]; then echo -e "${GREEN}成功${NC}" else echo -e "${RED}失败 (HTTP $del_code)${NC}" # Continue cleanup anyway fi # 6.2 Cleanup Local if [ -n "$r_path" ] && [ -d "$r_path" ]; then dir_name=$(basename "$r_path") # Stop Docker container if name matches folder name (common convention) if docker ps -a --format '{{.Names}}' | grep -q "^${dir_name}$"; then echo -n " 2. 停止并删除 Docker 容器 ($dir_name)... " docker rm -f "$dir_name" >/dev/null 2>&1 echo -e "${GREEN}完成${NC}" fi # Stop Host process (if PID file exists) if [ -f "$r_path/pid" ]; then pid=$(cat "$r_path/pid") echo -n " 2. 停止本地进程 (PID: $pid)... " kill "$pid" >/dev/null 2>&1 || true echo -e "${GREEN}完成${NC}" fi echo -n " 3. 删除本地目录... " rm -rf "$r_path" echo -e "${GREEN}完成${NC}" else echo " - 本地目录未找到或已清理" fi done echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo -e "${GREEN}✅ 批量删除操作完成${NC}" # Cleanup temps rm "$REMOTE_LIST" "$LOCAL_MAP" "$FINAL_LIST" ```