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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
527 changes: 527 additions & 0 deletions .github/workflows/auto-promote.yml

Large diffs are not rendered by default.

48 changes: 48 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,26 @@
(#952).
- `CRG_HOOK_WORKTREES=1` keeps the generated pre-commit hook active inside
a linked Git worktree (#953).
- `CRG_DISCOVERY_TIMEOUT` bounds each Git command that discovers what
changed when a review tool or command was not handed an explicit file
list. It defaults to 5 seconds, never exceeds `CRG_GIT_TIMEOUT`, and is
read on every call rather than frozen at import. `CRG_GIT_TIMEOUT` keeps
its 30-second default and still governs build, update and watch (#262).
- `staging` is promoted to `testing` automatically, once a day, when it has
commits `testing` lacks, every required status check is green on its tip,
and the promotion gate has not failed on the `testing` tip.
`.github/workflows/auto-promote.yml` opens the promotion pull request and
merges it with a merge commit, so contributor authorship survives; the
decision lives in `scripts/auto_promote.py` and reads the required
contexts from the `testing` ruleset at run time. It merges only a pull
request it opened itself — same repository, `staging` → `testing`,
labelled `auto-promotion`, and pinned with `--match-head-commit` to the
commit whose checks were read — all re-verified immediately before the
merge, so a fork branch named `staging`, a base branch changed after the
fact, or a promotion pull request opened by hand cannot be merged by it. A
hand-started run defaults to a dry run. Requires *Allow GitHub Actions to
create and approve pull requests* under Settings → Actions → General.
Promotion to `main` is never automatic and the workflow cannot target it.

### Changed

Expand All @@ -40,6 +60,34 @@
another branch. Detection uses the git directory's `commondir` file and
needs only `git rev-parse --absolute-git-dir` (Git 2.13). Reinstall
upgrades the exact hook block written by earlier releases (#953).
- Every MCP tool that can reach Git discovery, a graph traversal, FTS, an
embedding provider or the filesystem now runs its work on a worker thread
through one shared helper. A tool that exceeds `CRG_TOOL_TIMEOUT` answers
with `status: error` naming itself and the budget, instead of leaving the
client to time the request out itself as MCP error -32001. Only
`get_docs_section_tool` and `list_repos_tool` still run inline; they read
one small file each (#262, #46, #136).

`CRG_TOOL_TIMEOUT` keeps its meaning: it bounds read-only tools, and it
does **not** bound `build_or_update_graph_tool`, `run_postprocess_tool`,
`embed_graph_tool`, `generate_wiki_tool` or `apply_refactor_tool`. Those
write — to `graph.db`, to the wiki tree, to your source files — and a
timeout cancels the wait, not the worker, so bounding them would report
failure to the client while the write went on regardless.
- Change discovery is bounded honestly. Each Git command in the chain that
works out what changed gets `CRG_DISCOVERY_TIMEOUT` (5 seconds) rather
than the 30-second `CRG_GIT_TIMEOUT`, and runs with `require_vcs`, so
exhausting that budget raises a `ChangeDiscoveryError` and the tool
answers `status: error`. Shortening a budget that failed *silently* would
only have made #913's false all-clear easier to hit, and would have
extended it to the base resolution, where a timed-out merge base
degrades a three-dot diff into a two-dot one. Raising `CRG_GIT_TIMEOUT`
still raises discovery with it, so the documented remedy for slow Git
keeps working (#262).
- `get_minimal_context_tool` runs its change discovery on that same budget.
It previously spent up to ~130 seconds on five Git subprocesses of its
own — the tool agents are told to call first, and the one most likely to
hit a client's request ceiling (#262).

### Fixed

Expand Down
12 changes: 10 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,16 @@ bd close <id> # Complete work

Three long-lived branches, one direction: feature PR → `staging` (default) → `testing` → `main` → tag → PyPI.
Open every PR against `staging`. Never push to or open PRs against `testing` or `main`; those only
receive promotion PRs, which the maintainer merges by hand with a merge commit. Full rules in
CONTRIBUTING.md "Branching and promotion".
receive promotion PRs, always merged with a merge commit.

`staging` → `testing` is automatic: `.github/workflows/auto-promote.yml` runs once a day and merges
the promotion PR when `staging` is ahead, every required check is green, and the promotion gate has
not failed on `testing`. The decision lives in `scripts/auto_promote.py`. It merges only a PR it
opened itself — same repository, correctly aimed, labelled `auto-promotion`, pinned to the commit
whose checks were read — so a promotion PR you open by hand is left alone. It needs Settings →
Actions → General → Workflow permissions → *Allow GitHub Actions to create and approve pull
requests*. **Promotion to `main` is never automatic** — the maintainer opens and merges that PR by
hand. Full rules in CONTRIBUTING.md "Branching and promotion".

## Session Completion

Expand Down
76 changes: 65 additions & 11 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ feature branch --PR--> staging --PR--> testing --PR--> main --tag--> PyPI
| Branch | Purpose | Who merges into it |
| --------- | -------------------------------------------------------------- | --------------------------------------- |
| `staging` | Default branch. Every feature and fix PR lands here first. | Maintainers, once CI is green. |
| `testing` | Candidate for the next release. Gets a longer soak and manual QA. | Maintainer, via a promotion PR from `staging`. |
| `main` | Released code. Nothing reaches `main` without passing QA on `testing`. | Maintainer, via a promotion PR from `testing`. |
| `testing` | Candidate for the next release. Gets a longer soak and manual QA. | The `Auto promote` workflow, once a day, when `staging` is green. |
| `main` | Released code. Nothing reaches `main` without passing QA on `testing`. | Maintainer only, by hand, via a promotion PR from `testing`. |

Rules that apply to all three branches (enforced by repository rulesets):

Expand All @@ -74,14 +74,67 @@ merge commit when a PR has several authors so nobody loses attribution. Rebase-m
also allowed on `staging`.

**Promotion PRs** move everything on `staging` to `testing`, and later everything on
`testing` to `main`. Open one from the Actions tab (`Promote` workflow, pick the step) or
by hand with `gh pr create --base testing --head staging`. They are always merged with a
**merge commit**, never squashed, so every contributor stays the author of their commits;
the `testing` and `main` rulesets allow no other merge method. A promotion is the
maintainer's sign-off: CI green is necessary but not sufficient. A promotion PR opened by
the workflow shows an "Approve workflows to run" banner; the required checks are already
satisfied by the CI run on the source branch's tip, so the banner can be approved or
ignored.
`testing` to `main`. They are always merged with a **merge commit**, never squashed, so
every contributor stays the author of their commits; the `testing` and `main` rulesets
allow no other merge method. Opening a promotion PR re-queues every required check on a
commit that already had them green from the push run, so the PR sits blocked for a
quarter of an hour or so before it can be merged. That is normal.

`staging` → `testing` happens by itself. `testing` → `main` never does.

### Automatic promotion to `testing`

`.github/workflows/auto-promote.yml` runs once a day. If `staging` has commits `testing`
does not, every required status check is green on the `staging` tip, and the promotion
gate has not failed on the current `testing` tip, it opens the promotion PR and merges it
with a merge commit. It writes a summary on every run, including the runs that decide to
do nothing, so "nothing to promote" is never indistinguishable from "did not run".

**One repository setting is required.** Settings → Actions → General → Workflow
permissions → tick **Allow GitHub Actions to create and approve pull requests** → Save.
Without it `gh pr create` is refused and no promotion PR can be opened — by this workflow
or by the manual `Promote` one. Leave *Workflow permissions* itself on **Read repository
contents and packages permissions**: both workflows ask for the writes they need in their
own `permissions:` block. If the tick is missing, the run goes red and the job summary
gives that click path.

It merges only a PR it opened itself: same repository, `staging` → `testing`, carrying the
`auto-promotion` label, and at the commit whose checks were read. `gh pr list --head
staging` matches a branch of that name in any of this repository's forks, and a PR's base
branch can be changed by its author at any time without re-running a single check, so all
of that is verified again immediately before the merge, and the merge itself is pinned
with `--match-head-commit`. **A promotion PR you opened by hand is never touched** — it
has no `auto-promotion` label, so the daily run refuses it by number and says so.

It stops, without failing, when there is nothing to promote, when CI is still running,
when a required check failed or was skipped, when the promotion gate is still running on
`testing`, when the gate failed there, or when `staging` moved mid-run. Those states are
reported elsewhere already and tomorrow's run looks again. Three things turn the run red,
because nothing else reports them:

- **GitHub refused the merge.** The PR is left open and the refusal is quoted in the job
summary, with the causes listed in the order they actually occur; merge it by hand with
a merge commit.
- **The workflow is stuck on its own PR** — it conflicts, it is a draft, or GitHub holds
it for a rule with every required check green. A stalled promotion that reported itself
as a green warning every morning would stay stalled for ever.
- **A PR it was about to merge is not the one it decided on**, or the release gate could
not be started after the merge.

Every rule lives in `scripts/auto_promote.py` and is covered by
`tests/test_auto_promote.py`; the workflow fetches facts and obeys. The required contexts
are read from the `testing` ruleset at run time, so a renamed CI job cannot silently drop
out of the gate. The workflow has no input, variable or code path that can target `main`,
the PR it merges is re-checked at the door, and the tests assert both.

To see what it would decide without it doing anything, run the `Auto promote` workflow
from the Actions tab: a hand-started run defaults to a dry run.

**Promotion to `main` is never automatic.** Open it from the Actions tab (`Promote`
workflow, pick `testing -> main`) or by hand with
`gh pr create --base main --head testing`, read the promotion gate's verdict first, and
merge it yourself. That is the maintainer's sign-off: CI green is necessary but not
sufficient.

**Hotfixes** for a released version follow the same path. If a fix is urgent, open the PR
against `staging` and promote twice in a row; do not open PRs against `main`.
Expand Down Expand Up @@ -144,7 +197,8 @@ it). The report names every check, whether it passed, and for a failure the exac
assertion that moved: for example the property, its baseline and the measured delta.

The gate opens no pull request and merges nothing. Promotion to `main` stays the
maintainer's decision, made with the `Promote` workflow as before.
maintainer's decision, made with the `Promote` workflow as before. `Auto promote` cannot
reach `main` either: it promotes `staging` to `testing` and nothing else.

To run any of these by hand:

Expand Down
5 changes: 3 additions & 2 deletions README.hi-IN.md
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,8 @@ pip install "code-review-graph[all]" # All optional dependencies

| चर | विवरण | डिफ़ॉल्ट |
|----------|-------------|---------|
| `CRG_GIT_TIMEOUT` | Git संक्रियाओं की समय सीमा (सेकंड में) | `30` |
| `CRG_GIT_TIMEOUT` | Git संक्रियाओं की समय सीमा (सेकंड में; build / update / watch) | `30` |
| `CRG_DISCOVERY_TIMEOUT` | जब फ़ाइल सूची स्पष्ट रूप से नहीं दी गई हो, तब बदलाव पता करने वाली प्रत्येक Git कमांड की समय सीमा (सेकंड में)। सीमा पार होने पर त्रुटि मिलती है, "कोई बदलाव नहीं" नहीं | `5` (`CRG_GIT_TIMEOUT` स्पष्ट रूप से सेट हो तो वही) |
| `CRG_DATA_DIR` | ग्राफ़ डेटाबेस और बनी हुई फ़ाइलों की डायरेक्टरी | - |
| `CRG_HOOK_WORKTREES` | `1` करने पर pre-commit हुक जुड़े हुए git worktree में भी चलता है | - |
| `CRG_EMBEDDING_MODEL` | स्थानीय वेक्टर एम्बेडिंग का डिफ़ॉल्ट मॉडल | `all-MiniLM-L6-v2` |
Expand All @@ -529,7 +530,7 @@ pip install "code-review-graph[all]" # All optional dependencies
| `CRG_MAX_BFS_DEPTH` | ग्राफ़ ट्रैवर्सल की अधिकतम गहराई | `15` |
| `CRG_MAX_CHANGED_FUNCS` | एक बदलाव रिपोर्ट में विश्लेषित अधिकतम बदले हुए फ़ंक्शन | `500` |
| `CRG_MAX_TRANSITIVE_FRONTIER` | सकर्मक कॉलर/कॉली विस्तार में अधिकतम फ़्रंटियर आकार | `50` |
| `CRG_TOOL_TIMEOUT` | सीमित MCP टूल की समय सीमा (सेकंड; `0` से बंद) | `0` |
| `CRG_TOOL_TIMEOUT` | केवल-पठन MCP टूल की समय सीमा (सेकंड; `0` से बंद)। लिखने वाले टूल (build / postprocess / embed / wiki / apply-refactor) पर लागू नहीं | `0` |
| `CRG_CHURN_WINDOW_DAYS` | `detect-changes --churn` की कमिट गिनती की अवधि | `90` |
| `CRG_LEIDEN_SEED` | Leiden कम्युनिटी पहचान का सीड | `42` |
| `CRG_RECURSE_SUBMODULES` | `1`, `true` या `yes` होने पर git सबमॉड्यूल शामिल करें | - |
Expand Down
5 changes: 3 additions & 2 deletions README.ja-JP.md
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,8 @@ pip install "code-review-graph[all]" # All optional dependencies

| 変数 | 内容 | 既定値 |
|----------|-------------|---------|
| `CRG_GIT_TIMEOUT` | Git 操作のタイムアウト(秒) | `30` |
| `CRG_GIT_TIMEOUT` | Git 操作のタイムアウト(秒。build / update / watch) | `30` |
| `CRG_DISCOVERY_TIMEOUT` | ファイル一覧が明示されなかったとき、変更内容を特定する各 Git コマンドのタイムアウト(秒)。超過時はエラーを返し、「変更なし」とは報告しない | `5`(`CRG_GIT_TIMEOUT` を明示設定した場合はその値) |
| `CRG_DATA_DIR` | グラフのデータベースと生成物を置くディレクトリ | - |
| `CRG_HOOK_WORKTREES` | `1` にすると pre-commit フックをリンク済みの git worktree でも動かす | - |
| `CRG_EMBEDDING_MODEL` | ローカルのベクトル埋め込みの既定モデル | `all-MiniLM-L6-v2` |
Expand All @@ -529,7 +530,7 @@ pip install "code-review-graph[all]" # All optional dependencies
| `CRG_MAX_BFS_DEPTH` | グラフ探索の最大の深さ | `15` |
| `CRG_MAX_CHANGED_FUNCS` | 1 回の変更レポートで分析する変更関数の最大数 | `500` |
| `CRG_MAX_TRANSITIVE_FRONTIER` | 推移的な呼び出し元・呼び出し先の展開における最大フロンティア数 | `50` |
| `CRG_TOOL_TIMEOUT` | 制限付き MCP ツールのタイムアウト(秒。`0` で無効) | `0` |
| `CRG_TOOL_TIMEOUT` | 読み取り専用 MCP ツールのタイムアウト(秒。`0` で無効)。書き込みを行うツール(build / postprocess / embed / wiki / apply-refactor)には適用されない | `0` |
| `CRG_CHURN_WINDOW_DAYS` | `detect-changes --churn` のコミット数を数える期間 | `90` |
| `CRG_LEIDEN_SEED` | Leiden のコミュニティ検出のシード | `42` |
| `CRG_RECURSE_SUBMODULES` | `1`、`true`、`yes` のとき git サブモジュールを含める | - |
Expand Down
5 changes: 3 additions & 2 deletions README.ko-KR.md
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,8 @@ pip install "code-review-graph[all]" # All optional dependencies

| 변수 | 내용 | 기본값 |
|----------|-------------|---------|
| `CRG_GIT_TIMEOUT` | Git 작업의 제한 시간(초) | `30` |
| `CRG_GIT_TIMEOUT` | Git 작업의 제한 시간(초, build / update / watch) | `30` |
| `CRG_DISCOVERY_TIMEOUT` | 파일 목록이 주어지지 않았을 때 변경 내용을 찾는 각 Git 명령의 제한 시간(초). 초과하면 오류를 반환하며 '변경 없음'으로 보고하지 않음 | `5`(`CRG_GIT_TIMEOUT`을 직접 설정하면 그 값) |
| `CRG_DATA_DIR` | 그래프 데이터베이스와 생성물이 놓이는 디렉터리 | - |
| `CRG_HOOK_WORKTREES` | `1`로 두면 pre-commit 훅이 연결된 git worktree에서도 실행 | - |
| `CRG_EMBEDDING_MODEL` | 로컬 벡터 임베딩의 기본 모델 | `all-MiniLM-L6-v2` |
Expand All @@ -529,7 +530,7 @@ pip install "code-review-graph[all]" # All optional dependencies
| `CRG_MAX_BFS_DEPTH` | 그래프 순회의 최대 깊이 | `15` |
| `CRG_MAX_CHANGED_FUNCS` | 변경 보고 하나에서 분석하는 변경 함수의 최대 수 | `500` |
| `CRG_MAX_TRANSITIVE_FRONTIER` | 전이적 호출자/피호출자 확장의 최대 프런티어 크기 | `50` |
| `CRG_TOOL_TIMEOUT` | 제한된 MCP 도구의 제한 시간(초. `0`이면 해제) | `0` |
| `CRG_TOOL_TIMEOUT` | 읽기 전용 MCP 도구의 제한 시간(초. `0`이면 해제). 쓰기 작업(build / postprocess / embed / wiki / apply-refactor)에는 적용되지 않음 | `0` |
| `CRG_CHURN_WINDOW_DAYS` | `detect-changes --churn`의 커밋 수를 세는 기간 | `90` |
| `CRG_LEIDEN_SEED` | Leiden 커뮤니티 탐지의 시드 | `42` |
| `CRG_RECURSE_SUBMODULES` | `1`, `true`, `yes`일 때 git 서브모듈 포함 | - |
Expand Down
Loading
Loading