Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 49 additions & 78 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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 }}"

Expand All @@ -158,7 +130,6 @@ jobs:
exit 0
fi

# 两个都被跳过的情况不应该发生
if [[ "$FAST" == "skipped" ]] && [[ "$STD" == "skipped" ]]; then
echo "Error: Both CI jobs were skipped"
exit 1
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
158 changes: 158 additions & 0 deletions scripts/self-hosted/env-doctor.sh
Original file line number Diff line number Diff line change
@@ -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
Loading