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
45 changes: 24 additions & 21 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,31 +1,34 @@
# Default: catch-all
* @garrett4wade
# Default: catch-all (lead + two active maintainers as backups).
# Last matching rule wins, so this is the fallback for any path
# not covered by a more specific rule below.
* @garrett4wade @sitabulaixizawaluduo @fishcrap

# Core package
/areal/api/ @garrett4wade
/areal/engine/ @rchardx
/areal/experimental/inference_service @nuzant
/areal/experimental/agent_service/ @CormickKneey
/areal/infra/ @garrett4wade
/areal/infra/scheduler/ray.py @HwVanICI
/areal/models/ @rchardx
/areal/trainer/ @garrett4wade
/areal/api/ @garrett4wade @rchardx @sitabulaixizawaluduo
/areal/engine/ @rchardx @nuzant @garrett4wade @geshi001
/areal/experimental/inference_service/ @nuzant @guozhihao-224 @TaoZex
/areal/experimental/agent_service/ @CormickKneey @nuzant
/areal/experimental/training_service/ @sitabulaixizawaluduo @garrett4wade
/areal/experimental/weight_update/ @TaoZex @garrett4wade @sitabulaixizawaluduo
/areal/infra/ @HwVanICI @garrett4wade @guozhihao-224
/areal/models/ @rchardx @nuzant @geshi001 @PrometheusComing
/areal/trainer/ @garrett4wade @rchardx @fishcrap

# Tests & Examples
/tests/ @garrett4wade @rchardx @nuzant
/examples/ @garrett4wade
/tests/ @garrett4wade @sitabulaixizawaluduo
/examples/ @zhenanf @HwVanICI @CormickKneey @PrometheusComing

# Documentation
/docs/ @garrett4wade
/docs/ @garrett4wade @nuzant

# CI/CD & infrastructure
/.github/ @garrett4wade @nuzant
/Dockerfile @garrett4wade @fishcrap
pyproject.toml @garrett4wade @fishcrap
pyproject.vllm.toml @garrett4wade @fishcrap
uv.lock @garrett4wade @fishcrap
uv.vllm.lock @garrett4wade @fishcrap
/.github/ @garrett4wade @nuzant @sitabulaixizawaluduo
/Dockerfile @garrett4wade @fishcrap @sitabulaixizawaluduo
pyproject.toml @garrett4wade @fishcrap @sitabulaixizawaluduo
pyproject.vllm.toml @garrett4wade @fishcrap @sitabulaixizawaluduo
uv.lock @garrett4wade @fishcrap @sitabulaixizawaluduo
uv.vllm.lock @garrett4wade @fishcrap @sitabulaixizawaluduo

# Governance & community
GOVERNANCE.md @garrett4wade
CONTRIBUTING.md @garrett4wade
GOVERNANCE.md @garrett4wade @sitabulaixizawaluduo
CONTRIBUTING.md @garrett4wade @sitabulaixizawaluduo
42 changes: 42 additions & 0 deletions .github/ruleset.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "main-protection",
"target": "branch",
"enforcement": "active",
"conditions": {
"ref_name": {
"include": ["~DEFAULT_BRANCH"],
"exclude": []
}
},
"bypass_actors": [
{
"actor_id": 5,
"actor_type": "RepositoryRole",
"bypass_mode": "pull_request"
}
],
"rules": [
{ "type": "deletion" },
{ "type": "non_fast_forward" },
{ "type": "required_linear_history" },
{
"type": "pull_request",
"parameters": {
"required_approving_review_count": 2,
"dismiss_stale_reviews_on_push": true,
"require_code_owner_review": true,
"require_last_push_approval": true,
"required_review_thread_resolution": true
}
},
{
"type": "required_status_checks",
"parameters": {
"strict_required_status_checks_policy": false,
"required_status_checks": [
{ "context": "pre-commit" }
]
}
}
]
}
12 changes: 12 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,18 @@ repos:
files: ^areal/.*\.py$
types: [python]

# Format and lint .github/CODEOWNERS
- repo: local
hooks:
- id: format-codeowners
name: Format CODEOWNERS
entry: python3 areal/tools/format_codeowners.py
language: system
files: ^\.github/CODEOWNERS$
pass_filenames: false
always_run: false
require_serial: true

# Conventional Commits message check
- repo: https://github.com/compilerla/conventional-pre-commit
rev: v4.4.0
Expand Down
25 changes: 18 additions & 7 deletions GOVERNANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ considered a contributor. All contributors are expected to follow the

### Maintainers

Maintainers have write access to the repository and are responsible for reviewing and
merging pull requests, triaging issues, and guiding the technical direction of the
project.
Maintainers have write access to the repository and are responsible for reviewing pull
requests, triaging issues, and guiding the technical direction of the project. Two
maintainer approvals are required to merge a pull request (see
[Decision-Making](#decision-making)).

| Name | Organization | GitHub |
| ------------ | ------------------------- | --------------------- |
Expand All @@ -36,7 +37,11 @@ project.

Wei Fu ([@garrett4wade](https://github.com/garrett4wade)) serves as the lead maintainer.
The lead maintainer has final authority on technical decisions when maintainers cannot
reach consensus.
reach consensus, and holds the repository **administrator** role on GitHub. As an
administrator, the lead maintainer may bypass the two-approval requirement to merge
trivial changes (typo fixes, documentation-only edits, dependency bumps verified by CI)
or time-sensitive hotfixes. Bypasses must still go through a pull request and should be
disclosed in the PR description.

### Community Moderators

Expand All @@ -51,9 +56,15 @@ cannot be reached, the lead maintainer makes the final decision.

Pull request approval policy:

- Bug fixes or minor improvements: approved by at least one maintainer.
- New features, architectural changes, or API modifications: approved by at least two
maintainers or the lead maintainer.
- All pull requests require approval from at least **two maintainers** before they can
be merged. At least one approval must come from a code owner of the modified paths
(see [`.github/CODEOWNERS`](.github/CODEOWNERS)).
- The **lead maintainer**, acting as repository administrator, may bypass the
two-approval requirement for trivial or time-sensitive changes as described in the
[Lead Maintainer](#lead-maintainer-bdfl) section.
- The branch protection rules on `main` are documented in
[`.github/ruleset.json`](.github/ruleset.json) and are the source of truth for
mechanical enforcement of this policy.

## Becoming a Maintainer

Expand Down
133 changes: 133 additions & 0 deletions areal/tools/format_codeowners.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0

"""Pre-commit hook that formats and lints .github/CODEOWNERS.

Behavior:
- Strips trailing whitespace and ensures a single trailing newline.
- Re-aligns the owners column so every rule line has the owners list starting
at the same column (computed from the longest path in the file).
- Preserves trailing inline comments (e.g. ``/path @owner @owner2 # note``)
verbatim, separated from the owners by two spaces.
- Validates that every owner token starts with '@' and contains only valid
GitHub username/team characters.
- Errors on duplicate path patterns.
- Warns (does not fail) on rules with fewer than two owners, since the
governance policy expects ownership to degrade gracefully when a single
owner is unavailable.

Following the repo's other local hooks, the script exits non-zero if it had
to rewrite the file so CI flags the diff and the developer commits the fix.
"""

from __future__ import annotations

import re
import sys
from pathlib import Path

CODEOWNERS_PATH = Path(".github/CODEOWNERS")
OWNER_RE = re.compile(
r"^@[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?(?:/[A-Za-z0-9._-]+)?$"
)
INLINE_COMMENT_RE = re.compile(r"\s#.*$")
MIN_COLUMN = 32


def format_codeowners(path: Path) -> int:
"""Return 0 if no changes needed, 1 if the file was rewritten, 2 on error."""
if not path.is_file():
print(f"ERROR: {path} not found", file=sys.stderr)
return 2

original = path.read_text(encoding="utf-8")
raw_lines = original.splitlines()

parsed: list[tuple[str, ...]] = []
longest_path = 0
errors: list[str] = []

for lineno, line in enumerate(raw_lines, 1):
stripped = line.rstrip()
if not stripped or stripped.lstrip().startswith("#"):
parsed.append(("raw", stripped))
continue

# Split off any trailing inline comment (whitespace then '#'); the
# comment is preserved verbatim and re-emitted after the owners.
m = INLINE_COMMENT_RE.search(stripped)
if m:
inline_comment = m.group(0).strip()
rule_part = stripped[: m.start()].rstrip()
else:
inline_comment = ""
rule_part = stripped

tokens = rule_part.split()
if len(tokens) < 2:
errors.append(f"line {lineno}: rule has no owners: {stripped!r}")
continue

path_pat, *owners = tokens
for owner in owners:
if not OWNER_RE.match(owner):
errors.append(f"line {lineno}: invalid owner token {owner!r}")

parsed.append(("rule", path_pat, tuple(owners), inline_comment, lineno))
longest_path = max(longest_path, len(path_pat))

if errors:
for err in errors:
print(f"ERROR: {err}", file=sys.stderr)
return 2

column = max(MIN_COLUMN, ((longest_path + 5) // 4) * 4)

seen_paths: dict[str, int] = {}
rendered: list[str] = []
duplicate_errors: list[str] = []
single_owner_warnings: list[str] = []

for entry in parsed:
if entry[0] == "raw":
rendered.append(entry[1])
continue

_, path_pat, owners, inline_comment, lineno = entry
if path_pat in seen_paths:
duplicate_errors.append(
f"line {lineno}: duplicate path pattern {path_pat!r} "
f"(first defined on line {seen_paths[path_pat]})"
)
continue
seen_paths[path_pat] = lineno

if len(owners) < 2:
single_owner_warnings.append(
f"line {lineno}: {path_pat} has only one owner ({owners[0]}); "
f"governance expects >=2 to avoid single points of failure"
)

rule_line = f"{path_pat.ljust(column)}{' '.join(owners)}"
if inline_comment:
rule_line = f"{rule_line} {inline_comment}"
rendered.append(rule_line)

if duplicate_errors:
for err in duplicate_errors:
print(f"ERROR: {err}", file=sys.stderr)
return 2

for warn in single_owner_warnings:
print(f"WARN: {warn}", file=sys.stderr)

new = "\n".join(rendered).rstrip("\n") + "\n"
if new != original:
path.write_text(new, encoding="utf-8")
print(f"Rewrote {path} (column {column})", file=sys.stderr)
return 1
return 0


if __name__ == "__main__":
sys.exit(format_codeowners(CODEOWNERS_PATH))
Loading