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
2 changes: 1 addition & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
- [ ] Frontend:format、lint、coverage、build
- [ ] Compose Playwright E2E 或说明不适用原因
- [ ] Golden / Roundtrip / Migration / Failure-path(按变更适用)
- [ ] 阶段候选稳定后已添加 `ci:milestone`,最新 Head 的完整远程门禁通过
- [ ] 已选择 `ci:light` 或 `ci:milestone`,最新 Head 的对应远程门禁通过;轻量模式的本地定向验证已记录
- 证据:<!-- 命令、CI Run、截图或 Trace;不要粘贴 Secret -->

## 风险、审批与 Review Thread
Expand Down
36 changes: 0 additions & 36 deletions .github/workflows/backend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,42 +37,6 @@ on:
- "scripts/capacity_s19.py"
- "scripts/capacity_s29.py"
- ".github/workflows/backend-ci.yml"
push:
branches: [main]
paths:
- "backend/**"
- "skills/**"
- "scripts/smoke_s3.py"
- "scripts/smoke_s4.py"
- "scripts/smoke_s5.py"
- "scripts/smoke_s6.py"
- "scripts/smoke_s7.py"
- "scripts/smoke_s8.py"
- "scripts/smoke_s9.py"
- "scripts/smoke_s10.py"
- "scripts/smoke_s11.py"
- "scripts/smoke_s18.py"
- "scripts/smoke_s19.py"
- "scripts/smoke_s21.py"
- "scripts/smoke_s22.py"
- "scripts/smoke_s23.py"
- "scripts/smoke_s24.py"
- "scripts/smoke_s25.py"
- "scripts/smoke_s26.py"
- "scripts/smoke_s27.py"
- "scripts/smoke_s28.py"
- "scripts/smoke_s29.py"
- "scripts/smoke_s30.py"
- "scripts/smoke_s47.py"
- "scripts/verify_s47_2_contract_migration.py"
- "scripts/verify_s47_3_contract_migration.py"
- "scripts/verify_kafka_compat.py"
- "scripts/storage_transfer.py"
- "scripts/capacity_s11.py"
- "scripts/capacity_workflow.py"
- "scripts/capacity_s19.py"
- "scripts/capacity_s29.py"
- ".github/workflows/backend-ci.yml"
workflow_dispatch:

concurrency:
Expand Down
43 changes: 0 additions & 43 deletions .github/workflows/compose-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,49 +44,6 @@ on:
- "scripts/backup.sh"
- "scripts/verify_restore.sh"
- ".github/workflows/compose-ci.yml"
push:
branches: [main]
paths:
- "backend/**"
- "frontend/**"
- "mock-target/**"
- "compose.yaml"
- "deploy/compact/**"
- "deploy/compatibility/**"
- "deploy/s47/**"
- "scripts/smoke_s3.py"
- "scripts/smoke_s4.py"
- "scripts/smoke_s5.py"
- "scripts/smoke_s6.py"
- "scripts/smoke_s7.py"
- "scripts/smoke_s8.py"
- "scripts/smoke_s9.py"
- "scripts/smoke_s10.py"
- "scripts/smoke_s11.py"
- "scripts/smoke_s18.py"
- "scripts/smoke_s19.py"
- "scripts/smoke_s21.py"
- "scripts/smoke_s22.py"
- "scripts/smoke_s23.py"
- "scripts/smoke_s24.py"
- "scripts/smoke_s25.py"
- "scripts/smoke_s26.py"
- "scripts/smoke_s27.py"
- "scripts/smoke_s28.py"
- "scripts/smoke_s29.py"
- "scripts/smoke_s30.py"
- "scripts/smoke_s31.py"
- "scripts/smoke_s32.py"
- "scripts/smoke_s47.py"
- "scripts/verify_kafka_compat.py"
- "scripts/storage_transfer.py"
- "scripts/capacity_s11.py"
- "scripts/capacity_workflow.py"
- "scripts/capacity_s19.py"
- "scripts/capacity_s29.py"
- "scripts/backup.sh"
- "scripts/verify_restore.sh"
- ".github/workflows/compose-ci.yml"
workflow_dispatch:
inputs:
run_rc_gates:
Expand Down
5 changes: 0 additions & 5 deletions .github/workflows/frontend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,6 @@ on:
paths:
- "frontend/**"
- ".github/workflows/frontend-ci.yml"
push:
branches: [main]
paths:
- "frontend/**"
- ".github/workflows/frontend-ci.yml"
workflow_dispatch:

concurrency:
Expand Down
34 changes: 34 additions & 0 deletions .github/workflows/quick-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Quick CI

on:
pull_request:
types: [labeled]
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
quick:
if: ${{ github.event_name != 'pull_request' || github.event.label.name == 'ci:light' }}
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
persist-credentials: false
- name: Check changed files
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
if [[ -z "${BASE_SHA}" || -z "${HEAD_SHA}" ]]; then
git diff --check HEAD^ HEAD
exit 0
fi
git diff --check "${BASE_SHA}" "${HEAD_SHA}"
26 changes: 23 additions & 3 deletions .github/workflows/required-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ name: Required Gate Controller
on:
pull_request_target:
types: [labeled]
push:
branches: [main]
workflow_dispatch:

concurrency:
Expand All @@ -19,7 +17,7 @@ permissions:

jobs:
controller:
if: ${{ github.event_name != 'pull_request_target' || github.event.label.name == 'ci:milestone' }}
if: ${{ github.event_name != 'pull_request_target' || github.event.label.name == 'ci:milestone' || github.event.label.name == 'ci:light' }}
name: Required Gate Controller
runs-on: ubuntu-latest
timeout-minutes: 100
Expand All @@ -41,6 +39,21 @@ jobs:
fetch-depth: 0
persist-credentials: false
ref: ${{ github.event.pull_request.base.sha || github.sha }}
- name: Resolve current CI mode
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
mode='full'
if [[ -n "${PR_NUMBER}" ]]; then
mode="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" \
--jq 'if (([.labels[].name] | index("ci:milestone")) and ([.labels[].name] | index("ci:light"))) then "invalid" elif ([.labels[].name] | index("ci:milestone")) then "full" elif ([.labels[].name] | index("ci:light")) then "light" else "invalid" end')"
fi
if [[ "${mode}" == 'invalid' ]]; then
echo '当前 PR 缺少 CI 模式标签' >&2
exit 1
fi
echo "FLOWTEST_GATE_MODE=${mode}" >>"${GITHUB_ENV}"
- name: Resolve changed paths from trusted metadata
env:
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
Expand Down Expand Up @@ -76,6 +89,7 @@ jobs:
--sha "${FLOWTEST_GATE_SHA}"
--paths-file "${FLOWTEST_GATE_PATHS_FILE}"
--event-name "${GITHUB_EVENT_NAME}"
--mode "${FLOWTEST_GATE_MODE}"
- name: Complete trusted Required Gate status
if: always()
env:
Expand All @@ -96,6 +110,12 @@ jobs:
metadata_current='false'
description='PR 基线或 Head 已变更'
fi
current_mode="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" \
--jq 'if (([.labels[].name] | index("ci:milestone")) and ([.labels[].name] | index("ci:light"))) then "invalid" elif ([.labels[].name] | index("ci:milestone")) then "full" elif ([.labels[].name] | index("ci:light")) then "light" else "invalid" end')"
if [[ "${current_mode}" != "${FLOWTEST_GATE_MODE}" ]]; then
metadata_current='false'
description='PR 的 CI 模式标签已变更'
fi
fi
if [[ "${GATE_OUTCOME}" == 'success' && "${metadata_current}" == 'true' ]]; then
state='success'
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/security-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ name: Security CI
on:
pull_request:
types: [labeled]
push:
branches: [main]
workflow_dispatch:

concurrency:
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/standalone-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ on:
- "frontend/**"
- "deploy/standalone/**"
- "scripts/storage_transfer.py"
push:
branches: [main]
workflow_dispatch:

concurrency:
Expand Down
11 changes: 0 additions & 11 deletions .github/workflows/upgrade-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,6 @@ on:
- "scripts/storage_transfer.py"
- "scripts/verify_v2_v3_data.py"
- "scripts/verify_v2_v3_upgrade.sh"
push:
branches: [main]
paths:
- ".github/workflows/upgrade-ci.yml"
- "backend/**"
- "deploy/upgrade/**"
- "mock-target/**"
- "scripts/smoke_s11.py"
- "scripts/storage_transfer.py"
- "scripts/verify_v2_v3_data.py"
- "scripts/verify_v2_v3_upgrade.sh"
workflow_dispatch:

concurrency:
Expand Down
61 changes: 60 additions & 1 deletion backend/tests/test_required_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,46 @@ def test_required_gate_marks_irrelevant_checks_as_no_op() -> None:
}


def test_light_gate_requires_only_quick_ci() -> None:
plan = required_gate.build_gate_plan(["backend/app/services/projects.py"], mode="light")

assert _keys(plan) == {"quick"}
assert {spec.key for spec in plan.no_op} == {
"backend",
"frontend",
"security",
"compose",
"standalone",
"upgrade",
}


def test_required_gate_rejects_unknown_mode() -> None:
with pytest.raises(required_gate.RequiredGateError, match="未知 CI 模式"):
required_gate.build_gate_plan([], mode="skip")


def test_light_gate_accepts_documentation_only() -> None:
required_gate.enforce_light_scope(
["README.md", "docs/operations/deployment.md", ".github/PULL_REQUEST_TEMPLATE.md"],
"light",
)


@pytest.mark.parametrize(
"paths",
[
["backend/app/services/auth.py"],
["docs/ci-milestone.md", "frontend/src/App.tsx"],
["docs/assets/diagram.png"],
[],
],
)
def test_light_gate_rejects_runtime_or_non_document_paths(paths: list[str]) -> None:
with pytest.raises(required_gate.RequiredGateError, match="ci:light 只适用于"):
required_gate.enforce_light_scope(paths, "light")


def test_required_gate_selects_backend_dependent_checks() -> None:
plan = required_gate.build_gate_plan(["backend/app/services/projects.py"])

Expand Down Expand Up @@ -219,14 +259,16 @@ def test_required_gate_controller_runs_trusted_base_code() -> None:

assert "pull_request_target" in workflow["on"]
assert "pull_request" not in workflow["on"]
assert "push" not in workflow["on"]
assert workflow["on"]["pull_request_target"]["types"] == ["labeled"]
assert workflow["concurrency"]["cancel-in-progress"] == "true"
assert workflow["permissions"]["statuses"] == "write"
assert "checks" not in workflow["permissions"]
controller = workflow["jobs"]["controller"]
assert controller["if"] == (
"${{ github.event_name != 'pull_request_target' "
"|| github.event.label.name == 'ci:milestone' }}"
"|| github.event.label.name == 'ci:milestone' "
"|| github.event.label.name == 'ci:light' }}"
)
assert controller["name"] == "Required Gate Controller"
checkout = next(step for step in controller["steps"] if "uses" in step)
Expand Down Expand Up @@ -256,6 +298,22 @@ def test_required_gate_controller_runs_trusted_base_code() -> None:
assert complete_status["if"] == "always()"
assert ".base.sha, .head.sha" in complete_status["run"]
assert '"${BASE_SHA} ${HEAD_SHA}"' in complete_status["run"]
assert "${FLOWTEST_GATE_MODE}" in complete_status["run"]


def test_quick_ci_runs_only_for_light_label() -> None:
workflow = yaml.load(
(WORKSPACE_ROOT / ".github/workflows/quick-ci.yml").read_text(encoding="utf-8"),
Loader=yaml.BaseLoader,
)

assert workflow["on"]["pull_request"]["types"] == ["labeled"]
assert "push" not in workflow["on"]
assert workflow["jobs"]["quick"]["if"] == (
"${{ github.event_name != 'pull_request' || github.event.label.name == 'ci:light' }}"
)
quick_steps = workflow["jobs"]["quick"]["steps"]
assert any("git diff --check" in step.get("run", "") for step in quick_steps)


@pytest.mark.parametrize(
Expand All @@ -279,6 +337,7 @@ def test_pull_request_ci_runs_only_for_milestone_label(
)

assert workflow["on"]["pull_request"]["types"] == ["labeled"]
assert "push" not in workflow["on"]
assert workflow["concurrency"]["cancel-in-progress"] == "true"
expected_condition = (
"${{ github.event_name != 'pull_request' || github.event.label.name == 'ci:milestone' }}"
Expand Down
20 changes: 13 additions & 7 deletions docs/ci-milestone.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
# 阶段节点 CI
# PR CI 开关

FlowTest 的 PR 使用 `ci:milestone` 标签启动完整远程 CI。开发过程中的普通提交不会自动启动远程重任务,开发者应先完成本地定向检查,在一个功能阶段稳定后再运行完整门禁。
FlowTest 的 PR 通过 GitHub 标签选择一次远程检查。普通提交不会自动启动远程重任务;先完成适用的本地验证,再给稳定的候选提交加标签。

| 标签 | 用途 | PR 合并门禁 |
| --- | --- | --- |
| `ci:light` | 仅 `README.md`、PR 模板和 `docs/` 下的 Markdown 文档 | Quick CI:检查差异空白错误。不会运行功能测试、构建、容器或安全扫描;其他路径的 PR 会被 Required Gate 拒绝。 |
| `ci:milestone` | 功能、依赖、部署或需要完整回归的改动 | 按路径运行 Backend、Frontend、Security、Compose、Windows、Upgrade,并由 Required Gate 汇总。 |

两个标签同时存在时门禁失败;先移除旧标签再添加新标签。CI 工作流自身的修改仍须经受控 Bootstrap 合并,不能用轻量标签绕过。`main` 合并后不再重复运行同一组完整 CI;需要补跑时使用各工作流的 `workflow_dispatch`。

## 使用流程

1. 在同一功能阶段内集中开发、修复和本地验证。
2. 阶段候选稳定后,为 PR 添加 `ci:milestone` 标签。
3. Backend、Frontend、Security、Compose、Windows、Upgrade 和 Required Gate 根据变更路径运行。
4. 如果门禁后又修改了代码,先移除再重新添加 `ci:milestone`,使最新 PR Head 获得新的完整门禁结果。
5. 合并到 `main` 后仍自动运行完整 CI。
2. 根据改动风险,为 PR 添加 `ci:light` 或 `ci:milestone`。
3. 等待当前 PR Head 的 `Required Gate` 成功,再合并。
4. 如果门禁后又修改了代码,先移除再重新添加所选标签,使最新 Head 获得新的检查结果。改变模式时先移除旧标签,再添加新标签。

所有工作流按 PR 或分支设置并发组。新的阶段门禁启动时,会取消同一 PR 上仍在运行的旧门禁,避免重复占用 Runner。`workflow_dispatch` 保留为人工补跑入口。
所有工作流按 PR 设置并发组。新的门禁启动时,会取消同一 PR 上仍在运行的旧门禁,避免重复占用 Runner。GitHub 对 `labeled` 事件会创建其他工作流的跳过记录,但只有所选模式的检查会实际执行。
Loading
Loading