Skip to content
220 changes: 220 additions & 0 deletions .github/workflows/ci-required-gate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
name: CI Required Gate

on:
pull_request:
branches: [master, dev]
push:
branches: [master]
workflow_dispatch:

concurrency:
group: ci-required-gate-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true

permissions:
contents: read

jobs:
changes:
name: Detect Change Scope
runs-on: ubuntu-latest
outputs:
docs_only: ${{ steps.detect.outputs.docs_only }}
dashboard_changed: ${{ steps.detect.outputs.dashboard_changed }}
steps:
- name: Checkout
uses: actions/checkout@v6
Comment on lines +25 to +26
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Using @v6 tags for core GitHub actions will currently fail because those versions do not exist.

This workflow uses actions/checkout@v6, actions/setup-python@v6, and actions/setup-node@v6, but the latest published major versions are currently checkout@v4, setup-python@v5, and setup-node@v4. Please update these to existing versions (or pin to specific SHAs) so the jobs don’t fail at the uses: resolution step.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 未把 actions/checkout|setup-python|setup-node 从 @v6 改回旧版本。
    原因:当前官方仓库已存在 v6 标签(可解析),该建议前提不成立,改回旧版本无必要。
  2. 未扩大 docs_only 白名单(如 .github/**)。
    原因:当前“保守触发完整 CI”的策略更安全,避免配置类变更被误判为 docs-only。
  3. 未把 dashboard 的 Node 从 24 改到 LTS。
    原因:当前仓库 dashboard/release 现有主链路使用 Node 24 系,保持一致性优先。

with:
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
fetch-depth: 0

- name: Detect changed files
id: detect
shell: bash
run: |
set -euo pipefail

if [[ "${{ github.event_name }}" == "pull_request" ]]; then
base_sha="${{ github.event.pull_request.base.sha }}"
head_sha="${{ github.event.pull_request.head.sha }}"
else
base_sha="${{ github.event.before }}"
head_sha="${{ github.sha }}"
fi

if [[ -z "$base_sha" || "$base_sha" == "0000000000000000000000000000000000000000" ]]; then
base_sha="$(git rev-parse "${head_sha}^" 2>/dev/null || true)"
fi

if [[ -z "$base_sha" ]]; then
changed_files="$(git ls-tree -r --name-only "$head_sha")"
else
changed_files="$(git diff --name-only "$base_sha" "$head_sha")"
fi

docs_only=true
dashboard_changed=false

while IFS= read -r f; do
[[ -z "$f" ]] && continue

if [[ "$f" == dashboard/* ]]; then
dashboard_changed=true
fi

if [[ ! "$f" =~ ^docs/ && ! "$f" =~ ^docs-[^/]+/ && ! "$f" =~ ^README.*\.md$ && ! "$f" =~ ^changelogs/ ]]; then
docs_only=false
fi
done <<< "$changed_files"

echo "docs_only=$docs_only" >> "$GITHUB_OUTPUT"
echo "dashboard_changed=$dashboard_changed" >> "$GITHUB_OUTPUT"

lint:
name: Lint (Ruff)
needs: changes
if: needs.changes.outputs.docs_only != 'true'
runs-on: ubuntu-latest
timeout-minutes: 12
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12'

- name: Install uv
run: |
python -m pip install --upgrade pip
python -m pip install uv
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Outdated

- name: Sync dependencies
run: uv sync --group dev

- name: Ruff format check
run: uv run ruff format --check .

- name: Ruff lint check
run: uv run ruff check .

test:
name: Unit Tests
needs: [changes, lint]
if: needs.changes.outputs.docs_only != 'true'
runs-on: ubuntu-latest
timeout-minutes: 35
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12'

- name: Install uv
run: |
python -m pip install --upgrade pip
python -m pip install uv

- name: Run pytest suite (script performs uv sync --dev)
run: |
# scripts/run_pytests_ci.sh includes dependency sync (`uv sync --dev`) before pytest.
bash ./scripts/run_pytests_ci.sh ./tests

smoke:
name: Smoke Test
needs: [changes, lint]
if: needs.changes.outputs.docs_only != 'true'
runs-on: ubuntu-latest
timeout-minutes: 12
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12'

- name: Install uv
run: |
python -m pip install --upgrade pip
python -m pip install uv

- name: Sync dependencies
run: uv sync --group dev

- name: Startup smoke test
shell: bash
run: |
set -euo pipefail
uv run main.py &
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
app_pid=$!

cleanup() {
kill "$app_pid" 2>/dev/null || true
wait "$app_pid" 2>/dev/null || true
}
trap cleanup EXIT

for _ in {1..60}; do
if curl -sf http://localhost:6185 >/dev/null 2>&1; then
exit 0
fi
sleep 1
done

echo "Application failed to start within 60 seconds"
exit 1

dashboard:
name: Dashboard Build
needs: changes
if: needs.changes.outputs.dashboard_changed == 'true'
runs-on: ubuntu-latest
timeout-minutes: 18
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Setup pnpm
uses: pnpm/action-setup@v4.4.0
with:
version: 10.28.2

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
cache: 'pnpm'
cache-dependency-path: dashboard/pnpm-lock.yaml

- name: Build dashboard
run: |
pnpm --dir dashboard install --frozen-lockfile
pnpm --dir dashboard run build

gate:
name: CI Required Gate
if: always()
needs: [changes, lint, test, smoke, dashboard]
runs-on: ubuntu-latest
steps:
- name: Check upstream job results
shell: bash
run: |
set -euo pipefail
if [[ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
echo "One or more required jobs failed or were cancelled"
exit 1
fi

- name: Print job summary
run: |
echo "changes=${{ needs.changes.result }}"
echo "lint=${{ needs.lint.result }}"
echo "test=${{ needs.test.result }}"
echo "smoke=${{ needs.smoke.result }}"
echo "dashboard=${{ needs.dashboard.result }}"