diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d759fc482..55730f398 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,44 +8,9 @@ env: NODE_VERSION: '24' jobs: - # ==================== 变更检测 ==================== - changes: - runs-on: ubuntu-latest - outputs: - code: ${{ steps.filter.outputs.code }} - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - uses: dorny/paths-filter@v3 - id: filter - with: - filters: | - code: - - 'src/**' - - 'e2e/**' - - 'package.json' - - 'pnpm-lock.yaml' - - 'vite.config.ts' - - 'vitest.config.ts' - - 'playwright.config.ts' - - 'tsconfig*.json' - - # ==================== 检测 self-hosted runner 可用性 ==================== - check-runner: - runs-on: self-hosted - timeout-minutes: 1 - continue-on-error: true - outputs: - available: ${{ steps.check.outputs.available }} - steps: - - id: check - run: echo "available=true" >> $GITHUB_OUTPUT - # ==================== Self-hosted 快速路径 (无 action 下载) ==================== ci-fast: - needs: [changes, check-runner] - if: needs.check-runner.outputs.available == 'true' + if: vars.USE_SELF_HOSTED == 'true' runs-on: self-hosted timeout-minutes: 15 steps: @@ -57,94 +22,101 @@ jobs: git fetch origin ${{ github.event.pull_request.head.sha || github.sha }} --depth=1 git checkout ${{ github.event.pull_request.head.sha || github.sha }} - - name: Install dependencies - run: pnpm install --frozen-lockfile + - name: Detect changes + id: changes + run: | + CODE_CHANGED=$(git diff --name-only origin/${{ github.base_ref || 'main' }}...HEAD | grep -E '^src/|^e2e/' || true) + if [ -n "$CODE_CHANGED" ]; then + echo "code=true" >> $GITHUB_OUTPUT + else + echo "code=false" >> $GITHUB_OUTPUT + fi - - name: Run all checks (turbo parallel) + - name: Install & Run run: | - # 运行所有检查,输出到日志文件 + pnpm install --frozen-lockfile + + if [ "${{ steps.changes.outputs.code }}" == "true" ]; then + TASKS="typecheck:run build i18n:run test:run e2e:ci" + else + TASKS="typecheck:run build i18n:run test:run" + fi + set +e - pnpm turbo run typecheck:run build i18n:run test:run 2>&1 | tee turbo-output.log + pnpm turbo run $TASKS 2>&1 | tee turbo-output.log TURBO_EXIT=$? set -e - # 如果失败,打印详细错误 if [ $TURBO_EXIT -ne 0 ]; then echo "" echo "========== TURBO FAILED ==========" - echo "Exit code: $TURBO_EXIT" - echo "" - echo "========== ERROR DETAILS ==========" - # 提取错误信息 grep -A 50 "error\|Error\|ERROR\|failed\|Failed\|FAILED" turbo-output.log || cat turbo-output.log exit $TURBO_EXIT fi echo "All checks passed!" - - name: Run E2E (if code changed) - if: needs.changes.outputs.code == 'true' - run: | - set +e - pnpm e2e bioforest-chains --project "Mobile Chrome" 2>&1 | tee e2e-output.log - E2E_EXIT=$? - set -e - - if [ $E2E_EXIT -ne 0 ]; then - echo "" - echo "========== E2E FAILED ==========" - cat e2e-output.log - exit $E2E_EXIT - fi - - # ==================== GitHub-hosted 标准路径 (Fallback) ==================== + # ==================== GitHub-hosted 标准路径 ==================== ci-standard: - needs: [changes, check-runner] - if: needs.check-runner.outputs.available != 'true' + if: vars.USE_SELF_HOSTED != 'true' runs-on: ubuntu-latest timeout-minutes: 15 steps: - uses: actions/checkout@v4 with: fetch-depth: 1 + + - name: Detect changes + id: changes + uses: dorny/paths-filter@v3 + with: + filters: | + code: + - 'src/**' + - 'e2e/**' + - uses: oven-sh/setup-bun@v2 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: pnpm + - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Run all checks - run: pnpm turbo run typecheck:run build i18n:run test:run + - name: Cache Playwright browsers - if: needs.changes.outputs.code == 'true' + if: steps.changes.outputs.code == 'true' id: playwright-cache uses: actions/cache@v4 with: path: ~/.cache/ms-playwright key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }} + - name: Install Playwright - if: needs.changes.outputs.code == 'true' && steps.playwright-cache.outputs.cache-hit != 'true' + if: steps.changes.outputs.code == 'true' && steps.playwright-cache.outputs.cache-hit != 'true' run: pnpm exec playwright install --with-deps chromium + - name: Install Playwright deps - if: needs.changes.outputs.code == 'true' && steps.playwright-cache.outputs.cache-hit == 'true' + if: steps.changes.outputs.code == 'true' && steps.playwright-cache.outputs.cache-hit == 'true' run: pnpm exec playwright install-deps chromium - - name: E2E tests - if: needs.changes.outputs.code == 'true' - run: pnpm e2e bioforest-chains --project "Mobile Chrome" - # ==================== 最终状态检查 ==================== + - name: Run all checks + run: | + if [ "${{ steps.changes.outputs.code }}" == "true" ]; then + pnpm turbo run typecheck:run build i18n:run test:run e2e:ci + else + pnpm turbo run typecheck:run build i18n:run test:run + fi + + # ==================== 状态汇总 (分支保护需要) ==================== checks: runs-on: ubuntu-latest - needs: [check-runner, ci-fast, ci-standard] + needs: [ci-fast, ci-standard] if: always() steps: - name: Check results run: | - echo "Runner mode: ${{ needs.check-runner.outputs.available == 'true' && 'self-hosted (fast)' || 'github-hosted (standard)' }}" - - # ci-fast 或 ci-standard 其中一个会运行 FAST="${{ needs.ci-fast.result }}" STD="${{ needs.ci-standard.result }}" @@ -158,7 +130,6 @@ jobs: exit 0 fi - # 两个都被跳过的情况不应该发生 if [[ "$FAST" == "skipped" ]] && [[ "$STD" == "skipped" ]]; then echo "Error: Both CI jobs were skipped" exit 1 diff --git a/package.json b/package.json index a378bf5a1..a4144277a 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "e2e": "bun scripts/e2e.ts", "e2e:all": "turbo run e2e:run --", "e2e:run": "playwright test", + "e2e:ci": "playwright test bioforest-chains --project 'Mobile Chrome'", "e2e:ui": "playwright test --ui", "e2e:headed": "playwright test --headed", "e2e:update": "playwright test --update-snapshots", diff --git a/scripts/self-hosted/env-doctor.sh b/scripts/self-hosted/env-doctor.sh new file mode 100755 index 000000000..bc7ce1d63 --- /dev/null +++ b/scripts/self-hosted/env-doctor.sh @@ -0,0 +1,158 @@ +#!/bin/bash +# Self-hosted Runner 环境检查脚本 +# +# 检查项: +# 1. 必要工具: Node.js, pnpm, bun, Playwright +# 2. gh CLI 安装和登录状态 +# 3. gh 对仓库的权限 + +set -e + +# 从 git remote 获取仓库信息(支持 fork) +get_repo_from_git() { + local remote_url=$(git remote get-url origin 2>/dev/null) + if [ -n "$remote_url" ]; then + echo "$remote_url" | sed -E 's#.*github.com[:/]([^/]+/[^/]+?)(\.git)?$#\1#' | sed 's/\.git$//' + fi +} + +REPO=${GITHUB_REPOSITORY:-$(get_repo_from_git)} +if [ -z "$REPO" ]; then + echo "警告: 无法获取仓库信息,部分检查将跳过" + REPO="unknown/repo" +fi + +echo "==========================================" +echo " Self-hosted Runner 环境检查" +echo "==========================================" +echo "" + +ERRORS=0 +WARNINGS=0 + +# 检查函数 +check_command() { + local cmd=$1 + local name=$2 + local required=${3:-true} + + if command -v "$cmd" &> /dev/null; then + VERSION=$("$cmd" --version 2>/dev/null | head -1) + echo " ✓ $name: $VERSION" + return 0 + else + if [ "$required" = "true" ]; then + echo " ✗ $name: 未安装" + ((ERRORS++)) + else + echo " △ $name: 未安装 (可选)" + ((WARNINGS++)) + fi + return 1 + fi +} + +# 1. 检查必要工具 +echo "【1/4】检查必要工具" +echo "" +check_command "node" "Node.js" +check_command "pnpm" "pnpm" +check_command "bun" "Bun" +check_command "gh" "GitHub CLI" +echo "" + +# 2. 检查 Playwright +echo "【2/4】检查 Playwright" +echo "" +if command -v playwright &> /dev/null; then + VERSION=$(playwright --version 2>/dev/null || echo "已安装") + echo " ✓ Playwright: $VERSION" +elif [ -d "$HOME/.cache/ms-playwright" ]; then + BROWSERS=$(ls "$HOME/.cache/ms-playwright" 2>/dev/null | wc -l | tr -d ' ') + echo " ✓ Playwright browsers: $BROWSERS 个" +else + echo " △ Playwright: 未检测到浏览器缓存" + echo " 运行: pnpm exec playwright install chromium" + ((WARNINGS++)) +fi +echo "" + +# 3. 检查 gh CLI 状态 +echo "【3/4】检查 GitHub CLI 状态" +echo "" +if command -v gh &> /dev/null; then + # 检查登录状态 + if gh auth status &> /dev/null; then + USER=$(gh api user --jq '.login' 2>/dev/null || echo "unknown") + echo " ✓ 已登录: $USER" + + # 检查仓库权限 + echo "" + echo "【4/4】检查仓库权限 ($REPO)" + echo "" + + # 检查读取权限 + if gh repo view "$REPO" &> /dev/null; then + echo " ✓ 仓库访问: 可读" + else + echo " ✗ 仓库访问: 无权限" + ((ERRORS++)) + fi + + # 检查 variables 权限 + if gh variable list --repo "$REPO" &> /dev/null; then + echo " ✓ Variables: 可读写" + + # 检查 USE_SELF_HOSTED 变量 + CURRENT=$(gh variable get USE_SELF_HOSTED --repo "$REPO" 2>/dev/null || echo "未设置") + echo " ✓ USE_SELF_HOSTED: $CURRENT" + else + echo " ✗ Variables: 无权限" + echo " 需要 repo 或 admin:repo_hook 权限" + ((ERRORS++)) + fi + + # 检查 Actions 权限 + if gh api "repos/$REPO/actions/runners" &> /dev/null; then + RUNNERS=$(gh api "repos/$REPO/actions/runners" --jq '.total_count' 2>/dev/null || echo "0") + echo " ✓ Actions Runners: $RUNNERS 个已注册" + else + echo " △ Actions API: 无法访问" + ((WARNINGS++)) + fi + else + echo " ✗ 未登录" + echo " 运行: gh auth login" + ((ERRORS++)) + echo "" + echo "【4/4】跳过仓库权限检查 (需要先登录)" + fi +else + echo " ✗ gh CLI 未安装" + echo "" + echo "【4/4】跳过仓库权限检查 (需要先安装 gh)" +fi + +echo "" +echo "==========================================" +echo " 检查结果" +echo "==========================================" +echo "" + +if [ $ERRORS -gt 0 ]; then + echo " ✗ 发现 $ERRORS 个错误" + echo "" + echo "修复建议:" + echo " 1. 运行 ./scripts/self-hosted/env-setup.sh 安装依赖" + echo " 2. 运行 gh auth login 登录 GitHub" + echo "" + exit 1 +elif [ $WARNINGS -gt 0 ]; then + echo " △ 发现 $WARNINGS 个警告,但可以运行" + echo "" + exit 0 +else + echo " ✓ 所有检查通过!" + echo "" + exit 0 +fi diff --git a/scripts/self-hosted/setup-runners.sh b/scripts/self-hosted/setup-runners.sh index 248c2aaa2..ee7860f87 100755 --- a/scripts/self-hosted/setup-runners.sh +++ b/scripts/self-hosted/setup-runners.sh @@ -14,10 +14,25 @@ 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" +# 从 git remote 获取仓库信息(支持 fork) +get_repo_from_git() { + local remote_url=$(git remote get-url origin 2>/dev/null) + if [ -n "$remote_url" ]; then + # 支持 https://github.com/owner/repo.git 和 git@github.com:owner/repo.git + echo "$remote_url" | sed -E 's#.*github.com[:/]([^/]+/[^/]+?)(\.git)?$#\1#' | sed 's/\.git$//' + fi +} + +REPO=${GITHUB_REPOSITORY:-$(get_repo_from_git)} +if [ -z "$REPO" ]; then + echo "错误: 无法获取仓库信息" + echo "请在 git 仓库目录中运行,或设置 GITHUB_REPOSITORY 环境变量" + exit 1 +fi + echo "==========================================" echo " GitHub Actions Self-hosted Runner 配置" echo "==========================================" @@ -128,35 +143,63 @@ done echo "生成管理脚本..." # start-all.sh -cat > "$RUNNER_BASE_DIR/start-all.sh" << 'SCRIPT' +cat > "$RUNNER_BASE_DIR/start-all.sh" << SCRIPT #!/bin/bash -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SCRIPT_DIR="\$(cd "\$(dirname "\$0")" && pwd)" +REPO="$REPO" 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: 已在运行" +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" + echo " \$NAME: 启动中..." + cd "\$dir" nohup ./run.sh > runner.log 2>&1 & - echo " $NAME: PID $!" + echo " \$NAME: PID \$!" fi fi done + +# 设置 GitHub 变量启用 self-hosted echo "" -echo "查看日志: tail -f $SCRIPT_DIR/runner-*/runner.log" +echo "设置 USE_SELF_HOSTED=true..." +if command -v gh &> /dev/null && gh auth status &> /dev/null; then + gh variable set USE_SELF_HOSTED --body "true" --repo "\$REPO" 2>/dev/null && \ + echo " ✓ CI 已切换到 self-hosted 模式" || \ + echo " △ 无法设置变量,请手动设置" +else + echo " △ gh CLI 未登录,请手动设置 USE_SELF_HOSTED=true" +fi + +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' +cat > "$RUNNER_BASE_DIR/stop-all.sh" << SCRIPT #!/bin/bash +REPO="$REPO" + echo "停止所有 runners..." pkill -f "Runner.Listener" 2>/dev/null || true pkill -f "Runner.Worker" 2>/dev/null || true + +# 设置 GitHub 变量禁用 self-hosted +echo "" +echo "设置 USE_SELF_HOSTED=false..." +if command -v gh &> /dev/null && gh auth status &> /dev/null; then + gh variable set USE_SELF_HOSTED --body "false" --repo "\$REPO" 2>/dev/null && \ + echo " ✓ CI 已切换到 github-hosted 模式" || \ + echo " △ 无法设置变量,请手动设置" +else + echo " △ gh CLI 未登录,请手动设置 USE_SELF_HOSTED=false" +fi + +echo "" echo "完成" SCRIPT chmod +x "$RUNNER_BASE_DIR/stop-all.sh" diff --git a/turbo.json b/turbo.json index 270d58038..030c7cdf9 100644 --- a/turbo.json +++ b/turbo.json @@ -32,6 +32,19 @@ "outputs": ["e2e/report/**", "e2e/test-results/**"], "cache": true }, + "e2e:ci": { + "dependsOn": [], + "inputs": [ + "src/**", + "e2e/**/*.ts", + "e2e/__screenshots__/**", + "playwright.config.ts", + "!e2e/test-results/**", + "!e2e/report/**" + ], + "outputs": ["e2e/report/**", "e2e/test-results/**"], + "cache": true + }, "build": { "dependsOn": ["typecheck:run"], "inputs": ["src/**", "index.html", "vite.config.ts", "tsconfig*.json"],