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
25 changes: 14 additions & 11 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -320,14 +320,17 @@ reviews:
mode: warning
issue_assessment:
mode: warning
# `pr-label-sync.yml` reads what the Breaking changes section says; these check whether
# what it says is true of the diff, and whether the PR's own QA covers every behaviour it
# changes. Both are advisory — nothing here blocks a merge.
# `pr-label-sync.yml` reads the checkbox in the Breaking changes section; these check whether
# what the author ticked is true of the diff, and whether the PR's own QA covers every behaviour
# it changes. Both are advisory — nothing here blocks a merge.
custom_checks:
- name: Breaking changes match the diff
mode: warning
instructions: |
Compare the PR description's "## Breaking changes" section against the actual diff.
Compare the checkbox in the PR description's "## Breaking changes" section against the
actual diff. A ticked box (`- [x] This PR contains breaking changes`) is the author's
declaration that the PR has one; an unticked box declares that it has none. The prose or
table under the checkbox is the explanation and does not change the declaration.

Treat a change as breaking if the diff does any of the following:
- Renames, removes, or retypes a field in an API request or response shape, or changes
Expand All @@ -341,15 +344,15 @@ reviews:
- Changes an exported signature of the public SDK surface in `packages/js-core` or
`packages/surveys`.

FAIL if the diff contains at least one of the above and the "Breaking changes" section
says "None", is empty, or still holds the unedited template comment. Name the specific
file and line that is breaking.
FAIL if the diff contains at least one of the above and the checkbox is unticked, or the
section is empty or still holds the unedited template comment. Name the specific file and
line that is breaking, and ask the author to tick the box.

FAIL if the section claims a breaking change that has no corresponding change in the diff.
FAIL if the checkbox is ticked but no change in the diff matches the list above.

PASS if the section describes, in a table or in prose, each breaking change present in
the diff — `.github/pull_request_template.md` accepts either shape — or if the diff
contains none of the above and the section says "None".
PASS if the checkbox is ticked and the section documents each breaking change present in
the diff, in a table or in prose, or if the checkbox is unticked and the diff contains
none of the above — however the author words the explanation underneath.

Return inconclusive rather than failing when the diff alone does not let you determine
an API response shape — for example when the shape comes from a serializer or type that
Expand Down
21 changes: 15 additions & 6 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,24 @@ to grasp without opening files. -->

## Breaking changes

<!-- REQUIRED. Leave "None" if nothing changes for API/SDK consumers or self-hosters. It is breaking
if it changes an API/SDK shape or emitted value, removes or renames an endpoint, route, env var or
config key, changes a default or webhook payload, or needs manual migration action. If YES, fill the
table below — it feeds the release notes and the self-hoster migration guide, and applies the
`breaking-change` label automatically. -->
<!-- REQUIRED. Tick the box below if — and only if — this PR breaks something for API/SDK consumers or
self-hosters. It is breaking if it changes an API/SDK shape or emitted value (e.g. `EN` → `en-US`),
removes or renames an endpoint, route, env var or config key, changes a default or webhook payload, or
needs manual migration action.

The checkbox alone drives the label: ticking it applies `breaking-change`, which feeds the release notes
and the self-hoster migration guide. Leave its wording alone — `pr-label-sync.yml` finds it by that text,
ignores every other checkbox in this section, and never reads the prose. What you write underneath is
still read: by reviewers, and by the CodeRabbit "Breaking changes match the diff" check, which compares
the tick against the diff and expects a ticked box to be explained. Ticked → replace "None" with the
table below, one row per change, written for an external integrator; not ticked → keep "None" and say in
a line why. -->

- [ ] This PR contains breaking changes

None

<!-- Delete "None" above and use this table when there IS a breaking change:
<!-- Delete "None" above and use this table when the box IS ticked:
| Change | Before | After | Who's affected | Action required |
| --- | --- | --- | --- | --- |
| `language` field on responses | `EN`, `DE` | `en-US`, `de-DE` | API v1 consumers | Map the new BCP-47 locale codes in your integration |
Expand Down
82 changes: 46 additions & 36 deletions .github/workflows/pr-label-sync.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
name: PR Label Sync

# Keeps PR labels in sync with declared sections in the PR body (see .github/pull_request_template.md).
# Keeps PR labels in sync with checkboxes in the PR body (see .github/pull_request_template.md).
# Generic engine: to drive a new label from a new "## <heading>" section, add one entry to RULES
# below — no other changes needed.
#
# Currently configured:
# - "Breaking changes" section (a filled table / any content other than "None") -> `breaking-change`
# - the checkbox in the "Breaking changes" section -> `breaking-change`
# Release notes and the self-hoster migration guide are compiled from the PRs carrying it.
#
# Uses pull_request_target because adding a label needs a write-scoped token (incl. fork PRs). This is
Expand Down Expand Up @@ -40,48 +40,54 @@ jobs:
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
// A section is "declared" when it holds real content — a filled table or prose.
// A section drives its label from a GitHub task-list checkbox:
//
// The template asks for a bare "None" when there is nothing to declare, but authors
// routinely add a clause: "None — docs only", "N/A", "No breaking changes". Accepting only
// the exact word labelled those PRs as breaking, and this label feeds the GitHub release
// notes and the self-hoster migration guide, so a false positive is expensive.
// - [x] This PR contains breaking changes
//
// A negative opener alone is not enough to clear the section, because "No breaking changes,
// but the response shape changed" declares a real one. So an aside after the opener counts
// as "nothing here" only when *every* word in it is a scope word — docs, tests, internal,
// only… One unrecognised word (endpoint, renamed, shape) makes the section declared.
// Ticked -> the label belongs on the PR. Unticked -> it does not. The prose around the
// checkbox is never read, and that is the whole point. The previous version inferred intent
// from the section text, so any explanation after "None" ("None — same public API, only
// out-of-workspace deletions are refused") read as a declaration and mislabelled the PR.
// Deciding is now the author's (or agent's) job, stated in one unambiguous bit; the text can
// say whatever it needs to.
//
// That asymmetry is deliberate. A missed breaking change silently omits a migration-guide
// entry that self-hosters need; a spurious label is caught by whoever reads the release
// notes. So anything the vocabulary does not recognise errs towards labelling, and an
// unusual way of writing "nothing here" costs one label removal by hand.
const NEGATIVE_OPENER = /^(?:none|n\/?a|not applicable|no breaking changes?)\b/i;
const SCOPE_WORD =
/^(?:a|and|is|the|this|pr|no|not|nothing|none|n\/?a|applicable|only|just|pure|purely|docs?|documentation|readme|comments?|tests?|ci|chore|refactor(?:ing)?|typos?|styling?|formatting|internal|product|code|changes?)$/i;
// The trade-off: a forgotten tick silently omits a migration-guide entry that self-hosters
// need, where the old heuristic erred towards a spurious label instead. So the template asks
// for a deliberate answer, and PR review is the backstop.
//
// No checkbox in the section at all -> leave the label alone rather than assume "not
// breaking", so a restructured section or a hand-applied label survives.
//
// Each rule matches its own checkbox by the label text the template ships, rather than
// taking the first checkbox in the section: an unrelated item above it ("- [x] I read the
// list above") would otherwise flip the label. That couples this workflow to the template's
// wording, so a section that holds checkboxes but not *this* one warns rather than passing
// silently — the drift is then visible in the run instead of freezing the label.
const CHECKBOX_LINE = /^[ \t]*[-*+][ \t]+\[([ xX])\][ \t]*(.*)$/gm;

const isDeclared = (content) => {
const normalized = content
.replace(/^\s*[-*+]\s+/, "") // a leading list marker ("- none")
.replace(/[*_`]/g, "") // markdown emphasis / code ticks ("**None**")
.trim();
if (normalized === "") return false;
const opener = NEGATIVE_OPENER.exec(normalized);
if (!opener) return true;
// Split on whitespace and the punctuation that introduces an aside, but not on "/", so
// an "N/A" inside the aside survives as one token.
const aside = normalized
.slice(opener[0].length)
.split(/[\s—–:,.;()\[\]-]+/)
.filter(Boolean);
if (aside.length === 0) return false; // "None", "None.", "N/A"
return !(aside.length <= 6 && aside.every((word) => SCOPE_WORD.test(word)));
const checkboxFor = (declaration) => (content) => {
const boxes = [...content.matchAll(CHECKBOX_LINE)];
const box = boxes.find(([, , text]) => declaration.test(text.replace(/[*_`]/g, "").trim()));
if (box) return box[1].toLowerCase() === "x";
if (boxes.length > 0) {
core.warning(
`Found ${boxes.length} checkbox(es) but none matching ${declaration} — the PR ` +
"template and pr-label-sync.yml have drifted. Label left unchanged."
);
}
return null; // no signal in this section
};

// Label rules. Add an entry to sync a new label from a new "## <heading>" PR-body section.
// `isActive(content)` decides whether the label should be present for the given section text.
// `isActive(content)` returns true/false to set the label, or null to leave it unchanged.
const RULES = [
{ heading: "Breaking changes", label: "breaking-change", isActive: isDeclared },
{
heading: "Breaking changes",
label: "breaking-change",
// Prefix, not exact: an author who appends a clause ("… (see the table)") still gets a
// working label, while "I read the list above" still does not match.
isActive: checkboxFor(/^this pr contains breaking changes/i),
},
];

const pr = context.payload.pull_request;
Expand All @@ -107,6 +113,10 @@ jobs:
continue;
}
const active = rule.isActive(content);
if (active === null) {
core.info(`[${rule.label}] no checkbox under "## ${rule.heading}"; leaving unchanged.`);
continue;
}
const has = current.has(rule.label);
if (active && !has) {
await github.rest.issues.addLabels({
Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ Commits follow a lightweight Conventional Commit format (`fix:`, `chore:`, `feat

Every PR must use `.github/pull_request_template.md` and follow its inline guidance — the template is the source of truth for PR structure. The ticket line at the top is the only place a magic word (`Fixes`, `Ref`, `Closes`) may sit next to a ticket id: Linear and GitHub scan the whole body, so the same pair written in prose — inside backticks too — links and closes that ticket as well. When you need to name the convention in prose, write it without a resolvable id. All QA for a change happens on its own PR before review: the creator shows that every behaviour the diff changes is covered, and lists what is not under `Open gaps`; the reviewer challenges that list and asks for the missing coverage. There is no separate release QA pass per PR — release review only looks for problems arising from the interplay of several changes. Fill every section from the actual diff on PR open, and re-update it in the same turn on every change (new commits, scope or review fixes) so it never drifts — treat a stale section as a bug.

The checkbox under `## Breaking changes` is a decision you own, not a formality: judge the diff against the template's list of breaking changes and tick it (`- [x]`) when one applies, leave it unticked when none does. It is the only input to the `breaking-change` label, which feeds the release notes and the self-hoster migration guide, so a wrong answer either invents a migration entry or hides one. Re-check it whenever the diff grows. `pr-label-sync.yml` reads nothing but the tick, so the prose below the checkbox cannot change the label — but it is not free-form either: the CodeRabbit `Breaking changes match the diff` check compares the tick against the diff and expects a ticked box to document each breaking change, so explain your answer there in whatever shape fits (table or prose).

## Next.js Documentation

Do not rely on training data for Next.js behavior in this repo. For any Next.js-related work (routing, layouts, server/client components, caching, next.config, etc.), use the `nextjs-docs` skill, which indexes the version-pinned local docs in `.next-docs/`.
Expand Down
Loading
Loading