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

@@ -226,7 +226,7 @@ queues:
|---------|---------------------|-------------|
| `queue:work` | `--queue <name>`, repeatable | Runs a job worker in the foreground on the named queues (default: every known queue) until SIGINT or SIGTERM. It prints a startup summary on a terminal (environment, queues with their concurrency, database host, whether the `scheduled` queue runs here) and logs it as a `startup summary` record of component `queue` otherwise. On a signal it stops the worker within 10 seconds, prints and logs a `shutdown report` (jobs done, failed and handed back to the queue, uptime, stop errors) and exits 0, or 1 when the worker fails to stop. An unknown queue is an error that lists the known ones. |
| `schedule:run` | `--once` (bare) | Without `--once`: runs a worker on the `scheduled` queue that carries every plugin's periodic jobs, prints a startup summary (entry count and the next due entry; a `startup summary` record of component `schedule` off a terminal) and runs until SIGINT or SIGTERM, then stops within 10 seconds, prints a shutdown report (scheduled commands run and failed, uptime) and exits 0. With `--once`: runs, in-process and without River, every entry due in the current minute of `app.timezone` (a daily entry at its hour and minute; `Every(d)` of a minute or less on every run, a longer one when the minutes since midnight are a multiple of `d`), printing `Running scheduled command: <command> <args>` per entry or `No scheduled commands are ready to run.`. An unregistered command prints a warning and is skipped. It ends with `Ran N due entries (M failed)`; the exit status is the first command error, after every due entry ran. There is no overlap lock: two runs in one minute run the due entries twice, as Laravel does. System cron: `* * * * * cd /app && ./bin/app schedule:run --once`. |
| `queue:clear` | `[queue]` (default `default`) | Deletes the available, scheduled and retryable jobs of one queue in batches until none are left and prints `Cleared N jobs`. Running jobs are never touched. |
| `queue:clear` | `[queue]` (default `default`), `--force` | Deletes the available, scheduled and retryable jobs of one queue in batches until none are left and prints `Cleared N jobs from <queue>`. Running jobs are never touched. In production on an interactive terminal it asks first; `--force` skips the question, and off a terminal it never asks. |
## Dependencies

View File

@@ -2,6 +2,7 @@ package conga
import (
"context"
"errors"
"fmt"
"log/slog"
"os"
@@ -95,17 +96,33 @@ scheduled queue.`,
},
{
Name: "queue:clear",
Description: "Clear all queued jobs, by deleting all pending jobs.",
Description: "Clear pending jobs from a queue",
Long: `Deletes the available, scheduled and retryable jobs of one queue (default
"default") in batches until none are left, and prints how many it deleted.
Running jobs are never touched. In production, on an interactive terminal, it
asks first; --force skips the question. Off a terminal it never asks.`,
Args: []bonfire.Arg{{
Name: "queue",
Description: "The name of the queue to clear (default \"default\")",
}},
Flags: []bonfire.Flag{{
Name: "force",
Description: "Skip the confirmation asked in production on a terminal",
Bare: true,
}},
Examples: []bonfire.Example{
{Command: "queue:clear", Description: "Clear the default queue"},
{Command: "queue:clear imports --force", Description: "Clear the imports queue without the production question"},
},
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
queue, _ := in.Argument("queue")
queue = strings.TrimSpace(queue)
if queue == "" {
queue = defaultQueue
}
if err := confirmProduction(app, in, out, fmt.Sprintf("Delete every pending job of queue %q in production?", queue)); err != nil {
return err
}
return withDB(ctx, app, func() error {
return clearQueue(ctx, app, queue, out)
})
@@ -356,7 +373,33 @@ func clearQueue(ctx context.Context, app *backpack.App, queue string, out bonfir
}
total += len(res.Jobs)
}
out.Info(fmt.Sprintf("Cleared %d jobs", total))
out.Success(fmt.Sprintf("Cleared %d jobs from %s", total, queue))
return nil
}
// confirmProduction asks question before a destructive step when the
// environment is production and out is an interactive terminal, unless
// --force is set (D-28). Off a terminal, under -n or -q and in
// bonfire.Call it never asks. A "no" returns a Reported error (exit 1).
func confirmProduction(app *backpack.App, in bonfire.Input, out bonfire.Output, question string) error {
if in != nil {
if v, ok := in.Flag("force"); ok {
if force, _ := strconv.ParseBool(v); force {
return nil
}
}
}
if app == nil || app.Config == nil || app.Config.Environment() != "production" {
return nil
}
ok, err := bonfire.ConfirmProceed(out, question)
if err != nil {
return err
}
if !ok {
out.Warning("Cancelled.")
return bonfire.Reported(errors.New("queue:clear cancelled"))
}
return nil
}