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: 1 addition & 1 deletion .claude/rules/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ The required check names are in `AGENTS.md` (CI). Reading a red check: `.claude/
## PR title (`pr-title.yml`)

- Runs on opened, edited, reopened and synchronize, so every head commit gets the check.
- Runs `scripts/ci/check-pr-title.py`, then `scripts/ci/check-changelog.py --pr` on the merge commit's diff to its first parent, which also refuses a release PR that changes more than `apple/CHANGELOG.md` and `apple/changelog.d/`, then `scripts/check-banned-words.sh` on the PR's added lines and on its body. The banned-words step runs both and exits with the worse result.
- Runs `scripts/ci/check-pr-title.py`, then `scripts/ci/check-changelog.py --pr` on the merge commit's diff to its first parent, which also refuses a release PR that changes more than `apple/CHANGELOG.md` and `apple/changelog.d/`.

## Repo Hygiene (`repo-hygiene.yml`)

Expand Down
4 changes: 2 additions & 2 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<!--
Title: <type>(<scope>): <description>, with ! before the colon for a breaking change. At most 72 characters, GitHub's " (#123)" not counted. It becomes the squash commit subject.
Title: <type>(<scope>): <description>, with ! before the colon for a breaking change. It becomes the squash commit subject.
Types: feat, fix, refactor, perf, test, docs, build, ci, chore, style, revert.
Scopes, when one fits: ai-chat, ai-providers, mcp, copilot, inline-suggest, editor, datagrid, tabs, coordinator, sidebar, connections, connection-form, welcome, settings, toolbar, hig, ssh, ios, windowing, perf, launch, plugins, plugin-<name>, changelog, claude-md, docs, ci, release.
Platform scopes: ios for the iOS app, linux or linux-<area> for the Linux app. windows is kept for a Windows app, so window management is windowing.
Body: 80 to 150 words, 250 at most, screenshots aside. Delete these comments and any optional heading you do not use.
Leave out review history, test lists and counts, the investigation story and file-by-file change logs.
The PR title check also scans this body against scripts/banned-words.txt.
Use none of the terms in scripts/banned-words.txt.
-->

## Why
Expand Down
12 changes: 1 addition & 11 deletions .github/workflows/pr-title.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
scripts
apple/scripts/release

- name: Check the title is a Conventional Commits subject within 72 characters
- name: Check the title is a Conventional Commits subject
env:
TITLE: ${{ github.event.pull_request.title }}
run: python3 scripts/ci/check-pr-title.py "$TITLE"
Expand All @@ -38,13 +38,3 @@ jobs:
PR: ${{ github.event.pull_request.number }}
TITLE: ${{ github.event.pull_request.title }}
run: python3 scripts/ci/check-changelog.py --pr "$PR" --title "$TITLE" --base "$GITHUB_BASE_REF"

- name: Check the added lines and the description against the banned words
env:
BODY: ${{ github.event.pull_request.body }}
run: |
printf '%s\n' "$BODY" > "$RUNNER_TEMP/pr-body.md"
status=0
scripts/check-banned-words.sh --base HEAD^1 || status=1
scripts/check-banned-words.sh "$RUNNER_TEMP/pr-body.md" || status=1
exit "$status"
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Each app has its own guide beside its code: `apple/AGENTS.md` and `linux/AGENTS.
## Writing style and docs

- All writing: short sentences, plain words, specific numbers and names, no em dash, no filler.
- `scripts/banned-words.txt` is the list; never copy its terms. `scripts/check-banned-words.sh --staged` checks a staged change; the `PR title` job checks a PR's added lines and body.
- `scripts/banned-words.txt` is the list; never copy its terms. `scripts/check-banned-words.sh --staged` checks a staged change.
- One home per fact; others point to it by repo path and section. No drifting counts or timings; a plan is not a fact.
- Code comments say why, never what. Delete what-comments in code you touch.
- A path in a platform's docs is relative to its folder; a root doc writes it from the repository root.
Expand All @@ -36,7 +36,7 @@ Each app has its own guide beside its code: `apple/AGENTS.md` and `linux/AGENTS.

## Commits and PRs

- The PR title is the squash commit subject; branch commits use the same format. The body follows `.github/pull_request_template.md`, whose top comment holds the format, types and scopes. The `PR title` check (`scripts/ci/check-pr-title.py`) enforces the format and 72 characters, not scopes.
- The PR title is the squash commit subject; branch commits use the same format. The body follows `.github/pull_request_template.md`, whose top comment holds the format, types and scopes. The `PR title` check (`scripts/ci/check-pr-title.py`) enforces the format, not scopes.
- A user-visible change adds a fragment to its app's `changelog.d/`; only a release PR edits a `CHANGELOG.md`. Format: `.claude/rules/changelog.md`.
- A rename or signature change updates every caller and test in the same commit.

Expand Down
2 changes: 1 addition & 1 deletion linux/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ From the repository root, through the verification wrapper, which keeps the full
2. **Changelog.** A user-visible change adds `linux/changelog.d/<PR>.<type>.md`, never `apple/changelog.d/`; only a release PR edits `linux/CHANGELOG.md`. Format: the root `.claude/rules/changelog.md`.
3. **Translations.** User-facing strings go through `crate::i18n` (`gettext`, `gettext_f` with named placeholders, `ngettext_f`, `pgettext`). A new or removed file under `crates/app/src` or `data/resources` regenerates `po/POTFILES.in`. See `po/README.md`.
4. **Lint** the workspace: rustfmt, then clippy with and without default features.
5. **Commits and pull requests** follow the root rules: Conventional Commits, at most 72 characters, scope `linux` or `linux-<area>` (for example `linux-grid`, `linux-storage`), and the root `.github/pull_request_template.md`.
5. **Commits and pull requests** follow the root rules: Conventional Commits, scope `linux` or `linux-<area>` (for example `linux-grid`, `linux-storage`), and the root `.github/pull_request_template.md`.

## CI

Expand Down
2 changes: 1 addition & 1 deletion linux/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ This is the most common substantive change. Follow [docs/adding-drivers.md](docs

## Commits

Conventional Commits, single line, at most 72 characters, same rule as the macOS app. The scope is `linux`, or `linux-<area>` for one area of the app: `linux-grid`, `linux-browse`, `linux-structure`, `linux-editor`, `linux-history`, `linux-export`, `linux-sidebar`, `linux-picker`, `linux-session`, `linux-storage`, `linux-ssh`, `linux-core`, `linux-flatpak`, or a driver as `linux-postgres`, `linux-mysql`, `linux-sqlite`, `linux-mssql`, `linux-clickhouse`.
Conventional Commits, single line, same rule as the macOS app. The scope is `linux`, or `linux-<area>` for one area of the app: `linux-grid`, `linux-browse`, `linux-structure`, `linux-editor`, `linux-history`, `linux-export`, `linux-sidebar`, `linux-picker`, `linux-session`, `linux-storage`, `linux-ssh`, `linux-core`, `linux-flatpak`, or a driver as `linux-postgres`, `linux-mysql`, `linux-sqlite`, `linux-mssql`, `linux-clickhouse`.

```
fix(linux-sidebar): keep the filter text when the catalog refreshes
Expand Down
6 changes: 0 additions & 6 deletions scripts/ci/check-pr-title.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@
import sys

TYPES = ("feat", "fix", "refactor", "perf", "test", "docs", "build", "ci", "chore", "style", "revert")
MAX_TITLE = 72
SUBJECT = re.compile(rf"^(?:{'|'.join(TYPES)})(?:\([a-z0-9][a-z0-9-]*\))?!?: \S")
# A pick into a release line keeps its subject from main, whose trailing (#N) names its origin.
PROVENANCE = re.compile(r" \(#[0-9]+\)$")


def problems(title):
Expand All @@ -19,9 +16,6 @@ def problems(title):
+ ", ".join(TYPES)
+ " and the scope is lowercase words joined by hyphens, for example `fix(editor): keep the caret`"
)
length = len(PROVENANCE.sub("", title, count=1))
if length > MAX_TITLE:
found.append(f"the title is {length} characters, over the {MAX_TITLE} limit")
return found


Expand Down
28 changes: 0 additions & 28 deletions scripts/ci/test_check_pr_title.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,34 +40,6 @@ def test_a_missing_description_is_refused(self):
self.assertEqual(len(check.problems("fix(editor):no space")), 1)


class LengthTests(unittest.TestCase):
def test_the_title_alone_is_measured(self):
title = "fix: " + "x" * (check.MAX_TITLE - len("fix: "))
self.assertEqual(check.problems(title), [])
self.assertEqual(len(check.problems(title + "x")), 1)

def test_a_long_title_in_the_wrong_shape_reports_both(self):
self.assertEqual(len(check.problems("Fix " + "x" * 100)), 2)

def test_a_picks_trailing_pull_request_number_is_not_counted(self):
title = "fix: " + "x" * (check.MAX_TITLE - len("fix: "))
self.assertEqual(check.problems(title + " (#3443)"), [])
self.assertEqual(check.problems("chore(release): v0.82.0 (#3601)"), [])
problems = check.problems(title + "x (#3443)")
self.assertEqual(len(problems), 1)
self.assertIn("73 characters", problems[0])

def test_only_one_trailing_pull_request_number_is_skipped(self):
title = "fix: " + "x" * (check.MAX_TITLE - len("fix: "))
self.assertIn("77 characters", check.problems(title + " (#1) (#2)")[0])

def test_a_number_that_is_not_at_the_end_is_counted(self):
title = "fix: " + "x" * (check.MAX_TITLE - len("fix: ") - len(" (#3443)"))
self.assertEqual(check.problems(title + " (#3443)"), [])
self.assertEqual(len(check.problems(title + " (#3443)x")), 1)
self.assertEqual(len(check.problems(title + "x (#3443) ")), 1)


class OutputTests(unittest.TestCase):
def run_main(self, *argv):
captured = io.StringIO()
Expand Down
Loading