diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cdd26912e..ae6ed36cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,8 @@ jobs: docs: ${{ steps.filter.outputs.docs }} steps: - uses: actions/checkout@v4 + with: + fetch-depth: 1 - uses: dorny/paths-filter@v3 id: filter with: @@ -34,168 +36,140 @@ jobs: - '*.md' - '.github/**' - # ==================== 类型检查 ==================== - typecheck: - runs-on: ubuntu-latest - timeout-minutes: 10 + # ==================== 检测 self-hosted runner 可用性 ==================== + check-runner: + runs-on: self-hosted + timeout-minutes: 1 + continue-on-error: true + outputs: + available: ${{ steps.check.outputs.available }} steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: pnpm + - id: check + run: echo "available=true" >> $GITHUB_OUTPUT - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Type check - run: pnpm typecheck - - # ==================== 单元测试 ==================== - test: - runs-on: ubuntu-latest + # ==================== 快速检查 (合并 typecheck + build + i18n) ==================== + quick-checks: + needs: check-runner + runs-on: ${{ needs.check-runner.outputs.available == 'true' && 'self-hosted' || 'ubuntu-latest' }} timeout-minutes: 10 steps: - name: Checkout uses: actions/checkout@v4 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 with: - node-version: ${{ env.NODE_VERSION }} - cache: pnpm - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Unit tests - run: pnpm test - - # ==================== i18n 检查 ==================== - i18n: - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - name: Checkout - uses: actions/checkout@v4 - + fetch-depth: 1 + # GitHub-hosted: 需要安装环境 - name: Setup Bun + if: needs.check-runner.outputs.available != 'true' uses: oven-sh/setup-bun@v2 - - name: Setup pnpm + if: needs.check-runner.outputs.available != 'true' uses: pnpm/action-setup@v4 - - name: Setup Node.js + if: needs.check-runner.outputs.available != 'true' uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: pnpm - + # 通用步骤 - name: Install dependencies run: pnpm install --frozen-lockfile - + - name: Type check + run: pnpm typecheck + - name: Build + run: pnpm build - name: Check i18n keys run: pnpm i18n:check - # ==================== 构建检查 ==================== - build: - runs-on: ubuntu-latest + # ==================== 单元测试 (独立运行,耗时长) ==================== + test: + needs: check-runner + runs-on: ${{ needs.check-runner.outputs.available == 'true' && 'self-hosted' || 'ubuntu-latest' }} timeout-minutes: 10 steps: - name: Checkout uses: actions/checkout@v4 - + with: + fetch-depth: 1 - name: Setup pnpm + if: needs.check-runner.outputs.available != 'true' uses: pnpm/action-setup@v4 - - name: Setup Node.js + if: needs.check-runner.outputs.available != 'true' uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: pnpm - - name: Install dependencies run: pnpm install --frozen-lockfile - - - name: Build - run: pnpm build + - name: Unit tests + run: pnpm test # ==================== E2E 测试(仅代码变更时运行) ==================== e2e: - needs: changes + needs: [changes, check-runner] if: needs.changes.outputs.code == 'true' - runs-on: ubuntu-latest + runs-on: ${{ needs.check-runner.outputs.available == 'true' && 'self-hosted' || 'ubuntu-latest' }} timeout-minutes: 15 steps: - name: Checkout uses: actions/checkout@v4 - + with: + fetch-depth: 1 - name: Setup Bun + if: needs.check-runner.outputs.available != 'true' uses: oven-sh/setup-bun@v2 - - name: Setup pnpm + if: needs.check-runner.outputs.available != 'true' uses: pnpm/action-setup@v4 - - name: Setup Node.js + if: needs.check-runner.outputs.available != 'true' uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: pnpm - - name: Install dependencies run: pnpm install --frozen-lockfile - + # GitHub-hosted: 需要安装 Playwright - name: Cache Playwright browsers + if: needs.check-runner.outputs.available != 'true' id: playwright-cache uses: actions/cache@v4 with: path: ~/.cache/ms-playwright key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }} - - name: Install Playwright browsers - if: steps.playwright-cache.outputs.cache-hit != 'true' + if: needs.check-runner.outputs.available != 'true' && steps.playwright-cache.outputs.cache-hit != 'true' run: pnpm exec playwright install --with-deps chromium - - name: Install Playwright deps (if cached) - if: steps.playwright-cache.outputs.cache-hit == 'true' + if: needs.check-runner.outputs.available != 'true' && steps.playwright-cache.outputs.cache-hit == 'true' run: pnpm exec playwright install-deps chromium - + # E2E 测试 - name: E2E smoke (Mobile Chrome) run: pnpm e2e bioforest-chains --project "Mobile Chrome" # ==================== 最终状态检查 ==================== checks: runs-on: ubuntu-latest - needs: [typecheck, test, i18n, build, e2e] + needs: [check-runner, quick-checks, test, e2e] if: always() steps: + - name: Report runner status + run: | + if [[ "${{ needs.check-runner.outputs.available }}" == "true" ]]; then + echo "🚀 Using self-hosted runner (M1 Mac) - fast mode" + else + echo "☁️ Using GitHub-hosted runner (ubuntu-latest)" + fi - name: Check results run: | - if [[ "${{ needs.typecheck.result }}" == "failure" ]]; then - echo "Typecheck failed" + if [[ "${{ needs.quick-checks.result }}" == "failure" ]]; then + echo "Quick checks (typecheck/build/i18n) failed" exit 1 fi if [[ "${{ needs.test.result }}" == "failure" ]]; then echo "Tests failed" exit 1 fi - if [[ "${{ needs.i18n.result }}" == "failure" ]]; then - echo "i18n check failed" - exit 1 - fi - if [[ "${{ needs.build.result }}" == "failure" ]]; then - echo "Build failed" - exit 1 - fi if [[ "${{ needs.e2e.result }}" == "failure" ]]; then echo "E2E failed" exit 1 diff --git a/scripts/self-hosted/README.md b/scripts/self-hosted/README.md new file mode 100644 index 000000000..d6ca19370 --- /dev/null +++ b/scripts/self-hosted/README.md @@ -0,0 +1,82 @@ +# Self-hosted Runner 配置指南 + +配置 GitHub Actions self-hosted runner,加速 CI/CD。 + +## 快速开始 + +```bash +# 1. 配置环境 (Node.js, pnpm, bun, Playwright) +./scripts/self-hosted/env-setup.sh + +# 2. 获取 GitHub Runner 注册 token +TOKEN=$(gh api -X POST repos/BioforestChain/KeyApp/actions/runners/registration-token --jq '.token') + +# 3. 配置多个 runner (推荐 4 个) +./scripts/self-hosted/setup-runners.sh 4 "$TOKEN" + +# 4. 启动 runners +~/actions-runners/start-all.sh +``` + +## 脚本说明 + +| 脚本 | 说明 | +|------|------| +| `env-setup.sh` | 安装 Node.js, pnpm, bun, Playwright | +| `setup-runners.sh` | 配置多个 GitHub Actions runner | + +## CI 工作流程 + +**Self-hosted 可用时 (快速模式):** +- 跳过环境安装 (预装) +- 直接: checkout → pnpm install → run +- 多个 runner 并行执行 + +**Self-hosted 不可用时 (Fallback):** +- 自动切换到 GitHub-hosted ubuntu-latest +- 完整安装流程 +- 确保 CI 不中断 + +## Runner 管理 + +脚本生成在 `~/actions-runners/`: + +```bash +~/actions-runners/start-all.sh # 启动 +~/actions-runners/stop-all.sh # 停止 +~/actions-runners/status.sh # 状态 +~/actions-runners/restart-all.sh # 重启 +``` + +## 开机自启动 (macOS) + +```bash +# 创建 LaunchAgent +cat > ~/Library/LaunchAgents/com.github.actions-runner.plist << 'EOF' + + + + + Label + com.github.actions-runner + ProgramArguments + + sh + -c + $HOME/actions-runners/start-all.sh + + RunAtLoad + + + +EOF + +# 加载 +launchctl load ~/Library/LaunchAgents/com.github.actions-runner.plist +``` + +## 系统要求 + +- macOS (M1/M2) 或 Linux (x64/arm64) +- 8GB+ RAM (推荐 16GB) +- 稳定网络 diff --git a/scripts/self-hosted/env-setup.sh b/scripts/self-hosted/env-setup.sh new file mode 100755 index 000000000..8ba0daaca --- /dev/null +++ b/scripts/self-hosted/env-setup.sh @@ -0,0 +1,131 @@ +#!/bin/bash +# Self-hosted Runner 环境配置脚本 +# 在 macOS (M1/M2) 上配置 CI 所需的所有环境 +# +# 使用方法: +# chmod +x scripts/self-hosted/env-setup.sh +# ./scripts/self-hosted/env-setup.sh +# +# 此脚本会安装: +# - Node.js 24+ (via volta) +# - pnpm (via corepack) +# - bun +# - Playwright chromium + +set -e + +echo "==========================================" +echo " Self-hosted Runner 环境配置" +echo "==========================================" +echo "" + +# 颜色 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +check_command() { + if command -v "$1" &> /dev/null; then + echo -e "${GREEN}✓${NC} $1 已安装: $($1 --version 2>&1 | head -1)" + return 0 + else + echo -e "${RED}✗${NC} $1 未安装" + return 1 + fi +} + +# ==================== 检查当前环境 ==================== +echo "检查当前环境..." +echo "" + +check_command node || NODE_MISSING=1 +check_command pnpm || PNPM_MISSING=1 +check_command bun || BUN_MISSING=1 + +echo "" + +# ==================== 安装 Volta (Node.js 版本管理) ==================== +if [ "$NODE_MISSING" = "1" ] || ! command -v volta &> /dev/null; then + echo "安装 Volta (Node.js 版本管理)..." + curl https://get.volta.sh | bash + export VOLTA_HOME="$HOME/.volta" + export PATH="$VOLTA_HOME/bin:$PATH" + echo "" +fi + +# ==================== 安装 Node.js 24 ==================== +if [ "$NODE_MISSING" = "1" ]; then + echo "安装 Node.js 24..." + if command -v volta &> /dev/null; then + volta install node@24 + elif command -v fnm &> /dev/null; then + fnm install 24 + fnm use 24 + elif command -v nvm &> /dev/null; then + nvm install 24 + nvm use 24 + else + echo -e "${YELLOW}警告: 请手动安装 Node.js 24${NC}" + echo " 推荐使用 volta: https://volta.sh" + fi + echo "" +fi + +# ==================== 启用 corepack 并安装 pnpm ==================== +if [ "$PNPM_MISSING" = "1" ]; then + echo "启用 corepack 并安装 pnpm..." + corepack enable + corepack prepare pnpm@latest --activate + echo "" +fi + +# ==================== 安装 Bun ==================== +if [ "$BUN_MISSING" = "1" ]; then + echo "安装 Bun..." + curl -fsSL https://bun.sh/install | bash + export BUN_INSTALL="$HOME/.bun" + export PATH="$BUN_INSTALL/bin:$PATH" + echo "" +fi + +# ==================== 安装项目依赖 ==================== +echo "安装项目依赖..." +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +cd "$PROJECT_ROOT" +pnpm install +echo "" + +# ==================== 安装 Playwright ==================== +echo "安装 Playwright chromium..." +pnpm exec playwright install chromium +echo "" + +# ==================== 验证安装 ==================== +echo "==========================================" +echo " 验证安装结果" +echo "==========================================" +echo "" + +check_command node +check_command pnpm +check_command bun + +echo "" +echo "检查 Playwright..." +if [ -d "$HOME/.cache/ms-playwright" ] || [ -d "$HOME/Library/Caches/ms-playwright" ]; then + echo -e "${GREEN}✓${NC} Playwright 浏览器已安装" +else + echo -e "${YELLOW}⚠${NC} Playwright 浏览器可能未安装,请运行: pnpm exec playwright install chromium" +fi + +echo "" +echo "==========================================" +echo -e " ${GREEN}环境配置完成!${NC}" +echo "==========================================" +echo "" +echo "下一步: 配置 GitHub Actions Runner" +echo " ./scripts/self-hosted/setup-runners.sh " +echo "" diff --git a/scripts/self-hosted/setup-runners.sh b/scripts/self-hosted/setup-runners.sh new file mode 100755 index 000000000..248c2aaa2 --- /dev/null +++ b/scripts/self-hosted/setup-runners.sh @@ -0,0 +1,208 @@ +#!/bin/bash +# GitHub Actions Self-hosted Runner 批量配置脚本 +# +# 使用方法: +# ./scripts/self-hosted/setup-runners.sh +# +# 获取 token: +# gh api -X POST repos/OWNER/REPO/actions/runners/registration-token --jq '.token' +# +# 示例: +# ./scripts/self-hosted/setup-runners.sh 4 AXXXX... + +set -e + +NUM_RUNNERS=${1:-4} +GITHUB_TOKEN=${2:-} +REPO=${GITHUB_REPOSITORY:-"BioforestChain/KeyApp"} +RUNNER_VERSION="2.321.0" # https://github.com/actions/runner/releases +RUNNER_BASE_DIR="$HOME/actions-runners" + +echo "==========================================" +echo " GitHub Actions Self-hosted Runner 配置" +echo "==========================================" +echo "" +echo "Runner 数量: $NUM_RUNNERS" +echo "仓库: $REPO" +echo "安装目录: $RUNNER_BASE_DIR" +echo "" + +# 检查 token +if [ -z "$GITHUB_TOKEN" ]; then + echo "请提供 GitHub token 作为第二个参数" + echo "" + echo "获取 token 的方法:" + echo "" + echo " 方法 1 - 使用 gh CLI:" + echo " gh api -X POST repos/$REPO/actions/runners/registration-token --jq '.token'" + echo "" + echo " 方法 2 - GitHub 网页:" + echo " https://github.com/$REPO/settings/actions/runners/new" + echo " 复制页面上显示的 token" + echo "" + exit 1 +fi + +# 检测系统架构 +ARCH=$(uname -m) +OS=$(uname -s | tr '[:upper:]' '[:lower:]') + +case "$ARCH" in + arm64|aarch64) + RUNNER_ARCH="arm64" + ;; + x86_64) + RUNNER_ARCH="x64" + ;; + *) + echo "不支持的架构: $ARCH" + exit 1 + ;; +esac + +case "$OS" in + darwin) + RUNNER_OS="osx" + ;; + linux) + RUNNER_OS="linux" + ;; + *) + echo "不支持的操作系统: $OS" + exit 1 + ;; +esac + +RUNNER_TAR="actions-runner-${RUNNER_OS}-${RUNNER_ARCH}-${RUNNER_VERSION}.tar.gz" +RUNNER_URL="https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/${RUNNER_TAR}" + +echo "Runner 包: $RUNNER_TAR" +echo "" + +# 创建基础目录 +mkdir -p "$RUNNER_BASE_DIR" +cd "$RUNNER_BASE_DIR" + +# 下载 runner +if [ ! -f "$RUNNER_TAR" ]; then + echo "下载 GitHub Actions Runner v${RUNNER_VERSION}..." + curl -o "$RUNNER_TAR" -L "$RUNNER_URL" + echo "" +fi + +# 配置每个 runner +for i in $(seq 1 $NUM_RUNNERS); do + RUNNER_DIR="$RUNNER_BASE_DIR/runner-$i" + RUNNER_NAME="$(hostname)-runner-$i" + + echo "==========================================" + echo " 配置 Runner $i: $RUNNER_NAME" + echo "==========================================" + + # 如果已存在且已配置,跳过 + if [ -d "$RUNNER_DIR" ] && [ -f "$RUNNER_DIR/.runner" ]; then + echo "Runner $i 已配置,跳过" + continue + fi + + # 创建目录并解压 + mkdir -p "$RUNNER_DIR" + tar xzf "$RUNNER_TAR" -C "$RUNNER_DIR" + + # 配置 runner + cd "$RUNNER_DIR" + ./config.sh \ + --url "https://github.com/$REPO" \ + --token "$GITHUB_TOKEN" \ + --name "$RUNNER_NAME" \ + --labels "self-hosted,${RUNNER_OS},${RUNNER_ARCH}" \ + --work "_work" \ + --unattended \ + --replace + + cd "$RUNNER_BASE_DIR" + echo "" +done + +# 生成管理脚本 +echo "生成管理脚本..." + +# start-all.sh +cat > "$RUNNER_BASE_DIR/start-all.sh" << 'SCRIPT' +#!/bin/bash +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +echo "启动所有 runners..." +for dir in "$SCRIPT_DIR"/runner-*; do + if [ -d "$dir" ] && [ -f "$dir/run.sh" ]; then + NAME=$(basename "$dir") + if pgrep -f "$dir/bin/Runner.Listener" > /dev/null; then + echo " $NAME: 已在运行" + else + echo " $NAME: 启动中..." + cd "$dir" + nohup ./run.sh > runner.log 2>&1 & + echo " $NAME: PID $!" + fi + fi +done +echo "" +echo "查看日志: tail -f $SCRIPT_DIR/runner-*/runner.log" +SCRIPT +chmod +x "$RUNNER_BASE_DIR/start-all.sh" + +# stop-all.sh +cat > "$RUNNER_BASE_DIR/stop-all.sh" << 'SCRIPT' +#!/bin/bash +echo "停止所有 runners..." +pkill -f "Runner.Listener" 2>/dev/null || true +pkill -f "Runner.Worker" 2>/dev/null || true +echo "完成" +SCRIPT +chmod +x "$RUNNER_BASE_DIR/stop-all.sh" + +# status.sh +cat > "$RUNNER_BASE_DIR/status.sh" << 'SCRIPT' +#!/bin/bash +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +echo "Runner 状态:" +echo "" +for dir in "$SCRIPT_DIR"/runner-*; do + if [ -d "$dir" ]; then + NAME=$(basename "$dir") + if pgrep -f "$dir/bin/Runner.Listener" > /dev/null; then + PID=$(pgrep -f "$dir/bin/Runner.Listener") + echo " ✓ $NAME: 运行中 (PID: $PID)" + else + echo " ✗ $NAME: 已停止" + fi + fi +done +SCRIPT +chmod +x "$RUNNER_BASE_DIR/status.sh" + +# restart-all.sh +cat > "$RUNNER_BASE_DIR/restart-all.sh" << 'SCRIPT' +#!/bin/bash +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +"$SCRIPT_DIR/stop-all.sh" +sleep 2 +"$SCRIPT_DIR/start-all.sh" +SCRIPT +chmod +x "$RUNNER_BASE_DIR/restart-all.sh" + +echo "" +echo "==========================================" +echo " 配置完成!" +echo "==========================================" +echo "" +echo "管理脚本:" +echo " $RUNNER_BASE_DIR/start-all.sh - 启动所有 runners" +echo " $RUNNER_BASE_DIR/stop-all.sh - 停止所有 runners" +echo " $RUNNER_BASE_DIR/status.sh - 查看状态" +echo " $RUNNER_BASE_DIR/restart-all.sh - 重启所有 runners" +echo "" +echo "现在执行:" +echo " $RUNNER_BASE_DIR/start-all.sh" +echo ""