Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,37 @@ without `workspaces:read` (or `automations:read`, for automation names in
`nan approvals ls` and `nan runs show`) gets the ids instead, never an error.
`--json` prints the API's objects unchanged, with ids only.

### Notifications

Get told in Slack, Microsoft Teams, Discord or your own HTTPS endpoint when an
automation run waits for approval, fails or finishes, when an automation is
disabled, or when a webhook trigger keeps failing:

```bash
nan notifications ls [--json]
nan notifications show <NAME|id> [--json]
nan notifications add NAME --kind slack|teams|discord|https (--url-file PATH | --url-stdin)
[--event EVENT]... [--no-events] [--automation <name|id>] [--json]
nan notifications edit <NAME|id> [--name NEW] [--enable | --disable] [--url-file PATH | --url-stdin] [--json]
nan notifications rm <NAME|id> [--yes]
nan notifications test <NAME|id> [--json] # exit 1 when not delivered
nan notifications rotate <NAME|id> [--json] # https only: new signing secret, shown ONCE
nan notifications deliveries <NAME|id> [--limit 20] [--json]
nan notifications subscriptions <NAME|id> [--set|--add|--remove EVENT[=AUTOMATION]]... [--clear] [--json]
```

A webhook URL is a credential (whoever has it can post to your channel): it
comes from `--url-file` or a pipe into `--url-stdin`, never from the command
line, and NaN never shows it again (only the host and its last characters).
An `https` channel's signing secret is printed once, to stdout (inside the
JSON with `--json`); requests are signed like NaN's inbound webhooks. A
channel turns itself off after 20 failed deliveries in a row: fix the
receiver and turn it back on with `nan notifications edit NAME --enable`.
A platform token needs `notifications:read` to read and
`notifications:write` to change, added in the portal only. Without
notifications on your account, these commands exit 64 and point at the
portal. Docs: <https://nan.builders/docs/notifications>.

### Exit codes

`nan run`, `nan runs logs -f` and `nan automations run` exit with the run's outcome, so a script can
Expand Down
33 changes: 33 additions & 0 deletions cmd/json_output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ var hintFragments = []string{
"already exists",
"showing the",
"the next runs",
notifSecretNote,
"Store it now",
"previous secret keeps working",
"send a test",
"nan notifications",
"turned off",
}

// jsonFixtures writes the files the write commands read.
Expand All @@ -74,6 +80,10 @@ func jsonFixtures(t *testing.T) (automation, trigger, value string) {

func jsonCases(t *testing.T) map[string][]jsonCase {
automation, trigger, value := jsonFixtures(t)
hook := filepath.Join(t.TempDir(), "hook-url")
if err := os.WriteFile(hook, []byte(testHookURL+"\n"), 0o600); err != nil {
t.Fatal(err)
}
return map[string][]jsonCase{
"nan approvals ls": {{args: []string{"approvals", "ls", "--json"}}},
"nan approvals show": {{args: []string{"approvals", "show", testRunID, "--json"}}},
Expand Down Expand Up @@ -102,6 +112,25 @@ func jsonCases(t *testing.T) map[string][]jsonCase {
{args: []string{"automations", "triggers", "rotate", testAutoID, triggerUUID, "--json"}},
{args: []string{"automations", "triggers", "rotate", testAutoID, triggerUUID, "--url", "--secret", "--no-grace", "--json"}},
},
"nan notifications add": {
{args: []string{"notifications", "add", "deploys", "--kind", "slack", "--url-file", hook, "--json"}},
// The one-time signing secret is in the JSON; the note about it
// goes to stderr only.
{args: []string{"notifications", "add", "ci", "--kind", "https", "--url-stdin", "--event", "run.failed", "--json"}, stdin: testHookHTTPS},
},
"nan notifications deliveries": {{args: []string{"notifications", "deliveries", "ci receiver", "--limit", "3", "--json"}}},
"nan notifications edit": {
{args: []string{"notifications", "edit", "team alerts", "--disable", "--json"}},
{args: []string{"notifications", "edit", "ci receiver", "--url-file", hook, "--json"}},
},
"nan notifications ls": {{args: []string{"notifications", "ls", "--json"}}},
"nan notifications rotate": {{args: []string{"notifications", "rotate", "ci receiver", "--json"}}},
"nan notifications show": {{args: []string{"notifications", "show", "ci receiver", "--json"}}},
"nan notifications subscriptions": {
{args: []string{"notifications", "subscriptions", "ci receiver", "--json"}},
{args: []string{"notifications", "subscriptions", "team alerts", "--add", "run.finished", "--json"}},
},
"nan notifications test": {{args: []string{"notifications", "test", "team alerts", "--json"}}},
"nan run": {
{args: []string{"run", "review", "--json"}, lines: true},
{args: []string{"run", "review", "--detach", "--json"}, code: exitDetached},
Expand Down Expand Up @@ -182,6 +211,9 @@ func TestJSONStdoutIsOnlyJSON(t *testing.T) {
t.Errorf("stdout carries the hint %q:\n%s", h, stdout)
}
}
if strings.Contains(stdout+stderr+msg, "HOOKSECRET") {
t.Error("the webhook URL was printed")
}
if strings.Contains(stdout+stderr+msg, testValue) {
t.Error("the secret value was printed")
}
Expand Down Expand Up @@ -259,6 +291,7 @@ func runJSONCase(t *testing.T, c jsonCase) (code int, msg, stdout, stderr string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p := r.URL.Path
toWrite := p == "/api/auth/me" || p == "/api/account/secrets" || strings.HasPrefix(p, "/v1/secrets") ||
strings.HasPrefix(p, "/v1/notification-channels") ||
strings.HasSuffix(p, "/triggers") || strings.Contains(p, "/triggers/") ||
(strings.HasPrefix(p, "/v1/automations") && r.Method != http.MethodGet && !strings.HasSuffix(p, "/runs"))
if toWrite {
Expand Down
Loading
Loading