Skip to content
Open
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
40 changes: 40 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
## Linear issue
<!-- REQUIRED. Link the issue: an issue key like V2-123, or a linear.app URL.
CI blocks PRs with no linked Linear issue. -->

## Risk tier
<!-- Check exactly one. Boundary question: does this change node behavior, the wire
protocol, stored-data format, payments/economics, or the upgrade mechanism?
If yes -> T2/T3, and it rides the weekly release train.
If no -> T0/T1, and it may ship via the client track.
Propose the tier; a human confirms it at review. -->
- [ ] T0 — docs / tooling / CI / pure UX-output. Repo CI only.
- [ ] T1 — client-only, no network-facing behavior change. CI + prod compat smoke.
- [ ] T2 — node/client logic with behavioral surface, no protocol/format/economics change. Dev testnet + ADR.
- [ ] T3 — protocol / storage format / payments / routing. T2 evidence + adversarial testing.

## Compatibility
<!-- State the impact on each axis, or "none". -->
- Wire:
- Storage:
- API:

## Semver impact
<!-- Check exactly one. This is where the crate's bump level is decided, while the
context is fresh; the train-manifest skill maps it to a per-crate version bump. -->
- [ ] breaking
- [ ] feature
- [ ] fix

## Test evidence
<!-- Per the tier: what was run, at what scale, and the result. Attach or link artifacts. -->

## New dependency
<!-- Any new external dependency? Write "none", or list them. New deps need explicit
human acknowledgement in review. -->

## ADR
<!-- REQUIRED for Tier 2/3 — link the ADR. Write "n/a" for Tier 0/1. -->

## Mitigation / rollback
<!-- One line: how we back this out or limit the blast radius if it misbehaves. -->
184 changes: 184 additions & 0 deletions .github/scripts/check_pr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
#!/usr/bin/env python3
"""Release-process PR checks.

Two modes, each wired to its own required status check:

check_pr.py linear V2-720 — require a linked Linear issue on every PR
(main + rc-*).
check_pr.py template V2-719 — require the standard PR template to be present
and fully filled (enforced on PRs targeting main; a
no-op that passes on rc-* so hotfix/regression PRs are
not forced to carry the full template).

PR fields are read from the environment (set by the workflow from the event
payload): PR_TITLE, PR_BODY, PR_BRANCH, PR_BASE.

This file is duplicated verbatim across the six crate repos; keep them
identical when updating.
"""
import os
import re
import sys

# A Linear issue key (e.g. V2-123, ENG-45) or a linear.app URL. The key pattern
# is deliberately broad and case-insensitive so it also matches Linear-generated
# branch names (which are lower-cased, e.g. chrisoneil/v2-720-...). It can match
# the odd technical token like "UTF-8"; that permeability is acceptable for a
# gate backed by human review and the train-manifest verification, and it keeps
# false negatives (blocking a legitimate PR) rare.
LINEAR_KEY = re.compile(r"\b[A-Za-z][A-Za-z0-9]*-[0-9]+\b")
LINEAR_URL = re.compile(r"linear\.app/", re.IGNORECASE)


def env(name):
return os.environ.get(name, "") or ""


def fail(msg):
print(msg)
sys.exit(1)


def ok(msg):
print(msg)
sys.exit(0)


def strip_comments(text):
"""Remove HTML comments so template scaffolding does not count as content."""
return re.sub(r"<!--.*?-->", "", text, flags=re.DOTALL)


def has_linear_ref(*parts):
haystack = "\n".join(parts)
m = LINEAR_URL.search(haystack) or LINEAR_KEY.search(haystack)
return m.group(0) if m else None
Comment on lines +52 to +55


def sections(body):
"""Split a markdown body into {heading-lowercased: text-until-next-##}."""
result, current, buf = {}, None, []
for line in body.splitlines():
m = re.match(r"^\s*##\s+(.*?)\s*$", line)
if m:
if current is not None:
result[current] = "\n".join(buf)
current, buf = m.group(1).strip().lower(), []
elif current is not None:
buf.append(line)
if current is not None:
result[current] = "\n".join(buf)
return result


def check_linear():
ref = has_linear_ref(env("PR_TITLE"), env("PR_BODY"), env("PR_BRANCH"))
if ref:
ok(f"✅ Linear reference found: {ref}")
fail(
"❌ No linked Linear issue found.\n\n"
"Every PR must reference a Linear issue — an issue key like V2-123 in\n"
"the PR title, body, or branch name, or a linear.app link in the body.\n\n"
"Add it to the '## Linear issue' section of the PR description and update\n"
"the pull request."
)


def check_template():
base = env("PR_BASE")
if base and base != "main":
ok(f"✅ pr-template not enforced on base '{base}' (main only).")

body = env("PR_BODY")
secs = sections(body)
errors = []

# The Risk tier / Semver impact headings are the sentinels that the template
# is actually in use.
if "risk tier" not in secs or "semver impact" not in secs:
fail(
"❌ PR template not detected.\n\n"
"Your PR description must use .github/PULL_REQUEST_TEMPLATE.md (the\n"
"'## Risk tier' and '## Semver impact' sections are missing). Copy the\n"
"template into the PR body and fill every field."
)

for heading in (
"linear issue",
"risk tier",
"compatibility",
"semver impact",
"test evidence",
"new dependency",
"adr",
"mitigation / rollback",
):
if heading not in secs:
errors.append(f"missing section: ## {heading.title()}")

Comment on lines +106 to +118
# Exactly one Risk tier box checked.
tiers = re.findall(
r"^\s*-\s*\[[xX]\]\s*(T[0-3])\b", secs.get("risk tier", ""), re.MULTILINE
)
if len(tiers) != 1:
errors.append(
f"Risk tier: check exactly one box (found {len(tiers)} checked)"
)
tier = tiers[0] if len(tiers) == 1 else None

# Exactly one Semver impact box checked.
semver = re.findall(
r"^\s*-\s*\[[xX]\]\s*(breaking|feature|fix)\b",
secs.get("semver impact", ""),
re.MULTILINE | re.IGNORECASE,
)
if len(semver) != 1:
errors.append(
f"Semver impact: check exactly one box (found {len(semver)} checked)"
)

# Free-text sections that must not be empty.
for heading in ("test evidence", "new dependency", "mitigation / rollback"):
if heading in secs and not strip_comments(secs[heading]).strip():
errors.append(f"'## {heading.title()}' is empty")

# Compatibility: at least one axis filled (a value after the colon).
comp = strip_comments(secs.get("compatibility", ""))
if not re.search(r"^\s*-\s*(Wire|Storage|API)\s*:\s*\S", comp, re.MULTILINE):
errors.append(
"'## Compatibility': fill at least one of Wire/Storage/API (use 'none')"
)

# Linear reference present in its own section.
if "linear issue" in secs and not has_linear_ref(
strip_comments(secs["linear issue"])
):
errors.append("'## Linear issue': add the issue key or a linear.app link")

# ADR required for Tier 2/3.
if tier in ("T2", "T3"):
adr = strip_comments(secs.get("adr", ""))
if not re.search(r"https?://", adr):
errors.append(f"ADR is required for {tier}: add an ADR link in '## ADR'")

if errors:
fail(
"❌ PR template incomplete:\n"
+ "\n".join(f" - {e}" for e in errors)
+ "\n\nFill in .github/PULL_REQUEST_TEMPLATE.md completely and update the PR."
)
ok(f"✅ PR template complete (tier {tier}).")


def main():
mode = sys.argv[1] if len(sys.argv) > 1 else ""
if mode == "linear":
check_linear()
elif mode == "template":
check_template()
else:
fail(f"usage: check_pr.py [linear|template] (got: {mode!r})")


if __name__ == "__main__":
main()
42 changes: 42 additions & 0 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Release-process PR checks (Linear link + PR template).
# Issues: V2-720 (linear-link) and V2-719 (pr-template).
# This file is intentionally duplicated verbatim across the six crate repos;
# keep them identical when updating.
name: pr-checks

on:
pull_request:
types: [opened, edited, synchronize, reopened]
branches:
- main
- 'rc-*'
Comment on lines +8 to +12

permissions:
contents: read

jobs:
linear-link:
name: linear-link
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Require a linked Linear issue
Comment on lines +21 to +23
env:
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
PR_BRANCH: ${{ github.event.pull_request.head.ref }}
PR_BASE: ${{ github.base_ref }}
run: python3 .github/scripts/check_pr.py linear

pr-template:
name: pr-template
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Require the PR template fields
env:
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
PR_BRANCH: ${{ github.event.pull_request.head.ref }}
PR_BASE: ${{ github.base_ref }}
run: python3 .github/scripts/check_pr.py template
17 changes: 17 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,20 @@ on startup when no `--bootstrap` CLI argument is provided.
1. Verify you're targeting ports 10000-10999 only
2. Double-check service names contain "ant-node"
3. Never run broad `pkill` commands that could affect other services

## Pull requests: fill the template

When you open a pull request, you MUST use `.github/PULL_REQUEST_TEMPLATE.md` as
the PR body and fill it in completely — do not open a PR with an empty or ad-hoc
description.

- **Fill every field.** Leave nothing blank; if a value isn't determinable, ask
before opening the PR.
- **Link the Linear issue** — an issue key like `V2-123` or a `linear.app` URL.
CI blocks PRs with no linked Linear issue.
- **Check exactly one Risk tier box and exactly one Semver impact box.** Propose
them from the change; a human confirms them at review.
- An **ADR link is required for Tier 2/3**.

CI enforces the Linear link and the template fields (`linear-link` and
`pr-template` status checks); a PR that omits them fails its checks.
Loading