From 4d156b492a5154dd96d4148e5a9b7c78ce344610 Mon Sep 17 00:00:00 2001 From: cloudQuant Date: Sat, 22 Aug 2026 17:54:01 +0800 Subject: [PATCH 1/2] chore: ignore local governance worktrees --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c5eaf0c0..6310946a 100644 --- a/.gitignore +++ b/.gitignore @@ -197,6 +197,7 @@ tests/functional/strategies_regression/ # Local git worktrees for branch comparison (should not be tracked) .branch_compare_worktrees/ +.worktrees/ # Auto-generated transient outputs from regression tests tests/functional/strategies/**/strategy_backtest_results.csv From f39e3fd749f931d17a42751198bfad1913f693e4 Mon Sep 17 00:00:00 2001 From: cloudQuant Date: Sat, 22 Aug 2026 18:15:12 +0800 Subject: [PATCH 2/2] feat(governance): implement iteration 140 rollout --- .github/CODEOWNERS | 37 ++ .github/ISSUE_TEMPLATE/bug_report.yml | 59 +++ .github/ISSUE_TEMPLATE/config.yml | 8 + .github/ISSUE_TEMPLATE/feature_request.yml | 40 ++ .../PULL_REQUEST_TEMPLATE/master-hotfix.md | 24 + .github/PULL_REQUEST_TEMPLATE/promotion.md | 27 + .github/governance/baseline/README.md | 46 ++ .github/governance/metrics-schema.json | 98 ++++ .github/governance/metrics/README.md | 27 + .github/governance/rulesets/dev.json | 49 ++ .github/governance/rulesets/development.json | 50 ++ .github/governance/rulesets/master.json | 51 ++ .github/pull_request_template.md | 343 +----------- .github/workflows/pr-governance.yml | 144 ++++++ .github/workflows/test.yml | 112 ++++ AGENTS.md | 25 +- CONTRIBUTING.md | 164 ++---- SECURITY.md | 42 ++ .../developer-guide/branch-governance.md | 212 ++++++++ .../developer-guide/branch-governance_zh.md | 190 +++++++ docs/source/developer-guide/contributing.md | 487 +++--------------- .../source/developer-guide/contributing_zh.md | 452 ++-------------- docs/source/developer-guide/index.md | 8 +- docs/source/developer-guide/index_zh.md | 8 +- docs/source/developer-guide/release.md | 127 ++--- docs/source/developer-guide/release_zh.md | 158 ++---- docs/source/developer-guide/setup_zh.md | 17 +- ...55\344\273\243\350\256\241\345\210\222.md" | 308 +++++++++++ scripts/__init__.py | 1 + scripts/ci/__init__.py | 1 + scripts/ci/classify_pr_risk.py | 156 ++++++ scripts/ci/render_github_ruleset_payload.py | 57 ++ scripts/ci/verify_github_governance.py | 325 ++++++++++++ tests/unit/scripts/test_classify_pr_risk.py | 119 +++++ .../test_render_github_ruleset_payload.py | 43 ++ .../scripts/test_verify_github_governance.py | 164 ++++++ 36 files changed, 2708 insertions(+), 1471 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE/master-hotfix.md create mode 100644 .github/PULL_REQUEST_TEMPLATE/promotion.md create mode 100644 .github/governance/baseline/README.md create mode 100644 .github/governance/metrics-schema.json create mode 100644 .github/governance/metrics/README.md create mode 100644 .github/governance/rulesets/dev.json create mode 100644 .github/governance/rulesets/development.json create mode 100644 .github/governance/rulesets/master.json create mode 100644 .github/workflows/pr-governance.yml create mode 100644 SECURITY.md create mode 100644 docs/source/developer-guide/branch-governance.md create mode 100644 docs/source/developer-guide/branch-governance_zh.md create mode 100644 "docs/source/reference/optimization-docs/requirements/\350\277\255\344\273\243140-\347\244\276\345\214\272PR\346\262\273\347\220\206\344\270\216\344\270\211\345\210\206\346\224\257\345\215\217\344\275\234/\346\255\243\345\274\217\350\277\255\344\273\243\350\256\241\345\210\222.md" create mode 100644 scripts/__init__.py create mode 100644 scripts/ci/__init__.py create mode 100644 scripts/ci/classify_pr_risk.py create mode 100644 scripts/ci/render_github_ruleset_payload.py create mode 100644 scripts/ci/verify_github_governance.py create mode 100644 tests/unit/scripts/test_classify_pr_risk.py create mode 100644 tests/unit/scripts/test_render_github_ruleset_payload.py create mode 100644 tests/unit/scripts/test_verify_github_governance.py diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..51c70dfc --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,37 @@ +# CODEOWNERS +# +# Iteration 140 / 决策门 D2。 +# +# D2 已于 2026-08-22 确认:`@cloudQuant` 是本仓库的真实 GitHub 用户,且拥有 +# `cloudQuant/backtrader` 的管理员权限;因此它是有效的、可写入的 CODEOWNER。 +# Gitee 镜像使用 `yunjinqi` 身份,但该身份不是 GitHub CODEOWNERS token,故不在此列。 +# GitHub CODEOWNERS 只接受 `@username` 或 `@org/team-name`;禁止占位帐号。部署后必须 +# 运行 `scripts/ci/verify_github_governance.py` 并校验 codeowners/errors API 没有错误。 + +# 仓库治理与工具链 +/.github/ @cloudQuant +/scripts/ @cloudQuant +/docs/ @cloudQuant + +# 核心 line 系统(R2) +/backtrader/lineroot.py @cloudQuant +/backtrader/linebuffer.py @cloudQuant +/backtrader/lineseries.py @cloudQuant +/backtrader/lineiterator.py @cloudQuant +/backtrader/metabase.py @cloudQuant + +# 引擎 / 策略 / 经纪商 / 数据源(R2) +/backtrader/cerebro.py @cloudQuant +/backtrader/strategy.py @cloudQuant +/backtrader/broker.py @cloudQuant +/backtrader/brokers/ @cloudQuant +/backtrader/feeds/ @cloudQuant + +# 指标与策略回归套件 +/backtrader/indicators/ @cloudQuant +/tests/functional/strategies/ @cloudQuant + +# 打包与依赖(R3 供应链) +/setup.py @cloudQuant +/pyproject.toml @cloudQuant +/requirements.txt @cloudQuant diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..f57d965d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,59 @@ +name: Bug Report +description: 报告一个 bug,帮助定位与修复 +title: "[Bug]: " +labels: ["type:bug", "status:triage"] +body: + - type: markdown + attributes: + value: | + 感谢报告问题。请在提交前确认:该问题不是安全漏洞(安全问题请走 SECURITY.md 私密报告路径),且尚未被他人报告。 + - type: dropdown + id: target-branch + attributes: + label: 目标版本(分支) + description: 这个问题出现在哪个分支? + options: + - dev + - development + - master + - 不确定 + validations: + required: true + - type: input + id: environment + attributes: + label: 环境 + description: Python 版本、操作系统、backtrader 版本、安装方式 + placeholder: "Python 3.11, macOS, backtrader 1.3.0, pip install -e ." + validations: + required: true + - type: textarea + id: repro + attributes: + label: 最小复现 + description: 可独立运行的最小代码,能复现问题 + render: python + placeholder: "import backtrader as bt\n..." + validations: + required: true + - type: textarea + id: expected + attributes: + label: 预期行为 + description: 应该发生什么 + validations: + required: true + - type: textarea + id: actual + attributes: + label: 实际行为 + description: 实际发生了什么(含完整 traceback / 错误信息) + render: shell + validations: + required: true + - type: textarea + id: logs + attributes: + label: 日志 + description: 相关日志输出(可选) + render: shell diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..ec45d9ef --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: 💬 问题与讨论 + url: https://github.com/cloudQuant/backtrader/discussions + about: 一般使用问题、想法与讨论请到 Discussions + - name: 🔒 安全漏洞报告 + url: https://github.com/cloudQuant/backtrader/security/advisories/new + about: 请勿在公开 issue 中提交安全漏洞;走私密报告路径(见 SECURITY.md) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..a15b7edf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,40 @@ +name: Feature Request +description: 提出一个新功能或改进建议 +title: "[Feature]: " +labels: ["type:feature", "status:triage"] +body: + - type: markdown + attributes: + value: | + 感谢提出建议。请描述清楚问题与动机,便于评估价值与分支影响。 + - type: textarea + id: problem + attributes: + label: 问题 + description: 这个功能解决什么问题?使用场景是什么? + validations: + required: true + - type: textarea + id: target-users + attributes: + label: 目标用户 + description: 谁会受益? + validations: + required: true + - type: textarea + id: branch-impact + attributes: + label: 分支影响 + description: 该功能属于哪个分支定位?(常规功能→dev;仅优化版功能→development;不应涉及原始版基线) + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: 替代方案 + description: 现有替代方式,或考虑过的其他方案 + - type: textarea + id: additional + attributes: + label: 附加信息 + description: 示例、参考或实现思路(可选) diff --git a/.github/PULL_REQUEST_TEMPLATE/master-hotfix.md b/.github/PULL_REQUEST_TEMPLATE/master-hotfix.md new file mode 100644 index 00000000..cecdecc6 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/master-hotfix.md @@ -0,0 +1,24 @@ + + +## 在 master 上的独立最小复现 + + + +## 回归测试 + + + +## 原始 API 兼容性说明 + + + +## 关联前移 issue + + +- [ ] 已创建 `forward-port-required` issue +- [ ] 已分别评估 `dev` 与 `development` 是否受影响 diff --git a/.github/PULL_REQUEST_TEMPLATE/promotion.md b/.github/PULL_REQUEST_TEMPLATE/promotion.md new file mode 100644 index 00000000..6b840f29 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/promotion.md @@ -0,0 +1,27 @@ + + +## 变更范围 + + + +## 已排除内容 + + + +## 完整验证 + + + +## 性能 / 兼容性差异 + + + +## 回滚点 + + diff --git a/.github/governance/baseline/README.md b/.github/governance/baseline/README.md new file mode 100644 index 00000000..8b1b6fff --- /dev/null +++ b/.github/governance/baseline/README.md @@ -0,0 +1,46 @@ +# Governance Baseline + +This directory records **how** the pre-iteration governance snapshot was taken, +and **where** the evidence lives. It intentionally stores **no sensitive API +responses, tokens, or private credentials**. + +## Snapshot commands + +Run the following with a read-only / minimal-scope credential before any +governance change, and record the output to an external, non-committed store: + +```bash +# Default branch +gh repo view cloudQuant/backtrader --json defaultBranchRef + +# Rulesets +gh api repos/cloudQuant/backtrader/rulesets + +# Labels +gh label list --repo cloudQuant/backtrader --limit 100 + +# Long-lived branch SHAs +git ls-remote --heads origin master development dev +``` + +## Snapshot record + +| Item | Timestamp | Summary | Evidence location | +|---|---|---|---| +| Default branch | 2026-08-20 | `development` | `gh repo view ... --json defaultBranchRef` output (external) | +| Rulesets | 2026-08-20 | none configured | `gh api .../rulesets` output (external) | +| Branch protection | 2026-08-20 | none configured | not captured (see rulesets) | +| CODEOWNERS | 2026-08-20 | absent | repo state at `.github/CODEOWNERS` (added in Iteration 140) | +| Long-lived branches | 2026-08-20 | `master`, `development`, `dev` all present | `git ls-remote --heads origin` | +| D2 owner confirmation | 2026-08-22 | GitHub `@cloudQuant` is a real repository admin; Gitee mirror identity is `yunjinqi` | `gh api user`, collaborator-permission response, and configured push URLs (external) | +| Mirror comparison | 2026-08-22 | `master` and `dev` matched; `development` diverged (GitHub `95c7302f`, Gitee `d8b6da88`) | paired `git ls-remote --heads` output (external); tracked as a rollout repair item | +| Rollout start | 2026-08-22 | Rulesets are created in `evaluate`, not active, mode; blocking policy remains disabled through 2026-09-05 | Rulesets API response and Rule Insights (external) | + +> **Do not commit** API responses containing user lists, teams, or any token-like +> values. Commit only the command, the timestamp, the one-line summary, and a +> pointer to the external evidence store. + +## Re-baselining + +When any D0–D4 decision changes, or when rulesets/labels/CODEOWNERS are +modified, append a new row to the table above rather than editing history. diff --git a/.github/governance/metrics-schema.json b/.github/governance/metrics-schema.json new file mode 100644 index 00000000..32b75ec6 --- /dev/null +++ b/.github/governance/metrics-schema.json @@ -0,0 +1,98 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GovernanceMetrics", + "description": "Weekly governance metrics captured during Iteration 140 and after (M5).", + "type": "object", + "required": ["week_start", "repository", "metrics"], + "properties": { + "week_start": { + "type": "string", + "format": "date", + "description": "ISO date of the Monday that starts the reporting week." + }, + "repository": { + "type": "string", + "const": "cloudQuant/backtrader" + }, + "metrics": { + "type": "object", + "required": [ + "new_prs", + "first_response_time_hours", + "first_substantive_review_time_hours", + "merge_time_hours", + "close_reasons", + "ci_failure_rate", + "flake_rate", + "forward_port_completion_rate" + ], + "properties": { + "new_prs": { + "type": "integer", + "minimum": 0, + "description": "Number of new PRs opened this week." + }, + "target_branch_misroute_rate": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Fraction of PRs that landed on the wrong target branch. Exit threshold: < 5%." + }, + "first_response_time_hours": { + "type": "number", + "minimum": 0, + "description": "Median hours to first human response." + }, + "first_substantive_review_time_hours": { + "type": "number", + "minimum": 0, + "description": "Median hours to first substantive review." + }, + "merge_time_hours": { + "type": "number", + "minimum": 0, + "description": "Median hours from open to merge." + }, + "close_reasons": { + "type": "object", + "description": "Count of PRs closed, keyed by reason (merged, rejected, stale, split-requested, duplicate).", + "additionalProperties": { "type": "integer" } + }, + "ci_failure_rate": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Fraction of CI runs that failed." + }, + "flake_rate": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Fraction of CI failures attributable to flakiness." + }, + "prs_split_requested": { + "type": "integer", + "minimum": 0, + "description": "Number of PRs asked to be split this week." + }, + "unforwarded_hotfixes": { + "type": "integer", + "minimum": 0, + "description": "Open master hotfixes without a completed forward-port." + }, + "forward_port_completion_rate": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Fraction of master hotfixes with forward-port-complete." + }, + "mirror_sha_divergence": { + "type": "boolean", + "description": "Whether GitHub/Gitee long-lived branch SHAs diverged this week." + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/.github/governance/metrics/README.md b/.github/governance/metrics/README.md new file mode 100644 index 00000000..3378bfb9 --- /dev/null +++ b/.github/governance/metrics/README.md @@ -0,0 +1,27 @@ +# Weekly Governance Record + +Iteration 140 starts its observation window on **2026-08-22**. The triage +maintainer must publish one JSON record per completed Monday–Sunday week using +`../metrics-schema.json`; store the record in the approved internal evidence +location, not in this directory, when it contains contributor-identifying data. + +## Required operating record + +For each week, retain: + +1. The `GovernanceMetrics` JSON record, including a clear source/query note. +2. The GitHub Ruleset Insights result for each long-lived branch. +3. The three GitHub/Gitee branch SHA comparisons and any divergence reason. +4. Links to every `master` hotfix, its `forward-port-required` issue, and its + independently tested `dev`/`development` resolution. +5. A list of any administrator bypass, including the emergency reason and PR. + +The first activation decision is **not before 2026-09-05**. At that review, +the maintainer must also have a complete dry-run record for a normal `dev` PR, +a core `development` PR, and a `master` hotfix PR. Do not create zero-filled +metrics as a substitute for missing operational evidence. + +The review must additionally name and verify a second, independent GitHub +maintainer before treating the R2/R3 owner-plus-second-review requirement as +active. The currently confirmed `@cloudQuant` account alone is not evidence of +that separation of duties. diff --git a/.github/governance/rulesets/dev.json b/.github/governance/rulesets/dev.json new file mode 100644 index 00000000..61667196 --- /dev/null +++ b/.github/governance/rulesets/dev.json @@ -0,0 +1,49 @@ +{ + "branch": "dev", + "name": "dev-ruleset", + "description": "Daily development entry. Observe PR + review + CI requirements before activation.", + "enforcement": "evaluate", + "target": "branch", + "conditions": { + "ref_name": { + "include": ["refs/heads/dev"], + "exclude": [] + } + }, + "rules": [ + { "type": "deletion" }, + { "type": "non_fast_forward" }, + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 1, + "dismiss_stale_reviews_on_push": true, + "require_code_owner_review": false, + "require_last_push_approval": false, + "required_review_thread_resolution": true + } + }, + { + "type": "required_status_checks", + "parameters": { + "strict_required_status_checks_policy": false, + "required_status_checks": [ + { "context": "Lint" }, + { "context": "Test Summary" }, + { "context": "PR Governance" }, + { "context": "Tiered Validation" } + ] + } + } + ], + "_governance_notes": [ + "Any PR may target dev. No risk-based second approval is enforced by the ruleset; R1 requires a module-owner review recorded via the review flow." + ], + "_activation": { + "state": "observe", + "started_on": "2026-08-22", + "not_before": "2026-09-05", + "target_enforcement": "active", + "requires": "Reviewed Rule Insights, stable required checks, and recorded PR dry-runs." + } +} diff --git a/.github/governance/rulesets/development.json b/.github/governance/rulesets/development.json new file mode 100644 index 00000000..7eae9cc4 --- /dev/null +++ b/.github/governance/rulesets/development.json @@ -0,0 +1,50 @@ +{ + "branch": "development", + "name": "development-ruleset", + "description": "Improved and optimized version. Observe PR + owner + CI requirements before activation.", + "enforcement": "evaluate", + "target": "branch", + "conditions": { + "ref_name": { + "include": ["refs/heads/development"], + "exclude": [] + } + }, + "rules": [ + { "type": "deletion" }, + { "type": "non_fast_forward" }, + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 1, + "dismiss_stale_reviews_on_push": true, + "require_code_owner_review": true, + "require_last_push_approval": false, + "required_review_thread_resolution": true + } + }, + { + "type": "required_status_checks", + "parameters": { + "strict_required_status_checks_policy": false, + "required_status_checks": [ + { "context": "Lint" }, + { "context": "Test Summary" }, + { "context": "PR Governance" }, + { "context": "Tiered Validation" } + ] + } + } + ], + "_governance_notes": [ + "R2/R3 changes require owner + a second maintainer approval; the second approval is recorded via the review flow and risk labels, not a ruleset rule.", + "R2/R3 candidates must run `make test-strategies` (or runonce/runnext parity) before merge." + ], + "_activation": { + "state": "observe", + "started_on": "2026-08-22", + "not_before": "2026-09-05", + "target_enforcement": "active", + "requires": "Reviewed Rule Insights, stable required checks, and recorded PR dry-runs." + } +} diff --git a/.github/governance/rulesets/master.json b/.github/governance/rulesets/master.json new file mode 100644 index 00000000..a8e453d0 --- /dev/null +++ b/.github/governance/rulesets/master.json @@ -0,0 +1,51 @@ +{ + "branch": "master", + "name": "master-ruleset", + "description": "Original Backtrader baseline. Observe PR + review + CI requirements before activation.", + "enforcement": "evaluate", + "target": "branch", + "conditions": { + "ref_name": { + "include": ["refs/heads/master"], + "exclude": [] + } + }, + "rules": [ + { "type": "deletion" }, + { "type": "non_fast_forward" }, + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 1, + "dismiss_stale_reviews_on_push": true, + "require_code_owner_review": true, + "require_last_push_approval": false, + "required_review_thread_resolution": true + } + }, + { + "type": "required_status_checks", + "parameters": { + "strict_required_status_checks_policy": false, + "required_status_checks": [ + { "context": "Lint" }, + { "context": "Test Summary" }, + { "context": "PR Governance" }, + { "context": "Tiered Validation" } + ] + } + } + ], + "_governance_notes": [ + "Only `hotfix/master-*` PRs may target master; each must carry the `target:master-hotfix` label and an independent minimal repro on master.", + "R3 changes require explicit core-maintainer approval; admin bypass is allowed only with a recorded emergency reason.", + "Every master fix must create a forward-port issue and complete equivalent, independently-tested ports to dev/development before being governance-done." + ], + "_activation": { + "state": "observe", + "started_on": "2026-08-22", + "not_before": "2026-09-05", + "target_enforcement": "active", + "requires": "Reviewed Rule Insights, stable required checks, and recorded PR dry-runs." + } +} diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 18accfdd..082f9a6b 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,331 +1,36 @@ -# Backtrader 去元编程项目 - Pull Request + -## 📋 PR 概述 +## 问题与动机 -### 基本信息 -- **PR类型**: [ ] 元编程移除 [ ] 性能优化 [ ] Bug修复 [ ] 文档更新 [ ] 测试增强 -- **影响范围**: [ ] 核心系统 [ ] 单个模块 [ ] 测试代码 [ ] 文档 [ ] 工具 -- **破坏性变更**: [ ] 是 [ ] 否 -- **关联Issue**: #[Issue编号] + -### 变更摘要 - +## 目标分支与原因 -### 修改范围 - -- `backtrader/[模块名].py`: [变更描述] -- `tests/[测试文件].py`: [变更描述] +- **目标分支**: `dev` / `development` / `master`(三选一) +- **原因**: -## 🎯 元编程移除验证 ⭐⭐⭐⭐⭐ +## 风险级别 -### 移除的元编程模式 -- [ ] **移除的元类**: - - [ ] MetaStrategy - [替代方案描述] - - [ ] MetaIndicator - [替代方案描述] - - [ ] MetaLineSeries - [替代方案描述] - - [ ] MetaParams - [替代方案描述] - - [ ] MetaSingleton - [替代方案描述] - - [ ] 其他: [具体元类名] - [替代方案描述] +- **风险级别**: R0 / R1 / R2 / R3 +- **说明**: -- [ ] **移除的动态类创建**: - - [ ] type() 动态创建 - [替代方案描述] - - [ ] setattr 动态属性 - [替代方案描述] - - [ ] 其他: [具体模式] - [替代方案描述] +## 兼容性影响 -### 替代方案实现 -- [ ] **使用的替代技术**: - - [ ] 描述符 (Descriptors) - - [ ] 混入类 (Mixins) - - [ ] 组合模式 (Composition) - - [ ] `__init_subclass__` 方法 - - [ ] 依赖注入 - - [ ] 其他: [具体技术] + -- [ ] **实现完整性验证**: - - [ ] 所有原有功能已被替代实现 - - [ ] 边界条件处理完善 - - [ ] 错误处理机制保持一致 +## 执行过的命令与结果 -### 工具验证 -```bash -# 运行元编程检测工具 -python tools/metaclass_detector.py + -# 检查结果: -# - 元类使用点数量: [之前] → [现在] -# - 动态创建点数量: [之前] → [现在] -# - 验证结果: [ ] 通过 [ ] 未通过 -``` +## 关联 Issue -## 🔄 API兼容性验证 ⭐⭐⭐⭐ - -### 公共接口检查 -- [ ] **方法签名保持不变**: - - [ ] 所有公共方法签名未变化 - - [ ] 参数名称和默认值保持一致 - - [ ] 返回值类型保持兼容 - -- [ ] **属性访问保持一致**: - - [ ] 关键属性 (如 lines, params) 访问方式不变 - - [ ] 属性值类型和格式保持一致 - - [ ] 动态属性访问行为保持相同 - -### 行为兼容性 -- [ ] **核心功能验证**: - - [ ] 策略执行行为一致 - - [ ] 指标计算结果一致 - - [ ] 数据访问行为一致 - - [ ] 参数处理行为一致 - -- [ ] **异常处理验证**: - - [ ] 异常类型保持一致 - - [ ] 错误信息保持有用性 - - [ ] 边界条件处理一致 - -### 兼容性测试结果 -```bash -# 运行兼容性测试框架 -python tools/compatibility_tester.py - -# 测试结果: -# - API兼容性: [通过率]% -# - 行为兼容性: [通过率]% -# - 性能对比: [变化百分比]% -# - 总体评估: [ ] PASS [ ] FAIL -``` - -## ⚡ 性能影响评估 ⭐⭐⭐ - -### 性能基准测试 -- [ ] **执行性能对比**: - - [ ] 策略执行时间: [变化] ([百分比]%) - - [ ] 指标计算时间: [变化] ([百分比]%) - - [ ] 数据访问时间: [变化] ([百分比]%) - - [ ] 启动时间: [变化] ([百分比]%) - -- [ ] **内存使用分析**: - - [ ] 内存占用: [变化] ([百分比]%) - - [ ] 无内存泄漏验证 - - [ ] 垃圾回收影响评估 - -### 性能测试命令 -```bash -# 运行性能基准测试 -python tools/performance_benchmark.py --before [git-ref] --after HEAD - -# 性能目标: -# - 执行时间变化: ≤ 10% -# - 内存使用变化: ≤ 20% -# - 启动时间变化: ≤ 30% -``` - -### 关键路径优化 -- [ ] **热点代码路径检查**: - - [ ] 数据访问路径优化 - - [ ] 指标计算路径优化 - - [ ] 策略执行路径优化 - -## 🧪 测试覆盖验证 ⭐⭐⭐ - -### 测试覆盖率 -```bash -# 运行测试覆盖率检查 -python -m pytest tests/ --cov=backtrader --cov-report=term-missing - -# 覆盖率要求: -# - 新增代码覆盖率: ≥ 85% -# - 修改代码覆盖率: ≥ 90% -# - 关键路径覆盖率: ≥ 95% -``` - -- [ ] **测试类型覆盖**: - - [ ] 单元测试: [数量] 个 - - [ ] 集成测试: [数量] 个 - - [ ] 回归测试: [数量] 个 - - [ ] 性能测试: [数量] 个 - -### 新增测试用例 -- [ ] **功能测试**: - - [ ] 测试用例1: [描述] - - [ ] 测试用例2: [描述] - -- [ ] **边界条件测试**: - - [ ] 测试用例1: [描述] - - [ ] 测试用例2: [描述] - -- [ ] **异常情况测试**: - - [ ] 测试用例1: [描述] - - [ ] 测试用例2: [描述] - -## 📊 代码质量检查 ⭐⭐⭐ - -### 静态分析结果 -```bash -# 代码格式检查 -black --check backtrader/ - -# 静态分析 -pylint backtrader/ --score=yes - -# 类型检查 -mypy backtrader/ -``` - -- [ ] **代码规范检查**: - - [ ] Black 格式化: [ ] 通过 [ ] 未通过 - - [ ] Pylint 评分: [分数]/10 (要求 ≥ 8.0) - - [ ] MyPy 类型检查: [ ] 通过 [ ] 有警告 - -### 代码质量评估 -- [ ] **可读性和维护性**: - - [ ] 函数复杂度适中 (≤ 10) - - [ ] 类设计合理 (≤ 500行) - - [ ] 命名清晰一致 - - [ ] 注释和文档充分 - -- [ ] **架构设计**: - - [ ] 单一职责原则 - - [ ] 开闭原则 - - [ ] 依赖倒置原则 - - [ ] 接口隔离原则 - -## 📚 文档更新 ⭐⭐ - -### 文档变更 -- [ ] **API文档**: - - [ ] 新增API文档 - - [ ] 修改API文档 - - [ ] 废弃API说明 - -- [ ] **用户文档**: - - [ ] 使用指南更新 - - [ ] 迁移指南更新 - - [ ] 示例代码更新 - -- [ ] **开发文档**: - - [ ] 架构文档更新 - - [ ] 贡献指南更新 - - [ ] 故障排除指南更新 - -### 文档质量 -- [ ] **内容质量**: - - [ ] 信息准确完整 - - [ ] 示例代码可执行 - - [ ] 链接和引用有效 - -## 🔍 回归测试验证 - -### 完整测试套件 -```bash -# 运行完整的回归测试 -python -m pytest tests/ -v --timeout=300 - -# 测试结果统计: -# - 总测试数: [数量] -# - 通过数: [数量] -# - 失败数: [数量] -# - 跳过数: [数量] -# - 通过率: [百分比]% -``` - -- [ ] **核心功能测试**: - - [ ] 策略系统测试: [ ] 通过 - - [ ] 指标系统测试: [ ] 通过 - - [ ] 数据系统测试: [ ] 通过 - - [ ] 经纪商系统测试: [ ] 通过 - -- [ ] **集成测试**: - - [ ] 端到端测试: [ ] 通过 - - [ ] 多组件协作测试: [ ] 通过 - - [ ] 用户场景测试: [ ] 通过 - -### 已知问题 -- [ ] **当前已知问题**: - - [ ] 问题1: [描述] - [状态] - - [ ] 问题2: [描述] - [状态] - -## 📝 变更详情 - -### 主要变更 -1. **[变更类别]**: [详细描述] - - 文件: `[文件路径]` - - 变更: [具体变更内容] - - 原因: [变更原因] - -2. **[变更类别]**: [详细描述] - - 文件: `[文件路径]` - - 变更: [具体变更内容] - - 原因: [变更原因] - -### 技术实现细节 - - -### 设计决策 - - -## 🚨 风险评估 - -### 潜在风险 -- [ ] **高风险**: - - [ ] 风险1: [描述] - [缓解措施] - - [ ] 风险2: [描述] - [缓解措施] - -- [ ] **中等风险**: - - [ ] 风险1: [描述] - [缓解措施] - - [ ] 风险2: [描述] - [缓解措施] - -### 回滚计划 -- [ ] **回滚方案**: - - [ ] 回滚步骤已准备 - - [ ] 回滚测试已验证 - - [ ] 数据备份已完成 - -## ✅ 审查检查清单 - -### 自检完成项 -- [ ] 元编程完全移除 -- [ ] 兼容性测试通过 -- [ ] 性能测试通过 -- [ ] 代码质量检查通过 -- [ ] 测试覆盖率达标 -- [ ] 文档已更新 -- [ ] 工具验证通过 - -### 待审查项 -- [ ] 代码架构设计 -- [ ] 实现方案合理性 -- [ ] 边界条件处理 -- [ ] 错误处理机制 -- [ ] 性能优化机会 -- [ ] 安全性考虑 - -## 📞 联系信息 - -**PR提交者**: [姓名] -**联系方式**: [邮箱/Slack] -**提交时间**: [日期时间] -**预计审查时间**: [时间估算] - ---- - -## 🔧 审查者使用指南 - -### 审查步骤 -1. **克隆分支**: `git checkout [branch-name]` -2. **运行自动化检查**: 按照上述命令执行各项检查 -3. **手动代码审查**: 重点关注元编程移除和兼容性 -4. **测试验证**: 运行完整测试套件 -5. **文档审查**: 检查文档更新的准确性 - -### 审查标准 -- **必须通过**: 元编程移除、API兼容性、性能要求 -- **建议改进**: 代码质量、文档完整性、测试覆盖 -- **可选优化**: 性能优化、架构改进 - -### 审查反馈模板 -请使用代码审查规范中的反馈模板进行审查反馈。 - ---- - -**模板版本**: 1.0 -**最后更新**: 2025年05月30日 \ No newline at end of file + diff --git a/.github/workflows/pr-governance.yml b/.github/workflows/pr-governance.yml new file mode 100644 index 00000000..304939c4 --- /dev/null +++ b/.github/workflows/pr-governance.yml @@ -0,0 +1,144 @@ +name: PR Governance + +# Iteration 140 / M3. +# +# This workflow intentionally uses pull_request rather than pull_request_target: +# it checks out contributor content only with a read-only token and never exposes +# repository secrets or an admin credential. The policy starts report-only and +# becomes blocking only after the documented observation period. + +on: + pull_request: + types: [opened, synchronize, reopened, edited, labeled, unlabeled] + branches: [dev, development, master] + workflow_dispatch: + inputs: + base_ref: + description: "Manual dry-run target branch" + required: true + default: "dev" + type: choice + options: [dev, development, master] + head_ref: + description: "Manual dry-run source branch" + required: true + default: "feature/manual-governance-check" + type: string + labels: + description: "Manual dry-run labels as a JSON array" + required: true + default: "[]" + type: string + changed_paths: + description: "Manual dry-run changed paths (comma-separated)" + required: false + default: "docs/source/developer-guide/branch-governance.md" + type: string + body: + description: "Manual dry-run PR body" + required: false + default: "" + type: string + +permissions: + contents: read + +concurrency: + group: pr-governance-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + +jobs: + governance: + name: PR Governance + runs-on: ubuntu-latest + steps: + - name: Checkout evaluated revision + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + fetch-depth: 0 + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Collect changed paths + env: + EVENT_NAME: ${{ github.event_name }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + MANUAL_PATHS: ${{ inputs.changed_paths }} + shell: bash + run: | + set -euo pipefail + paths_file="$RUNNER_TEMP/changed-paths.txt" + if [ "$EVENT_NAME" = "pull_request" ]; then + git diff --name-only "$BASE_SHA" "$HEAD_SHA" > "$paths_file" + else + printf '%s' "$MANUAL_PATHS" | tr ',' '\n' | sed '/^[[:space:]]*$/d' > "$paths_file" + fi + echo "Changed paths:" + sed -n '1,200p' "$paths_file" + + - name: Classify and report + id: classify + env: + CHANGED_PATHS: ${{ runner.temp }}/changed-paths.txt + shell: bash + run: | + set -euo pipefail + python scripts/ci/classify_pr_risk.py \ + --paths-file "$CHANGED_PATHS" \ + --github-output "$GITHUB_OUTPUT" + + - name: Enforce policy after observation period + env: + BASE_REF: ${{ github.base_ref || inputs.base_ref }} + HEAD_REF: ${{ github.head_ref || inputs.head_ref }} + PR_LABELS: ${{ github.event_name == 'pull_request' && toJson(github.event.pull_request.labels.*.name) || inputs.labels }} + PR_BODY: ${{ github.event_name == 'pull_request' && github.event.pull_request.body || inputs.body }} + GOVERNANCE_BLOCKING: ${{ vars.GOVERNANCE_BLOCKING || 'false' }} + RISK: ${{ steps.classify.outputs.risk }} + AREA: ${{ steps.classify.outputs.area }} + shell: bash + run: | + set -euo pipefail + echo "Target branch: $BASE_REF" + echo "Head branch: $HEAD_REF" + echo "Suggested risk: $RISK" + echo "Suggested area: $AREA" + echo "Labels: $PR_LABELS" + + violations=() + if [ "$BASE_REF" = "master" ]; then + case "$HEAD_REF" in + hotfix/master-*) ;; + *) violations+=("routine PRs must not target master; use dev or development") ;; + esac + + if ! printf '%s' "$PR_LABELS" | grep -Fq '"target:master-hotfix"'; then + violations+=("master hotfix PRs require the target:master-hotfix label") + fi + if ! printf '%s' "$PR_BODY" | grep -Eqi '最小复现|minimal repro|reproduction'; then + violations+=("master hotfix PRs require a documented minimal reproduction") + fi + fi + + if [ "${#violations[@]}" -eq 0 ]; then + echo "::notice::PR routing and observable metadata satisfy the current policy." + exit 0 + fi + + for violation in "${violations[@]}"; do + if [ "$GOVERNANCE_BLOCKING" = "true" ]; then + echo "::error::$violation" + else + echo "::warning::$violation (observation mode; not blocking)" + fi + done + if [ "$GOVERNANCE_BLOCKING" = "true" ]; then + exit 1 + fi + echo "::notice::Blocking remains disabled until the observation evidence is reviewed." diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 015a8cbd..91d37b62 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,6 +24,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: + contents: read + jobs: lint: name: Lint @@ -277,3 +280,112 @@ jobs: echo "At least one matrix test job failed" exit 1 fi + + pr-risk: + name: PR Risk Classification + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + outputs: + risk: ${{ steps.classify.outputs.risk }} + area: ${{ steps.classify.outputs.area }} + steps: + - name: Checkout evaluated revision + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Classify changed paths + id: classify + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + shell: bash + run: | + set -euo pipefail + paths_file="$RUNNER_TEMP/changed-paths.txt" + git diff --name-only "$BASE_SHA" "$HEAD_SHA" > "$paths_file" + python scripts/ci/classify_pr_risk.py \ + --paths-file "$paths_file" \ + --github-output "$GITHUB_OUTPUT" + + branch-specific-validation: + name: Branch-Specific Validation + needs: pr-risk + if: >- + github.event_name == 'pull_request' && needs.pr-risk.result == 'success' && + (github.base_ref == 'master' || + (github.base_ref == 'development' && + (needs.pr-risk.outputs.risk == 'R2' || needs.pr-risk.outputs.risk == 'R3'))) + runs-on: ubuntu-latest + steps: + - name: Checkout evaluated revision + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: 'pip' + + - name: Install dependencies + shell: bash + run: | + python -m pip install --upgrade pip setuptools wheel + pip install -e ".[dev]" + + - name: Run development R2/R3 strategy regression gate + if: github.base_ref == 'development' + shell: bash + run: make test-strategies + + - name: Run original-baseline hotfix gate + if: github.base_ref == 'master' + shell: bash + env: + PYTEST_ADDOPTS: '' + run: | + pytest tests/ -n auto --tb=short --timeout=300 -q + + tiered-validation-summary: + name: Tiered Validation + needs: [pr-risk, branch-specific-validation] + if: always() + runs-on: ubuntu-latest + steps: + - name: Publish stable branch-specific result + env: + EVENT_NAME: ${{ github.event_name }} + BASE_REF: ${{ github.base_ref }} + RISK_RESULT: ${{ needs.pr-risk.result }} + TARGET_RESULT: ${{ needs.branch-specific-validation.result }} + shell: bash + run: | + set -euo pipefail + if [ "$EVENT_NAME" != 'pull_request' ]; then + echo "No pull-request tier is applicable." + exit 0 + fi + if [ "$RISK_RESULT" != 'success' ]; then + echo "PR risk classification failed or was skipped: $RISK_RESULT" + exit 1 + fi + if [ "$TARGET_RESULT" = 'success' ]; then + echo "Branch-specific validation passed for $BASE_REF." + exit 0 + fi + if [ "$TARGET_RESULT" = 'skipped' ]; then + echo "No extra branch-specific validation is required for this PR tier." + exit 0 + fi + echo "Branch-specific validation did not pass: $TARGET_RESULT" + exit 1 diff --git a/AGENTS.md b/AGENTS.md index 2df727cb..2c2d5dd3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,14 +20,23 @@ keeping the public API compatible. ### Branch context -- `dev` — active development; the canonical branch. All work lands here first. -- `master` — stable, aligned with upstream behavior. Used as the correctness - baseline (regression tests bake master's metrics as expected values). -- Other branches (`crypto`, `ctp`, `dev_cython`, `development`, etc.) are - feature/experiment branches; do not target them unless asked. - -> **Do not push directly to `master`.** Push to `dev`. `git push` is configured -> to push to both GitHub (`cloudQuant/backtrader`) and Gitee +This repository uses a **three-branch model** (authoritative source: +`docs/source/developer-guide/branch-governance.md`): + +- `dev` — daily development entry. Routine features, ordinary bug fixes, docs, + tests, refactors, and community contributions land here first. +- `development` — improved & optimized version. Optimization capabilities, + architecture improvements, and optimization-only regression fixes. +- `master` — original Backtrader baseline. Only bug/compatibility/security + fixes that reproduce on the original baseline, via `hotfix/master-*` PRs. + Used as the correctness baseline (regression tests bake master's metrics as + expected values). + +Other branches (`crypto`, `ctp`, `dev_cython`, etc.) are feature/experiment +branches; do not target them unless asked. + +> **Do not push directly to `master` or `development`.** Push to `dev`. `git +> push` is configured to push to both GitHub (`cloudQuant/backtrader`) and Gitee > (`yunjinqi/backtrader`) remotes. ## Implementation status / reality checks diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f06c5e93..8ad47306 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,38 +2,31 @@ 感谢你对 Backtrader 的贡献兴趣!本文档说明如何参与项目开发。 +分支角色、PR 目标分支选择、风险分级与 promotion/hotfix 协议的**权威来源**见 +[Branch Governance](docs/source/developer-guide/branch-governance.md)。本文档与 +其冲突时,以分支治理文档为准。 + ## 快速开始 ### 环境搭建 ```bash - # 1. Fork 并克隆仓库 - -git clone +git clone https://github.com/cloudQuant/backtrader.git cd backtrader -# 2. 切换到 dev 分支 - +# 2. 切到目标分支(见下方「分支策略」选择正确的目标分支) git checkout dev # 3. 安装依赖 - pip install -r requirements.txt -# 4. 编译 Cython 加速文件 (可选但推荐) - -cd backtrader && python -W ignore compile_cython_numba_files.py && cd .. - -# 5. 安装开发模式 - +# 4. 安装开发模式(纯 Python,无独立 Cython 编译步骤) pip install -e . -# 6. 验证安装 - +# 5. 验证安装 python -c "import backtrader as bt; print(bt.__version__)" make test-fast # 快速回归(约 3.5 分钟) - ``` ### 开发命令速查 @@ -52,25 +45,36 @@ make quality-check # 全部质量检查 make docs # 生成文档(en + zh) ``` -- -- +--- ## 分支策略 -| 分支 | 用途 | +本仓库使用**三分支模型**(不同于常见 GitFlow)。完整定义与决策表见 +[Branch Governance](docs/source/developer-guide/branch-governance.md)。 -|------|------| +| 分支 | 定位 | 允许进入的变更 | +|------|------|----------------| +| `dev` | **日常开发入口** | 常规功能、普通 Bug 修复、文档、测试、重构、社区贡献 | +| `development` | **改进与优化版本** | 优化版能力、架构优化、仅在优化版存在的回归修复 | +| `master` | **原始 Backtrader 基线** | 已在原始版复现的 Bug、兼容性或安全修复(仅 `hotfix/master-*`) | -| `dev` | **主开发分支** — 所有新功能和修复提交到这里 | +**选择目标分支:** -| `master` | 稳定版本,仅从 dev 合并经过验证的代码 | +1. 文档、测试、常规功能、普通 Bug 修复 → **`dev`** +2. 仅在优化架构中出现的问题或优化版功能 → **`development`** +3. 原始 Backtrader 的真实 Bug / 安全问题 → **`master`**(`hotfix/master-*` 分支) -**工作流程**: +**工作流程(以 `dev` 为例):** + +```bash +git checkout -b feature/your-feature dev # 从目标分支创建功能分支 +# 开发、测试、提交 +# 向 dev 提交 Pull Request +``` -1. 从 `dev` 创建功能分支: `git checkout -b feature/your-feature dev` -2. 开发、测试、提交 -3. 向 `dev` 提交 Pull Request +> 不要把 `development` 当作 `master` 的上游,也不要把优化版代码批量回灌到 `master`。 -- -- +--- ## 代码规范 @@ -83,15 +87,12 @@ make docs # 生成文档(en + zh) ### 架构规则 -1. **禁止新增元类**— 使用 `donew()` + `BaseMixin` 模式 - -2.**保持 API 兼容**— 现有用户代码必须无修改可运行 -3.**初始化顺序**— 先调用 `super().__init__()` 再访问 `self.p` +1. **禁止新增元类** — 使用 `donew()` + `BaseMixin` 模式 +2. **保持 API 兼容** — 现有用户代码必须无修改可运行 +3. **初始化顺序** — 先调用 `super().__init__()` 再访问 `self.p` ```python - # ✅ 正确 - class MyIndicator(bt.Indicator): params = (('period', 20),) @@ -100,17 +101,15 @@ class MyIndicator(bt.Indicator): self.sma = bt.indicators.SMA(self.data, period=self.p.period) # ❌ 错误 — self.p 在 super().__init__() 之前不可用 - class BadIndicator(bt.Indicator): def __init__(self): print(self.p.period) # 会失败! super().__init__() - ``` ### 提交信息规范 -使用 [Conventional Commits]( +使用 [Conventional Commits](https://www.conventionalcommits.org/): ```bash feat: add live tick aggregation to BtApiFeed @@ -119,10 +118,9 @@ perf: cache broker reference in total_value.next() docs: update CTP live trading guide test: add regression coverage for live broker startup refactor: extract retry logic to _retry_api_call() - ``` -- -- +--- ## 测试要求 @@ -132,25 +130,6 @@ refactor: extract retry logic to _retry_api_call() - 修复 bug 必须有回归测试 - 不得删除或弱化现有测试 -### 测试规范 - -```python -import pytest - - -def test_sma_calculation(sample_data, cerebro_engine): - """Verify the SMA indicator calculates correctly (use fixtures, not manual setup).""" - cerebro_engine.adddata(sample_data) - results = cerebro_engine.run() - assert len(results) > 0 - - -@pytest.mark.slow -def test_full_year_multi_timeframe_regression(sample_data, cerebro_engine): - """Heavy end-to-end run; tagged `slow` so `make test-fast` can skip it.""" - ... -``` - ### 测试分级(重要) 策略回归套件很大,按耗时分级运行(详见 `Makefile` 与 `README`): @@ -158,7 +137,7 @@ def test_full_year_multi_timeframe_regression(sample_data, cerebro_engine): | 命令 | 范围 | 大致耗时 | 用途 | | --- | --- | --- | --- | | `make test-fast` | 全部非策略测试 + 最快 ~35% 策略测试 | ~3.5min | 日常开发反馈 | -| `make test-strategies` | 全部策略回归(多时间框架时钟回归网) | ~9min | 改动 `cerebro`/`strategy`/line 系统/`_periodset` 后必跑 | +| `make test-strategies` | 全部策略回归(多时间框架时钟回归网) | ~9min | 改动 `cerebro`/`strategy`/line 系统后必跑 | | `make test-slow` | test-fast 跳过的最慢 ~65% 策略 | — | 补充验证 | | `make test-all` | 全量并行 | ~10min | 提交/发版前 | @@ -185,14 +164,9 @@ from backtrader.utils.log_message import get_logger logger = get_logger(__name__) # -> "backtrader." ``` -- 默认静默(库导入时只挂 `NullHandler`);用户用 `bt.configure_logging(...)` 开启。 -- 禁止静默吞异常:`except ...: pass` 必须带解释性注释,或落 `logger.debug/warning(..., exc_info=True)`。 -- 热路径(`next`/`once`/`__len__`/`__getattribute__` 等)加日志要用 - `if logger.isEnabledFor(logging.DEBUG):` 守护,避免格式化开销。 - 完整规范见 `docs/LOGGING_GUIDELINES.md`。 -- -- +--- ## Pull Request 流程 @@ -200,36 +174,15 @@ logger = get_logger(__name__) # -> "backtrader." - [ ] 代码通过 `make format-check` - [ ] 代码通过 `make lint` (无新增 warning) -- [ ] 所有测试通过 `pytest tests/ -v` +- [ ] 相关测试通过(改动核心路径时跑 `make test-strategies`) - [ ] 新功能有测试覆盖 -- [ ] 更新 `CHANGELOG.md` (Unreleased 部分) - -### 2. PR 描述模板 - -```markdown - -## 变更说明 - -简要描述做了什么。 - -## 变更类型 - -- [ ] Bug 修复 -- [ ] 新功能 -- [ ] 性能优化 -- [ ] 文档更新 -- [ ] 重构 - -## 测试 - -- 描述如何测试这些变更 -- 附上测试命令 - -## 影响范围 +- [ ] 确定唯一目标分支(见上方「分支策略」) +- [ ] 确定风险级别 R0–R3(见分支治理文档) -- 列出可能受影响的模块 +### 2. PR 描述 -``` +使用仓库内的 [PR 模板](.github/pull_request_template.md):必填字段仅包括问题与动机、 +目标分支与原因、风险级别、兼容性影响、执行过的命令/结果、关联 issue。 ### 3. 审查标准 @@ -239,47 +192,34 @@ logger = get_logger(__name__) # -> "backtrader." - 性能不退化 - API 向后兼容 -- -- +--- ## 报告问题 -### Bug 报告 - -请包含: +使用 [Issue Forms](.github/ISSUE_TEMPLATE/) 提交。 -1.**环境**: Python 版本, 操作系统, backtrader 版本 +### Bug 报告 -1. **复现步骤**: 最小可复现代码 -2. **预期行为**vs**实际行为** -3. **错误日志**(完整 traceback) +请包含:环境(Python 版本、操作系统、backtrader 版本、目标分支)、最小复现、 +预期行为 vs 实际行为、错误日志(完整 traceback)。 ### 功能请求 -请说明: - -1.**使用场景**: 为什么需要这个功能 +请说明:使用场景、期望行为、目标分支影响、替代方案。 -1. **期望行为**: 功能应该如何工作 -2. **替代方案**: 是否有现有的替代方式 +安全问题请走 `SECURITY.md` 定义的私密报告路径,**不要**在公开 issue 中提交。 -- -- +--- ## 项目结构导航 | 目录 | 说明 | 开发频率 | - |------|------|----------| - -| `backtrader/brokers/` | Broker 实现 | 🔥 高 | - +| `backtrader/brokers/` | Broker 实现 | 高 | | `backtrader/feeds/` | 数据源 | 中 | - | `backtrader/indicators/` | 技术指标 | 中 | - | `backtrader/analyzers/` | 分析器 | 低 | - -| `tests/functional/strategies/` | 策略回归套件(最大、分级) | 🔥 高 | - +| `tests/functional/strategies/` | 策略回归套件(最大、分级) | 高 | | `docs/` | 文档 | 中 | 详细架构参见 `docs/ARCHITECTURE.md`。 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..c5b625da --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,42 @@ +# Security Policy + +## Supported Versions + +| Branch | 定位 | 安全修复策略 | +|---|---|---| +| `master` | 原始 Backtrader 基线 | 仅修复在原始版复现的安全问题(`hotfix/master-*`) | +| `development` | 改进与优化版本 | 仅修复在优化版复现的安全问题 | +| `dev` | 日常开发入口 | 常规安全问题先在此落地 | + +## Reporting a Vulnerability + +**请勿在公开 issue、讨论或 PR 中披露漏洞细节。** + +请通过 GitHub 私密报告路径提交: + +- GitHub Security Advisory: + +### 报告内容 + +请尽量包含: + +1. 受影响的分支/版本; +2. 漏洞类型与影响范围(数据泄露、代码执行、拒绝服务等); +3. 最小复现步骤; +4. 建议的修复方向(可选)。 + +### 响应承诺 + +- 维护者将在 **5 个工作日内**确认收到报告; +- 将在修复与公开披露前与你保持沟通; +- 若你希望在公开披露前保密,请明确说明,维护者将配合协调披露时间; +- 公开披露将遵循 [GitHub 的协调披露](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities) 流程。 + +## 范围 + +本策略适用于 `cloudQuant/backtrader` 仓库内的代码与配置。依赖库的漏洞请直接上报对应上游项目,或通过上述私密路径告知维护者。 + +## 不在公开渠道处理 + +- 不公开提交 API key、令牌、凭据或漏洞细节; +- 已公开的凭据请立即轮换,并通过私密路径告知维护者。 diff --git a/docs/source/developer-guide/branch-governance.md b/docs/source/developer-guide/branch-governance.md new file mode 100644 index 00000000..28a57dfc --- /dev/null +++ b/docs/source/developer-guide/branch-governance.md @@ -0,0 +1,212 @@ +--- +title: Branch Governance +description: Three-branch model, PR routing, risk levels, and promotion/hotfix protocols +--- + +# Branch Governance + +> Status: **In rollout** (Iteration 140; rulesets are in a two-week observation period from 2026-08-22) +> Repository: `cloudQuant/backtrader` +> Long-lived branches: `master`, `development`, `dev` + +This document is the authoritative source for branch roles, PR routing, review +requirements, and cross-branch fix propagation. It supersedes any earlier +"`development` → `master` release chain" wording. When any other document +(`README.md`, `CONTRIBUTING.md`, `AGENTS.md`, PR/issue templates) disagrees with +this file, this file wins. + +## 1. Mandatory branch facts + +These definitions are the non-negotiable premise of this repository's +governance. They do **not** follow common GitFlow. + +| Branch | Role | Allowed changes | Forbidden | +|---|---|---|---| +| `master` | Original Backtrader baseline | Bug, compatibility, or security fixes that reproduce on the original baseline | Routine features, optimization refactors, routine merges from `dev`/`development` | +| `development` | Improved & optimized version | Optimization capabilities, architecture improvements, regression fixes that only exist in the optimized version, controlled-integration daily development | Being treated as `master`'s release candidate or a reverse-sync source | +| `dev` | Daily development entry | Routine features, ordinary bug fixes, docs, tests, refactors, community contributions | Bypassing review to reach `development`/`master` directly | + +## 2. Governance decisions (D0–D4) + +Recorded decisions from Iteration 140. Unresolved items are blockers and must +not silently default to an assumption. + +| ID | Decision | Outcome | +|---|---|---| +| D0 | GitHub default branch | **Keep `development`** (decided 2026-08-20). Contributors select the target branch explicitly via the PR template and `pr-governance` workflow, rather than relying on the default branch. | +| D1 | GitHub/Gitee authority & sync | **GitHub `cloudQuant/backtrader` is the review authority; Gitee `yunjinqi/backtrader` is a controlled mirror.** The mirror owner verifies long-lived-branch SHAs after every merge (see §8). | +| D2 | Owner team & admin bypass | **Resolved 2026-08-22:** `@cloudQuant` is the real GitHub user with admin access to this repository and is the CODEOWNER. Gitee mirroring uses the real `yunjinqi` account. No placeholder owner is permitted; any emergency bypass must be recorded in the PR. | +| D3 | Branch approval thresholds | `dev`: 1 approval + `Lint` + `Test Summary`. `development`: owner review; R2/R3 require owner + a second maintainer. `master`: R3 only (see §4). Rulesets begin in `evaluate` mode and may not be changed to `active` before 2026-09-05 and the observation evidence is reviewed. **Open exception:** only one real GitHub maintainer has been confirmed, so the second, independent R2/R3 approval is not yet enforceable and blocks active rollout for those paths. | +| D4 | Merge Queue threshold | Not enabled this iteration. Re-evaluate only after ≥3 PRs/day pending merge for 4 consecutive weeks, or recurring baseline conflicts. | + +## 3. PR target-branch decision table + +| Situation | Default target | Required evidence | Post-merge action | +|---|---|---|---| +| Docs, tests, routine features, ordinary bug fixes | `dev` | Associated tests, fast gate, ≥1 maintainer approval | Candidate for the next `dev → development` promotion | +| Problem that only exists in the optimized architecture, or an optimization-only feature | `development` | Optimization-only minimal repro, risk note, domain-owner approval | Decide whether an equivalent fix is needed in `dev` | +| Real bug / security issue in original Backtrader | `master` | Independent repro on `master`, regression test, original-API compatibility note | Create a forward-port issue; never close `dev`/`development` risk with "fixed on master" | +| Fix whose semantics differ across branches | Separate PRs | Independent implementation + tests per target branch | Cross-link PRs/issues; forbid blind merge or cherry-pick | + +## 4. Risk levels (R0–R3) + +| Level | Typical paths | Minimum review | Minimum verification | +|---|---|---|---| +| R0 docs/tests | `docs/`, test comments, non-behavioral tooling | 1 maintainer | Format, affected tests, docs build | +| R1 routine module | Localized fix to a single indicator/analyzer/feed | 1 module owner | Fast CI + new/modified regression tests | +| R2 core/compatibility | `lineroot`, `linebuffer`, `lineseries`, `lineiterator`, `cerebro`, `strategy`, `broker`, `brokers/`, `feeds/`, `metabase` | Domain owner + a second maintainer | Fast CI, `make test-strategies`, runonce/runnext or compatibility evidence | +| R3 baseline/security/release | `master` hotfix, supply chain, security, public-API break risk | Explicit core-maintainer approval | Full target-branch suite, minimal repro, regression, release/security check | + +## 5. Target workflow + +```text +Routine community contribution +fork / feature/* ── PR + fast gate ──> dev + │ + │ controlled promotion PR + full gate + ▼ + development + +Optimization-only issue +feature/* ── PR + optimization gate ──> development + +Real original-baseline bug +hotfix/master-* ── PR + original-baseline gate ──> master + │ + └─ create forward-port task: evaluate and port to dev / development separately +``` + +## 6. Promotion protocol (`dev` → `development`) + +A promotion is a **controlled PR**, not a routine merge. + +1. Open a `promotion/dev-YYYYMMDD` PR targeting `development`. +2. Describe: change scope, explicitly excluded content, full verification, + performance/compatibility differences, and the rollback point. +3. Run the full gate (`make test-strategies`, runonce/runnext parity, or + strategy-baseline comparison) for R2/R3 content. +4. Get owner + second-maintainer approval per §4. +5. Merge; record the promotion in the weekly governance summary (§12). + +## 7. `master` hotfix forward-port protocol + +Every `master` fix must produce a **linked forward-port issue** before the fix +is considered governance-complete. + +1. Reproduce on `master` independently; land `hotfix/master-*` PR (R3 gate). +2. Create a `forward-port-required` issue describing the fix. +3. Evaluate `dev` and `development` **separately** for whether each is affected. +4. For each affected branch, implement an **equivalent port with independent + tests** — never a blind cross-branch merge or cherry-pick. +5. Mark `forward-port-complete` only after each affected branch is verified. + +A hotfix that has not completed forward-port is **not** governance-done. + +## 8. GitHub / Gitee mirror consistency + +- GitHub is the review authority; Gitee is a controlled mirror. +- After each long-lived-branch merge, verify both remotes report the same SHA: + +```bash +git ls-remote --heads https://github.com/cloudQuant/backtrader.git master development dev +git ls-remote --heads https://gitee.com/yunjinqi/backtrader.git master development dev +``` + +- The mirror owner is the same maintainer using GitHub `@cloudQuant` and Gitee + `yunjinqi`. Any divergence must raise an alert and be resolved by that owner. + A documented reason is required for any recorded difference. + +## 9. Labels + +Standardized label taxonomy (applied via GitHub; see the runbook in §10): + +| Prefix | Values | +|---|---| +| `target:*` | `target:dev`, `target:development`, `target:master-hotfix` | +| `type:*` | `type:bug`, `type:feature`, `type:docs`, `type:tests`, `type:refactor` | +| `area:*` | `area:core`, `area:broker`, `area:feeds`, `area:indicators`, `area:analyzers`, `area:observers`, `area:tests`, `area:docs`, `area:ci` | +| `risk:*` | `risk:R0`, `risk:R1`, `risk:R2`, `risk:R3` | +| `status:*` | `status:triage`, `status:review`, `status:blocked` | +| Action | `needs-repro`, `needs-tests`, `ready-to-merge`, `blocked`, `backport-or-forward-port-required`, `forward-port-required`, `forward-port-complete` | + +## 10. External setup runbook (manual, admin-only) + +The repository-internal artifacts below (manifest files, `CODEOWNERS`, and the +verification script) express the expected GitHub configuration. Applying the +real settings requires admin access and is done manually via UI/API — **CI never +holds admin credentials.** + +### 10.1 Default branch (D0) + +No change: keep `development` as the default branch. Target-branch selection is +enforced by the PR template and the `PR Governance` workflow, not the default. + +### 10.2 Rulesets (D3) + +Apply one ruleset per long-lived branch, matching +`.github/governance/rulesets/{dev,development,master}.json`: + +1. Repository → Settings → Rules → Rulesets → **New ruleset**. +2. Set the target to the branch (or `fnmatch` pattern) per manifest. +3. Start with `evaluate` enforcement until 2026-09-05. The manifests require a + pull request, ≥1 approval, `Lint`, `Test Summary`, `PR Governance`, and + `Tiered Validation`; they also block force-push/deletion and require resolved + conversations. +4. GitHub Rulesets cannot natively decide a PR head-branch naming convention or + require a label. After observation, the required `PR Governance` check is + the enforcement point for `hotfix/master-*` and `target:master-hotfix` on + `master`; the template and human review require the minimal repro and R3 + evidence. +5. Apply the JSON through the repository Rulesets API or UI, then verify it with + `scripts/ci/verify_github_governance.py`. Do not give the CI workflow an + admin token. + +The current manifests intentionally require one approval because only one real +GitHub maintainer (`@cloudQuant`) has been confirmed. They do **not** make a +single person count as the required second independent maintainer for R2/R3. +Add and verify a second maintainer before enabling active rules for those paths; +until then, record the exception and keep the Rulesets in observation mode. + +### 10.3 CODEOWNERS (D2) + +`.github/CODEOWNERS` uses the confirmed GitHub user `@cloudQuant`, not an +organization placeholder. It has repository-admin access as of 2026-08-22; +the Gitee `yunjinqi` identity is intentionally not a GitHub CODEOWNERS entry. +After the file reaches the default branch, verify the GitHub +`codeowners/errors` response is empty. + +### 10.4 Labels + +Create the labels in §9 once (Repository → Issues → Labels). The +`classify_pr_risk.py` script emits suggested labels; maintainers retain final +override authority. + +## 11. Verification + +```bash +# Export read-only API responses without committing them. +gh api --paginate repos/cloudQuant/backtrader/rulesets > /tmp/backtrader-rulesets.json +gh api repos/cloudQuant/backtrader/codeowners/errors > /tmp/backtrader-codeowners-errors.json + +# Ruleset + CODEOWNERS consistency. `evaluate` is expected during observation. +python scripts/ci/verify_github_governance.py \ + --rulesets-json /tmp/backtrader-rulesets.json \ + --codeowners-errors-json /tmp/backtrader-codeowners-errors.json \ + --expected-enforcement evaluate + +# PR risk classification +python scripts/ci/classify_pr_risk.py --paths backtrader/cerebro.py + +# Unit tests for both scripts +pytest tests/unit/scripts/ -q +``` + +## 12. Observation and activation record + +The administrator creates the three Rulesets with `evaluate` enforcement and +keeps `GOVERNANCE_BLOCKING=false` through **2026-09-05**. During that period, +record all Rule Insights, missing check contexts, mirror drifts, and PR routing +exceptions in the weekly governance record. Only after a maintainer reviews +that evidence may the administrator change the three Rulesets to `active` and +set `GOVERNANCE_BLOCKING=true`; the activation commit/PR must link the evidence. +This is intentionally not an automatic time-based switch. diff --git a/docs/source/developer-guide/branch-governance_zh.md b/docs/source/developer-guide/branch-governance_zh.md new file mode 100644 index 00000000..4b8d185b --- /dev/null +++ b/docs/source/developer-guide/branch-governance_zh.md @@ -0,0 +1,190 @@ +--- +title: 分支治理 +description: 三分支模型、PR 分流、风险分级与 promotion/hotfix 协议 +--- + +# 分支治理 + +> 状态:**实施中**(迭代 140;Ruleset 自 2026-08-22 起处于两周观察期) +> 适用仓库:`cloudQuant/backtrader` +> 长期分支:`master`、`development`、`dev` + +本文档是分支角色、PR 分流、审查要求与跨分支修复传播的**权威来源**。它取代任何 +早前的「`development` → `master` 发布链」表述。当其他文档(`README.md`、 +`CONTRIBUTING.md`、`AGENTS.md`、PR/issue 模板)与本文档冲突时,以本文档为准。 + +## 1. 强制性分支事实 + +以下定义是本仓库治理的不可协商前提,**不遵循**常见 GitFlow。 + +| 分支 | 角色 | 允许进入的变更 | 禁止的变更 | +|---|---|---|---| +| `master` | 原始 Backtrader 基线 | 已在原始版复现的 Bug、兼容性或安全修复 | 日常功能、优化版重构、`dev`/`development` 的例行合并 | +| `development` | 改进与优化版本 | 优化版能力、架构优化、仅在优化版存在的回归修复、受控集成的日常开发成果 | 被当作 `master` 的发布候选或反向同步源 | +| `dev` | 日常开发入口 | 常规功能、普通 Bug 修复、文档、测试、重构、社区贡献 | 绕过评审直接进入 `development`/`master` | + +## 2. 治理决策(D0–D4) + +迭代 140 的已记录决策。未决项为阻塞项,不得静默默认假设。 + +| 编号 | 决策 | 结论 | +|---|---|---| +| D0 | GitHub 默认分支 | **保持 `development`**(2026-08-20 决策)。贡献者通过 PR 模板与 `pr-governance` workflow 显式选择目标分支,而非依赖默认分支。 | +| D1 | GitHub/Gitee 权威来源与同步 | **GitHub `cloudQuant/backtrader` 为审查权威;Gitee `yunjinqi/backtrader` 为受控镜像。** 镜像负责人在每次长期分支合并后核对 SHA(见 §8)。 | +| D2 | Owner 团队与管理员 bypass | **已于 2026-08-22 解决:**`@cloudQuant` 是对本仓库有管理员权限的真实 GitHub 用户,作为 CODEOWNER;Gitee 镜像使用真实 `yunjinqi` 身份。禁止占位 owner;任何紧急 bypass 必须在 PR 中留痕。 | +| D3 | 分支审批门槛 | `dev`:1 次批准 + `Lint` + `Test Summary`。`development`:owner 审查;R2/R3 需 owner + 第二位维护者。`master`:仅 R3(见 §4)。Ruleset 先以 `evaluate` 运行,在 2026-09-05 前不得转为 `active`,且需先审查观察证据。**未关闭例外:**当前只确认一名真实 GitHub maintainer,R2/R3 的第二位独立审批尚无法实施,因而阻塞这些路径的 active rollout。 | +| D4 | Merge Queue 阈值 | 本迭代不启用。仅在连续 4 周每天 ≥3 个待合并 PR 或频繁基线冲突后再评估。 | + +## 3. PR 目标分支决策表 + +| 情况 | 默认目标 | 必需证据 | 合并后动作 | +|---|---|---|---| +| 文档、测试、常规功能、普通 Bug 修复 | `dev` | 关联测试、快速门禁、≥1 位维护者批准 | 纳入下一次 `dev → development` promotion 候选 | +| 仅在优化架构出现的问题或优化版功能 | `development` | 优化版最小复现、风险说明、领域 owner 批准 | 判断 `dev` 是否需要等价修复 | +| 原始 Backtrader 的真实 Bug / 安全问题 | `master` | 在 `master` 的独立复现、回归测试、原始 API 兼容性说明 | 创建前移 issue;不得以「已修 master」关闭 `dev`/`development` 风险 | +| 跨分支语义不同的修复 | 分别建 PR | 每个目标分支的独立实现与测试 | 交叉链接 PR/issue;禁止盲目 merge 或 cherry-pick | + +## 4. 风险级别(R0–R3) + +| 等级 | 典型路径 | 最低评审 | 最低验证 | +|---|---|---|---| +| R0 文档/测试 | `docs/`、测试注释、非行为性工具 | 1 位维护者 | 格式、受影响测试、文档构建 | +| R1 常规模块 | 单个 indicator/analyzer/feed 的局部修复 | 1 位模块 owner | 快速 CI + 新增/修改回归测试 | +| R2 核心/兼容性 | `lineroot`、`linebuffer`、`lineseries`、`lineiterator`、`cerebro`、`strategy`、`broker`、`brokers/`、`feeds/`、`metabase` | 领域 owner + 第二位维护者 | 快速 CI、`make test-strategies`、runonce/runnext 或兼容性证据 | +| R3 原始版/安全/发布 | `master` hotfix、供应链、安全、公开 API 破坏风险 | 核心维护者明确批准 | 目标分支全量测试、最小复现、回归、发布/安全检查 | + +## 5. 目标工作流 + +```text +常规社区贡献 +fork / feature/* ── PR + 快速门禁 ──> dev + │ + │ 受控 promotion PR + 全量门禁 + ▼ + development + +优化版专属问题 +feature/* ── PR + optimization 门禁 ──> development + +原始版真实 Bug +hotfix/master-* ── PR + original-baseline 门禁 ──> master + │ + └─ 建立前移任务:分别评估并移植到 dev / development +``` + +## 6. Promotion 协议(`dev` → `development`) + +Promotion 是**受控 PR**,不是例行合并。 + +1. 新建 `promotion/dev-YYYYMMDD` PR,目标 `development`。 +2. 描述:变更范围、显式排除的内容、完整验证、性能/兼容性差异、回滚点。 +3. R2/R3 内容需跑全量门禁(`make test-strategies`、runonce/runnext 一致性或策略基线比对)。 +4. 按 §4 取得 owner + 第二位维护者批准。 +5. 合并;在每周治理摘要中记录本次 promotion(§12)。 + +## 7. `master` hotfix 前移协议 + +每个 `master` 修复在治理完成前必须产生一个 **linked 前移 issue**。 + +1. 在 `master` 独立复现;落地 `hotfix/master-*` PR(R3 门禁)。 +2. 创建 `forward-port-required` issue 描述修复。 +3. 分别评估 `dev` 与 `development` 是否受影响。 +4. 对每个受影响分支,实现**带独立测试的等价移植**——禁止盲目跨分支 merge 或 cherry-pick。 +5. 每个受影响分支验证通过后才标记 `forward-port-complete`。 + +未完成前移的 hotfix **不算**治理完成。 + +## 8. GitHub / Gitee 镜像一致性 + +- GitHub 为审查权威;Gitee 为受控镜像。 +- 每次长期分支合并后,验证两端同一 SHA: + +```bash +git ls-remote --heads https://github.com/cloudQuant/backtrader.git master development dev +git ls-remote --heads https://gitee.com/yunjinqi/backtrader.git master development dev +``` + +- 镜像负责人使用 GitHub `@cloudQuant` 与 Gitee `yunjinqi` 两个真实身份。任何漂移必须 + 告警并由该负责人处理;任何记录的差异必须有书面原因。 + +## 9. 标签 + +标准化标签分类(通过 GitHub 应用;见 §10 运行手册): + +| 前缀 | 取值 | +|---|---| +| `target:*` | `target:dev`、`target:development`、`target:master-hotfix` | +| `type:*` | `type:bug`、`type:feature`、`type:docs`、`type:tests`、`type:refactor` | +| `area:*` | `area:core`、`area:broker`、`area:feeds`、`area:indicators`、`area:analyzers`、`area:observers`、`area:tests`、`area:docs`、`area:ci` | +| `risk:*` | `risk:R0`、`risk:R1`、`risk:R2`、`risk:R3` | +| `status:*` | `status:triage`、`status:review`、`status:blocked` | +| 动作 | `needs-repro`、`needs-tests`、`ready-to-merge`、`blocked`、`backport-or-forward-port-required`、`forward-port-required`、`forward-port-complete` | + +## 10. 外部设置运行手册(手动、仅管理员) + +以下仓库内产物(manifest 文件、`CODEOWNERS` 与校验脚本)表达了预期的 GitHub 配置。 +应用真实设置需要管理员权限,须通过 UI/API 手动完成——**CI 永不持有管理员凭据**。 + +### 10.1 默认分支(D0) + +无需变更:保持 `development` 为默认分支。目标分支选择由 PR 模板与 +`PR Governance` workflow 强制,而非默认分支。 + +### 10.2 Rulesets(D3) + +按 `.github/governance/rulesets/{dev,development,master}.json`,为每个长期分支应用 +一条 ruleset: + +1. 仓库 → Settings → Rules → Rulesets → **New ruleset**。 +2. 按 manifest 将目标设为分支(或 `fnmatch` 模式)。 +3. 先以 `evaluate` 执行至 2026-09-05。manifest 要求 PR、≥1 批准、`Lint`、 + `Test Summary`、`PR Governance`、`Tiered Validation`,并禁止 force-push/删除、要求 + 解决会话。 +4. GitHub Ruleset 不能原生按 PR 源分支命名或标签判断。观察期结束后,required 的 + `PR Governance` check 才负责对 `master` 强制 `hotfix/master-*` 与 + `target:master-hotfix`;最小复现和 R3 证据仍由模板与人工评审把关。 +5. 通过仓库 Rulesets API 或 UI 应用 JSON,然后用 + `scripts/ci/verify_github_governance.py` 验证;不得把管理员 token 交给 CI。 + +当前 manifest 有意只要求一次批准,因为目前只确认了一名真实 GitHub maintainer +(`@cloudQuant`)。这**不能**把同一人算作 R2/R3 所需的第二位独立维护者。启用这些路径的 +active 规则前,必须新增并核验第二名 maintainer;在此之前记录例外并保持 Ruleset 观察模式。 + +### 10.3 CODEOWNERS(D2) + +`.github/CODEOWNERS` 使用已确认的 GitHub 用户 `@cloudQuant`,不是组织占位符;截至 +2026-08-22 它拥有仓库管理员权限。Gitee `yunjinqi` 身份不应作为 GitHub CODEOWNERS +条目。文件进入默认分支后,必须确认 GitHub `codeowners/errors` 响应为空。 + +### 10.4 标签 + +一次性创建 §9 的标签(Repository → Issues → Labels)。`classify_pr_risk.py` 脚本 +输出建议标签;维护者保留最终覆盖权。 + +## 11. 验证 + +```bash +# 导出只读 API 响应,不提交到仓库。 +gh api --paginate repos/cloudQuant/backtrader/rulesets > /tmp/backtrader-rulesets.json +gh api repos/cloudQuant/backtrader/codeowners/errors > /tmp/backtrader-codeowners-errors.json + +# Ruleset + CODEOWNERS 一致性;观察期预期 enforcement 为 evaluate。 +python scripts/ci/verify_github_governance.py \ + --rulesets-json /tmp/backtrader-rulesets.json \ + --codeowners-errors-json /tmp/backtrader-codeowners-errors.json \ + --expected-enforcement evaluate + +# PR 风险分级 +python scripts/ci/classify_pr_risk.py --paths backtrader/cerebro.py + +# 两个脚本的单元测试 +pytest tests/unit/scripts/ -q +``` + +## 12. 观察期与激活记录 + +管理员以 `evaluate` 创建三条 Ruleset,并在 **2026-09-05** 前保持 +`GOVERNANCE_BLOCKING=false`。这段时间记录所有 Rule Insights、缺失 check context、 +镜像漂移与 PR 分流例外,写入每周治理记录。只有维护者审阅证据后,管理员才能把三条 +Ruleset 改为 `active` 并设置 `GOVERNANCE_BLOCKING=true`;激活的 commit/PR 必须关联该 +证据。禁止按日期自动切换。 diff --git a/docs/source/developer-guide/contributing.md b/docs/source/developer-guide/contributing.md index 05e00cfb..b1b26ec6 100644 --- a/docs/source/developer-guide/contributing.md +++ b/docs/source/developer-guide/contributing.md @@ -1,460 +1,109 @@ -- -- - +--- title: Contributing to Backtrader description: Guidelines for contributing to Backtrader - -- -- +--- # Contributing to Backtrader -Thank you for your interest in contributing to Backtrader! This document provides guidelines and workflows for contributing to the project. - -## Table of Contents - -- [Getting Started](#getting-started) -- [Pull Request Workflow](#pull-request-workflow) -- [Code Review Standards](#code-review-standards) -- [Issue Reporting Guidelines](#issue-reporting-guidelines) -- [Community Guidelines](#community-guidelines) -- [License and Contributor Agreement](#license-and-contributor-agreement) -- [Developer Certificate of Origin](#developer-certificate-of-origin-dco) - -## Getting Started - -### Prerequisites +感谢你对 Backtrader 的贡献兴趣!本文档是根目录 [`CONTRIBUTING.md`](../../../CONTRIBUTING.md) +与 [Branch Governance](branch-governance.md) 的收敛版,供开发者指南内阅读。 -- Python 3.8 or higher -- Git -- Basic knowledge of Python programming -- Familiarity with quantitative trading concepts (helpful but not required) +分支角色、PR 目标分支选择、风险分级与 promotion/hotfix 协议的**权威来源**是 +[Branch Governance](branch-governance.md)。任何冲突以分支治理文档为准。 -### First Time Setup +## 快速开始 ```bash - -# 1. Fork the repository on GitHub - -# Click "Fork" button at - -# 2. Clone your fork - -git clone +# 1. Fork 仓库:https://github.com/cloudQuant/backtrader +# 2. 克隆并添加上游 +git clone https://github.com/YOUR_USERNAME/backtrader.git cd backtrader +git remote add upstream https://github.com/cloudQuant/backtrader.git -# 3. Add upstream remote - -git remote add upstream - -# 4. Install dependencies - +# 3. 安装依赖与开发模式(纯 Python,无独立 Cython 编译步骤) pip install -r requirements.txt - -# 5. Install in development mode - pip install -e . -# 6. Compile Cython extensions (recommended for performance) - -cd backtrader && python -W ignore compile_cython_numba_files.py && cd .. - -```bash - -### Branch Naming Conventions - -Use descriptive branch names that indicate the type of change: - -| Prefix | Purpose | Example | +# 4. 验证 +make test-fast +``` -|--------|---------|---------| +## 目标分支选择 -| `feat/` | New feature | `feat/websocket-reconnect` | +本仓库使用**三分支模型**。先确定你的贡献属于哪一类: -| `fix/` | Bug fix | `fix/indicator-calculation` | +| 情况 | 目标分支 | +|------|----------| +| 文档、测试、常规功能、普通 Bug 修复 | `dev` | +| 仅在优化架构出现的问题或优化版功能 | `development` | +| 原始 Backtrader 的真实 Bug / 安全问题 | `master`(`hotfix/master-*`) | -| `refactor/` | Code refactoring | `refactor/broker-optimization` | +完整决策表与风险分级(R0–R3)见 [Branch Governance](branch-governance.md)。 -| `docs/` | Documentation | `docs/api-reference` | +## 分支命名约定 -| `test/` | Test improvements | `test/coverage-increase` | +| 前缀 | 用途 | 示例 | +|------|------|------| +| `feat/` | 新功能 | `feat/websocket-reconnect` | +| `fix/` | Bug 修复 | `fix/indicator-calculation` | +| `refactor/` | 代码重构 | `refactor/broker-optimization` | +| `docs/` | 文档 | `docs/api-reference` | +| `test/` | 测试改进 | `test/coverage-increase` | +| `perf/` | 性能优化 | `perf/line-buffer-cache` | +| `hotfix/master-*` | 原始版热修复 | `hotfix/master-order-cancel` | -| `perf/` | Performance | `perf/line-buffer-cache` | +## 提交信息 -## Pull Request Workflow - -### Step 1: Create a Feature Branch - -```bash - -# Sync with upstream - -git fetch upstream -git checkout dev -git merge upstream/dev - -# Create your feature branch - -git checkout -b feat/your-feature-name - -```bash - -### Step 2: Make Your Changes - -- Write clean, readable code -- Follow the [Code Style](style.md) guidelines -- Add tests for new functionality -- Update documentation as needed - -### Step 3: Commit Your Changes - -Follow [Conventional Commits]( format: +遵循 [Conventional Commits](https://www.conventionalcommits.org/): ```bash : +``` -[optional body] - -```bash - -- *Valid types:** -- `feat`: New feature -- `fix`: Bug fix -- `refactor`: Code refactoring -- `docs`: Documentation changes -- `test`: Test additions or modifications -- `chore`: Maintenance tasks -- `perf`: Performance improvements - -- *Examples:** - -```bash -git commit -m "feat: add WebSocket health check to CCXTFeed" -git commit -m "fix: handle order-not-found in CCXTBroker.cancel()" -git commit -m "perf: cache broker reference in total_value.next()" -git commit -m "docs: update CCXT live trading guide" - -```bash - -### Step 4: Run Tests - -```bash - -# Run pre-commit tests (P0 + P1) - -pytest tests/ -v -m "priority_p0 or priority_p1" - -# Run full test suite - -pytest tests/ -v -n 4 - -# Check code formatting - -make format-check - -# Run linting - -make lint - -```bash - -### Step 5: Push and Create Pull Request - -```bash - -# Push to your fork - -git push origin feat/your-feature-name - -# Create pull request on GitHub - -# Target: dev branch - -```bash - -### Pull Request Description Template - -```markdown - -## Summary - -Brief description of what this PR does and why. - -## Changes - -- List of major changes - -## Type of Change - -- [ ] Bug fix -- [ ] New feature -- [ ] Performance improvement -- [ ] Documentation update -- [ ] Refactoring -- [ ] Breaking change - -## Testing - -- Describe testing approach -- Include test commands - -```bash -pytest tests/path/to/test.py -v - -```bash - -## Checklist - -- [ ] Code follows style guidelines -- [ ] Tests pass locally -- [ ] New tests added for new features -- [ ] Documentation updated -- [ ] CHANGELOG.md updated (for user-facing changes) -- [ ] No merge conflicts with target branch - -## Related Issues - -Fixes #123 -Related to #456 - -```bash - -## Code Review Standards - -### Review Process - -1. **Automated Checks**: All PRs must pass CI/CD checks -2. **Peer Review**: At least one maintainer approval required -3. **Test Coverage**: New code requires corresponding tests -4. **Documentation**: API changes require documentation updates - -### Review Criteria - -Maintainers review pull requests for: - -| Aspect | Criteria | - -|--------|----------| - -| **Functionality**| Works as intended, no regressions | - -|**Code Quality**| Readable, maintainable, follows conventions | - -|**Testing**| Adequate coverage, edge cases handled | - -|**Documentation**| Clear docstrings, user-facing changes documented | - -|**Performance**| No significant degradation, optimizations documented | - -### Addressing Review Feedback - -- Respond to all review comments -- Make requested changes or provide justification -- Mark conversations as resolved when addressed -- Request re-review after significant changes - -### Approval Requirements - -- Small changes: Single maintainer approval -- Medium changes: Two maintainer approvals -- Large/Complex changes: Core team consensus - -## Issue Reporting Guidelines - -### Bug Reports - -Include the following information: - -```markdown +有效类型:`feat`、`fix`、`refactor`、`docs`、`test`、`chore`、`perf`。 -## Environment - -- Python version: 3.11.0 -- Operating system: Ubuntu 22.04 -- Backtrader version: 1.0.0 (dev branch) -- Installation method: pip install -e . - -## Description - -Clear description of the bug. - -## Steps to Reproduce - -1. Create a Cerebro instance -2. Add data feed with... -3. Run strategy -4. Observe error - -## Expected Behavior - -What should happen. - -## Actual Behavior - -What actually happens (include error messages). - -## Code Sample +## 运行测试 ```bash -import backtrader as bt - -# Minimal reproducible example - -```bash - -## Additional Context - -Logs, screenshots, or other relevant information. - -```bash - -### Feature Requests - -Provide the following information: - -```markdown - -## Problem Statement +make test-fast # 日常开发反馈(~3.5min) +make test-strategies # 改动 cerebro/strategy/line 系统后必跑(~9min) +make test-all # 全量(~10min) +make format-check # 格式检查 +make lint # 代码检查 +``` -What problem does this solve? What is the use case? +## 代码审查标准 -## Proposed Solution +| 方面 | 标准 | +|------|------| +| 功能正确性 | 按预期工作,无回归 | +| 代码质量 | 可读、可维护、遵循约定 | +| 测试 | 覆盖充分,处理边界情况 | +| 文档 | 清晰的文档字符串,面向用户的变更已记录 | +| 性能 | 无显著退化,优化已记录 | -Detailed description of the desired feature. +审批门槛(D3):`dev` 1 次批准;`development` R2/R3 需 owner + 第二位维护者; +`master` 仅 R3。详见 [Branch Governance](branch-governance.md)。 -## Alternatives Considered +## 报告问题 -What other approaches did you consider? +- **Bug**:使用 [Issue Forms](../../../.github/ISSUE_TEMPLATE/) 提交,包含环境、目标分支、最小复现、预期/实际行为、日志。 +- **功能请求**:说明问题、目标用户、分支影响、替代方案。 +- **安全问题**:走 [`SECURITY.md`](../../../SECURITY.md) 私密报告路径,不要公开提交。 -## Additional Context +## 许可证与 DCO -Examples, references, or implementation ideas. - -```bash - -## Community Guidelines - -### Code of Conduct - -- Be respectful and inclusive -- Welcome newcomers and help them learn -- Focus on constructive feedback -- Assume good intentions - -### Communication Channels - -- **Issues**: Bug reports and feature requests -- **Discussions**: Questions and ideas -- **Pull Requests**: Code contributions - -### Getting Help - -- Search existing issues and discussions first -- Provide minimal reproducible examples -- Share relevant environment details -- Be patient with volunteer maintainers - -## License and Contributor Agreement - -### License - -Backtrader is licensed under the GNU General Public License v3.0 (GPLv3). - -By contributing to Backtrader, you agree that your contributions will be licensed under the GPLv3. - -### Copyright - -Copyright is retained by the original contributor. The project includes attribution in: - -- LICENSE file -- CONTRIBUTORS file -- Release notes - -## Developer Certificate of Origin (DCO) - -### What is DCO? - -The DCO is a simple statement that you certify you have the right to submit your contribution. - -### DCO Sign-off - -To certify your contribution, add a `Signed-off-by` line to your commit messages: - -```bash -git commit -m "feat: add new indicator - -Signed-off-by: Your Name " - -```bash - -### Automatic Sign-off - -Configure Git to automatically add sign-off: - -```bash -git config --global commit.signoff true - -```bash -Then use `-s` flag: +Backtrader 采用 GPLv3 许可。通过贡献,你同意贡献将在 GPLv3 下许可,并按 +[Developer Certificate of Origin](https://developercertificate.org/) 认证你有权提交: ```bash git commit -s -m "feat: add new indicator" +``` -```bash - -### DCO Certification - -By signing off, you certify: - -> Developer Certificate of Origin -> Version 1.1 -> -> Copyright (C) 2004, 2006 The Linux Foundation and its contributors. -> 1 Letterman Drive -> Suite D4700 -> San Francisco, CA, 94129 -> -> Everyone is permitted to copy and distribute verbatim copies of this -> license document, but changing it is not allowed. -> -> -> Developer's Certificate of Origin 1.1 -> -> By making a contribution to this project, I certify that: -> -> (a) The contribution was created in whole or in part by me and I -> have the right to submit it under the open source license -> indicated in the file; or -> -> (b) The contribution is based upon previous work that, to the best -> of my knowledge, is covered under an appropriate open source -> license and I have the right under that license to submit that -> work with modifications, whether created in whole or in part -> by me, under the same open source license (unless I am -> permitted to submit under a different license), as indicated -> in the file; or -> -> (c) The contribution was provided directly to me by some other -> person who certified (a), (b) or (c) and I have not modified -> it. -> -> (d) I understand and agree that this project and the contribution -> are public and that a record of the contribution (including all -> personal information I submit with it, including my sign-off) is -> maintained indefinitely and may be redistributed consistent with -> this project or the open source license(s) involved. - -## Recognition - -Contributors are recognized in: - -- `CONTRIBUTORS` file -- Release notes -- Project documentation (for significant contributions) - -Thank you for contributing to Backtrader! - -## See Also +## 另请参阅 -- [Development Setup](setup.md) -- [Code Style](style.md) -- [Testing Guide](testing.md) -- [Project Context](../project-context.md) +- [Branch Governance](branch-governance.md) +- [开发环境设置](setup.md) +- [代码风格](style.md) +- [测试指南](testing.md) +- [发布流程](release.md) diff --git a/docs/source/developer-guide/contributing_zh.md b/docs/source/developer-guide/contributing_zh.md index 952a9a21..5a570393 100644 --- a/docs/source/developer-guide/contributing_zh.md +++ b/docs/source/developer-guide/contributing_zh.md @@ -1,455 +1,109 @@ -- -- - +--- title: 贡献指南 description: Backtrader 贡献指南 - -- -- +--- # 贡献指南 -感谢您对 Backtrader 的贡献兴趣!本文档提供了参与项目开发的指南和工作流程。 - -## 目录 +感谢您对 Backtrader 的贡献兴趣!本文档是根目录 [`CONTRIBUTING.md`](../../../CONTRIBUTING.md) +与 [分支治理](branch-governance_zh.md) 的收敛版,供开发者指南内阅读。 -- [快速开始](#快速开始) -- [Pull Request 流程](#pull-request-流程) -- [代码审查标准](#代码审查标准) -- [Issue 报告指南](#issue-报告指南) -- [社区准则](#社区准则) -- [许可证和贡献者协议](#许可证和贡献者协议) -- [开发者来源证书 (DCO)](#开发者来源证书-dco) +分支角色、PR 目标分支选择、风险分级与 promotion/hotfix 协议的**权威来源**是 +[分支治理](branch-governance_zh.md)。任何冲突以分支治理文档为准。 ## 快速开始 -### 前置要求 - -- Python 3.8 或更高版本 -- Git -- Python 编程基础知识 -- 了解量化交易概念(有帮助但非必需) - -### 首次设置 - ```bash - -# 1. 在 GitHub 上 Fork 仓库 - -# 访问 并点击 "Fork" 按钮 - -# 2. 克隆你的 Fork - -git clone +# 1. Fork 仓库:https://github.com/cloudQuant/backtrader +# 2. 克隆并添加上游 +git clone https://github.com/你的用户名/backtrader.git cd backtrader +git remote add upstream https://github.com/cloudQuant/backtrader.git -# 3. 添加上游远程仓库 - -git remote add upstream - -# 4. 安装依赖 - +# 3. 安装依赖与开发模式(纯 Python,无独立 Cython 编译步骤) pip install -r requirements.txt - -# 5. 以开发模式安装 - pip install -e . -# 6. 编译 Cython 扩展(推荐,以获得更好性能) +# 4. 验证 +make test-fast +``` -cd backtrader && python -W ignore compile_cython_numba_files.py && cd .. +## 目标分支选择 -```bash +本仓库使用**三分支模型**。先确定您的贡献属于哪一类: -### 分支命名约定 +| 情况 | 目标分支 | +|------|----------| +| 文档、测试、常规功能、普通 Bug 修复 | `dev` | +| 仅在优化架构出现的问题或优化版功能 | `development` | +| 原始 Backtrader 的真实 Bug / 安全问题 | `master`(`hotfix/master-*`) | -使用描述性的分支名来指示变更类型: +完整决策表与风险分级(R0–R3)见 [分支治理](branch-governance_zh.md)。 -| 前缀 | 用途 | 示例 | +## 分支命名约定 +| 前缀 | 用途 | 示例 | |------|------|------| - | `feat/` | 新功能 | `feat/websocket-reconnect` | - | `fix/` | Bug 修复 | `fix/indicator-calculation` | - | `refactor/` | 代码重构 | `refactor/broker-optimization` | - | `docs/` | 文档 | `docs/api-reference` | - | `test/` | 测试改进 | `test/coverage-increase` | - | `perf/` | 性能优化 | `perf/line-buffer-cache` | +| `hotfix/master-*` | 原始版热修复 | `hotfix/master-order-cancel` | -## Pull Request 流程 +## 提交信息 -### 步骤 1: 创建功能分支 - -```bash - -# 与上游同步 - -git fetch upstream -git checkout dev -git merge upstream/dev - -# 创建你的功能分支 - -git checkout -b feat/your-feature-name - -```bash - -### 步骤 2: 进行更改 - -- 编写清晰、可读的代码 -- 遵循[代码风格](style_zh.md)指南 -- 为新功能添加测试 -- 更新相关文档 - -### 步骤 3: 提交更改 - -遵循 [Conventional Commits]( 格式: +遵循 [Conventional Commits](https://www.conventionalcommits.org/): ```bash : +``` -[可选的正文] - -```bash - -- *有效类型:** -- `feat`: 新功能 -- `fix`: Bug 修复 -- `refactor`: 代码重构 -- `docs`: 文档更改 -- `test`: 测试添加或修改 -- `chore`: 维护任务 -- `perf`: 性能改进 - -- *示例:** - -```bash -git commit -m "feat: 为 CCXTFeed 添加 WebSocket 健康检查" -git commit -m "fix: 处理 CCXTBroker.cancel() 中的 order-not-found" -git commit -m "perf: 在 total_value.next() 中缓存 broker 引用" -git commit -m "docs: 更新 CCXT 实盘交易指南" - -```bash - -### 步骤 4: 运行测试 - -```bash - -# 运行预提交测试 (P0 + P1) - -pytest tests/ -v -m "priority_p0 or priority_p1" - -# 运行完整测试套件 - -pytest tests/ -v -n 4 - -# 检查代码格式 - -make format-check - -# 运行代码检查 - -make lint - -```bash - -### 步骤 5: 推送并创建 Pull Request - -```bash - -# 推送到你的 Fork - -git push origin feat/your-feature-name - -# 在 GitHub 上创建 Pull Request +有效类型:`feat`、`fix`、`refactor`、`docs`、`test`、`chore`、`perf`。 -# 目标分支: dev - -```bash - -### Pull Request 描述模板 - -```markdown - -## 概述 - -简要描述此 PR 的作用和原因。 - -## 变更内容 - -- 主要变更列表 - -## 变更类型 - -- [ ] Bug 修复 -- [ ] 新功能 -- [ ] 性能改进 -- [ ] 文档更新 -- [ ] 代码重构 -- [ ] 破坏性变更 - -## 测试 - -- 描述测试方法 -- 包含测试命令 - -```bash -pytest tests/path/to/test.py -v - -```bash - -## 检查清单 - -- [ ] 代码符合风格指南 -- [ ] 本地测试通过 -- [ ] 为新功能添加测试 -- [ ] 更新文档 -- [ ] 更新 CHANGELOG.md(面向用户的变更) -- [ ] 与目标分支无合并冲突 - -## 相关 Issues - -Fixes #123 -Related to #456 +## 运行测试 ```bash +make test-fast # 日常开发反馈(~3.5min) +make test-strategies # 改动 cerebro/strategy/line 系统后必跑(~9min) +make test-all # 全量(~10min) +make format-check # 格式检查 +make lint # 代码检查 +``` ## 代码审查标准 -### 审查流程 - -1. **自动检查**:所有 PR 必须通过 CI/CD 检查 -2. **同行评审**:至少需要一位维护者批准 -3. **测试覆盖**:新代码需要相应的测试 -4. **文档更新**:API 变更需要更新文档 - -### 审查标准 - -维护者从以下方面审查 Pull Request: - | 方面 | 标准 | - |------|------| +| 功能正确性 | 按预期工作,无回归 | +| 代码质量 | 可读、可维护、遵循约定 | +| 测试 | 覆盖充分,处理边界情况 | +| 文档 | 清晰的文档字符串,面向用户的变更已记录 | +| 性能 | 无显著退化,优化已记录 | -| **功能正确性**| 按预期工作,无回归 | - -|**代码质量**| 可读、可维护、符合约定 | - -|**测试**| 覆盖充分,处理边界情况 | - -|**文档**| 清晰的文档字符串,面向用户的变更已记录 | - -|**性能**| 无显著退化,优化已记录 | - -### 处理审查反馈 - -- 回应所有审查意见 -- 进行请求的更改或提供理由 -- 解决后标记对话为已解决 -- 重大更改后请求重新审查 - -### 批准要求 - -- 小更改:一位维护者批准 -- 中等更改:两位维护者批准 -- 大型/复杂更改:核心团队共识 - -## Issue 报告指南 - -### Bug 报告 - -包含以下信息: - -```markdown - -## 环境 - -- Python 版本:3.11.0 -- 操作系统:Ubuntu 22.04 -- Backtrader 版本:1.0.0 (dev 分支) -- 安装方式:pip install -e . - -## 问题描述 - -清晰描述 Bug。 - -## 复现步骤 - -1. 创建 Cerebro 实例 -2. 添加数据源... -3. 运行策略 -4. 观察错误 - -## 预期行为 - -应该发生什么。 - -## 实际行为 - -实际发生了什么(包含错误信息)。 - -## 代码示例 - -```bash -import backtrader as bt - -# 最小可复现代码 - -```bash - -## 附加信息 - -日志、截图或其他相关信息。 - -```bash - -### 功能请求 - -提供以下信息: - -```markdown - -## 问题陈述 - -这解决了什么问题?用例是什么? - -## 建议的解决方案 +审批门槛(D3):`dev` 1 次批准;`development` R2/R3 需 owner + 第二位维护者; +`master` 仅 R3。详见 [分支治理](branch-governance_zh.md)。 -所需功能的详细描述。 +## 报告问题 -## 考虑的替代方案 +- **Bug**:使用 [Issue Forms](../../../.github/ISSUE_TEMPLATE/) 提交,包含环境、目标分支、最小复现、预期/实际行为、日志。 +- **功能请求**:说明问题、目标用户、分支影响、替代方案。 +- **安全问题**:走 [`SECURITY.md`](../../../SECURITY.md) 私密报告路径,不要公开提交。 -您还考虑了哪些其他方法? +## 许可证与 DCO -## 附加信息 - -示例、参考或实现想法。 - -```bash - -## 社区准则 - -### 行为准则 - -- 尊重和包容 -- 欢迎新手并帮助他们学习 -- 专注于建设性反馈 -- 假设良好意图 - -### 沟通渠道 - -- **Issues**:Bug 报告和功能请求 -- **Discussions**:问题和想法 -- **Pull Requests**:代码贡献 - -### 获取帮助 - -- 首先搜索现有的 issues 和 discussions -- 提供最小可复现示例 -- 分享相关环境详情 -- 对志愿者维护者保持耐心 - -## 许可证和贡献者协议 - -### 许可证 - -Backtrader 采用 GNU General Public License v3.0 (GPLv3) 许可。 - -通过向 Backtrader 贡献,您同意您的贡献将在 GPLv3 下许可。 - -### 版权 - -版权由原始贡献者保留。项目在以下位置包含致谢: - -- LICENSE 文件 -- CONTRIBUTORS 文件 -- 发布说明 - -## 开发者来源证书 (DCO) - -### 什么是 DCO? - -DCO 是一个简单的声明,证明您有权提交您的贡献。 - -### DCO 签署 - -要认证您的贡献,请在提交消息中添加 `Signed-off-by` 行: - -```bash -git commit -m "feat: 添加新指标 - -Signed-off-by: 你的名字 " - -```bash - -### 自动签署 - -配置 Git 自动添加签署: - -```bash -git config --global commit.signoff true - -```bash -然后使用 `-s` 标志: +Backtrader 采用 GPLv3 许可。通过贡献,您同意贡献将在 GPLv3 下许可,并按 +[Developer Certificate of Origin](https://developercertificate.org/) 认证您有权提交: ```bash git commit -s -m "feat: 添加新指标" - -```bash - -### DCO 认证 - -通过签署,您证明: - -> 开发者来源证书 -> 版本 1.1 -> -> 版权所有 (C) 2004, 2006 The Linux Foundation 及其贡献者。 -> 1 Letterman Drive -> Suite D4700 -> San Francisco, CA, 94129 -> -> 任何人都可以复制和分发本许可文档的逐字副本, -> 但不允许更改。 -> -> -> 开发者来源证书 1.1 -> -> 通过向本项目做出贡献,我证明: -> -> (a) 该贡献全部或部分由我创建,我有权根据文件中 -> 指明的开源许可证提交它;或 -> -> (b) 该贡献基于以前的工作,据我所知,这些工作受 -> 适当的开源许可证保护,我有权根据该许可证 -> 提交修改后的作品(无论全部或部分由我创建), -> 使用相同的开源许可证(除非我被允许根据不同 -> 许可证提交),如文件中所指明;或 -> -> (c) 该贡献由认证了 (a)、(b) 或 (c) 的其他人直接 -> 提供给我,且我未对其进行修改。 -> -> (d) 我理解并同意该项目和贡献是公开的,贡献记录 -> (包括我提交的所有个人信息,包括我的签署)将 -> 无限期保存,并可根据本项目或所涉及的开源许可 -> 证重新分发。 - -## 认可 - -贡献者在以下位置获得认可: - -- `CONTRIBUTORS` 文件 -- 发布说明 -- 项目文档(重大贡献) - -感谢您为 Backtrader 做出贡献! +``` ## 另请参阅 +- [分支治理](branch-governance_zh.md) - [开发环境设置](setup_zh.md) - [代码风格](style_zh.md) - [测试指南](testing_zh.md) -- [项目上下文](../project-context.md) +- [发布流程](release_zh.md) diff --git a/docs/source/developer-guide/index.md b/docs/source/developer-guide/index.md index 7e281822..4181aba2 100644 --- a/docs/source/developer-guide/index.md +++ b/docs/source/developer-guide/index.md @@ -1,9 +1,7 @@ -- -- - +--- title: Developer Guide Index description: Guidelines for contributors - -- -- +--- # Developer Guide @@ -23,6 +21,8 @@ Welcome to the Backtrader development guide. This section covers everything you | [Contributing](contributing.md) | Contribution guidelines | +| [Branch Governance](branch-governance.md) | Three-branch model, PR routing, risk levels, promotion/hotfix protocols | + | [Release Workflow](release.md) | Version management and release process | ## Getting Started diff --git a/docs/source/developer-guide/index_zh.md b/docs/source/developer-guide/index_zh.md index 15f2199d..058df3d3 100644 --- a/docs/source/developer-guide/index_zh.md +++ b/docs/source/developer-guide/index_zh.md @@ -1,9 +1,7 @@ -- -- - +--- title: 开发者指南 description: 贡献者指南 - -- -- +--- # 开发者指南 @@ -23,6 +21,8 @@ description: 贡献者指南 | [贡献](contributing_zh.md) | 贡献指南 | +| [分支治理](branch-governance_zh.md) | 三分支模型、PR 分流、风险分级、promotion/hotfix 协议 | + | [发布流程](release_zh.md) | 版本管理和发布流程 | ## 快速开始 diff --git a/docs/source/developer-guide/release.md b/docs/source/developer-guide/release.md index 6dc1c109..54a49923 100644 --- a/docs/source/developer-guide/release.md +++ b/docs/source/developer-guide/release.md @@ -61,57 +61,32 @@ Examples: ## Branching Strategy -```bash -dev (main development branch) - ├── Active development - ├── All feature branches merge here - └── Release candidates branch from here - -master (stable releases) - ├── Production-ready code only - ├── Merges from dev via release PR - └── Tags created from commits here - -feature/* (short-lived branches) - ├── Created from dev - └── Merge back to dev via PR - -release/*(preparation branches) - ├── Created from dev for final testing - └── Merge to both dev and master after release - -```bash - -### Release Branch Lifecycle +> This repository uses a **three-branch model**, not a "dev → master release +> chain". See [Branch Governance](branch-governance.md) for the authoritative +> definitions. The sections below describe release semantics only. -```bash - -# 1. Create release branch from dev - -git checkout dev -git pull origin dev -git checkout -b release/1.1.0 - -# 2. Finalize version updates and testing +### Original vs optimized release semantics -# (see Pre-Release Checklist below) - -# 3. Merge release to master - -git checkout master -git merge --no-ff release/1.1.0 -git tag -a v1.1.0 -m "Release v1.1.0" +| Branch | Role | Release semantics | +|--------|------|-------------------| +| `master` | Original Backtrader baseline | Receives only `hotfix/master-*` bug/compatibility/security fixes that reproduce on the original baseline; **not** a routine release target | +| `development` | Improved & optimized version | Release source for optimized capabilities; integrated from `dev` via controlled promotion; **not** an upstream of `master` | +| `dev` | Daily development entry | Routine integration; periodically promoted to `development` via a promotion PR | -# 4. Merge release back to dev +### Promotion (`dev` → `development`) -git checkout dev -git merge --no-ff release/1.1.0 +A controlled promotion is a PR, not a routine merge. See +[Branch Governance §6](branch-governance.md#6-promotion-protocol-dev--development). -# 5. Push all changes +### Original-baseline hotfix (`master`) -git push origin master dev --tags +Only `hotfix/master-*`, and every fix must create a forward-port issue and +separately evaluate `dev` and `development`. See +[Branch Governance §7](branch-governance.md#7-master-hotfix-forward-port-protocol). -```bash +> Semantic versioning and tags are described below; tags are placed on the +> merge commit of the corresponding branch, not via a bidirectional `release/*` +> merge. ## Pre-Release Checklist @@ -350,20 +325,19 @@ rm -rf /tmp/test-env ```bash -### Step 5: Merge to Master - -```bash +### Step 5: Tag the release -# Checkout master +> Under the three-branch model, the tag is placed on the merge commit of the +> corresponding branch — do **not** merge optimized code back into `master`. +> The original baseline (`master`) only receives `hotfix/master-*` fixes. See +> [Branch Governance](branch-governance.md). -git checkout master -git pull origin master - -# Merge release branch +```bash -git merge --no-ff release/1.1.0 -m "Merge release/1.1.0 into master" +# Tag the release on its target branch (example: development) -# Create annotated tag +git checkout development +git pull origin development git tag -a v1.1.0 -m "Release v1.1.0 @@ -381,16 +355,13 @@ See CHANGELOG.md for full details." ```bash -# Push master and tags +# Push the tagged branch -git push origin master +git push origin development git push origin v1.1.0 -# Merge release back to dev - -git checkout dev -git merge --no-ff release/1.1.0 -m "Merge release/1.1.0 back to dev" -git push origin dev +# Routine changes integrate into dev, then promote to development via a +# controlled promotion PR (not a merge-back). See Branch Governance §6. # Publish to PyPI (optional, for public releases) @@ -512,7 +483,7 @@ If you're upgrading from v1.0.0: 1. Update imports: `from backtrader.plot import Plotly` (new module) 2. Review CCXT configuration for new WebSocket features -3. Recompile Cython extensions: `cd backtrader && python compile_cython_numba_files.py` +3. No Cython recompile is needed — the package is pure Python. ### Installation @@ -659,35 +630,33 @@ git push origin v2.0.0 ## Emergency Releases -For critical security issues or severe bugs: +For critical security issues or severe bugs on the original baseline: + +> Under the three-branch model, a `master` hotfix must **not** be blindly merged +> into `dev`/`development`. Complete the forward-port protocol (equivalent, +> independently-tested ports) instead. See +> [Branch Governance §7](branch-governance.md#7-master-hotfix-forward-port-protocol). ```bash -# Create hotfix branch from master +# Create a hotfix branch from master git checkout master -git checkout -b hotfix/critical-security-fix - -# Apply fix - -# ... make changes ... +git checkout -b hotfix/master-critical-security-fix -# Test thoroughly +# Apply fix and test thoroughly on master -pytest tests/ -v -m priority_p0 +pytest tests/ -v -# Merge to master and dev +# Merge into master (R3 gate; requires target:master-hotfix label) git checkout master -git merge hotfix/critical-security-fix +git merge hotfix/master-critical-security-fix git tag -a v1.1.1 -m "Hotfix: Critical security fix" +git push origin master -git checkout dev -git merge hotfix/critical-security-fix - -# Push - -git push origin master dev --tags +# Forward-port: create a forward-port-required issue, then independently +# evaluate and port the fix to dev and development — do not cross-branch merge. ```bash diff --git a/docs/source/developer-guide/release_zh.md b/docs/source/developer-guide/release_zh.md index 9d933d48..2e580dda 100644 --- a/docs/source/developer-guide/release_zh.md +++ b/docs/source/developer-guide/release_zh.md @@ -85,59 +85,28 @@ MAJOR.MINOR.PATCH ## 发布分支策略 -### 分支模型 +> 本仓库采用**三分支模型**,不遵循「dev → master 发布链」。权威定义见 +> [分支治理](branch-governance_zh.md)。以下仅说明发布口径。 -```bash - ┌─────────────────┐ - │ master (稳定) │ - │ v1.0.0, v1.1.0 │ - └────────▲────────┘ - │ 合并 - │ -┌─────────────┐ ┌─────┴──────┐ ┌─────────────┐ -│ feature/* │──────▶│ dev │───────▶│ release/* │ -│ 功能分支 │ │ (主开发) │ │ 发布准备分支 │ -└─────────────┘ └────────────┘ └─────────────┘ - │ - ▼ - ┌─────────────┐ - │ ctp │ - │ CTP 期货开发 │ - └─────────────┘ - -```bash - -### 分支说明 - -| 分支 | 用途 | 合并目标 | 保护规则 | - -|------|------|----------|----------| +### 原始版与优化版的发布口径 -| `master` | 稳定发布版本 | 仅来自 `release/*` | 必须通过 PR,需要审核 | +| 分支 | 定位 | 发布口径 | +|------|------|----------| +| `master` | 原始 Backtrader 基线 | 仅通过 `hotfix/master-*` 接收已在原始版复现的 Bug/兼容性/安全修复;**不是**日常发布终点 | +| `development` | 改进与优化版本 | 优化版能力的发布源;通过受控 promotion 从 `dev` 集成,**不是** `master` 的上游 | +| `dev` | 日常开发入口 | 常规功能与修复的日常集成,定期通过 promotion PR 提升到 `development` | -| `dev` | 主开发分支 | 接受 `feature/*` 合并 | 必须通过 PR,需要 CI 通过 | +### Promotion(dev → development) -| `release/x.y.z` | 发布准备分支 | 合并到 `master` 和 `dev` | 严格版本控制 | +受控 promotion 是 PR,不是例行合并。流程见 [分支治理 §6](branch-governance_zh.md#6-promotion-协议dev--development)。 -| `feature/*` | 功能开发分支 | 合并到 `dev` | 常规开发 | +### 原始版热修复(master) -| `hotfix/*` | 紧急修复分支 | 合并到 `master` 和 `dev` | 快速通道 | +仅 `hotfix/master-*`,且必须创建前移 issue 并分别评估 `dev` 与 `development`。 +流程见 [分支治理 §7](branch-governance_zh.md#7-master-hotfix-前移协议)。 -### 发布分支创建 - -```bash - -# 从 dev 分支创建发布分支 - -git checkout dev -git pull origin dev -git checkout -b release/1.2.0 - -# 推送到远程 - -git push -u origin release/1.2.0 - -```bash +> 版本号与 tag 的语义化版本管理详见下文;tag 打在对应分支的合并提交上,不再通过 +> `release/*` 分支双向合并。 - -- @@ -350,71 +319,43 @@ git commit -m "release: v1.2.0 — 准备发布 ```bash -### 步骤 5: 合并到 master - -```bash +### 步骤 5: 在对应分支上打标签 -# 切换到 master - -git checkout master -git pull origin master - -# 合并发布分支 - -git merge release/1.2.0 -m "Merge release/1.2.0 into master" - -# 推送到远程 - -git push origin master +> 三分支模型下,tag 打在对应分支的合并提交上,**不再**把优化版代码 merge 回 +> `master`。原始版(`master`)仅接收 `hotfix/master-*` 修复。详见 +> [分支治理](branch-governance_zh.md)。 ```bash -### 步骤 6: 创建版本标签 - -```bash - -# 创建带注释的标签 - -git tag -a v1.2.0 -m "v1.2.0: CTP 期货支持与 WebSocket 订单推送 - -新增功能: +# 在对应分支上打标签(示例为 development) -- CTP 期货完整支持 -- WebSocket 订单推送 -- 自适应速率限制 +git checkout development +git pull origin development -改进: +git tag -a v1.2.0 -m "Release v1.2.0" -- CCXT Broker 错误处理 -- 15% 性能提升 - -修复: - -- 数据长度问题 -- CrossOver 依赖顺序" - -# 推送标签到远程 +# 推送到远程 +git push origin development git push origin v1.2.0 ```bash -### 步骤 7: 合并回 dev +### 步骤 6: Promotion 而非回灌 + +> 发布标签已在步骤 5 打在对应分支上。**不要**把优化版代码 `merge master back to +> dev`(这是反向同步源,违反三分支模型)。日常变更集成到 `dev`,再通过受控 +> promotion PR 提升到 `development`。详见 +> [分支治理 §6](branch-governance_zh.md#6-promotion-协议dev--development)。 ```bash -# 切换到 dev +# 日常变更直接提交到 dev(无需从 master 回灌) git checkout dev git pull origin dev -# 合并 master(确保 dev 包含发布变更) - -git merge master -m "Merge master back to dev after v1.2.0 release" - -# 推送 - -git push origin dev +# ... 常规开发 ... ```bash @@ -645,17 +586,17 @@ git push origin --delete release/1.2.0 ```bash -# 1. 从 master 创建 hotfix 分支 +# 1. 从 master 创建 hotfix 分支(必须 hotfix/master-* 命名) git checkout master git pull origin master -git checkout -b hotfix/1.2.1 +git checkout -b hotfix/master-1.2.1 # 2. 修复问题 # (进行必要的代码修改) -# 3. 测试验证 +# 3. 在 master 上独立测试验证 pytest tests/ -n 4 -v @@ -663,21 +604,18 @@ pytest tests/ -n 4 -v git commit -am "hotfix: 修复严重数据损坏问题" -# 5. 合并到 master +# 5. 合并到 master(R3 门禁,需 target:master-hotfix 标签) git checkout master -git merge hotfix/1.2.1 +git merge hotfix/master-1.2.1 # 6. 创建标签和发布 git tag -a v1.2.1 -m "v1.2.1: 紧急修复" git push origin master v1.2.1 -# 7. 合并回 dev - -git checkout dev -git merge master -git push origin dev +# 7. 前移:创建 forward-port-required issue,分别评估并独立移植到 +# dev 与 development(禁止跨分支盲目 merge) # 8. PyPI 发布 @@ -748,20 +686,16 @@ cd backtrader && pip install -U . ```bash -# 完整发布流程 +# 完整发布流程(三分支模型,不做 dev→master 双向合并) -git checkout dev && git pull -git checkout -b release/1.2.0 +git checkout development && git pull # 编辑 setup.py, CHANGELOG.md git add setup.py CHANGELOG.md git commit -m "release: v1.2.0" -git checkout master && git merge release/1.2.0 git tag -a v1.2.0 -m "v1.2.0" -git push origin master v1.2.0 -git checkout dev && git merge master -git push origin dev +git push origin development v1.2.0 make clean python -m build twine upload dist/* @@ -785,11 +719,9 @@ gh release create v1.2.0 --notes "发布说明..." 发布中: -- [ ] 发布分支已创建 - [ ] 版本更新已提交 -- [ ] 已合并到 master -- [ ] 标签已创建并推送 -- [ ] 已合并回 dev +- [ ] 标签已在对应分支创建并推送 +- [ ] promotion PR 已合并(如适用) - [ ] 构建包已验证 - [ ] 已上传到 PyPI - [ ] GitHub Release 已创建 diff --git a/docs/source/developer-guide/setup_zh.md b/docs/source/developer-guide/setup_zh.md index eb3fd734..124b2030 100644 --- a/docs/source/developer-guide/setup_zh.md +++ b/docs/source/developer-guide/setup_zh.md @@ -296,21 +296,10 @@ if __name__ == '__main__': ```bash -## Cython 编译 +## 关于 Cython -对于性能关键的开发,编译 Cython 扩展: - -```bash - -# Unix/Mac - -cd backtrader && python -W ignore compile_cython_numba_files.py && cd .. && pip install -U . - -# Windows - -cd backtrader; python -W ignore compile_cython_numba_files.py; cd ..; pip install -U . - -```bash +本项目为**纯 Python** 实现,无需独立的 Cython 编译步骤(仓库中没有需要编译的 +`.pyx` 文件,`setup.py` 也无 `ext_modules`)。直接 `pip install -e .` 即可完成安装。 ## 常见问题 diff --git "a/docs/source/reference/optimization-docs/requirements/\350\277\255\344\273\243140-\347\244\276\345\214\272PR\346\262\273\347\220\206\344\270\216\344\270\211\345\210\206\346\224\257\345\215\217\344\275\234/\346\255\243\345\274\217\350\277\255\344\273\243\350\256\241\345\210\222.md" "b/docs/source/reference/optimization-docs/requirements/\350\277\255\344\273\243140-\347\244\276\345\214\272PR\346\262\273\347\220\206\344\270\216\344\270\211\345\210\206\346\224\257\345\215\217\344\275\234/\346\255\243\345\274\217\350\277\255\344\273\243\350\256\241\345\210\222.md" new file mode 100644 index 00000000..248653cb --- /dev/null +++ "b/docs/source/reference/optimization-docs/requirements/\350\277\255\344\273\243140-\347\244\276\345\214\272PR\346\262\273\347\220\206\344\270\216\344\270\211\345\210\206\346\224\257\345\215\217\344\275\234/\346\255\243\345\274\217\350\277\255\344\273\243\350\256\241\345\210\222.md" @@ -0,0 +1,308 @@ +# 迭代140:社区 PR 治理与三分支协作正式迭代计划 + +> 计划日期:2026-08-20 +> 状态:**实施中 / Ruleset 观察期**(自 2026-08-22 起) +> 适用仓库:`cloudQuant/backtrader` +> 适用分支:`master`、`development`、`dev` +> 范围来源:社区贡献与 PR 数量增长的治理需求,以及已确认的三分支真实语义。 + +> 当前实施快照(2026-08-22):D0 保持 GitHub 默认分支 `development`;D1 确认 +> GitHub `cloudQuant/backtrader` 为审查权威、Gitee `yunjinqi/backtrader` 为受控镜像; +> D2 确认真实 GitHub owner 为 `@cloudQuant`,Gitee 镜像身份为 `yunjinqi`;D3 的 +> 基础 Ruleset 先以 `evaluate` 观察两周,最早于 2026-09-05 根据 API 证据转为 `active`, +> 但 R2/R3 的“owner + 第二位独立维护者”尚未具备第二个已确认的 GitHub maintainer,故为 +> 有时限阻塞项;D4 仍明确不启用 Merge Queue。M5 的四周运行指标和三类真实 PR 演练尚不能 +> 提前宣称完成。 + +## 1. 背景与问题定义 + +社区贡献增多后,项目需要的不是更长的 PR 描述,而是能稳定回答下列问题的制度和自动化: + +1. 这个贡献应落到哪个分支? +2. 谁必须评审?需要哪些测试与兼容性证据? +3. 什么情况下允许合并、暂缓、关闭或要求拆分? +4. 原始版和优化版的修复如何传播,而不把不兼容的整条分支错误合并? + +本仓库有一个不能套用常见 GitFlow 的前提,以下定义是本计划的**强制性事实**: + +| 分支 | 正确定位 | 允许进入的变更 | 禁止的变更 | +|---|---|---|---| +| `master` | 原始 Backtrader 基线 | 已在原始版复现的 Bug、兼容性或安全修复 | 日常功能、优化版重构、`dev`/`development` 的例行合并 | +| `development` | 改进与优化版本 | 优化版能力、架构优化、仅在优化版存在的回归修复、经过受控集成的日常开发成果 | 将其作为 `master` 的发布候选或反向同步源 | +| `dev` | 日常开发入口 | 常规功能、普通 Bug 修复、文档、测试、重构和社区贡献 | 绕过评审直接进入 `development`/`master` 的替代通道 | + +### 1.1 已核验的基线 + +- GitHub 默认分支当前为 `development`,与“`dev` 是日常贡献入口”的目标存在入口歧义;是否调整默认分支必须由维护者显式决策。 +- `master`、`development`、`dev` 当前均未配置 GitHub Branch Protection / Ruleset。 +- 仓库没有 `.github/CODEOWNERS`,无法按核心模块自动请求领域评审。 +- `.github/pull_request_template.md` 内容冗长、重复,并引用不存在的检测脚本;`CONTRIBUTING.md` 与双语开发者指南仍包含已失效的 Cython 步骤和不完整的分支说明。 +- CI 已具备 `Lint`、跨操作系统/Python 版本测试、`Test Summary` 和文档检查,但当前所有 PR 的验证等级尚未按目标分支和风险分层。 + +### 1.2 本迭代必须解决的两个问题 + +**问题 A:PR 入口、分流、审查与验收缺少可执行治理。** + +结果是贡献者不知道目标分支、维护者重复进行低价值问答、核心文件没有明确 owner、CI 通过被误判为可合并。 + +**问题 B:三分支角色未在 GitHub 设置、文档和自动化中一致表达。** + +结果是有人把 `master` 当作发布终点,或把 `development` 的修复错误移植到原始基线;PR #21 一类仅适用于优化版的修复会被错误评估。 + +## 2. 目标、非目标与成功定义 + +### 2.1 目标 + +1. 让贡献者在开 PR 前能确定唯一目标分支和最小验证要求。 +2. 让 `master`、`development`、`dev` 的权限、评审、CI 和热修复传播规则与真实定位一致。 +3. 让核心路径(line 系统、`Cerebro`、`Strategy`、`Broker`、`metabase`、feeds)获得明确的领域审查责任。 +4. 在不降低正确性标准的前提下,缩短普通 PR 的反馈时间,并把重型验证放到正确的风险场景。 +5. 将 GitHub 配置以仓库内 manifest 和可验证证据记录下来,避免“设置只存在于某位管理员脑中”。 + +### 2.2 非目标 + +1. 不改变 `master` 的原始实现、不将优化版代码批量回灌到 `master`。 +2. 不在本迭代重构 Backtrader 核心业务逻辑。 +3. 不因治理需要强制开启 Merge Queue;只有达到明确吞吐阈值后再单独决策。 +4. 不自动给外部贡献者写入权限,也不把 GitHub 管理员令牌放入仓库或 CI 日志。 +5. 不删除现有 issue、PR、标签或历史分支;任何清理均须单独审批。 + +## 3. 目标工作流 + +```text +常规社区贡献 +fork / feature/* ── PR + fast gate ──> dev + │ + │ 受控 promotion PR + full gate + ▼ + development + +优化版专属问题 +feature/* ── PR + optimization gate ──> development + +原始版真实 Bug +hotfix/master-* ── PR + original-baseline gate ──> master + │ + └─ 建立前移任务:分别评估并移植到 dev / development +``` + +### 3.1 PR 目标分支决策表 + +| 情况 | 默认目标 | 必需证据 | 合并后的后续动作 | +|---|---|---|---| +| 文档、测试、常规功能、普通 Bug 修复 | `dev` | 关联测试、快速门禁、至少一位维护者批准 | 纳入下一次 `dev → development` promotion 候选 | +| 仅在优化架构中出现的问题或优化版功能 | `development` | 优化版最小复现、风险说明、领域 owner 批准 | 判断是否需要在 `dev` 实现等价修复 | +| 原始 Backtrader 的真实 Bug / 安全问题 | `master` | 在 `master` 的独立复现、回归测试、原始 API 兼容性说明 | 创建前移 issue,不能以“已修 master”关闭 `dev`/`development` 风险 | +| 跨分支语义不同的修复 | 分别建 PR | 每个目标分支的独立实现与测试 | 交叉链接 PR / issue,禁止盲目 merge 或 cherry-pick | + +### 3.2 风险级别 + +| 等级 | 典型路径 | 最低评审 | 最低验证 | +|---|---|---|---| +| R0 文档/测试 | `docs/`、测试注释、非行为性工具 | 1 位维护者 | 格式、受影响测试、文档构建 | +| R1 常规模块 | 单个 indicator/analyzer/feed 的局部修复 | 1 位模块 owner | 快速 CI + 新增/修改回归测试 | +| R2 核心/兼容性 | `lineroot`、`linebuffer`、`lineseries`、`lineiterator`、`cerebro`、`strategy`、`broker`、`metabase` | 领域 owner + 第二位维护者 | 快速 CI、`make test-strategies`、runonce/runnext 或兼容性证据 | +| R3 原始版/安全/发布 | `master` hotfix、供应链、安全、公开 API 破坏风险 | 核心维护者明确批准 | 目标分支全量测试、最小复现、回归、发布/安全检查 | + +## 4. 决策门(未通过不得进入实施) + +| 编号 | 决策 | 推荐值 | 决策人 | 证据 / 退出条件 | +|---|---|---|---|---| +| D0 | GitHub 默认分支 | 若社区常规 PR 目标为 `dev`,将默认分支改为 `dev`;否则保留 `development` 并实现明确的 PR 目标提示 | 仓库管理员 + 核心维护者 | 书面选择、fork/新 PR 目标验证 | +| D1 | GitHub/Gitee 的权威来源与同步责任 | GitHub 为 PR 与审查权威;Gitee 为受控镜像 | 仓库管理员 | 三个长期分支的同步方式、失败告警与责任人明确 | +| D2 | Owner 团队与管理员 bypass | 使用真实 GitHub 用户/团队;仅紧急场景允许可审计 bypass | 核心维护者 | Owner 矩阵获确认,禁止占位帐号进入 `CODEOWNERS` | +| D3 | 分支审批门槛 | `dev` 1 批准;`development` 的 R2/R3 至少 owner + 第二维护者;`master` 仅 R3 | 核心维护者 | Ruleset manifest 与实际设置一致;若只有一名已确认维护者,必须记录为有时限阻塞项,不得假装满足第二人审查 | +| D4 | Merge Queue 启用阈值 | 连续 4 周出现每天 ≥3 个待合并 PR 或频繁基线冲突时再评估 | 维护者轮值负责人 | 指标报告和 `merge_group` CI 预演通过 | + +## 5. 工作流、任务与交付物 + +### M0:基线冻结与决策记录 + +**优先级:P0;依赖:无;建议负责人:治理负责人 + 仓库管理员。** + +1. 将本计划确认的三分支定义写入治理章程,不得再使用“`development → master` 发布链”的表述。 +2. 收集 D0-D4 的书面结论,特别是默认分支和真实 GitHub owner/team 名称。 +3. 导出当前 GitHub Ruleset、分支保护、标签、Actions workflow 与长期分支 SHA,作为实施前快照;不得导出 token 或私有凭据。 +4. 建立每周治理指标基线:新 PR 数、首次响应时间、首次实质评审时间、合并时间、关闭原因、CI 失败/flake 比率、跨分支前移完成率。 + +**文件与外部交付物:** + +- 新增 `docs/source/developer-guide/branch-governance.md` 与 `branch-governance_zh.md`。 +- 新增 `.github/governance/baseline/README.md`,只记录导出命令、时间戳、摘要和证据位置,不提交敏感 API 响应。 +- 新增 `.github/governance/metrics-schema.json`。 + +**验证与退出条件:** + +```bash +gh repo view cloudQuant/backtrader --json defaultBranchRef +gh api repos/cloudQuant/backtrader/rulesets +gh label list --repo cloudQuant/backtrader --limit 100 +git ls-remote --heads origin master development dev +``` + +- D0-D4 全部有负责人和结论;未决项必须标为阻塞,不得以默认假设进入 M2。 + +### M1:贡献入口、文档与模板收敛 + +**优先级:P0;依赖:M0/D0;建议负责人:文档 owner + 社区 triage 负责人。** + +1. 用当前真实安装方式、测试命令和分支模型重写根目录 `CONTRIBUTING.md`。 +2. 同步更新 `docs/source/developer-guide/contributing.md`、`contributing_zh.md`、`index.md`、`index_zh.md`,使中英文说明与根贡献指南一致。 +3. 替换 `.github/pull_request_template.md` 为简短模板,必填字段仅包括:问题与动机、目标分支与原因、风险级别、兼容性影响、执行过的命令/结果、关联 issue。 +4. 新增 Issue Forms: + - `.github/ISSUE_TEMPLATE/bug_report.yml`:环境、目标版本(master/dev/development)、最小复现、预期/实际、日志; + - `.github/ISSUE_TEMPLATE/feature_request.yml`:问题、目标用户、分支影响、替代方案; + - `.github/ISSUE_TEMPLATE/config.yml`:将一般问答导向 Discussions/支持渠道,并说明安全问题不应公开提交。 +5. 新增 `SECURITY.md`,定义私密报告路径与响应承诺;不在公开 issue 中处理凭据或漏洞细节。 +6. 更新 `AGENTS.md` 中的分支事实,消除“`dev` 是唯一 canonical 分支”或“`master` 是发布终点”等过度简化描述。 + +**优化效果:** 贡献者在提交前即可选择正确基线,维护者不再反复纠正 Cython、目标分支与无效测试命令。 + +**验证与退出条件:** + +```bash +rg -n 'compile_cython_numba_files|Target: dev|development.*master' \ + CONTRIBUTING.md docs/source/developer-guide AGENTS.md +git diff --check +make docs +``` + +- 上述检索只保留计划允许的语义;英文和中文流程图、分支表、命令与根贡献指南一致。 + +### M2:所有权、标签与分支保护落地 + +**优先级:P0;依赖:M0/D1/D2/D3;建议负责人:仓库管理员 + 核心模块 owner。** + +1. 新增 `.github/CODEOWNERS`,至少覆盖: + - `/.github/`、`/scripts/`、`/docs/`; + - `/backtrader/lineroot.py`、`linebuffer.py`、`lineseries.py`、`lineiterator.py`、`metabase.py`; + - `/backtrader/cerebro.py`、`strategy.py`、`broker.py`、`brokers/`、`feeds/`; + - `/backtrader/indicators/`、`tests/functional/strategies/`、`setup.py`、`pyproject.toml`、`requirements.txt`。 +2. 建立 `.github/governance/rulesets/dev.json`、`development.json`、`master.json` 作为可审计期望 manifest;真实 GitHub 设置由管理员通过 UI/API 应用,CI 不持有管理员权限。 +3. 新增标签分类与说明:`target:*`、`type:*`、`area:*`、`risk:*`、`status:*`、`needs-repro`、`needs-tests`、`ready-to-merge`、`blocked`、`backport-or-forward-port-required`。 +4. 分两周 rollout Ruleset:第 1 周 observe/documented mode,第 2 周启用以下 active 门槛: + - 所有长期分支:禁止 force push 和删除,要求解决会话; + - `dev`:PR、至少一位批准、`Lint` 和 `Test Summary`; + - `development`:PR、owner review;R2/R3 的第二批准由标签/维护者流程要求并记录; + - `master`:仅 `hotfix/master-*` PR,必须带 `target:master-hotfix` 与最小复现,允许 bypass 仅限记录了紧急原因的管理员。 +5. 实现 `scripts/ci/verify_github_governance.py`,读取无凭据/最小权限的 GitHub API 响应,将当前 ruleset、必要状态检查和 CODEOWNERS 校验结果与 manifest 对比。 +6. 为校验脚本新增 `tests/unit/scripts/test_verify_github_governance.py`,fixture 必须为脱敏 JSON,不能依赖在线 GitHub。 + +**验证与退出条件:** + +```bash +gh api repos/cloudQuant/backtrader/rulesets +gh api repos/cloudQuant/backtrader/codeowners/errors +/Users/yunjinqi/opt/anaconda3/bin/conda run -n base python -m pytest \ + tests/unit/scripts/test_verify_github_governance.py -q +git diff --check +``` + +- 三个长期分支均被 active Ruleset 覆盖,且 `verify_github_governance.py` 能证明其规则符合 manifest;CODEOWNERS 无语法或权限错误;manifest 与实际 GitHub 设置差异为零或被记录为有批准的例外。若另行采用传统 Branch Protection,`/branches//protection` 的成功响应只能作为补充证据,不能替代 Ruleset 覆盖校验。 + +### M3:按目标分支和风险分层的 CI / PR 自动化 + +**优先级:P1;依赖:M1、M2;建议负责人:CI owner + 测试架构负责人。** + +1. 在 `.github/workflows/pr-governance.yml` 新增 PR 元信息校验,先以 report-only 模式运行:提示目标分支、标签、风险级别、缺失复现或测试说明;不得在 triage 前把普通外部 PR 永久阻塞。 +2. 在观察期后,仅将下列错误转为阻塞:常规 PR 直接目标为 `master`、缺少 `master` hotfix 证据、已标 R2/R3 却未请求相应 owner。 +3. 调整 `.github/workflows/test.yml` 或拆分为目标分支专用 workflow: + - `dev` PR:维持快速门禁; + - `development` PR:R2/R3 增加 `make test-strategies`、runonce/runnext 或策略基线比对; + - `master` hotfix:在 `master` 检出环境运行原版最小复现、回归测试与兼容性检查; + - push / nightly:保留完整矩阵,不能因缩短 PR 反馈而取消。 +4. 所有设为 required 的检查都必须在每个相应 PR 上稳定报告成功/失败;路径过滤跳过的 job 必须有同名汇总 check,否则 GitHub Ruleset 会错误阻塞 PR。 +5. 新增 `scripts/ci/classify_pr_risk.py`(或同等受测逻辑),根据变更路径产生建议标签;维护者保留最终覆盖权并记录原因。 +6. 对高风险区建立“测试不足”判定规则:不得只因代码行数小就降低审查;也不得以大量实现细节耦合的测试掩盖缺少真实最小复现。 + +**验证与退出条件:** + +```bash +/Users/yunjinqi/opt/anaconda3/bin/conda run -n base python -m pytest \ + tests/unit/scripts/test_verify_github_governance.py tests/unit/scripts/test_classify_pr_risk.py -q +make test-fast +make test-strategies # 仅在 R2/R3 的 development 候选 PR 上 +gh workflow run "PR Governance" --ref dev # 仅在 workflow 合并后由维护者手动预演 +``` + +- 至少用一个 `dev` 普通 PR、一个 `development` 核心 PR 和一个 `master` hotfix 草稿 PR 完成 dry-run;三者的检查集合、审查请求和阻塞原因符合决策表。 + +### M4:promotion、原版热修复前移与双远端一致性 + +**优先级:P1;依赖:M2、M3;建议负责人:发布负责人 + 分支 owner。** + +1. 定义 `dev → development` 的 promotion PR 模板:变更范围、已排除内容、完整验证、性能/兼容性差异、回滚点。 +2. 定义 `master` hotfix 前移协议:每个 master 修复必须创建 linked issue,分别判断 `dev` 与 `development` 是否受影响;使用“等价移植并独立测试”,不得默认执行跨分支全量 merge。 +3. 为 PR 添加 `forward-port-required` / `forward-port-complete` 标签及 issue checklist;未完成前移不得把热修复标为治理完成。 +4. 规定 GitHub 为审查权威、Gitee 为镜像;在每次长期分支合并后验证两端同一 SHA,失败须告警并由镜像负责人处理。 +5. 新增 `docs/source/developer-guide/branch-governance.md` 的 promotion/hotfix 运行手册,以及 `release.md` / `release_zh.md` 中对原始版与优化版发布口径的交叉说明。 + +**验证与退出条件:** + +```bash +git ls-remote --heads https://github.com/cloudQuant/backtrader.git master development dev +git ls-remote --heads https://gitee.com/yunjinqi/backtrader.git master development dev +gh issue list --repo cloudQuant/backtrader --label forward-port-required --state open +``` + +- 用一个历史或演练 hotfix 走完“复现 → master PR → 前移 issue → dev/development 独立验证 → 完成标签”的闭环;GitHub/Gitee 三个长期分支的 SHA 一致或差异有记录原因。 + +### M5:运行度量、复盘与后续扩展 + +**优先级:P2;依赖:M0-M4;建议负责人:轮值 triage maintainer。** + +1. 每周发布治理摘要:PR 入口准确率、首次响应、首次实质评审、合并周期、CI 失败率、flake、被要求拆分的 PR 数、未前移 hotfix 数。 +2. 每月审计 owner 覆盖、管理员 bypass、标签滥用、长期无响应 PR 和 GitHub/Gitee 同步延迟。 +3. 当 D4 阈值达到时,单独立项 Merge Queue:先让 CI 支持 `merge_group`,再启用队列;不得直接开启后发现 required check 不触发。 +4. 基于数据调整 R0-R3 门槛;除非有明确证据,不降低核心路径的验证要求。 + +**退出条件:** 连续 4 周运行数据表明:普通 PR 目标分支误投率低于 5%、所有 master hotfix 有前移结论、核心 PR 均有 owner 审查、未出现规则误阻塞的未解释事件。 + +## 6. 实施顺序与并行边界 + +| 工作流 | 可并行 | 前置依赖 | 负责人 | +|---|---|---|---| +| A:M0 决策与基线 | 否 | 无 | 治理负责人 / 管理员 | +| B:M1 文档与模板 | 是 | D0 已定 | 文档 owner | +| C:M2 CODEOWNERS / Ruleset | 是 | D1-D3 已定 | 管理员 / 模块 owner | +| D:M3 CI 自动化 | 部分 | M1、M2 manifest | CI owner | +| E:M4 promotion / 镜像 | 部分 | M2、M3 | 发布负责人 | +| F:M5 指标与复盘 | 否 | M0-M4 上线 | triage 轮值 | + +禁止让多个开发者同时修改 `.github/pull_request_template.md`、`.github/CODEOWNERS`、Ruleset manifest 或 `test.yml`;这些文件必须指定单一整合负责人,以免把治理变更本身变成冲突源。 + +## 7. 验收矩阵 + +| 维度 | 验收证据 | 通过标准 | +|---|---|---| +| 分支语义 | `branch-governance` 文档、`CONTRIBUTING.md`、`AGENTS.md`、PR 模板 | 四处描述完全一致,且明确 master 不接收日常开发 | +| GitHub 设置 | Ruleset API 输出、manifest 比对报告;若使用传统保护则附保护分支响应 | 三个长期分支均有符合角色的有效规则 | +| 所有权 | CODEOWNERS API 错误列表、PR review request 截图/事件 | 核心路径有真实有权限的 owner,变更会自动请求评审 | +| 贡献入口 | Bug/feature form、PR 模板、普通 fork 演练 | 贡献者可明确选择 dev/development/master-hotfix 路径 | +| CI | 三类草稿 PR 的 check 清单、nightly/push 记录 | required check 稳定报告;高风险分支得到更强而非更弱的验证 | +| 前移与镜像 | linked issue、标签历史、双远端 SHA 对比 | master 修复不漏传;不发生 development 批量回灌 master | +| 可维护性 | 周报和月度复盘 | 指标可复现,例外可解释,规则能按数据调整 | + +## 8. 风险、回滚与防误伤措施 + +| 风险 | 后果 | 防护 / 回滚 | +|---|---|---| +| 把 `development` 误当作 `master` 的上游 | 原始版被引入不兼容优化 | master ruleset、hotfix 命名、target 标签与维护者双重审查 | +| Ruleset 一次性启用导致外部 PR 全部受阻 | 社区贡献流失 | 先 observe 两周;保留有时限且可审计的管理员 bypass | +| 条件 CI 没有发出 required check | PR 被 GitHub 错误永久阻塞 | 每个 required check 使用稳定汇总 job;先用草稿 PR 验证三种目标 | +| CODEOWNERS 指向无权限或离职帐号 | 自动审查失效 | API 校验、季度 owner 审计、owner 轮值和备用人 | +| master 修复盲目 cherry-pick | 优化版语义回归 | 只允许独立等价移植与各自测试,不允许默认跨分支 merge | +| GitHub/Gitee 长期分支漂移 | 用户拿到不同版本 | 长期分支 SHA 监控、失败告警、明确镜像负责人 | + +## 9. 本迭代完成定义 + +本迭代只有在以下全部满足时才可标记完成: + +1. D0-D4 已签署或记录为有时限的例外; +2. 三分支语义已同步到仓库文档、PR/Issue 入口和 GitHub 设置; +3. `master`、`development`、`dev` 的 Ruleset 与 CODEOWNERS 已生效并通过 API 证据验证; +4. 三类 PR(普通 dev、优化版 development、master hotfix)均完成过端到端演练; +5. 热修复前移和 GitHub/Gitee 镜像均有可查询证据; +6. 没有以“CI 绿色”“可自动合并”或“测试数量很多”替代人工风险审查与目标分支判断。 + +在这之前,本计划只能视为治理设计,不得宣称仓库已具备可规模化的社区 PR 管理能力。 diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 00000000..e5c0a84c --- /dev/null +++ b/scripts/__init__.py @@ -0,0 +1 @@ +"""Package marker for repository helper scripts.""" diff --git a/scripts/ci/__init__.py b/scripts/ci/__init__.py new file mode 100644 index 00000000..820f1109 --- /dev/null +++ b/scripts/ci/__init__.py @@ -0,0 +1 @@ +"""CI helper scripts (governance verification, PR risk classification).""" diff --git a/scripts/ci/classify_pr_risk.py b/scripts/ci/classify_pr_risk.py new file mode 100644 index 00000000..5fa93863 --- /dev/null +++ b/scripts/ci/classify_pr_risk.py @@ -0,0 +1,156 @@ +"""Classify a PR's risk level and suggest labels from its changed paths. + +Deterministic, path-based classifier. Emits *suggested* labels; maintainers +retain final override authority and record the reason. +""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +# Highest-first ordering; a PR takes the highest risk across all changed paths. +_RISK_ORDER = {"R0": 0, "R1": 1, "R2": 2, "R3": 3} + +# R3 — supply chain / security / release (explicit core-maintainer approval). +_R3_PREFIXES = ( + "setup.py", + "pyproject.toml", + "requirements.txt", + "MANIFEST.in", + "SECURITY.md", + ".github/workflows/", +) + +# R2 — core / compatibility (domain owner + second maintainer). +_R2_PREFIXES = ( + "backtrader/lineroot.py", + "backtrader/linebuffer.py", + "backtrader/lineseries.py", + "backtrader/lineiterator.py", + "backtrader/metabase.py", + "backtrader/cerebro.py", + "backtrader/strategy.py", + "backtrader/broker.py", + "backtrader/brokers/", + "backtrader/feeds/", +) + +# R0 — docs / tests / non-behavioral tooling. +_R0_PREFIXES = ("docs/", "tests/", ".github/ISSUE_TEMPLATE/", ".github/PULL_REQUEST_TEMPLATE/") +_R0_SUFFIXES = (".md",) + +# area:* suggestion, first match wins. +_AREA_RULES: Tuple[Tuple[str, str], ...] = ( + ("backtrader/broker.py", "area:broker"), + ("backtrader/brokers/", "area:broker"), + ("backtrader/feeds/", "area:feeds"), + ("backtrader/indicators/", "area:indicators"), + ("backtrader/analyzers/", "area:analyzers"), + ("backtrader/observers/", "area:observers"), + ("docs/", "area:docs"), + ("tests/", "area:tests"), + (".github/", "area:ci"), + ("scripts/", "area:ci"), + ("backtrader/", "area:core"), +) + + +def _normalize(path: str) -> str: + return path.replace("\\", "/") + + +def _classify_single(path: str) -> str: + p = _normalize(path) + for prefix in _R3_PREFIXES: + if p == prefix or p.startswith(prefix): + return "R3" + for prefix in _R2_PREFIXES: + if p == prefix or p.startswith(prefix): + return "R2" + if p.endswith(_R0_SUFFIXES) or p.startswith(_R0_PREFIXES): + return "R0" + return "R1" + + +def classify_risk(paths: List[str]) -> str: + """Return the highest risk level (R0–R3) across the changed paths.""" + if not paths: + return "R0" + highest = max(_RISK_ORDER[_classify_single(p)] for p in paths) + for level, order in _RISK_ORDER.items(): + if order == highest: + return level + return "R0" + + +def suggest_area(paths: List[str]) -> str: + """Suggest a single ``area:*`` label from the changed paths.""" + for path in paths: + p = _normalize(path) + for prefix, label in _AREA_RULES: + if p == prefix.rstrip("/") or p.startswith(prefix): + return label + return "area:core" + + +def suggest_labels(paths: List[str]) -> Dict[str, str]: + """Return suggested labels (risk + area) for the changed paths.""" + return {"risk": classify_risk(paths), "area": suggest_area(paths)} + + +def paths_from_file(path: Path) -> List[str]: + """Read one changed path per line from a UTF-8 file.""" + return [line.strip() for line in path.read_text(encoding="utf-8").splitlines() if line.strip()] + + +def write_github_output(path: Path, labels: Dict[str, str]) -> None: + """Write fixed classifier values in the GitHub Actions output-file format.""" + path.open("a", encoding="utf-8").write(f"risk={labels['risk']}\narea={labels['area']}\n") + + +def main(argv: Optional[List[str]] = None) -> int: + parser = argparse.ArgumentParser( + description="Classify a PR's risk level and suggest labels from changed paths." + ) + source = parser.add_mutually_exclusive_group(required=True) + source.add_argument( + "--paths", + nargs="+", + help="Changed file paths (space-separated).", + ) + source.add_argument( + "--paths-file", + type=Path, + help="UTF-8 file containing one changed path per line.", + ) + parser.add_argument( + "--format", + choices=("text", "json"), + default="text", + help="Output format (default: text).", + ) + parser.add_argument( + "--github-output", + type=Path, + help="Optional GitHub Actions output file to receive risk and area values.", + ) + args = parser.parse_args(argv) + + paths = args.paths if args.paths is not None else paths_from_file(args.paths_file) + labels = suggest_labels(paths) + if args.format == "json": + print(json.dumps(labels, sort_keys=True)) + else: + print(f"risk={labels['risk']}") + print(f"area={labels['area']}") + print("NOTE: these are suggestions; maintainers retain final override authority.") + if args.github_output is not None: + write_github_output(args.github_output, labels) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/ci/render_github_ruleset_payload.py b/scripts/ci/render_github_ruleset_payload.py new file mode 100644 index 00000000..b6e41b8d --- /dev/null +++ b/scripts/ci/render_github_ruleset_payload.py @@ -0,0 +1,57 @@ +"""Render a GitHub Rulesets API payload from an Iteration 140 manifest. + +The tracked manifests include local audit metadata (the branch key, rollout +notes, and activation record). GitHub's REST endpoint must not receive those +keys. This program removes them deterministically; it never calls GitHub and +never handles credentials. +""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from typing import Any, Dict, List, Optional + + +_LOCAL_ONLY_KEYS = {"branch", "_governance_notes", "_activation"} +_REQUIRED_API_KEYS = {"name", "target", "enforcement", "conditions", "rules"} + + +def render_payload(manifest: Dict[str, Any], enforcement: Optional[str] = None) -> Dict[str, Any]: + """Return the API-safe subset of a tracked ruleset manifest.""" + missing = sorted(_REQUIRED_API_KEYS - set(manifest)) + if missing: + raise ValueError(f"manifest is missing required API keys: {', '.join(missing)}") + + payload = {key: value for key, value in manifest.items() if key not in _LOCAL_ONLY_KEYS} + if enforcement is not None: + payload["enforcement"] = enforcement + return payload + + +def main(argv: Optional[List[str]] = None) -> int: + parser = argparse.ArgumentParser( + description="Render an Iteration 140 Rulesets API payload without local audit metadata." + ) + parser.add_argument("manifest", type=Path, help="Path to dev/development/master manifest JSON.") + parser.add_argument( + "--enforcement", + choices=("active", "disabled", "evaluate"), + help="Override enforcement for a controlled rollout or rollback.", + ) + parser.add_argument("--output", type=Path, help="Write JSON to this file instead of stdout.") + args = parser.parse_args(argv) + + manifest = json.loads(args.manifest.read_text(encoding="utf-8")) + payload = render_payload(manifest, args.enforcement) + rendered = json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True) + "\n" + if args.output is None: + print(rendered, end="") + else: + args.output.write_text(rendered, encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/ci/verify_github_governance.py b/scripts/ci/verify_github_governance.py new file mode 100644 index 00000000..89c845a6 --- /dev/null +++ b/scripts/ci/verify_github_governance.py @@ -0,0 +1,325 @@ +"""Verify GitHub governance configuration against the tracked manifests. + +The verifier deliberately has no GitHub client and no credential handling. A +maintainer exports sanitized read-only API responses with ``gh api`` and passes +their paths here. Local-only syntax checks are available for development, but +they are never evidence that a Ruleset or CODEOWNERS is live on GitHub. + +Exit code: 0 = consistent, 1 = a missing proof or configuration difference. +""" + +from __future__ import annotations + +import argparse +import json +import re +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple + + +_REPO_ROOT = Path(__file__).resolve().parents[2] + +_LONG_BRANCHES = ("dev", "development", "master") +_REQUIRED_RULE_TYPES = {"deletion", "non_fast_forward", "pull_request", "required_status_checks"} + +# Owner tokens that must never appear in CODEOWNERS (D2 forbids placeholders). +_PLACEHOLDER_OWNERS = {"todo", "placeholder", "owner", "xxx", "example", "someone", "me"} + +_OWNER_RE = re.compile(r"^@[\w-]+(?:/[\w-]+)?$") + + +def parse_codeowners(content: str) -> List[Tuple[str, List[str]]]: + """Parse a CODEOWNERS file into ``(pattern, [owners])`` pairs.""" + entries: List[Tuple[str, List[str]]] = [] + for raw in content.splitlines(): + line = raw.strip() + if not line or line.startswith("#"): + continue + if " #" in line: + line = line.split(" #", 1)[0].strip() + parts = line.split() + if len(parts) >= 2: + entries.append((parts[0], parts[1:])) + return entries + + +def validate_codeowners(entries: List[Tuple[str, List[str]]]) -> List[str]: + """Return syntax/placeholder findings for parsed CODEOWNERS entries.""" + findings: List[str] = [] + for pattern, owners in entries: + for owner in owners: + name = owner.lstrip("@").split("/", 1)[0].lower() + if name in _PLACEHOLDER_OWNERS: + findings.append(f"{pattern}: placeholder owner {owner!r} is not allowed") + elif not _OWNER_RE.match(owner): + findings.append( + f"{pattern}: invalid owner {owner!r} (expected @username or @org/team)" + ) + return findings + + +def _branch_for_ruleset(ruleset: Dict[str, Any]) -> Optional[str]: + include = ruleset.get("conditions", {}).get("ref_name", {}).get("include", []) + for ref in include: + if isinstance(ref, str) and ref.startswith("refs/heads/"): + return ref[len("refs/heads/") :] + return None + + +def _rules_by_type(ruleset: Dict[str, Any]) -> Dict[str, Dict[str, Any]]: + return { + rule["type"]: rule + for rule in ruleset.get("rules", []) + if isinstance(rule, dict) and isinstance(rule.get("type"), str) + } + + +def _status_check_contexts(ruleset: Dict[str, Any]) -> List[str]: + rule = _rules_by_type(ruleset).get("required_status_checks") + if rule is None: + return [] + checks = rule.get("parameters", {}).get("required_status_checks", []) + return [check.get("context", "") for check in checks if isinstance(check, dict)] + + +def _check_pull_request_rule( + branch: str, expected: Dict[str, Any], actual: Dict[str, Any] +) -> List[str]: + """Compare explicitly tracked pull-request parameters.""" + findings: List[str] = [] + expected_rule = _rules_by_type(expected).get("pull_request", {}) + actual_rule = _rules_by_type(actual).get("pull_request", {}) + expected_parameters = expected_rule.get("parameters", {}) + actual_parameters = actual_rule.get("parameters", {}) + for key, expected_value in expected_parameters.items(): + actual_value = actual_parameters.get(key) + if actual_value != expected_value: + findings.append( + f"{branch}: pull_request.{key} is {actual_value!r}, expected {expected_value!r}" + ) + return findings + + +def check_ruleset_coverage( + api_rulesets: List[Dict[str, Any]], + manifests: Dict[str, Dict[str, Any]], + expected_enforcement: Optional[str] = None, +) -> List[str]: + """Compare a Rulesets API response against all long-lived branch manifests.""" + findings: List[str] = [] + by_branch: Dict[str, List[Dict[str, Any]]] = {} + for ruleset in api_rulesets: + branch = _branch_for_ruleset(ruleset) + if branch is not None: + by_branch.setdefault(branch, []).append(ruleset) + + for branch in _LONG_BRANCHES: + expected = manifests.get(branch) + if expected is None: + findings.append(f"{branch}: missing manifest under .github/governance/rulesets/") + continue + + matches = by_branch.get(branch, []) + if not matches: + findings.append(f"{branch}: no ruleset covers refs/heads/{branch}") + continue + if len(matches) != 1: + findings.append( + f"{branch}: expected exactly one matching ruleset, found {len(matches)}" + ) + continue + + actual = matches[0] + if actual.get("name") != expected.get("name"): + findings.append( + f"{branch}: ruleset name is {actual.get('name')!r}, expected {expected.get('name')!r}" + ) + if actual.get("target") != expected.get("target"): + findings.append( + f"{branch}: ruleset target is {actual.get('target')!r}, expected {expected.get('target')!r}" + ) + if actual.get("conditions") != expected.get("conditions"): + findings.append(f"{branch}: ruleset conditions differ from the manifest") + + wanted_enforcement = expected_enforcement or expected.get("enforcement") + if actual.get("enforcement") != wanted_enforcement: + findings.append( + f"{branch}: ruleset enforcement is {actual.get('enforcement')!r}, " + f"expected {wanted_enforcement!r}" + ) + + expected_rules = _rules_by_type(expected) + actual_rules = _rules_by_type(actual) + missing_types = _REQUIRED_RULE_TYPES - set(actual_rules) + if missing_types: + findings.append(f"{branch}: missing required rule types {sorted(missing_types)}") + unexpected_types = set(actual_rules) - set(expected_rules) + if unexpected_types: + findings.append(f"{branch}: unexpected rule types {sorted(unexpected_types)}") + + expected_contexts = set(_status_check_contexts(expected)) + actual_contexts = set(_status_check_contexts(actual)) + missing_contexts = expected_contexts - actual_contexts + extra_contexts = actual_contexts - expected_contexts + if missing_contexts: + findings.append(f"{branch}: missing required status checks {sorted(missing_contexts)}") + if extra_contexts: + findings.append(f"{branch}: unexpected required status checks {sorted(extra_contexts)}") + + findings.extend(_check_pull_request_rule(branch, expected, actual)) + + return findings + + +def check_codeowners_api_errors(response: Any) -> List[str]: + """Normalize GitHub's codeowners/errors response into findings.""" + if isinstance(response, dict): + errors = response.get("errors") + if errors is None: + message = response.get("message", "response has no 'errors' field") + return [f"codeowners/errors API response is not valid: {message}"] + elif isinstance(response, list): + errors = response + else: + return ["codeowners/errors API response must be an object or array"] + + if not isinstance(errors, list): + return ["codeowners/errors API response contains a non-list 'errors' field"] + return [ + f"CODEOWNERS API error: {json.dumps(error, ensure_ascii=False, sort_keys=True)}" + for error in errors + ] + + +def load_manifests(directory: Path) -> Dict[str, Dict[str, Any]]: + """Load all ``*.json`` manifests keyed by their ``branch`` field.""" + manifests: Dict[str, Dict[str, Any]] = {} + if not directory.is_dir(): + return manifests + for path in sorted(directory.glob("*.json")): + data = json.loads(path.read_text(encoding="utf-8")) + branch = data.get("branch") + if isinstance(branch, str): + manifests[branch] = data + return manifests + + +def _load_json(path: Path, label: str) -> Tuple[Optional[Any], Optional[str]]: + if not path.is_file(): + return None, f"{label} file not found: {path}" + try: + return json.loads(path.read_text(encoding="utf-8")), None + except json.JSONDecodeError as error: + return None, f"{label} file is not valid JSON: {error}" + + +def _ruleset_list(response: Any) -> Tuple[Optional[List[Dict[str, Any]]], Optional[str]]: + if isinstance(response, list) and all(isinstance(item, dict) for item in response): + return response, None + if isinstance(response, dict) and isinstance(response.get("rulesets"), list): + rulesets = response["rulesets"] + if all(isinstance(item, dict) for item in rulesets): + return rulesets, None + return None, "Rulesets API response must be a JSON array of objects" + + +def main(argv: Optional[List[str]] = None) -> int: + parser = argparse.ArgumentParser( + description="Verify GitHub governance config against the in-repo manifests." + ) + parser.add_argument( + "--rulesets-json", + type=Path, + help="Sanitized JSON from `gh api repos/OWNER/REPO/rulesets`.", + ) + parser.add_argument( + "--codeowners-errors-json", + type=Path, + help="Sanitized JSON from `gh api repos/OWNER/REPO/codeowners/errors`.", + ) + parser.add_argument( + "--expected-enforcement", + choices=("active", "disabled", "evaluate"), + help="Override the manifest enforcement for a time-bounded rollout check.", + ) + parser.add_argument( + "--local-only", + action="store_true", + help="Only validate tracked manifests and local CODEOWNERS syntax; not deployment evidence.", + ) + parser.add_argument( + "--codeowners", + type=Path, + default=_REPO_ROOT / ".github" / "CODEOWNERS", + help="Path to the CODEOWNERS file (default: repository file).", + ) + parser.add_argument( + "--manifests-dir", + type=Path, + default=_REPO_ROOT / ".github" / "governance" / "rulesets", + help="Directory containing dev/development/master manifest JSON files.", + ) + args = parser.parse_args(argv) + + findings: List[str] = [] + try: + manifests = load_manifests(args.manifests_dir) + except (OSError, json.JSONDecodeError) as error: + manifests = {} + findings.append(f"unable to load ruleset manifests: {error}") + if set(manifests) != set(_LONG_BRANCHES): + findings.append( + "ruleset manifests must cover exactly dev, development, master; " + f"found {sorted(manifests)}" + ) + + if args.codeowners.is_file(): + content = args.codeowners.read_text(encoding="utf-8") + findings.extend(validate_codeowners(parse_codeowners(content))) + else: + findings.append(f"CODEOWNERS not found at {args.codeowners}") + + if args.rulesets_json is None: + if not args.local_only: + findings.append("missing --rulesets-json; local syntax is not live Ruleset evidence") + else: + response, error = _load_json(args.rulesets_json, "Rulesets API response") + if error is not None: + findings.append(error) + else: + rulesets, error = _ruleset_list(response) + if error is not None: + findings.append(error) + else: + findings.extend( + check_ruleset_coverage(rulesets, manifests, args.expected_enforcement) + ) + + if args.codeowners_errors_json is None: + if not args.local_only: + findings.append( + "missing --codeowners-errors-json; local syntax is not GitHub CODEOWNERS evidence" + ) + else: + response, error = _load_json(args.codeowners_errors_json, "CODEOWNERS API response") + if error is not None: + findings.append(error) + else: + findings.extend(check_codeowners_api_errors(response)) + + if findings: + for finding in findings: + print(f"[FAIL] {finding}") + return 1 + + if args.local_only: + print( + "[OK] local governance manifests and CODEOWNERS syntax are valid (remote proof skipped)" + ) + else: + print("[OK] GitHub governance config matches manifests") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/unit/scripts/test_classify_pr_risk.py b/tests/unit/scripts/test_classify_pr_risk.py new file mode 100644 index 00000000..6d1b0556 --- /dev/null +++ b/tests/unit/scripts/test_classify_pr_risk.py @@ -0,0 +1,119 @@ +"""Tests for scripts/ci/classify_pr_risk.py.""" + +from __future__ import annotations + +import sys +from pathlib import Path + +_REPO_ROOT = Path(__file__).resolve().parents[3] +if str(_REPO_ROOT) not in sys.path: + sys.path.insert(0, str(_REPO_ROOT)) + +from scripts.ci.classify_pr_risk import ( # noqa: E402 + classify_risk, + paths_from_file, + suggest_area, + suggest_labels, + write_github_output, +) + +# --------------------------------------------------------------------------- +# Risk classification +# --------------------------------------------------------------------------- + + +def test_docs_paths_are_r0(): + assert classify_risk(["docs/source/index.md"]) == "R0" + + +def test_tests_paths_are_r0(): + assert classify_risk(["tests/unit/test_foo.py"]) == "R0" + + +def test_markdown_at_root_is_r0(): + assert classify_risk(["README.md", "CONTRIBUTING.md"]) == "R0" + + +def test_indicator_path_is_r1(): + assert classify_risk(["backtrader/indicators/sma.py"]) == "R1" + + +def test_unknown_path_defaults_to_r1(): + assert classify_risk(["backtrader/analyzers/sharpe.py"]) == "R1" + + +def test_cerebro_is_r2(): + assert classify_risk(["backtrader/cerebro.py"]) == "R2" + + +def test_line_system_is_r2(): + assert classify_risk(["backtrader/lineroot.py", "backtrader/linebuffer.py"]) == "R2" + + +def test_feeds_and_brokers_are_r2(): + assert classify_risk(["backtrader/feeds/csvgeneric.py"]) == "R2" + assert classify_risk(["backtrader/brokers/bbroker.py"]) == "R2" + + +def test_supply_chain_is_r3(): + assert classify_risk(["setup.py", "pyproject.toml", "requirements.txt"]) == "R3" + + +def test_workflows_are_r3(): + assert classify_risk([".github/workflows/test.yml"]) == "R3" + + +def test_mixed_paths_take_highest_risk(): + assert classify_risk(["docs/x.md", "backtrader/cerebro.py"]) == "R2" + assert classify_risk(["docs/x.md", "setup.py"]) == "R3" + + +def test_windows_path_separators_are_normalized(): + assert classify_risk(["backtrader\\cerebro.py"]) == "R2" + + +# --------------------------------------------------------------------------- +# Area suggestion +# --------------------------------------------------------------------------- + + +def test_area_broker(): + assert suggest_area(["backtrader/broker.py"]) == "area:broker" + + +def test_area_feeds(): + assert suggest_area(["backtrader/feeds/csvgeneric.py"]) == "area:feeds" + + +def test_area_indicators(): + assert suggest_area(["backtrader/indicators/sma.py"]) == "area:indicators" + + +def test_area_docs(): + assert suggest_area(["docs/source/index.md"]) == "area:docs" + + +def test_area_ci(): + assert suggest_area([".github/workflows/test.yml"]) == "area:ci" + + +def test_area_core_default(): + assert suggest_area(["backtrader/cerebro.py"]) == "area:core" + + +def test_suggest_labels_contains_risk_and_area(): + labels = suggest_labels(["backtrader/cerebro.py"]) + assert labels["risk"] == "R2" + assert labels["area"] == "area:core" + + +def test_paths_file_preserves_one_path_per_line(tmp_path): + paths_file = tmp_path / "changed-paths.txt" + paths_file.write_text("docs/source/index.md\nbacktrader/cerebro.py\n", encoding="utf-8") + assert paths_from_file(paths_file) == ["docs/source/index.md", "backtrader/cerebro.py"] + + +def test_github_output_contains_only_fixed_classifier_values(tmp_path): + output = tmp_path / "github-output.txt" + write_github_output(output, {"risk": "R2", "area": "area:core"}) + assert output.read_text(encoding="utf-8") == "risk=R2\narea=area:core\n" diff --git a/tests/unit/scripts/test_render_github_ruleset_payload.py b/tests/unit/scripts/test_render_github_ruleset_payload.py new file mode 100644 index 00000000..4c717530 --- /dev/null +++ b/tests/unit/scripts/test_render_github_ruleset_payload.py @@ -0,0 +1,43 @@ +"""Tests for rendering GitHub Rulesets API payloads from tracked manifests.""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + + +_REPO_ROOT = Path(__file__).resolve().parents[3] +if str(_REPO_ROOT) not in sys.path: + sys.path.insert(0, str(_REPO_ROOT)) + +from scripts.ci.render_github_ruleset_payload import render_payload # noqa: E402 + + +def test_render_payload_strips_local_audit_metadata(): + manifest = json.loads( + (_REPO_ROOT / ".github" / "governance" / "rulesets" / "dev.json").read_text( + encoding="utf-8" + ) + ) + payload = render_payload(manifest) + assert payload["name"] == "dev-ruleset" + assert payload["enforcement"] == "evaluate" + assert "branch" not in payload + assert "_governance_notes" not in payload + assert "_activation" not in payload + + +def test_render_payload_can_override_enforcement(): + payload = render_payload( + { + "branch": "dev", + "name": "dev-ruleset", + "target": "branch", + "enforcement": "evaluate", + "conditions": {}, + "rules": [], + }, + enforcement="active", + ) + assert payload["enforcement"] == "active" diff --git a/tests/unit/scripts/test_verify_github_governance.py b/tests/unit/scripts/test_verify_github_governance.py new file mode 100644 index 00000000..711b515e --- /dev/null +++ b/tests/unit/scripts/test_verify_github_governance.py @@ -0,0 +1,164 @@ +"""Tests for the offline GitHub-governance verifier. + +Fixtures are sanitized in-memory dictionaries. They never contact GitHub. +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + + +_REPO_ROOT = Path(__file__).resolve().parents[3] +if str(_REPO_ROOT) not in sys.path: + sys.path.insert(0, str(_REPO_ROOT)) + +from scripts.ci.verify_github_governance import ( # noqa: E402 + check_codeowners_api_errors, + check_ruleset_coverage, + load_manifests, + main, + parse_codeowners, + validate_codeowners, +) + + +def _manifests() -> dict: + return load_manifests(_REPO_ROOT / ".github" / "governance" / "rulesets") + + +def _api_rulesets() -> list[dict]: + """Build an API-shaped response from each tracked manifest.""" + rulesets: list[dict] = [] + for manifest in _manifests().values(): + rulesets.append( + { + "name": manifest["name"], + "target": manifest["target"], + "enforcement": manifest["enforcement"], + "conditions": manifest["conditions"], + "rules": manifest["rules"], + } + ) + return rulesets + + +# --------------------------------------------------------------------------- +# CODEOWNERS parsing and validation +# --------------------------------------------------------------------------- + + +def test_parse_codeowners_skips_comments_and_blanks(): + content = "# a comment\n\n \n/backtrader/cerebro.py @cloudQuant\n" + entries = parse_codeowners(content) + assert entries == [("/backtrader/cerebro.py", ["@cloudQuant"])] + + +def test_parse_codeowners_extracts_multiple_owners_and_inline_comment(): + content = "/scripts/ @octocat @org/team # inline comment\n" + entries = parse_codeowners(content) + assert entries == [("/scripts/", ["@octocat", "@org/team"])] + + +def test_validate_codeowners_rejects_placeholder(): + findings = validate_codeowners([("/backtrader/cerebro.py", ["@TODO"])]) + assert findings + assert any("placeholder" in finding.lower() for finding in findings) + + +def test_validate_codeowners_accepts_user_and_team(): + entries = [ + ("/backtrader/cerebro.py", ["@cloudQuant"]), + ("/scripts/", ["@cloudQuant/core-maintainers"]), + ] + assert validate_codeowners(entries) == [] + + +def test_validate_codeowners_rejects_non_owner_token(): + findings = validate_codeowners([("/backtrader/cerebro.py", ["docs@example.com"])]) + assert findings + + +def test_codeowners_api_errors_accepts_empty_response(): + assert check_codeowners_api_errors({"errors": []}) == [] + + +def test_codeowners_api_errors_reports_api_payload(): + findings = check_codeowners_api_errors({"errors": [{"kind": "invalid_owner"}]}) + assert findings + assert "invalid_owner" in findings[0] + + +def test_codeowners_api_errors_reports_not_found_response(): + findings = check_codeowners_api_errors({"message": "Not Found"}) + assert findings + assert "Not Found" in findings[0] + + +# --------------------------------------------------------------------------- +# Ruleset coverage +# --------------------------------------------------------------------------- + + +def test_ruleset_coverage_passes_when_all_branches_match_manifests(): + assert check_ruleset_coverage(_api_rulesets(), _manifests()) == [] + + +def test_ruleset_coverage_reports_missing_branch(): + api = [ruleset for ruleset in _api_rulesets() if ruleset["name"] != "master-ruleset"] + findings = check_ruleset_coverage(api, _manifests()) + assert any("master" in finding for finding in findings) + + +def test_ruleset_coverage_reports_wrong_enforcement(): + api = _api_rulesets() + for ruleset in api: + if ruleset["name"] == "development-ruleset": + ruleset["enforcement"] = "active" + findings = check_ruleset_coverage(api, _manifests()) + assert any("development" in finding and "enforcement" in finding for finding in findings) + + +def test_ruleset_coverage_reports_missing_required_check(): + api = _api_rulesets() + for ruleset in api: + if ruleset["name"] == "dev-ruleset": + checks = next( + rule for rule in ruleset["rules"] if rule["type"] == "required_status_checks" + ) + checks["parameters"]["required_status_checks"] = [{"context": "Lint"}] + findings = check_ruleset_coverage(api, _manifests()) + assert any("missing required status checks" in finding for finding in findings) + + +def test_ruleset_coverage_reports_pull_request_parameter_drift(): + api = _api_rulesets() + for ruleset in api: + if ruleset["name"] == "master-ruleset": + rule = next(rule for rule in ruleset["rules"] if rule["type"] == "pull_request") + rule["parameters"]["require_code_owner_review"] = False + findings = check_ruleset_coverage(api, _manifests()) + assert any("require_code_owner_review" in finding for finding in findings) + + +def test_manifests_are_valid_json_and_cover_three_branches(): + manifests = _manifests() + assert set(manifests) == {"dev", "development", "master"} + for branch, manifest in manifests.items(): + json.dumps(manifest) + assert manifest["branch"] == branch + assert manifest["enforcement"] == "evaluate" + + +# --------------------------------------------------------------------------- +# Command-line proof requirements +# --------------------------------------------------------------------------- + + +def test_main_requires_remote_proofs_by_default(): + assert main([]) == 1 + + +def test_main_allows_explicit_local_only_check(): + assert main(["--local-only"]) == 0