Skip to content

Commit 7835b4c

Browse files
committed
Tooling: Revamp checker script incl parallel exec.
- Modular checks in scripts/check/checks/ with shared interface - Parallel execution with dependency graph and CPU-based scaling - Single-line output with aligned formatting and app icons - Colored durations (green <5s, yellow 5-15s, orange >15s) - Proper singular/plural agreement in all messages - TTY-aware status line showing running checks - Dynamic help generation from check registry - New flags: --fail-fast, --include-slow, --check - Documentation in docs/tooling/checker-script.md
1 parent 33af339 commit 7835b4c

40 files changed

Lines changed: 2161 additions & 1191 deletions
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
I want to work with you on the checker script, which is in `scripts/check/`. I want these improvements:
2+
3+
1. I want it to be a bit more modular. It seems better if each check has its own file. I want them to live in one dir, in `scripts/check/checks/`, and the files named after the
4+
category they fall in, and the check itself, like `{app}-{tech-optional}-{check}.go`, e.g. `desktop-rust-clippy.go`, `desktop-svelte-knip.go`, `website-prettier.go`,
5+
`license-server-prettier.go`, etc.
6+
7+
2. I want checks to implement a common interface. They'd get the *CheckContext like they do now, but not just return `error` but also some value when they succeed, probably
8+
something like (pseudocode): `{resultCode: success|warn, resultText: string}`, and then if the result includes no line break then the main script would output `• {checkname}...
9+
<green|yellow>{OK|warn}</green|yellow> ({ms}ms) - <green|yellow>{resultText}</green|yellow>`, and if it does have a linebreak then
10+
```
11+
• {checkname}... <green|yellow>{OK|warn}</green|yellow> ({ms}ms)
12+
<green|yellow>{resultText}</green|yellow>
13+
```
14+
15+
3. Instead of the current display of categories like
16+
```
17+
🦀 Rust checks (desktop)...
18+
• rustfmt... OK (402ms)
19+
```
20+
I want single line like `Desktop: 🦀 Rust checks / rustfmt... OK (402ms)` so there would be no categories.
21+
22+
4. I want to start running in parallel whatever we can. I want to set up an easy to read dependency graph. I think if we mark for every check to know what other checks it needs to
23+
wait for, that's intuitive enough. For example, I want ESLint to wait for Prettier so that the formatting is fixed by the time it gets there. An I want the main thread to run
24+
whatever tests that have no pending dependencies, as many in parallel as reasonable. What Rust's parallel executor thing does it that it auto-sclaes the number of threads to the
25+
number of CPU cores available. I'd love something like that if it's easy to do in Go, otherwise let's just do 10 or sg.
26+
- With the parallel runs, make sure that whatever multi-line output the script has (either the success/warning message, or an error message) to be printed immediately after the
27+
line that has the check name, and do this safely without the race condition between the checks running in parallel.
28+
29+
5. If any of the tests fail, I want the script to continue even if some tests fail, except for dependent scripts. This, unless a `--fail-fast` (or whatever name is idiomatic) arg
30+
is present.
31+
32+
6. I want the registry.go to be changed to a different format, to hold all checks in one hard-coded array or sg with this format for each check (pseudocode): `{id: string (unique,
33+
e.g. "license-server-prettier"), displayName: string (not unique, e.g. "prettier", to be displayed together with the app+tech), app: enum<desktop|website|license-server|other>,
34+
tech:"🦀 Rust"|"🎨 Svelte"|"🚀 Astro"|"⸆⸉ TS"|"", isSlow: boolean, dependendsOn: string[] (a list of IDs can come here)}`
35+
36+
7. I want to introduce an "is slow" property, and only mark the Rust Linux test as slow for now.
37+
38+
8. Slow tests should not be included by default, only if they are added by a `--check` arg or if an `--include-slow` arg is present.
39+
40+
9. I want the list of checks for `--help` to be a dynamically generated rather than a static list.
41+
42+
10. I want the script to keep a list of running checks, and display their IDs, capped at, say 60 chars width with ellipses, like `Waiting for: desktop-rustfmt, desktop-clippy,
43+
desktop-prettier...`, and this list to always the the last line, and get updated as the script runs.
44+
45+
11. I want each check to output some meaningful short success message, that includes stats on what was done, e.g. `All 12 tests in 4 files passed` or `Fixed formatting in 3 files`.
46+
47+
12. Write docs for the script in `docs/tooling/checker-script.md` that helps the dev (me and agents) use it and maintain it (e.g. adding a new test, etc.).
48+
49+
Please use a task list to make sure you don't forget any of these. No need to go sequentially, go in whatever order you want to go.

docs/tooling/checker-script.md

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
# Checker script
2+
3+
The checker script (`scripts/check`) runs code quality checks for all apps in the monorepo. It supports parallel
4+
execution, dependency management between checks, and various filtering options.
5+
6+
## Quick start
7+
8+
```bash
9+
# Run all checks (excludes slow checks by default)
10+
go run ./scripts/check
11+
12+
# Run checks for a specific app
13+
go run ./scripts/check --app desktop
14+
15+
# Run a specific check
16+
go run ./scripts/check --check desktop-rust-clippy
17+
18+
# Run multiple specific checks
19+
go run ./scripts/check --check desktop-rust-rustfmt --check desktop-rust-clippy
20+
21+
# Include slow checks
22+
go run ./scripts/check --include-slow
23+
24+
# CI mode (no auto-fixing, stop on first failure)
25+
go run ./scripts/check --ci --fail-fast
26+
```
27+
28+
## Command-line options
29+
30+
| Option | Description |
31+
| ----------------------------- | ------------------------------------------------ |
32+
| `--app NAME` | Run checks for a specific app |
33+
| `--rust`, `--rust-only` | Run only Rust checks (desktop) |
34+
| `--svelte`, `--svelte-only` | Run only Svelte checks (desktop) |
35+
| `--check ID` | Run specific checks by ID (repeatable) |
36+
| `--ci` | Disable auto-fixing (for CI) |
37+
| `--verbose` | Show detailed output |
38+
| `--include-slow` | Include slow checks (excluded by default) |
39+
| `--fail-fast` | Stop on first failure |
40+
| `-h`, `--help` | Show help message |
41+
42+
## Available apps
43+
44+
- `desktop` - The Tauri desktop app (Rust + Svelte)
45+
- `website` - The marketing website (Astro)
46+
- `license-server` - The license server (Cloudflare Worker)
47+
48+
## How it works
49+
50+
### Parallel execution
51+
52+
Checks run in parallel by default, using a number of workers equal to the CPU count. The script respects dependencies
53+
between checks—if check B depends on check A, B won't start until A completes successfully.
54+
55+
### Dependencies
56+
57+
Checks can declare dependencies on other checks. For example:
58+
- `desktop-rust-clippy` depends on `desktop-rust-rustfmt` (formatting should happen before linting)
59+
- `desktop-svelte-eslint` depends on `desktop-svelte-prettier`
60+
- `desktop-rust-tests` depends on `desktop-rust-clippy`
61+
62+
If a dependency fails, dependent checks are marked as "BLOCKED" and don't run.
63+
64+
### Continue on failure
65+
66+
By default, the script continues running independent checks even if some fail. Use `--fail-fast` to stop on the first
67+
failure (useful in CI).
68+
69+
### Slow checks
70+
71+
Some checks are marked as "slow" (for example, `desktop-rust-tests-linux` which runs tests in Docker). These are
72+
excluded by default. Use `--include-slow` to run them, or specify them directly with `--check`.
73+
74+
## Output format
75+
76+
Each check outputs a single line:
77+
78+
```
79+
• Desktop: 🦀 Rust / clippy... OK (1.23s) - No warnings
80+
```
81+
82+
Format: `• {App}: {Tech} / {CheckName}... {Status} ({Duration}) - {Message}`
83+
84+
Status can be:
85+
- `OK` (green) - Check passed
86+
- `warn` (yellow) - Check passed with warnings
87+
- `SKIPPED` (yellow) - Check was skipped (for example, missing config file)
88+
- `FAILED` (red) - Check failed
89+
- `BLOCKED` (yellow) - Check couldn't run because a dependency failed
90+
91+
A status line at the bottom shows currently running checks.
92+
93+
## File structure
94+
95+
```
96+
scripts/check/
97+
├── main.go # Entry point, CLI parsing
98+
├── runner.go # Parallel execution engine
99+
├── registry.go # Check lookup helpers
100+
├── colors.go # ANSI colors and output helpers
101+
├── utils.go # Utility functions
102+
├── types.go # Type re-exports (mostly empty)
103+
└── checks/ # Check implementations
104+
├── common.go # Shared types and utilities
105+
├── registry.go # Check definitions with metadata
106+
└── *.go # Individual check files
107+
```
108+
109+
## Adding a new check
110+
111+
1. Create a new file in `scripts/check/checks/` following the naming convention `{app}-{tech}-{checkname}.go`:
112+
113+
```go
114+
package checks
115+
116+
import (
117+
"fmt"
118+
"os/exec"
119+
"path/filepath"
120+
)
121+
122+
// RunMyCheck does something useful.
123+
func RunMyCheck(ctx *CheckContext) (CheckResult, error) {
124+
cmd := exec.Command("some-tool", "some-args")
125+
cmd.Dir = filepath.Join(ctx.RootDir, "apps", "desktop")
126+
output, err := RunCommand(cmd, true)
127+
if err != nil {
128+
return CheckResult{}, fmt.Errorf("check failed\n%s", indentOutput(output))
129+
}
130+
return Success("Checked 42 files"), nil
131+
}
132+
```
133+
134+
2. Add the check definition to `scripts/check/checks/registry.go`:
135+
136+
```go
137+
{
138+
ID: "desktop-mytech-mycheck",
139+
DisplayName: "mycheck",
140+
App: AppDesktop,
141+
Tech: "🔧 MyTech",
142+
IsSlow: false,
143+
DependsOn: []string{"desktop-mytech-formatter"}, // optional
144+
Run: RunMyCheck,
145+
},
146+
```
147+
148+
3. Test your check:
149+
150+
```bash
151+
go run ./scripts/check --check desktop-mytech-mycheck
152+
```
153+
154+
## Check implementation guidelines
155+
156+
### Return values
157+
158+
- Return `Success(message)` on success with a short, informative message
159+
- Return `Warning(message)` for non-fatal issues
160+
- Return `Skipped(reason)` when the check can't run (for example, missing config)
161+
- Return `CheckResult{}, error` on failure
162+
163+
### Success messages
164+
165+
Include useful stats in success messages:
166+
167+
-`12 tests passed`
168+
-`Checked 42 files`
169+
-`No lint errors`
170+
-`OK` (too generic)
171+
172+
### Error messages
173+
174+
Include the command output in error messages using `indentOutput()`:
175+
176+
```go
177+
return CheckResult{}, fmt.Errorf("check failed\n%s", indentOutput(output))
178+
```
179+
180+
### CI vs local mode
181+
182+
Use `ctx.CI` to change behavior:
183+
184+
```go
185+
if ctx.CI {
186+
cmd = exec.Command("tool", "--check") // Just check, don't fix
187+
} else {
188+
cmd = exec.Command("tool", "--fix") // Auto-fix locally
189+
}
190+
```
191+
192+
### Dependencies
193+
194+
Set `DependsOn` to ensure checks run in the right order:
195+
196+
- Formatters should run before linters
197+
- Linters should run before tests
198+
- Type checkers should run before tests
199+
200+
## Troubleshooting
201+
202+
### Check is blocked
203+
204+
A check shows "BLOCKED" when its dependency failed. Fix the dependency first.
205+
206+
### Check is slow
207+
208+
Add `IsSlow: true` to the check definition if it takes more than a few seconds. Users can include it with
209+
`--include-slow`.
210+
211+
### Check needs a tool installed
212+
213+
Use `CommandExists()` to check if a tool is installed, and auto-install if possible:
214+
215+
```go
216+
if !CommandExists("some-tool") {
217+
installCmd := exec.Command("cargo", "install", "some-tool")
218+
if _, err := RunCommand(installCmd, true); err != nil {
219+
return CheckResult{}, fmt.Errorf("failed to install some-tool: %w", err)
220+
}
221+
}
222+
```

0 commit comments

Comments
 (0)