From faaa5c940821d2c1f33ceaa9832860f1e4ab2066 Mon Sep 17 00:00:00 2001 From: json cheng Date: Fri, 17 Apr 2026 23:57:06 +0800 Subject: [PATCH] [repo] feat : add github branch policy checks --- .claude/CLAUDE.md | 10 ++++ .claude/learnings.md | 1 + .github/scripts/validate_branch_policy.py | 62 +++++++++++++++++++++++ .github/workflows/branch_policy.yml | 27 ++++++++++ docs/development/github_branch_policy.md | 53 +++++++++++++++++++ 5 files changed, 153 insertions(+) create mode 100644 .github/scripts/validate_branch_policy.py create mode 100644 .github/workflows/branch_policy.yml create mode 100644 docs/development/github_branch_policy.md diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index a359e5ed3b8b..ce2b9c4f6f2d 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -2,6 +2,16 @@ When working with a branch, do not use rebase or amend - add new commits instead Do not commit to the master branch. Create a new branch for every task. +For feature-specific work, prefer opening pull requests against the corresponding module branch, such as `parser/...`, instead of merging directly into `master`. + +For new development branches, prefer creating them under the corresponding module prefix, such as `parser/...` for parser work. + +Development branches must use the `dev-` prefix after the module namespace. Prefer branch names of the form `/dev-`, for example `parser/dev-gql-query-root-followup`. + +Module trunk branches must also use an explicit marker after the module namespace. Prefer names of the form `/main-` so they are easy to distinguish from development branches. + +Commit messages must use the format `[module] kind : summary`, where `module` identifies the affected area, `kind` is a tag such as `fix`, `feat`, `docs`, or `chore`, and `summary` starts with a lowercase word unless it begins with a proper noun. For example: `[parser] feat : add call clause`. + When writing text such as documentation, comments, or commit messages, wrap literal names from ClickHouse SQL language, classes and functions, or literal excerpts from log messages inside inline code blocks, such as: `MergeTree`. When writing text such as documentation, comments, or commit messages, write names of functions and methods as `f` instead of `f()` - we prefer it for mathematical purity when it refers a function itself rather than its application. diff --git a/.claude/learnings.md b/.claude/learnings.md index aade0235ae36..f24dcc7220ee 100644 --- a/.claude/learnings.md +++ b/.claude/learnings.md @@ -37,3 +37,4 @@ - If `graph` grammar generation must stay on `ANTLR` `4.13.2`, prefer downloading the exact complete jar into `/usr/local/lib` instead of relying on Ubuntu's older `antlr4` package. - For `ClickHouse` devcontainers on macOS, keep the source tree bind-mounted but move the build directory onto a Docker named volume or another container-local path to avoid Docker Desktop bind-mount I/O bottlenecks. - On macOS devcontainers, the cleanest compromise is often mounting the Docker volume directly at `/workspace/ClickHouse/build`, so editors keep the familiar path while build I/O stays off the host bind mount. +- GitHub rulesets can require PRs and block direct pushes on target branches, but they do not natively express "base branch X only accepts head branches matching Y"; enforce that part with a required `pull_request` workflow. diff --git a/.github/scripts/validate_branch_policy.py b/.github/scripts/validate_branch_policy.py new file mode 100644 index 000000000000..4a88b0c046d0 --- /dev/null +++ b/.github/scripts/validate_branch_policy.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 + +import os +import re +import sys + + +BRANCH_PATTERN = re.compile(r"^(?P[^/]+)/(?Pdev|main)-(?P[A-Za-z0-9._-]+)$") + + +def fail(message: str) -> int: + print(message, file=sys.stderr) + return 1 + + +def parse_branch(name: str): + match = BRANCH_PATTERN.fullmatch(name) + return match.groupdict() if match else None + + +def main() -> int: + base = os.environ.get("BASE_REF", "").strip() + head = os.environ.get("HEAD_REF", "").strip() + + if not base or not head: + return fail("Missing `BASE_REF` or `HEAD_REF` environment variable.") + + if base == "master": + head_branch = parse_branch(head) + if not head_branch or head_branch["kind"] != "main": + return fail( + f"`master` only accepts pull requests from `/main-*` branches. " + f"Received head branch `{head}`." + ) + + print(f"`master` <- `{head}` is allowed.") + return 0 + + base_branch = parse_branch(base) + if not base_branch or base_branch["kind"] != "main": + print(f"No branch policy applies to base branch `{base}`.") + return 0 + + head_branch = parse_branch(head) + if not head_branch or head_branch["kind"] != "dev": + return fail( + f"`{base}` only accepts pull requests from `{base_branch['module']}/dev-*` branches. " + f"Received head branch `{head}`." + ) + + if head_branch["module"] != base_branch["module"]: + return fail( + f"`{base}` only accepts pull requests from the same module namespace. " + f"Expected `{base_branch['module']}/dev-*`, received `{head}`." + ) + + print(f"`{base}` <- `{head}` is allowed.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/branch_policy.yml b/.github/workflows/branch_policy.yml new file mode 100644 index 000000000000..01c173aac40e --- /dev/null +++ b/.github/workflows/branch_policy.yml @@ -0,0 +1,27 @@ +name: branch policy + +on: + pull_request: + types: + - opened + - reopened + - synchronize + - edited + - ready_for_review + branches: + - master + - "*/main-*" + +jobs: + validate: + name: validate + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Validate source and target branch policy + env: + BASE_REF: ${{ github.base_ref }} + HEAD_REF: ${{ github.head_ref }} + run: python3 .github/scripts/validate_branch_policy.py diff --git a/docs/development/github_branch_policy.md b/docs/development/github_branch_policy.md new file mode 100644 index 000000000000..05a83b4b2e1d --- /dev/null +++ b/docs/development/github_branch_policy.md @@ -0,0 +1,53 @@ +# GitHub Branch Policy + +This repository uses two layers of enforcement for branch flow: + +1. GitHub rulesets protect the target branches. +2. The `branch policy / validate` workflow checks whether the pull request source branch is allowed for the target branch. + +## Branch naming + +- Development branches: `/dev-` +- Module main branches: `/main-` +- Default branch: `master` + +Examples: + +- `parser/dev-gql-query-root-followup` +- `parser/main-gql-refactor` + +## Allowed pull request flow + +- `master` only accepts pull requests from `/main-*` +- `/main-*` only accepts pull requests from the same module's `/dev-*` + +Examples: + +- `parser/dev-gql-query-root-followup` -> `parser/main-gql-refactor` +- `parser/main-gql-refactor` -> `master` + +## Required GitHub rulesets + +GitHub rulesets can require pull requests and block direct pushes on protected targets, but the head-branch restriction is enforced by the workflow because GitHub does not provide a native rule for "base branch X only accepts head branches matching Y". + +After merging this file and the workflow, configure these repository rulesets in GitHub: + +1. Ruleset for `master` + - Target branches: `master` + - Require a pull request before merging + - Restrict updates + - Require status checks to pass before merging + - Required check: `branch policy / validate` + +2. Ruleset for module main branches + - Target branches: `*/main-*` + - Require a pull request before merging + - Restrict updates + - Require status checks to pass before merging + - Required check: `branch policy / validate` + +Optional hardening: + +- Apply rules to administrators +- Require at least one approval before merging +- Require linear history if you want to avoid merge commits on protected branches