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
2 changes: 2 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<!--
Thanks for contributing! See CONTRIBUTING.md and GOVERNANCE.md.
A substantive PR needs a backing accepted issue or accepted RFC.
Title: `type(scope)!: lowercase description` (types and examples in
CONTRIBUTING.md, Pull Requests); the `PR Title` check refuses anything else.
-->

## What & why
Expand Down
5 changes: 3 additions & 2 deletions .github/branch-protection.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
{
"_comment": "Branch protection policy for main. Applied by an administrator via scripts/apply-branch-protection.sh; CI never mutates it. See docs/dev/branch-protection.md for rationale. Four required contexts, each a name that does not change when CI changes: CI Gate is the ci.yml job `ci_gate`, which needs every ci.yml job that blocks main and runs scripts/ci_gate.py over their results (a skip is accepted only where the job's own `if:` was false; failure and cancellation are always red), so which jobs block main is the `needs:` list in ci.yml and the table in that script, changed in a reviewed pull request and never on this rule. Check Workflow Action Pins is required on its own, not only through the gate: it runs scripts/check-merge-group-triggers.py, which pins the gate's wiring and holds its `needs:` equal to the script's table, and a malformed gate that reported a skip could otherwise hide that validator's red. GQ Logic Tests and Fix Regression Gate are their own workflows and keep their own names. .github/CODEOWNERS is advisory, require_code_owner_reviews stays false and no approvals are required, so the four contexts are the gate. The format fence, the RustFS S3 suite and the Azurite suite run on pull requests as reporting contexts and again after merge. A red main branch is stop-the-line until fixed or reverted. Every merge into main goes through the merge queue with the squash method; the branch-protection endpoint this file drives has no field for the queue, so it is set on the rule page and checked there after every apply. Each required context's workflow also runs on merge_group (scripts/check-merge-group-triggers.py enforces it); Fix Regression Gate's merge-group run reports a pass without a check. No administrator bypass: enforce_admins is true.",
"_comment": "Branch protection policy for main. Applied by an administrator via scripts/apply-branch-protection.sh; CI never mutates it. See docs/dev/branch-protection.md for rationale. Five required contexts, each a name that does not change when CI changes: CI Gate is the ci.yml job `ci_gate`, which needs every ci.yml job that blocks main and runs scripts/ci_gate.py over their results (a skip is accepted only where the job's own `if:` was false; failure and cancellation are always red), so which jobs block main is the `needs:` list in ci.yml and the table in that script, changed in a reviewed pull request and never on this rule. Check Workflow Action Pins is required on its own, not only through the gate: it runs scripts/check-merge-group-triggers.py, which pins the gate's wiring and holds its `needs:` equal to the script's table, and a malformed gate that reported a skip could otherwise hide that validator's red. GQ Logic Tests, Fix Regression Gate and PR Title are their own workflows and keep their own names. .github/CODEOWNERS is advisory, require_code_owner_reviews stays false and no approvals are required, so the five contexts are the gate. The format fence, the RustFS S3 suite and the Azurite suite run on pull requests as reporting contexts and again after merge. A red main branch is stop-the-line until fixed or reverted. Every merge into main goes through the merge queue with the squash method; the branch-protection endpoint this file drives has no field for the queue, so it is set on the rule page and checked there after every apply. Each required context's workflow also runs on merge_group (scripts/check-merge-group-triggers.py enforces it); Fix Regression Gate's merge-group run reports a pass without a check, and PR Title's merge-group run checks the squash subject the queue is about to land. PR Title holds every title to type(scope)!: description (scripts/check-pr-title.py; CONTRIBUTING.md, Pull Requests), so the squash subject on main names the kind of change. No administrator bypass: enforce_admins is true.",
"required_status_checks": {
"strict": true,
"contexts": [
"CI Gate",
"Check Workflow Action Pins",
"GQ Logic Tests",
"Fix Regression Gate"
"Fix Regression Gate",
"PR Title"
]
},
"enforce_admins": true,
Expand Down
58 changes: 58 additions & 0 deletions .github/workflows/pr-title.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: PR Title

# Every merge into `main` is a squash whose subject is the pull request
# title, so the title is the line `git log` keeps. This is a policy check on
# the pull request, so it must run code the pull request cannot edit:
# `pull_request_target` takes this workflow and `scripts/check-pr-title.py`
# from the base branch, and the head is never fetched or checked out. The
# `edited` event re-runs the check when the title changes. On the merge
# queue the group commit's message already carries the squash subject the
# queue will land, so the merge-group run checks that subject with the same
# script; a title edit after queueing does not change the captured subject
# (see docs/dev/branch-protection.md, Merge queue).
on:
pull_request_target:
types:
- opened
- synchronize
- reopened
- edited
merge_group:

concurrency:
# A merge-group run has no pull request number; its ref is unique per entry.
group: pr-title-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
pr_title:
name: PR Title
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
steps:
# The base branch on a pull request; the queued commit on the merge group.
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1

- name: Self-test the checker
run: python3 scripts/check-pr-title.py --self-test

- name: Check the pull request title
if: github.event_name == 'pull_request_target'
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: python3 scripts/check-pr-title.py --title "$PR_TITLE"

- name: Check the subject the merge queue lands
if: github.event_name == 'merge_group'
env:
HEAD_COMMIT_MESSAGE: ${{ github.event.merge_group.head_commit.message }}
run: python3 scripts/check-pr-title.py --squash-subject "$HEAD_COMMIT_MESSAGE"

- name: Refuse an event no step handles
if: github.event_name != 'pull_request_target' && github.event_name != 'merge_group'
run: |
echo "::error::PR Title ran on ${GITHUB_EVENT_NAME}, which no step handles"
exit 1
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,10 @@ Set `OMNIGRAPH_UPDATE_OPENAPI=1` only when the drift is intentional.
- Add a permanent `changelog.d/<slug>.<category>.md` note for user-visible changes;
follow [release-note authoring](docs/dev/documentation.md#release-notes).
Keep private tickets and planning shorthand out of public history.
- Title a pull request `type(scope)!: description` and read the title back
after opening it; the `PR Title` check refuses every other shape, and the
squash subject on `main` is that title
([CONTRIBUTING.md](CONTRIBUTING.md), Pull Requests).
- Recheck exact flags, environment variables, routes, and constants in source
before documenting them.
- Keep this file a map. New deep content goes in its audience-owned guide.
21 changes: 21 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,27 @@ CI runs both.

## Pull Requests

- **Title the PR `type(scope)!: description`** ([Conventional
Commits](https://www.conventionalcommits.org/en/v1.0.0/)). Every merge is a
squash whose subject is the title, so the title is the line `git log` keeps.
The `PR Title` check refuses anything else and re-runs when you edit the
title:
- `type` is one of `feat`, `fix`, `perf`, `refactor`, `docs`, `test`, `ci`,
`build`, `revert`, `rfc`, `release`; there is no `chore`, say what kind
of change it is.
- `(scope)` is optional: lowercase names, commas between several
(`refactor(blob,schema)!: …`).
- `!` marks a breaking change; the change also carries a
`changelog.d/<slug>.breaking.md` note.
- the description starts lowercase (a backtick or a digit is fine), has no
trailing period, and the whole title stays within 100 characters.
- an RFC PR is `rfc: <the RFC's subject>`, not `docs(rfc):`; RFC numbers
stay out of titles (RFCs are named by date and slug). A revert is
`revert: <what is undone>`; retitle GitHub's `Revert "…"` default.
- examples: `feat(server): return exact merge publication receipts`,
`fix(merge): decide fast_forward by the target's state`,
`test(gq): cover inherited indexes after fast-forward`,
`docs: fix a broken link in the upgrade guide`.
- **Link the backing `accepted` issue or accepted RFC** (`Closes #123`, or
reference the RFC) — or mark the PR as trivial per the fast-lane.
- Keep changes focused; one logical change per PR.
Expand Down
32 changes: 25 additions & 7 deletions docs/dev/branch-protection.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ protection.

## Current policy

`main` requires these four PR status contexts, names that do not change when
`main` requires these five PR status contexts, names that do not change when
CI changes:

- `CI Gate`
- `Check Workflow Action Pins`
- `GQ Logic Tests`
- `Fix Regression Gate`
- `PR Title`

Checks are strict; under the merge queue the entry is built on the current
`main`, and GitHub does not require the author to update the pull request
Expand Down Expand Up @@ -126,8 +127,8 @@ temporary `gh-readonly-queue/main/…` branch from the current `main` plus the
entries ahead of it, fires the `merge_group` event there, and merges only when
every required context has reported green on that branch. A required context
that never reports on the merge-group branch holds the entry until the queue's
status-check timeout removes it, so `ci.yml`, `gq-logic-tests.yml`, and
`fix-regression-gate.yml` list `merge_group` under `on:`, the `Format
status-check timeout removes it, so `ci.yml`, `gq-logic-tests.yml`,
`fix-regression-gate.yml`, and `pr-title.yml` list `merge_group` under `on:`, the `Format
(rustfmt)` and `Lint (clippy)` job conditions accept the event beside
`pull_request`, and `scripts/check-merge-group-triggers.py`, a step of `Check
Workflow Action Pins`, fails the build when a workflow owning a required
Expand Down Expand Up @@ -160,6 +161,23 @@ stays open: a body or label edit after queueing re-runs the gate on the pull
request only; whether a red result there removes the entry is not documented
by GitHub, and the red context stays visible on the pull request either way.

`PR Title` (`pr-title.yml`) holds the pull request title to
`type(scope)!: description` ([CONTRIBUTING.md](../../CONTRIBUTING.md), Pull
Requests; `scripts/check-pr-title.py` is the check). Every merge is a squash
whose subject is the title, so the title is the one line `git log` keeps for
the change. On the pull request it runs on `pull_request_target`, so the
checker comes from `main`, and a title edit (`edited`) re-runs it. On the
merge-group run the group commit's message carries the squash subject the
queue will land (`merge_group.head_commit.message`, first line, with the
` (#N)` suffix GitHub appends), so the same checker runs on that subject.
A title edited after queueing does not change that captured subject: the
edit re-runs only the pull-request check, and whether a red result there
removes the entry is undocumented, as for the regression gate above. That
the landed commit's subject is the captured one is inferred from the
payload, not documented; one observed queue run after rollout confirms it.
This merge-group run executes the queued commit's copy of the checker, like
every merge-group run; the policy copy ran on the pull request.

Landing a pull request: click **Merge when ready** once the reporting
contexts have reported; GitHub queues the pull request when its required
contexts are green. A queued entry whose merge-group checks fail, or that hits
Expand All @@ -183,7 +201,7 @@ the post-merge runs (`Test Workspace`, the fence, the shards, Azurite, the
vocabulary audit, the edge release) cover the whole group at once, and a
revert picks the entry's own squash commit out of that push.

Changing one of the four required context names still needs the rule page:
Changing one of the five required context names still needs the rule page:
the old name never reports again, so loosen the live rule first, land the
change (the JSON edit rides in the same pull request as the workflow edit;
the validator refuses the pair split), then re-apply the JSON; other pull
Expand Down Expand Up @@ -212,8 +230,8 @@ If the gate or the validator cannot execute at all (the checkout action or
the runners are down), no `needs:` edit helps, and the settings-based route
remains: an administrator unticks the context that cannot report on the rule
page, lands the fix, and re-applies the JSON. Unticking `CI Gate` relaxes
every job inside it at once and leaves the other three contexts required;
hold unrelated merges while it is unticked, and verify all four names and
every job inside it at once and leaves the other four contexts required;
hold unrelated merges while it is unticked, and verify all five names and
the queue values on the page afterwards. Unticking the queue alone changes
nothing, because the status requirement binds with or without the queue.
Untick the queue only for a queue-specific failure while the pull request's
Expand Down Expand Up @@ -256,7 +274,7 @@ The script removes the JSON's explanatory `_comment` and updates
`repos/ModernRelay/omnigraph/branches/main/protection`. It requires an
authenticated `gh` token with repository-administration permission.

To change the policy (the four names or any other setting the JSON carries;
To change the policy (the five names or any other setting the JSON carries;
which jobs block `main` is changed in `ci.yml` instead, see CI Gate):

1. Update `.github/branch-protection.json` in a PR.
Expand Down
34 changes: 28 additions & 6 deletions docs/dev/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ puts each changed path in one class:

| Class | Paths | Jobs that run |
|---|---|---|
| documentation | `docs/**/*.md` (`.mdx`, `.rst`, `.adoc`), `changelog.d/*.md`, `changelog.d/release.json`, the root `README.md`, `AGENTS.md`, `CLAUDE.md`, `CHANGELOG.md`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `LICENSE`, `LICENSE.md` | the always-on guards (`Classify Changes`, `Check AGENTS.md Links`, `Check Workflow Action Pins`, `Fix Regression Gate`, `Storage Upgrade Compatibility`, `Dependency Guard (cargo deny)`; of these only `Check AGENTS.md Links` reads documentation, through `scripts/check-docs.py`) |
| documentation | `docs/**/*.md` (`.mdx`, `.rst`, `.adoc`), `changelog.d/*.md`, `changelog.d/release.json`, the root `README.md`, `AGENTS.md`, `CLAUDE.md`, `CHANGELOG.md`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `LICENSE`, `LICENSE.md` | the always-on guards (`Classify Changes`, `Check AGENTS.md Links`, `Check Workflow Action Pins`, `Fix Regression Gate`, `PR Title`, `Storage Upgrade Compatibility`, `Dependency Guard (cargo deny)`; of these only `Check AGENTS.md Links` reads documentation, through `scripts/check-docs.py`) |
| GQT cases | `.gqt` files anywhere under `crates/omnigraph-gqt/cases/` (recursive discovery) | the guards plus `GQ Logic Tests` (`run_gqt`) |
| deployment | `Dockerfile`, `.dockerignore`, `docker/**`, `deploy/**` | the guards plus `Azure Contract Guards`, `Container Entrypoint`, `Azure Deployment Validation` (`run_deployment`) |
| engine input | every other path: `crates/**` (a text fixture under a crate is source code; only the `.gqt` corpus is a class of its own), `tools/**`, `Cargo.toml`, `Cargo.lock`, `rust-toolchain.toml`, `.cargo/**`, `scripts/**`, `.github/**`, anything unlisted | every job (`run_full_ci`, which also sets `run_gqt` and `run_deployment`) |
Expand Down Expand Up @@ -79,16 +79,17 @@ Merge queue entries trigger `ci.yml`, `gq-logic-tests.yml`, and `dst.yml`
through `merge_group` (`checks_requested`). These runs check out the combined
queue commit; the classifier diffs the group's base and head, so a
documentation-only entry skips the same work it skips on a pull request.
Workspace tests fail fast as they do on pull requests. The PR metadata gate
stays on `pull_request_target` and reports a pass on the queue; the
Workspace tests fail fast as they do on pull requests. The PR metadata gates
stay on `pull_request_target`; on the queue `Fix Regression Gate` reports a
pass and `PR Title` checks the squash subject the entry is about to land; the
vocabulary audit, Azurite, the format fence, the RustFS shards and the
deployment jobs keep their pull-request or post-merge schedule and never run
on the queue; the DST pinned suite runs there as a reporting context. Queue
runs never publish releases or save the main branch's caches. Details:
[branch-protection.md](branch-protection.md), Merge queue.

Branch protection requires four contexts: `CI Gate`, `Check Workflow Action
Pins`, `GQ Logic Tests` and `Fix Regression Gate`. `CI Gate` is the `ci_gate`
Branch protection requires five contexts: `CI Gate`, `Check Workflow Action
Pins`, `GQ Logic Tests`, `Fix Regression Gate` and `PR Title`. `CI Gate` is the `ci_gate`
job of `ci.yml`: it needs `classify_changes`, `check_agents_md`,
`workflow_action_pins`, `cargo_deny`, `graph_vocabulary_guard`, `fmt`,
`lint`, `test`, `storage_upgrade_compatibility` and `test_aws_feature`, runs
Expand All @@ -99,7 +100,9 @@ restates it. Those ten jobs are what blocks `main`; a new blocking job joins
`needs:` and the table in the same pull request, and
`scripts/check-merge-group-triggers.py` holds the two equal and pins the
gate's wiring. `Check Workflow Action Pins` is required on its own so a
malformed gate cannot hide that validator's red. Details and the recovery
malformed gate cannot hide that validator's red. `GQ Logic Tests`, `Fix
Regression Gate` and `PR Title` are their own workflows and keep their own
names. Details and the recovery
routes: [branch-protection.md](branch-protection.md), CI Gate.

`Storage Upgrade Compatibility` (`storage_upgrade_compatibility` in `ci.yml`)
Expand Down Expand Up @@ -217,6 +220,23 @@ only as data for the diff range, never checked out or executed. It runs on
body edits and label changes as well as pushes, builds nothing, and runs for
every change class. On the merge queue's branch it reports a pass
without a check ([branch-protection.md](branch-protection.md), Merge queue).
`PR Title` (`pr-title.yml`) holds the pull request title to
`type(scope)!: description`: a type from `feat fix perf refactor docs test ci
build revert rfc release`, an optional lowercase scope list, `!` for a
breaking change, and a description that starts lowercase (a backtick or digit
also passes), carries no trailing period, and keeps the title within 100
characters with no whitespace but the plain space; `docs(rfc):` is refused in favour of `rfc:`, and GitHub's
`Revert "…"` title in favour of `revert: …` (the rule and its examples:
[CONTRIBUTING.md](../../CONTRIBUTING.md), Pull Requests).
`scripts/check-pr-title.py` is the check; it runs its `--self-test` first and
prints one actionable failure line with the accepted shape and two examples,
as a log line and as a GitHub error annotation. It is a policy check on
`pull_request_target` like the regression gate, re-run on every title edit,
and the pull request head is never fetched. On the merge queue's branch it
checks the first line of the group commit's message, minus the ` (#N)`
suffix: the subject the queue lands, fixed when the group is built, so a
title edit after queueing cannot change what lands unchecked
([branch-protection.md](branch-protection.md), Merge queue).

The `Check AGENTS.md Links` context also runs `scripts/check-docs.py`, which
validates local documentation links, user/developer audience boundaries, RFC
Expand Down Expand Up @@ -457,6 +477,8 @@ python3 scripts/check-workflow-action-pins.py
python3 scripts/check-storage-upgrade-ci.py --self-test
python3 scripts/check-merge-group-triggers.py --self-test
python3 scripts/ci_gate.py --self-test
python3 scripts/check-pr-title.py --self-test
python3 scripts/check-pr-title.py --title "<the title you will give the pull request>"
python3 scripts/check-ci-cells.py --self-test
python3 scripts/check-release-vocabulary-gates.py
python3 scripts/check-container-binary-contract.py
Expand Down
Loading
Loading