feat: Artisan checklist for application commands, hints and coverage gate

- migrate, migrate:rollback and deferred:purge gain --pretend and --force;
  they and queue:clear confirm only in production on an interactive
  terminal, so deploys over SSH proceed as before
- result lines: Applied N migrations, Rolled back <id> of <plugin>,
  Cleared N jobs from <queue>
- --json for migrate:status, key:generate, route:list, admin:token:create,
  admin:token:list, admin:token:scopes and websockets:health; -q keeps the
  key and the token
- hints for an empty DSN, an unreachable database, an invalid app.key, a
  missing uploads bucket and an application binary run below its root
- flare and websockets:health return bonfire.Reported after printing
- long help and examples for every application command, greeter:hello and
  blog:publish; cmd/summer TestCommandsDocumented fails on any gap
This commit is contained in:
Jakub Zych
2026-10-21 13:16:21 +02:00
parent 168d65cd1b
commit 9d06a78f62
29 changed files with 831 additions and 77 deletions

View File

@@ -12,28 +12,39 @@ Every application binary carries the framework's runtime commands. The examples
| Command | Flags | Purpose |
|---------|-------|---------|
| `migrate` | none | Runs the framework migrations, then every plugin's migrations in dependency order. |
| `migrate:rollback` | `--plugin <id>` | Rolls back the last migration of the plugin; without the flag, of the last activated plugin that has migrations. |
| `migrate:status` | none | Prints a table of plugin, history table and applied migration IDs. |
| `migrate` | `--pretend`, `--force` | Runs the framework migrations, then every plugin's migrations in dependency order, printing how many each set applied and `Applied N migrations` (or `Nothing to migrate`). `--pretend` lists the pending migrations as `<plugin>: <id>` and applies nothing. |
| `migrate:rollback` | `--plugin <id>`, `--pretend`, `--force` | Rolls back the last migration of the plugin; without the flag, of the last activated plugin that has migrations. Prints `Rolled back <id> of <plugin>`; `--pretend` only names it. |
| `migrate:status` | `--json` | Prints a table of plugin, history table and applied migration IDs. `--json` prints every declared migration of those plugins with `plugin`, `table`, `id` and `applied`. |
```sh
./bin/acme migrate --pretend
./bin/acme migrate
./bin/acme migrate:status
./bin/acme migrate:status --json
./bin/acme migrate:rollback --plugin acme.blog
```
Each plugin keeps its own migration history table, so rolling back one plugin never touches another. The migrations are listed in [lagoon](../../modules/lagoon/README.md).
### Confirmation in production
`migrate`, `migrate:rollback`, `deferred:purge` and `queue:clear` change or delete data. When the environment is `production` and the command runs on an interactive terminal, they ask before doing it; answering no prints a cancellation and exits 1. `--force` skips the question. Off a terminal (a deploy over SSH, a container entrypoint, the scheduler) and with `-n` they never ask and proceed as before, so deploy scripts need no change.
```sh
./bin/acme migrate --force
```
When the database cannot be reached, or `database.dsn` is empty, the error names the fix (`docker compose up -d --wait`, `cp .env.example .env`). Run from a directory below the application root, an application binary finds no `config/` and adds a hint to its error with the command to run instead, for example `cd ../..; bin/acme migrate`.
## Pending uploads and related records
An admin form holds the files uploaded and the related records created for a record that is not saved yet as deferred bindings. Saving the record commits them; abandoned ones are removed by `deferred:purge`:
| Command | Flags | Purpose |
|---------|-------|---------|
| `deferred:purge` | `--days <n>` | Deletes deferred bindings older than `n` days, the unattached uploads they hold and the records created under deferral, and prints the counts. Records that were only linked are kept. Without the flag, `database.deferred_bindings.purge_days` applies, else 5 days. |
| `deferred:purge` | `--days <n>`, `--pretend`, `--force` | Deletes deferred bindings older than `n` days, the unattached uploads they hold and the records created under deferral, and prints the counts. Records that were only linked are kept. Without the flag, `database.deferred_bindings.purge_days` applies, else 5 days. `--pretend` counts the bindings that would go and deletes nothing. |
```sh
./bin/acme deferred:purge
./bin/acme deferred:purge --pretend
./bin/acme deferred:purge --days=1
```
@@ -43,10 +54,11 @@ The scheduler already runs it every day at 03:00; see [Task scheduling](../plugi
| Command | Flags | Purpose |
|---------|-------|---------|
| `key:generate` | none | Prints a fresh base64 32-byte key for `app.key`. It writes nothing and needs no database. |
| `key:generate` | `--json` | Prints a fresh base64 32-byte key for `app.key`. It writes nothing and needs no database. With `-q` it prints only the key; `--json` prints `{"key": "..."}`. |
```sh
./bin/acme key:generate
KEY=$(./bin/acme key:generate -q)
```
Copy the printed value into `SUMMER_APP__KEY`. When you rotate the key, keep the old one in `app.previous_keys` so existing encrypted columns can still be read.
@@ -56,7 +68,7 @@ Copy the printed value into `SUMMER_APP__KEY`. When you rotate the key, keep the
| Command | Flags | Purpose |
|---------|-------|---------|
| `serve` | `--addr` (default `:8080`) | Opens the database and uploads bucket, builds the router, binds the address, starts the in-process job worker and serves HTTP until SIGINT or SIGTERM. Once it listens it prints a startup summary; on a signal it drains requests and the worker within 10 seconds and prints a shutdown report. |
| `route:list` | none | Builds the router the way `serve` does, without opening the database or listening, and prints every route with its method, pattern, plugin, middleware and raw flag, followed by the admin status line. |
| `route:list` | `--json` | Builds the router the way `serve` does, without opening the database or listening, and prints every route with its method, pattern, plugin, middleware and raw flag, followed by the admin status line. `--json` prints the routes as a JSON array. |
```sh
./bin/acme route:list
@@ -111,10 +123,10 @@ Jobs still running at the deadline are cancelled; River records the cancelled at
|---------|---------------------|---------|
| `admin:create` | `--email` (required), `--login`, `--role <code>`, `--superuser`, `--password` (deprecated) | Creates an activated backend administrator. `--login` defaults to the lower-cased email. The password is read from a hidden prompt, or from stdin when the input is not a terminal. |
| `admin:reset-password` | `<identifier>` (login or email), `--password` (deprecated) | Sets a new password, read like the one of `admin:create`, and revokes every JWT issued before the reset. Personal API tokens are unchanged. |
| `admin:token:create` | `<identifier>`, `--name`, `--scope` (repeatable), `--expires` (`30d`, `90d`, `365d`, `never`, or `YYYY-MM-DD`) | Creates a personal API token for the named administrator and prints exactly the plaintext and a newline on stdout. |
| `admin:token:list` | `[<identifier>]`, `--revoked` | Lists tokens (hint only, never the hash or plaintext). |
| `admin:token:create` | `<identifier>`, `--name`, `--scope` (repeatable), `--expires` (`30d`, `90d`, `365d`, `never`, or `YYYY-MM-DD`), `--json` | Creates a personal API token for the named administrator and prints exactly the plaintext and a newline on stdout, also under `-q`. `--json` prints `id`, `name`, `token`, `scopes` and `expires_at`. |
| `admin:token:list` | `[<identifier>]`, `--revoked`, `--json` | Lists tokens (hint only, never the hash or plaintext); `--json` as a JSON array. |
| `admin:token:revoke` | `<id>` | Soft-revokes any user's active token by id. |
| `admin:token:scopes` | none | Lists framework and plugin scopes available for tokens. |
| `admin:token:scopes` | `--json` | Lists framework and plugin scopes available for tokens; `--json` as a JSON array. |
```sh
./bin/acme admin:create --email admin@example.com --superuser
@@ -133,7 +145,7 @@ When the admin is mounted and the `backend_users` table is empty, `serve` prints
| Command | Arguments and flags | Purpose |
|---------|---------------------|---------|
| `queue:work` | `--queue <name>`, repeatable | Runs a job worker in the foreground on the named queues (default: every known queue) until SIGINT or SIGTERM, with a startup summary and a shutdown report. An unknown queue is an error that lists the known ones. |
| `queue:clear` | `[queue]` (default `default`) | Deletes the waiting, scheduled and retryable jobs of one queue and prints how many it cleared. Running jobs are never touched. |
| `queue:clear` | `[queue]` (default `default`), `--force` | Deletes the waiting, scheduled and retryable jobs of one queue and prints `Cleared N jobs from <queue>`. Running jobs are never touched. |
| `schedule:run` | `--once` | Without `--once`, runs a scheduler-only worker until stopped, with a startup summary and a shutdown report. With `--once`, runs the entries due in the current minute, prints `Ran N due entries (M failed)` and exits, for system cron. |
```sh
@@ -152,9 +164,9 @@ These commands are not added by the generated `main`. An application that uses C
| Command | Arguments and flags | Purpose |
|---------|---------------------|---------|
| `websockets:health` | none | Calls the Centrifugo `info` API and prints the configuration. Exits 1 when the API key is missing or the call fails. The key itself is never printed. |
| `websockets:health` | `--json` | Calls the Centrifugo `info` API and prints the configuration. Exits 1 when the API key is missing or the call fails. The key itself is never printed. `--json` prints `healthy`, `api_url`, `enabled`, `api_key_set` and `error` for monitoring. |
| `websockets:generate-vapid-keys` | `--update`, `--show-current` | Shows the configured VAPID keys, truncated, then generates a new pair. With `--update` it saves them to the environment's `overrides.yaml`; without it, it prints the variables to set by hand. |
| `websockets:test-push` | `<user_id>`, `--show-config` | Lists a user's push subscriptions and, after confirmation, sends one encrypted test notification to each. |
| `websockets:test-push` | `<user_id>`, `--show-config` | Lists a user's push subscriptions and, after confirmation (yes by default, so `-n` sends without asking), sends one encrypted test notification to each. |
```sh
./bin/acme websockets:health

View File

@@ -179,6 +179,19 @@ fmt.Println("exit code:", code)
A command that has already printed its problems, such as a checker that lists every finding, returns `bonfire.Reported(err)`: the binary exits 1 and prints nothing more. A mistake in how the command was called, such as an unknown flag or a missing argument, exits 2 and the block suggests `help <command>`; wrap your own usage checks with `bonfire.Usage` to get the same exit code.
An application binary also explains the most common mistake: running it from the wrong directory. When a command fails, the working directory has no `config/` directory and a parent directory holds `summer.yaml`, the block says "This command runs from the application root." and suggests the same command from there:
```text
ERROR lagoon: database.dsn is empty (set SUMMER_DATABASE__DSN)
This command runs from the application root.
Try:
cd ../..; bin/acme migrate
```
Only the message changes: the binary still reads its configuration from `config/` in the working directory. `summer` is not affected, because its commands find the application root themselves.
## Documentation coverage
`bonfire.CheckDocs` reports each command that lacks a description, long help, an example, or a description of one of its arguments or options. It only reports; the framework's own test fails on any problem, and a plugin repository can call it from a test and log the problems: