Files
opencode/skill/gitea/create-runner.md

11 KiB
Raw Blame History

description, agent
description agent
创建并启动 Gitea Actions Runner 的完整脚本 general

Create Runner Script

本文档包含用于创建 Gitea Actions Runner 的完整 Bash 脚本。该脚本支持 Host 模式和 Docker 模式。

脚本文件

你可以将以下内容保存为 create_runner.sh 并赋予执行权限 (chmod +x create_runner.sh)。

#!/bin/bash

# Gitea Runner Creation Script
# Generated by OpenCode Skill

set -e

# ==========================================
# 1. Choose Mode & Check Dependencies
# ==========================================

echo "请选择 Runner 运行模式:"
echo "  1. Host Mode   (直接在宿主机运行,支持 macOS/iOS 原生构建)"
echo "  2. Docker Mode (在容器中运行,环境隔离,适合标准 Linux 构建)"
echo ""
read -p "请输入选项 [1/2] (默认 1): " mode_choice

if [ "$mode_choice" = "2" ]; then
  RUNNER_MODE="docker"
  echo "✅ 已选择: Docker Mode"
else
  RUNNER_MODE="host"
  echo "✅ 已选择: Host Mode"
fi
echo ""

# Check dependencies based on mode
if [ "$RUNNER_MODE" = "host" ]; then
  echo "检查 act_runner 安装状态..."
  if command -v act_runner &> /dev/null; then
    version=$(act_runner --version 2>&1 | head -n1)
    echo "✓ act_runner 已安装: $version"
  else
    echo "⚠️  act_runner 未安装"
    echo "正在使用 Homebrew 安装..."
    if ! command -v brew &> /dev/null; then
      echo "❌ 需要先安装 Homebrew"
      exit 1
    fi
    brew install act_runner
    if [ $? -eq 0 ]; then
      echo "✓ act_runner 安装成功"
    else
      echo "❌ act_runner 安装失败"
      exit 1
    fi
  fi
else
  # Docker mode checks
  echo "检查 Docker 环境..."
  if command -v docker &> /dev/null; then
    if docker info &> /dev/null; then
      docker_ver=$(docker --version)
      echo "✓ Docker 已安装并运行: $docker_ver"
    else
      echo "❌ Docker 已安装但未运行"
      echo "   请启动 Docker Desktop"
      exit 1
    fi
  else
    echo "❌ Docker 未安装"
    echo "   Docker 模式需要预先安装 Docker"
    echo "   请访问 https://www.docker.com/products/docker-desktop/ 下载安装"
    exit 1
  fi
fi
echo ""

# ==========================================
# 2. Load Gitea Configuration
# ==========================================

config_file="$HOME/.config/gitea/config.env"

if [ ! -f "$config_file" ]; then
  echo "❌ Gitea 配置不存在,请先运行 /gitea-reset"
  exit 1
fi

source "$config_file"

if [ -z "$GITEA_URL" ] || [ -z "$GITEA_TOKEN" ]; then
  echo "❌ Gitea 配置不完整 (缺少 URL 或 TOKEN)"
  echo "   请运行 /gitea-reset 重新配置"
  exit 1
fi

echo "✓ 已加载 Gitea 配置: $GITEA_URL"
echo ""

# ==========================================
# 3. Generate Runner Name
# ==========================================

if [ -n "$1" ]; then
  runner_name="$1"
  echo "使用指定的 Runner 名称: $runner_name"
else
  hostname=$(hostname -s 2>/dev/null || echo "unknown")
  runner_name="runner-$hostname-$RUNNER_MODE"
  echo "生成 Runner 名称: $runner_name"
fi

if [[ ! "$runner_name" =~ ^[a-zA-Z0-9_-]+$ ]]; then
  echo "❌ Runner 名称无效 (仅限字母、数字、下划线、连字符)"
  exit 1
fi
echo ""

# ==========================================
# 4. Check Runner Existence
# ==========================================

runners_dir="$HOME/.config/gitea/runners"
runner_dir="$runners_dir/$runner_name"

if [ "$RUNNER_MODE" = "host" ]; then
  if [ -d "$runner_dir" ]; then
    echo "❌ Runner 目录已存在: $runner_dir"
    echo "   请使用 /gitea-delete-runner 删除旧 Runner 或指定新名称"
    exit 1
  fi
else
  # Docker mode: check directory AND container
  if [ -d "$runner_dir" ]; then
    echo "❌ Runner 配置目录已存在: $runner_dir"
    exit 1
  fi
  if docker ps -a --format '{{.Names}}' | grep -q "^${runner_name}$"; then
    echo "❌ Docker 容器已存在: $runner_name"
    echo "   请先删除旧容器: docker rm -f $runner_name"
    exit 1
  fi
fi

# ==========================================
# 5. Detect System Environment & Labels
# ==========================================

echo "生成 Labels..."

if [ "$RUNNER_MODE" = "host" ]; then
  OS=$(uname -s)
  case "$OS" in
    Darwin) os_label="macOS" ;;
    Linux)  os_label="ubuntu" ;;
    *)      os_label="unknown" ;;
  esac
  
  ARCH=$(uname -m)
  case "$ARCH" in
    arm64|aarch64) arch_label="ARM64" ;;
    x86_64)        arch_label="x64" ;;
    *)             arch_label="unknown" ;;
  esac
  
  combined=$(echo "${OS}-${ARCH}" | tr '[:upper:]' '[:lower:]')
  labels="self-hosted:host,${os_label}:host,${arch_label}:host,${combined}:host"

else
  # Docker mode uses standard labels mapping to images
  # Format: label:docker://image
  labels="ubuntu-latest:docker://node:16-bullseye,ubuntu-22.04:docker://node:16-bullseye,ubuntu-20.04:docker://node:16-buster,linux:docker://node:16-bullseye"
fi

echo "✓ Labels ($RUNNER_MODE):"
echo "  $labels"
echo ""

# ==========================================
# 6. Create Runner Directory
# ==========================================

echo "创建 Runner 目录..."
mkdir -p "$runner_dir"/{cache,workspace}
if [ "$RUNNER_MODE" = "docker" ]; then
  # Docker mode might strictly need data dir mapping
  mkdir -p "$runner_dir/data"
fi
echo "✓ 目录: $runner_dir"
echo ""

# ==========================================
# 7. Get Registration Token
# ==========================================

echo "正在获取 Runner 注册 Token..."

# 默认尝试全局 Runner管理员权限
echo "尝试创建全局 Runner可用于所有组织和仓库..."

response=$(curl -s -w "\n%{http_code}" \
  -H "Authorization: token $GITEA_TOKEN" \
  "${GITEA_URL}/api/v1/admin/runners/registration-token")

http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | sed '$d')

# 如果全局 Runner 失败(权限不足),降级到组织 Runner
if [ "$http_code" != "200" ]; then
  echo "⚠️  全局 Runner 权限不足 (HTTP $http_code)"
  echo "   全局 Runner 需要管理员 Token"
  echo ""
  echo "降级到组织 Runner..."
  
  runner_level="organization"
  
  if [ -n "$GITEA_DEFAULT_ORG" ]; then
    org_name="$GITEA_DEFAULT_ORG"
  else
    read -p "请输入组织名称: " org_input
    if [ -z "$org_input" ]; then
      echo "❌ 必须指定组织名称"
      exit 1
    fi
    org_name="$org_input"
  fi
  
  echo "使用组织: $org_name"
  
  response=$(curl -s -w "\n%{http_code}" -X POST \
    -H "Authorization: token $GITEA_TOKEN" \
    "${GITEA_URL}/api/v1/orgs/$org_name/actions/runners/registration-token")
  
  http_code=$(echo "$response" | tail -n1)
  body=$(echo "$response" | sed '$d')
else
  echo "✓ 使用全局 Runner"
  runner_level="global"
fi

if [ "$http_code" != "200" ]; then
  echo "❌ 获取注册 Token 失败 (HTTP $http_code)"
  echo "$body"
  exit 1
fi

# Need jq for parsing json
if ! command -v jq &> /dev/null; then
    # Simple grep fallback if jq not available, but jq is better
    registration_token=$(echo "$body" | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
else
    registration_token=$(echo "$body" | jq -r '.token')
fi

if [ -z "$registration_token" ]; then
    echo "❌ 无法解析注册 Token"
    exit 1
fi

echo "✓ 注册 Token 已获取"
echo ""

# ==========================================
# 8. Start Runner (Register & Run)
# ==========================================

echo "启动 Runner..."

if [ "$RUNNER_MODE" = "host" ]; then
  # Host Mode Config
  cat > "$runner_dir/config.yaml" << EOF
log:
  level: info
runner:
  file: .runner
  capacity: 1
  timeout: 3h
  shutdown_timeout: 30s
  insecure: false
  fetch_timeout: 5s
  fetch_interval: 2s
  labels: []
cache:
  enabled: true
  dir: "$runner_dir/cache"
  host: "127.0.0.1"
  port: 0
host:
  workdir_parent: "$runner_dir/workspace"
EOF

  echo "注册 Host Runner..."
  act_runner register \
    --config "$runner_dir/config.yaml" \
    --instance "$GITEA_URL" \
    --token "$registration_token" \
    --name "$runner_name" \
    --labels "$labels" \
    --no-interactive

  if [ $? -ne 0 ]; then
     echo "❌ 注册失败"
     exit 1
  fi

  echo "启动后台进程..."
  nohup act_runner daemon --config "$runner_dir/config.yaml" \
    > "$runner_dir/runner.log" 2>&1 &
  runner_pid=$!
  sleep 3
  
  if ps -p $runner_pid > /dev/null 2>&1; then
    echo "✓ Host Runner 正在后台运行 (PID: $runner_pid)"
    # Save PID
    echo $runner_pid > "$runner_dir/pid"
  else
    echo "❌ 启动失败,请检查日志"
    exit 1
  fi

else
  # Docker Mode
  # Strategy: Use environment variables for auto-registration on startup
  # This avoids the "Instance Address Empty" issue seen with 'act_runner register' inside containers
  
  cat > "$runner_dir/config.yaml" << EOF
log:
  level: info
runner:
  file: /data/.runner
  capacity: 2
  timeout: 3h
  shutdown_timeout: 30s
  insecure: false
  fetch_timeout: 5s
  fetch_interval: 2s
  labels: []
cache:
  enabled: true
  dir: "/data/cache"
  host: "host.docker.internal"
  port: 9040
container:
  # 使用 host 网络模式,确保 runner 和 job 容器共处一个网络,以便 cache 能够正常访问
  network: "host"
  privileged: false
  options:
  workdir_parent: /data/workspace
  valid_volumes: []
  docker_host: ""
  force_pull: false
host:
  workdir_parent: /data/workspace
EOF

  echo "启动 Docker 容器 (自动注册)..."
  
  # 使用 host 网络模式,确保 runner 和 job 容器共处一个网络,以便 cache 能够正常访问
  docker run -d \
    --name "$runner_name" \
    --restart always \
    --network host \
    -v "$runner_dir/config.yaml:/config.yaml" \
    -v "$runner_dir/data:/data" \
    -v "/var/run/docker.sock:/var/run/docker.sock" \
    -e GITEA_INSTANCE_URL="$GITEA_URL" \
    -e GITEA_RUNNER_REGISTRATION_TOKEN="$registration_token" \
    -e GITEA_RUNNER_NAME="$runner_name" \
    -e GITEA_RUNNER_LABELS="$labels" \
    gitea/act_runner:latest daemon --config /config.yaml
    
  if [ $? -eq 0 ]; then
    echo "✓ Docker Runner 容器已启动: $runner_name"
    # 等待容器启动并查看日志
    sleep 5
    echo "容器日志:"
    docker logs "$runner_name" 2>&1 | tail -20
  else
    echo "❌ 容器启动失败"
    exit 1
  fi
fi
echo ""

# ==========================================
# 9. Display Summary
# ==========================================

echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "✅ Runner 创建成功!"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "基本信息:"
echo "  名称: $runner_name"
echo "  模式: $RUNNER_MODE"
echo "  状态: 🟢 运行中"

if [ "$RUNNER_MODE" = "host" ]; then
  echo "  PID:  $runner_pid"
  echo "  日志: $runner_dir/runner.log"
  echo "  停止: kill \$(cat $runner_dir/pid)"
else
  echo "  容器: $runner_name"
  echo "  命令: docker logs -f $runner_name"
  echo "       docker stop $runner_name"
fi

echo ""
echo "配置文件: $runner_dir/config.yaml"
echo "Labels:   $labels"
echo ""