ci: harden contributor checks (#254)

* ci: harden contributor checks

* ci: pin setup-uv action release

* ci: split workflow checks

* docs: clarify required checks
This commit is contained in:
Elliot Chen
2026-06-06 10:47:16 +08:00
committed by GitHub
parent 3527ea3eb2
commit 873e7535fb
12 changed files with 529 additions and 225 deletions

39
.github/BRANCH_PROTECTION.md vendored Normal file
View File

@ -0,0 +1,39 @@
# Branch Protection Baseline
Use this as the admin checklist for `main` after the EverOS 1.0 history reset.
## Required Repository Rule
- Require pull requests before merging.
- Require two approving reviews for normal work.
- Require conversation resolution before merge.
- Block force pushes.
- Block branch deletion.
- Do not grant routine admin bypasses.
## Required Status Checks
Mark these checks as required before merge:
- `CI / lint`
- `CI / unit tests`
- `CI / integration tests`
- `CI / package build`
- `Docs / links`
- `Commit lint / commit messages`
## Optional Repository Checks
Do not require checks that are not emitted for every pull request. Treat these
as advisory unless GitHub shows they run on all normal PRs:
- `.github/dependabot.yml`
## Merge Policy
- Work on feature branches.
- Push branches normally; do not force-push shared branches.
- Merge through PRs after checks are green.
- Delete merged branches.
Temporary admin bypass should be reserved for repository recovery work only.

View File

@ -24,6 +24,7 @@
## Checklist
- [ ] I kept the change scoped to the relevant area.
- [ ] I am opening this from a separate branch, not pushing directly to `main`.
- [ ] I updated docs, examples, or setup notes when behavior changed.
- [ ] I added or updated tests when the change affects behavior.
- [ ] I did not commit secrets, `.env` files, dependency folders, or generated output.

View File

@ -2,7 +2,7 @@ name: CI
on:
push:
branches: [main, dev, master]
branches: [main]
pull_request:
# Cancel superseded runs on the same ref to save CI minutes.
@ -14,14 +14,14 @@ permissions:
contents: read
jobs:
ci:
name: lint + test + integration
lint:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v4
uses: astral-sh/setup-uv@v8.2.0
with:
enable-cache: true
cache-dependency-glob: uv.lock
@ -35,8 +35,62 @@ jobs:
- name: Lint (ruff + import-linter + datetime + openapi drift)
run: make lint
unit:
name: unit tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v8.2.0
with:
enable-cache: true
cache-dependency-glob: uv.lock
- name: Set up Python
run: uv python install 3.12
- name: Install dependencies (frozen)
run: make install-deps
- name: Unit tests
run: make test
integration:
name: integration tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v8.2.0
with:
enable-cache: true
cache-dependency-glob: uv.lock
- name: Set up Python
run: uv python install 3.12
- name: Install dependencies (frozen)
run: make install-deps
- name: Integration tests
run: make integration
package:
name: package build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v8.2.0
with:
enable-cache: true
cache-dependency-glob: uv.lock
- name: Set up Python
run: uv python install 3.12
- name: Build and smoke-test package
run: make package

28
.github/workflows/commits.yml vendored Normal file
View File

@ -0,0 +1,28 @@
name: Commit lint
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
concurrency:
group: commit-lint-${{ github.ref }}
cancel-in-progress: true
jobs:
messages:
name: commit messages
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Validate Conventional Commit subjects
env:
GITHUB_EVENT_BEFORE: ${{ github.event.before }}
GITHUB_PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: make check-commits

View File

@ -7,6 +7,7 @@ on:
- ".github/ISSUE_TEMPLATE/**"
- ".github/PULL_REQUEST_TEMPLATE.md"
- ".github/workflows/docs.yml"
- "scripts/check_docs.py"
push:
branches: [main]
paths:
@ -14,6 +15,7 @@ on:
- ".github/ISSUE_TEMPLATE/**"
- ".github/PULL_REQUEST_TEMPLATE.md"
- ".github/workflows/docs.yml"
- "scripts/check_docs.py"
permissions:
contents: read
@ -28,110 +30,5 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: Validate active relative Markdown links
run: |
python3 - <<'PY'
from pathlib import Path
import re
import sys
# Validate every Markdown file in the documentation surface. Globbing
# (rather than a hand-maintained list) means new docs are covered
# automatically and phantom links cannot slip in. Skip vendored trees.
skip_dirs = {".git", "node_modules", ".venv", ".uv-cache"}
files = sorted(
p
for p in Path(".").rglob("*.md")
if not any(part in skip_dirs for part in p.parts)
)
missing = []
for path in files:
if not path.exists():
continue
text = path.read_text()
active = re.sub(r"<!--.*?-->", "", text, flags=re.S)
for raw in re.findall(r"\[[^\]]*\]\(([^)]+)\)", active):
link = raw.split("#", 1)[0]
if not link or link.startswith(("http://", "https://", "mailto:")):
continue
target = (path.parent / link).resolve()
try:
target.relative_to(Path.cwd().resolve())
except ValueError:
missing.append((path, raw, "outside repository"))
continue
if not target.exists():
missing.append((path, raw, "missing"))
if missing:
for path, raw, reason in missing:
print(f"{path}: {raw} -> {reason}")
sys.exit(1)
print("Active relative Markdown links resolve.")
PY
- name: Validate use-case banner links
run: |
python3 - <<'PY'
from pathlib import Path
import re
import sys
files = {
Path("README.md"): "## Use Cases",
Path("use-cases/README.md"): "## Use Cases",
}
failures = []
warnings = []
primary_link_pattern = re.compile(
r"^\[(?:Code|Plugin|Live Demo|Learn more)\]\(([^)]+)\)",
flags=re.M,
)
for path, heading in files.items():
text = path.read_text()
start = text.find(heading)
if start == -1:
failures.append(f"{path}: missing {heading}")
continue
table_start = text.find("<table>", start)
table_end = text.find("</table>", table_start)
if table_start == -1 or table_end == -1:
failures.append(f"{path}: missing use-case table")
continue
table = text[table_start:table_end]
cells = re.findall(r"<td[^>]*>(.*?)</td>", table, flags=re.S)
for index, cell in enumerate(cells, start=1):
title_match = re.search(r"####\s+(.+)", cell)
title = title_match.group(1).strip() if title_match else f"use case {index}"
banner_match = re.search(r"\[!\[[^\]]*\]\([^)]+\)\]\(([^)]+)\)", cell)
primary_match = primary_link_pattern.search(cell)
if not banner_match and primary_match:
warnings.append(f"{path}: {title}: primary link has no linked banner")
elif banner_match and not primary_match:
failures.append(f"{path}: {title}: missing primary link")
elif banner_match and primary_match and banner_match.group(1) != primary_match.group(1):
failures.append(
f"{path}: {title}: banner link {banner_match.group(1)} "
f"does not match primary link {primary_match.group(1)}"
)
if warnings:
print("\n".join(f"warning: {warning}" for warning in warnings))
if failures:
print("\n".join(failures))
sys.exit(1)
print("Use-case banner links match primary links.")
PY
- name: Validate issue template YAML
run: |
ruby -e 'require "yaml"; Dir[".github/ISSUE_TEMPLATE/*.yml"].sort.each { |p| YAML.load_file(p); puts "YAML ok: #{p}" }'
- name: Validate Markdown docs
run: make docs-check