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:
@@ -134,7 +134,7 @@ if err := app.Publish[flare.SubscriptionSource](blogSubscriptions{db: gdb}); err
|
||||
| `websockets:generate-vapid-keys [--update] [--show-current]` | Shows the configured keys, truncated to the first 8 and last 4 characters with their length and a check mark when they decode to a valid pair. `--show-current` stops there. Otherwise it generates a new P-256 pair, validates its length and base64url alphabet and prints both keys. With `--update` the keys are saved through `compass.Config.Set` and `compass.Config.Persist` to `env/<environment>/overrides.yaml` in the config directory (mode 0600; other keys in the file are kept). Without it, the command prints `SUMMER_PUSH__PUBLIC_KEY=…` and `SUMMER_PUSH__PRIVATE_KEY=…` lines to set by hand. |
|
||||
| `websockets:test-push <user_id> [--show-config]` | Prints the push configuration: enabled, whether each key is set with its length (never the value), the subject and its format. It then reads the user's subscriptions from the published `flare.SubscriptionSource`, lists them (endpoint shortened to 60 characters, user agent, when subscribed and last used) and asks `Send test notification?` (default yes; a non-interactive run takes the default). It sends one encrypted test push to each subscription and reports each result. `--show-config` is accepted for compatibility; the configuration is always shown. |
|
||||
|
||||
`websockets:test-push` exits 1 when no subscription source is published (`no subscription source registered`), when the user is unknown or has no subscriptions, when push is disabled (it lists the subscriptions but sends nothing), and when any send fails. The test payload is `{"title":"<app.name> test","body":"This is a test push notification sent at HH:MM:SS","data":{"test":true,"timestamp":<unix>}}`.
|
||||
Both commands print their own failure messages and return `bonfire.Reported` errors, so the binary exits 1 without a second error block. `websockets:test-push` exits 1 when no subscription source is published (`no subscription source registered`), when the user is unknown or has no subscriptions, when push is disabled (it lists the subscriptions but sends nothing), and when any send fails. The test payload is `{"title":"<app.name> test","body":"This is a test push notification sent at HH:MM:SS","data":{"test":true,"timestamp":<unix>}}`.
|
||||
|
||||
## Dependencies
|
||||
|
||||
|
||||
@@ -14,12 +14,16 @@ import (
|
||||
"git.golem15.com/golem15/summercms/modules/bonfire"
|
||||
)
|
||||
|
||||
// Returned after a command has printed why it failed, so the binary exits 1
|
||||
// without repeating the message.
|
||||
var (
|
||||
errGenerateFailed = errors.New(GenerateVAPIDKeysCommandName + ": failed")
|
||||
errTestPushFailed = errors.New(TestPushCommandName + ": failed")
|
||||
)
|
||||
// generateFailed and testPushFailed are returned after a command has
|
||||
// printed why it failed: bonfire.Reported makes the binary exit 1 without
|
||||
// repeating the message.
|
||||
func generateFailed() error {
|
||||
return bonfire.Reported(errors.New(GenerateVAPIDKeysCommandName + ": failed"))
|
||||
}
|
||||
|
||||
func testPushFailed() error {
|
||||
return bonfire.Reported(errors.New(TestPushCommandName + ": failed"))
|
||||
}
|
||||
|
||||
// Names of the push console commands.
|
||||
const (
|
||||
@@ -49,6 +53,16 @@ func generateVAPIDKeysCommand(app *backpack.App) bonfire.Command {
|
||||
return bonfire.Command{
|
||||
Name: GenerateVAPIDKeysCommandName,
|
||||
Description: "Generate VAPID keys for Web Push notifications",
|
||||
Long: `Shows the configured VAPID keys (truncated), generates a new P-256 key pair,
|
||||
validates it and prints both keys in full with the environment lines to set.
|
||||
--update writes them to the environment's overrides file instead of leaving
|
||||
that to you; --show-current only shows the configured keys. Changing the keys
|
||||
invalidates every existing browser push subscription.`,
|
||||
Examples: []bonfire.Example{
|
||||
{Command: GenerateVAPIDKeysCommandName, Description: "Generate a key pair and print it"},
|
||||
{Command: GenerateVAPIDKeysCommandName + " --show-current", Description: "Show the configured keys and stop"},
|
||||
{Command: GenerateVAPIDKeysCommandName + " --update", Description: "Generate a pair and save it to the overrides file"},
|
||||
},
|
||||
Flags: []bonfire.Flag{
|
||||
{Name: "update", Bare: true, Description: "Save the new keys to the environment's overrides file"},
|
||||
{Name: "show-current", Bare: true, Description: "Show the configured keys (truncated) and stop"},
|
||||
@@ -67,13 +81,13 @@ func generateVAPIDKeysCommand(app *backpack.App) bonfire.Command {
|
||||
keys, err := GenerateVAPIDKeys()
|
||||
if err != nil {
|
||||
out.Error("Failed to generate VAPID keys: " + err.Error())
|
||||
return errGenerateFailed
|
||||
return generateFailed()
|
||||
}
|
||||
out.Success("New keys generated successfully!")
|
||||
out.Println()
|
||||
|
||||
if !validateNewKeys(out, keys) {
|
||||
return errGenerateFailed
|
||||
return generateFailed()
|
||||
}
|
||||
out.Println()
|
||||
out.Println(fmt.Sprintf("Public Key (%d chars):", len(keys.PublicKey)))
|
||||
@@ -88,7 +102,7 @@ func generateVAPIDKeysCommand(app *backpack.App) bonfire.Command {
|
||||
if flagSet(in, "update") {
|
||||
if err := saveKeys(app, keys); err != nil {
|
||||
out.Error("Failed to save the keys: " + err.Error())
|
||||
return errGenerateFailed
|
||||
return generateFailed()
|
||||
}
|
||||
out.Success(fmt.Sprintf("Keys saved to env/%s/overrides.yaml in the config directory", app.Config.Environment()))
|
||||
out.Println()
|
||||
@@ -211,6 +225,15 @@ func testPushCommand(app *backpack.App) bonfire.Command {
|
||||
return bonfire.Command{
|
||||
Name: TestPushCommandName,
|
||||
Description: "Test push notification system and send test notification",
|
||||
Long: `Shows the push configuration, looks up the user's browser subscriptions through
|
||||
the application's subscription source and, after a confirmation (yes by
|
||||
default, so -n sends without asking), sends each one a test notification. It
|
||||
exits 1 when the configuration is incomplete, the user has no subscription
|
||||
or any send fails, after listing the common causes.`,
|
||||
Examples: []bonfire.Example{
|
||||
{Command: TestPushCommandName + " 42", Description: "Send a test notification to user 42's browsers"},
|
||||
{Command: TestPushCommandName + " 42 -n", Description: "Send without the confirmation question"},
|
||||
},
|
||||
Args: []bonfire.Arg{{Name: "user_id", Description: "User ID to send test notification to", Required: true}},
|
||||
Flags: []bonfire.Flag{
|
||||
// Accepted for compatibility: the configuration is always shown.
|
||||
@@ -223,7 +246,7 @@ func testPushCommand(app *backpack.App) bonfire.Command {
|
||||
svc, err := From(app)
|
||||
if err != nil {
|
||||
out.Error(err.Error())
|
||||
return errTestPushFailed
|
||||
return testPushFailed()
|
||||
}
|
||||
cfg := svc.Config()
|
||||
showPushConfig(out, cfg)
|
||||
@@ -232,23 +255,23 @@ func testPushCommand(app *backpack.App) bonfire.Command {
|
||||
userID, err := strconv.ParseUint(strings.TrimSpace(rawID), 10, 64)
|
||||
if err != nil || userID == 0 || userID > uint64(^uint(0)) {
|
||||
out.Error(fmt.Sprintf("User %s not found", rawID))
|
||||
return errTestPushFailed
|
||||
return testPushFailed()
|
||||
}
|
||||
|
||||
src, ok := app.Lookup[SubscriptionSource]()
|
||||
if !ok || src == nil {
|
||||
out.Error("no subscription source registered")
|
||||
out.Println("The application must publish a flare.SubscriptionSource to read stored push subscriptions.")
|
||||
return errTestPushFailed
|
||||
return testPushFailed()
|
||||
}
|
||||
subs, err := src.Subscriptions(ctx, uint(userID))
|
||||
if errors.Is(err, ErrUserNotFound) {
|
||||
out.Error(fmt.Sprintf("User %d not found", userID))
|
||||
return errTestPushFailed
|
||||
return testPushFailed()
|
||||
}
|
||||
if err != nil {
|
||||
out.Error("Failed to read push subscriptions: " + err.Error())
|
||||
return errTestPushFailed
|
||||
return testPushFailed()
|
||||
}
|
||||
out.Info(fmt.Sprintf("Testing push for user ID %d", userID))
|
||||
out.Println()
|
||||
@@ -265,7 +288,7 @@ func testPushCommand(app *backpack.App) bonfire.Command {
|
||||
out.Println(" - Check browser console for errors")
|
||||
out.Println(" - Ensure HTTPS is being used")
|
||||
out.Println(" - Verify service worker is registered")
|
||||
return errTestPushFailed
|
||||
return testPushFailed()
|
||||
}
|
||||
|
||||
out.Success(fmt.Sprintf("Found %d subscription(s)", len(subs)))
|
||||
@@ -291,7 +314,7 @@ func testPushCommand(app *backpack.App) bonfire.Command {
|
||||
}
|
||||
if !cfg.Enabled {
|
||||
out.Error("Push is disabled (push.enabled is false); no notification was sent")
|
||||
return errTestPushFailed
|
||||
return testPushFailed()
|
||||
}
|
||||
return sendTestPush(ctx, out, svc, appName(app), subs)
|
||||
},
|
||||
@@ -378,7 +401,7 @@ func sendTestPush(ctx context.Context, out bonfire.Output, svc *Service, name st
|
||||
out.Println(" - Subscription endpoint no longer valid")
|
||||
out.Println(" - Endpoint host not in push.allowed_hosts")
|
||||
out.Println(" - Push service unavailable or network connectivity issues")
|
||||
return errTestPushFailed
|
||||
return testPushFailed()
|
||||
}
|
||||
out.Success("Test notification sent successfully!")
|
||||
out.Println("Check the browser for the notification.")
|
||||
|
||||
Reference in New Issue
Block a user