Skip to content

Commit 430268a

Browse files
committed
Add plain check command and refactor pre-commit to use it
Extract core validation checks (custom commands, code linting, preflight, migrations, tests) into a new `plain check` command. Pre-commit now delegates to `plain check` instead of duplicating the logic. Supports --skip-test for faster iteration.
1 parent 8ef8a38 commit 430268a

File tree

7 files changed

+154
-79
lines changed

7 files changed

+154
-79
lines changed

.claude/rules/plain.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,17 @@ Examples:
7171
- **plain-tunnel** — Remote access to local dev server
7272
- **plain-vendor** — Vendor CDN scripts and styles
7373

74+
## Checks
75+
76+
`uv run plain check` runs core validation checks: code linting, preflight, migration state, and tests. Stops on first failure.
77+
78+
```
79+
uv run plain check # Run all checks
80+
uv run plain check --skip-test # Skip tests (faster iteration)
81+
```
82+
83+
`uv run plain pre-commit` wraps `plain check` with additional commit-specific steps (custom commands, uv lock, build).
84+
7485
## Shell
7586

7687
`uv run plain shell` opens an interactive Python shell with Plain configured and database access.

plain-dev/plain/dev/README.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -94,14 +94,16 @@ A built-in pre-commit hook that you can install with `plain pre-commit --install
9494

9595
Runs:
9696

97-
- Custom commands defined in `pyproject.toml` at `tool.plain.pre-commit.run`
98-
- `plain code check`, if [`plain.code`](../../plain-code/plain/code/README.md) is installed
99-
- `uv lock --locked`, if using uv
100-
- `plain preflight`
101-
- `plain migrate --check`
102-
- `plain makemigrations --dry-run --check`
97+
- `uv lock --check`, if using uv
98+
- `plain check` (custom commands, code linting, preflight, migrations, tests)
10399
- `plain build`
104-
- `plain test`
100+
101+
Custom commands can be defined in `pyproject.toml` at `tool.plain.check.run` and will run as part of `plain check`:
102+
103+
```toml
104+
[tool.plain.check.run]
105+
my-check = {cmd = "echo 'running my check'"}
106+
```
105107

106108
## Settings
107109

Lines changed: 4 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1+
from __future__ import annotations
2+
13
import os
2-
import subprocess
3-
import sys
4-
import tomllib
5-
from importlib.util import find_spec
64
from pathlib import Path
75

86
import click
97

108
from plain.cli import register_cli
11-
from plain.cli.print import print_event
9+
from plain.cli.check import check_short, run_core_checks
1210
from plain.cli.runtime import without_runtime_setup
1311

1412

@@ -46,75 +44,9 @@ def install() -> None:
4644
def run_checks() -> None:
4745
"""Run all pre-commit checks"""
4846

49-
pyproject = Path("pyproject.toml")
50-
51-
if pyproject.exists():
52-
with open(pyproject, "rb") as f:
53-
pyproject = tomllib.load(f)
54-
for name, data in (
55-
pyproject.get("tool", {})
56-
.get("plain", {})
57-
.get("pre-commit", {})
58-
.get("run", {})
59-
).items():
60-
cmd = data["cmd"]
61-
print_event(f"Custom: {cmd}")
62-
result = subprocess.run(cmd, shell=True)
63-
if result.returncode != 0:
64-
sys.exit(result.returncode)
65-
66-
# Run this first since it's probably the most likely to fail
67-
if find_spec("plain.code"):
68-
check_short("plain code check", "plain", "code", "check")
69-
7047
if Path("uv.lock").exists():
7148
check_short("uv lock --check", "uv", "lock", "--check")
7249

73-
if plain_db_connected():
74-
check_short("plain preflight", "plain", "preflight", "--quiet")
75-
check_short("plain migrate --check", "plain", "migrate", "--check")
76-
check_short(
77-
"plain makemigrations --dry-run --check",
78-
"plain",
79-
"makemigrations",
80-
"--dry-run",
81-
"--check",
82-
)
83-
else:
84-
check_short("plain preflight", "plain", "preflight", "--quiet")
85-
click.secho("--> Skipping migration checks", bold=True, fg="yellow")
50+
run_core_checks(skip_test=False)
8651

8752
check_short("plain build", "plain", "build")
88-
89-
if find_spec("plain.pytest"):
90-
print_event("plain test")
91-
result = subprocess.run(["plain", "test"])
92-
if result.returncode != 0:
93-
sys.exit(result.returncode)
94-
95-
96-
def plain_db_connected() -> bool:
97-
result = subprocess.run(
98-
[
99-
"plain",
100-
"migrations",
101-
"list",
102-
],
103-
stdout=subprocess.DEVNULL,
104-
stderr=subprocess.DEVNULL,
105-
)
106-
return result.returncode == 0
107-
108-
109-
def check_short(message: str, *args: str) -> None:
110-
print_event(message, newline=False)
111-
env = {**os.environ, "FORCE_COLOR": "1"}
112-
result = subprocess.run(
113-
args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env
114-
)
115-
if result.returncode != 0:
116-
click.secho("✘", fg="red")
117-
click.secho(result.stdout.decode("utf-8"))
118-
sys.exit(1)
119-
else:
120-
click.secho("✔", fg="green")

plain/plain/agents/.claude/rules/plain.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,17 @@ Examples:
7171
- **plain-tunnel** — Remote access to local dev server
7272
- **plain-vendor** — Vendor CDN scripts and styles
7373

74+
## Checks
75+
76+
`uv run plain check` runs core validation checks: code linting, preflight, migration state, and tests. Stops on first failure.
77+
78+
```
79+
uv run plain check # Run all checks
80+
uv run plain check --skip-test # Skip tests (faster iteration)
81+
```
82+
83+
`uv run plain pre-commit` wraps `plain check` with additional commit-specific steps (custom commands, uv lock, build).
84+
7485
## Shell
7586

7687
`uv run plain shell` opens an interactive Python shell with Plain configured and database access.

plain/plain/cli/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ Plain includes several built-in commands:
165165

166166
| Command | Description |
167167
| --------------------- | ---------------------------------------- |
168+
| `plain check` | Run core validation checks |
168169
| `plain shell` | Interactive Python shell |
169170
| `plain run <script>` | Execute a Python script with app context |
170171
| `plain server` | Production-ready WSGI server |
@@ -179,6 +180,30 @@ Plain includes several built-in commands:
179180

180181
Additional commands are added by installed packages (like `plain models migrate` from plain.models).
181182

183+
### `plain check`
184+
185+
Runs core validation checks in order, stopping on first failure:
186+
187+
1. Custom commands (see below)
188+
2. `plain code check` (if `plain.code` is installed)
189+
3. `plain preflight --quiet`
190+
4. `plain migrate --check` (if DB connected)
191+
5. `plain makemigrations --dry-run --check` (if DB connected)
192+
6. `plain test` (if `plain.pytest` is installed)
193+
194+
Use `--skip-test` to skip tests for faster iteration.
195+
196+
#### Custom check commands
197+
198+
Add custom commands to `plain check` by defining them in `pyproject.toml`:
199+
200+
```toml
201+
[tool.plain.check.run]
202+
my-check = {cmd = "echo 'running my check'"}
203+
```
204+
205+
Custom commands run first, before any built-in checks.
206+
182207
## FAQs
183208

184209
#### How do I run commands that don't need the app to be set up?

plain/plain/cli/check.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
from __future__ import annotations
2+
3+
import os
4+
import subprocess
5+
import tomllib
6+
from importlib.util import find_spec
7+
from pathlib import Path
8+
9+
import click
10+
11+
from plain.cli.print import print_event
12+
from plain.cli.runtime import common_command
13+
14+
15+
def plain_db_connected() -> bool:
16+
result = subprocess.run(
17+
["plain", "migrations", "list"],
18+
stdout=subprocess.DEVNULL,
19+
stderr=subprocess.DEVNULL,
20+
)
21+
return result.returncode == 0
22+
23+
24+
def check_short(message: str, *args: str) -> None:
25+
print_event(message, newline=False)
26+
env = {**os.environ, "FORCE_COLOR": "1"}
27+
result = subprocess.run(
28+
args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env
29+
)
30+
if result.returncode != 0:
31+
click.secho("✘", fg="red")
32+
click.secho(result.stdout.decode("utf-8"))
33+
raise SystemExit(1)
34+
else:
35+
click.secho("✔", fg="green")
36+
37+
38+
def run_custom_checks() -> None:
39+
"""Run custom checks from [tool.plain.check.run] in pyproject.toml."""
40+
pyproject_path = Path("pyproject.toml")
41+
42+
if not pyproject_path.exists():
43+
return
44+
45+
with open(pyproject_path, "rb") as f:
46+
pyproject = tomllib.load(f)
47+
48+
for name, data in (
49+
pyproject.get("tool", {}).get("plain", {}).get("check", {}).get("run", {})
50+
).items():
51+
cmd = data["cmd"]
52+
print_event(f"Custom: {cmd}")
53+
result = subprocess.run(cmd, shell=True)
54+
if result.returncode != 0:
55+
raise SystemExit(result.returncode)
56+
57+
58+
def run_core_checks(*, skip_test: bool = False) -> None:
59+
"""Run core validation checks: custom, code, preflight, migrations, tests."""
60+
61+
run_custom_checks()
62+
63+
if find_spec("plain.code"):
64+
check_short("plain code check", "plain", "code", "check")
65+
66+
if plain_db_connected():
67+
check_short("plain preflight", "plain", "preflight", "--quiet")
68+
check_short("plain migrate --check", "plain", "migrate", "--check")
69+
check_short(
70+
"plain makemigrations --dry-run --check",
71+
"plain",
72+
"makemigrations",
73+
"--dry-run",
74+
"--check",
75+
)
76+
else:
77+
check_short("plain preflight", "plain", "preflight", "--quiet")
78+
click.secho("--> Skipping migration checks", bold=True, fg="yellow")
79+
80+
if not skip_test and find_spec("plain.pytest"):
81+
print_event("plain test")
82+
result = subprocess.run(["plain", "test"])
83+
if result.returncode != 0:
84+
raise SystemExit(result.returncode)
85+
86+
87+
@common_command
88+
@click.command()
89+
@click.option("--skip-test", is_flag=True, help="Skip running tests")
90+
def check(skip_test: bool) -> None:
91+
"""Run core validation checks"""
92+
run_core_checks(skip_test=skip_test)

plain/plain/cli/core.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from .agent import agent
1313
from .build import build
1414
from .changelog import changelog
15+
from .check import check
1516
from .chores import chores
1617
from .docs import docs
1718
from .formatting import PlainContext
@@ -33,6 +34,7 @@ def plain_cli() -> None:
3334
pass
3435

3536

37+
plain_cli.add_command(check)
3638
plain_cli.add_command(docs)
3739
plain_cli.add_command(request)
3840
plain_cli.add_command(agent)

0 commit comments

Comments
 (0)