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
103 changes: 88 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,32 +186,105 @@ flashduty field list [flags] # List custom field definitions

Supports `--name`.

### `statuspage` - Status Page Management (5 command groups)
### `status-page` - Status Page Management (28 commands)

The group is `status-page` (hyphenated), not `statuspage`. Nested object/array
fields carry no typed flag and must be supplied as JSON through `--data`;
`--data -` reads the entire request body from stdin. Positional arguments and
explicitly-set typed flags override the matching keys inside `--data`.

**Pages, components, sections**

```bash
flashduty status-page list # List status pages (JSON: {"items":[...]})
flashduty status-page info <page-id> # Page detail, incl. component and section IDs
flashduty status-page create --name <name> --url-name <slug> --type <public|internal> \
--date-view <calendar|list> --display-uptime-mode <chart_and_percentage|chart|none>
flashduty status-page update <page-id> [--name <name>] [--url-name <slug>] ... # Update a page
flashduty status-page delete <page-id> # Delete a page
flashduty status-page component-upsert <page-id> --data '{"components":[{"name":"API","section_id":"<section-id>"}]}'
flashduty status-page component-delete <component-id> [<id2>...] --page-id <page-id>
flashduty status-page section-upsert <page-id> --data '{"sections":[{"name":"Core"}]}'
flashduty status-page section-delete <section-id> [<id2>...] --page-id <page-id>
```

**Events (incident / maintenance) and their timeline**

```bash
flashduty status-page change-active-list <page-id> --type <incident|maintenance> # Only in-progress events
flashduty status-page change-list <page-id> --type <incident|maintenance> --status <status>
flashduty status-page change-info --page-id <page-id> --change-id <change-id>
flashduty status-page change-create <page-id> --type <incident|maintenance> --title <title> \
--status <status> --description <text> --data '{"updates":[...]}'
flashduty status-page change-update --page-id <page-id> --change-id <change-id> [--title <title>]
flashduty status-page change-delete --page-id <page-id> --change-id <change-id>
flashduty status-page change-timeline-create --page-id <page-id> --change-id <change-id> \
--status <status> --description <text> [--data '{"component_changes":[...]}']
flashduty status-page change-timeline-update --page-id <page-id> --change-id <change-id> --update-id <update-id> [--description <text>]
flashduty status-page change-timeline-delete --page-id <page-id> --change-id <change-id> --update-id <update-id>
```

`change-create` takes `<page-id>` as a **required positional argument**, and its
required `updates` array (with the nested `component_changes`) has no flag — so a
real `change-create` call always carries a `--data` payload:

```bash
flashduty status-page change-create 5750613685214 --type incident \
--title "API latency elevated" --status investigating \
--description "Investigating elevated latency." \
--data '{"updates":[{"status":"investigating","description":"Team is investigating.","component_changes":[{"component_id":"01KC3GAZ6ZJE40H55GM31RPWZE","status":"degraded"}]}]}'
```

The whole body can also come from stdin with `--data -`:

```bash
cat change.json | flashduty status-page change-create 5750613685214 --data -
```

Resolving an incident goes through `change-timeline-create`; every component the
event touched must be moved back to `operational`:

```bash
flashduty status-page change-timeline-create --page-id 5750613685214 --change-id 5821693893131 \
--status resolved --description "Recovered." \
--data '{"component_changes":[{"component_id":"01KC3GAZ6ZJE40H55GM31RPWZE","status":"operational"}]}'
```

**Subscribers and templates**

```bash
flashduty status-page subscriber-list <page-id> [--component-ids <ids>] [--page <n>] [--limit <n>]
flashduty status-page subscriber-import <page-id> --method <email|im> --data '{"subscribers":[...]}'
flashduty status-page subscriber-export <page-id> [--component-ids <ids>]
flashduty status-page template-list <page-id> --type <pre_defined|message>
flashduty status-page template-upsert <page-id> --type <pre_defined|message> --data '{"template":{...}}'
flashduty status-page template-delete --page-id <page-id> --template-id <template-id> --type <pre_defined|message>
```

**Migration from Atlassian Statuspage**

```bash
flashduty statuspage list [--id <ids>] # List status pages
flashduty statuspage changes --page-id <id> --type <incident|maintenance> # List active changes
flashduty statuspage create-incident --page-id <id> --title <title> # Create status incident
flashduty statuspage create-timeline --page-id <id> --change <id> --message <msg> # Add timeline update
flashduty statuspage migrate structure --from atlassian --source-page-id <id> --api-key <key> # Start structure/history migration
flashduty statuspage migrate email-subscribers --from atlassian --source-page-id <id> --target-page-id <id> --api-key <key> # Start email subscriber migration
flashduty statuspage migrate status --job-id <id> # Check migration job status
flashduty statuspage migrate cancel --job-id <id> # Cancel a running migration job
flashduty status-page migrate-structure <source-page-id> --api-key <key> [--url-name <slug>] # Structure + history
flashduty status-page migrate-email-subscribers --source-page-id <id> --target-page-id <id> --api-key <key>
flashduty status-page migration-status <job-id> # Check migration job status
flashduty status-page migration-cancel <job-id> # Cancel a running migration job
```

Migration jobs are asynchronous. After starting `structure` or `email-subscribers`, use:
Migration jobs are asynchronous. After starting `migrate-structure` or
`migrate-email-subscribers`, poll the returned `job_id`:

```bash
flashduty statuspage migrate status --job-id <job_id>
flashduty status-page migration-status <job-id>
```

Typical flow:

```bash
flashduty statuspage migrate structure --from atlassian --source-page-id page_123 --api-key $ATLASSIAN_STATUSPAGE_API_KEY
flashduty statuspage migrate status --job-id <structure_job_id>
flashduty statuspage migrate email-subscribers --from atlassian --source-page-id page_123 --target-page-id <target_page_id> --api-key $ATLASSIAN_STATUSPAGE_API_KEY
flashduty statuspage migrate status --job-id <subscriber_job_id>
flashduty status-page migrate-structure page_123 --api-key $ATLASSIAN_STATUSPAGE_API_KEY
flashduty status-page migration-status <structure_job_id>
flashduty status-page migrate-email-subscribers --source-page-id page_123 \
--target-page-id <target_page_id> --api-key $ATLASSIAN_STATUSPAGE_API_KEY
flashduty status-page migration-status <subscriber_job_id>
```

### `template` - Notification Template Management (4 commands)
Expand Down
99 changes: 94 additions & 5 deletions README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,13 +185,102 @@ flashduty field list [flags] # 列出自定义字段定义

支持 `--name`。

### `statuspage` - 状态页管理(4 个命令)
### `status-page` - 状态页管理(28 个命令)

命令组名是 `status-page`(带连字符),不是 `statuspage`。嵌套对象、数组类字段没有
对应的 flag,必须通过 `--data` 传 JSON;`--data -` 表示整个请求体从 stdin 读取。
位置参数和显式设置的 flag 会覆盖 `--data` 里的同名字段。

**状态页、组件、分组**

```bash
flashduty status-page list # 列出状态页(JSON 形如 {"items":[...]})
flashduty status-page info <page-id> # 状态页详情,含组件 ID 和分组 ID
flashduty status-page create --name <name> --url-name <slug> --type <public|internal> \
--date-view <calendar|list> --display-uptime-mode <chart_and_percentage|chart|none>
flashduty status-page update <page-id> [--name <name>] [--url-name <slug>] ... # 更新状态页
flashduty status-page delete <page-id> # 删除状态页
flashduty status-page component-upsert <page-id> --data '{"components":[{"name":"API","section_id":"<section-id>"}]}'
flashduty status-page component-delete <component-id> [<id2>...] --page-id <page-id>
flashduty status-page section-upsert <page-id> --data '{"sections":[{"name":"核心服务"}]}'
flashduty status-page section-delete <section-id> [<id2>...] --page-id <page-id>
```

**事件(故障 / 维护)与时间线**

```bash
flashduty status-page change-active-list <page-id> --type <incident|maintenance> # 只列进行中的事件
flashduty status-page change-list <page-id> --type <incident|maintenance> --status <status>
flashduty status-page change-info --page-id <page-id> --change-id <change-id>
flashduty status-page change-create <page-id> --type <incident|maintenance> --title <title> \
--status <status> --description <text> --data '{"updates":[...]}'
flashduty status-page change-update --page-id <page-id> --change-id <change-id> [--title <title>]
flashduty status-page change-delete --page-id <page-id> --change-id <change-id>
flashduty status-page change-timeline-create --page-id <page-id> --change-id <change-id> \
--status <status> --description <text> [--data '{"component_changes":[...]}']
flashduty status-page change-timeline-update --page-id <page-id> --change-id <change-id> --update-id <update-id> [--description <text>]
flashduty status-page change-timeline-delete --page-id <page-id> --change-id <change-id> --update-id <update-id>
```

`change-create` 的 `<page-id>` 是**必填位置参数**;必填的 `updates` 数组(以及嵌套在里面的
`component_changes`)没有对应的 flag,所以真实的 `change-create` 调用一定带 `--data`:

```bash
flashduty status-page change-create 5750613685214 --type incident \
--title "API 延迟升高" --status investigating \
--description "正在排查延迟升高问题。" \
--data '{"updates":[{"status":"investigating","description":"团队正在排查。","component_changes":[{"component_id":"01KC3GAZ6ZJE40H55GM31RPWZE","status":"degraded"}]}]}'
```

整个请求体也可以用 `--data -` 从 stdin 读:

```bash
cat change.json | flashduty status-page change-create 5750613685214 --data -
```

关闭事件走 `change-timeline-create`,并且事件涉及的每个组件都要改回 `operational`:

```bash
flashduty status-page change-timeline-create --page-id 5750613685214 --change-id 5821693893131 \
--status resolved --description "已恢复。" \
--data '{"component_changes":[{"component_id":"01KC3GAZ6ZJE40H55GM31RPWZE","status":"operational"}]}'
```

**订阅者与模板**

```bash
flashduty status-page subscriber-list <page-id> [--component-ids <ids>] [--page <n>] [--limit <n>]
flashduty status-page subscriber-import <page-id> --method <email|im> --data '{"subscribers":[...]}'
flashduty status-page subscriber-export <page-id> [--component-ids <ids>]
flashduty status-page template-list <page-id> --type <pre_defined|message>
flashduty status-page template-upsert <page-id> --type <pre_defined|message> --data '{"template":{...}}'
flashduty status-page template-delete --page-id <page-id> --template-id <template-id> --type <pre_defined|message>
```

**从 Atlassian Statuspage 迁移**

```bash
flashduty status-page migrate-structure <source-page-id> --api-key <key> [--url-name <slug>] # 迁移结构与历史
flashduty status-page migrate-email-subscribers --source-page-id <id> --target-page-id <id> --api-key <key>
flashduty status-page migration-status <job-id> # 查询迁移任务状态
flashduty status-page migration-cancel <job-id> # 取消正在跑的迁移任务
```

迁移任务是异步的。启动 `migrate-structure` 或 `migrate-email-subscribers` 之后,
用返回的 `job_id` 轮询:

```bash
flashduty status-page migration-status <job-id>
```

典型流程:

```bash
flashduty statuspage list [--id <ids>] # 列出状态页
flashduty statuspage changes --page-id <id> --type <incident|maintenance> # 列出活跃的变更
flashduty statuspage create-incident --page-id <id> --title <title> # 创建状态页事件
flashduty statuspage create-timeline --page-id <id> --change <id> --message <msg> # 添加时间线更新
flashduty status-page migrate-structure page_123 --api-key $ATLASSIAN_STATUSPAGE_API_KEY
flashduty status-page migration-status <structure_job_id>
flashduty status-page migrate-email-subscribers --source-page-id page_123 \
--target-page-id <target_page_id> --api-key $ATLASSIAN_STATUSPAGE_API_KEY
flashduty status-page migration-status <subscriber_job_id>
```

### `template` - 通知模板管理(4 个命令)
Expand Down
2 changes: 1 addition & 1 deletion e2e/auth_global_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func TestHelpForEverySubcommand(t *testing.T) {
// Test 110: all top-level commands show help without errors
commands := []string{
"channel", "member", "team", "field", "escalation-rule",
"statuspage", "template", "change", "config", "login",
"status-page", "template", "change", "config", "login",
}
for _, cmd := range commands {
t.Run(cmd, func(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion e2e/edge_case_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func TestNoTruncOnFieldList(t *testing.T) {
}

func TestNoTruncOnStatusPageList(t *testing.T) {
r := runCLI(t, "statuspage", "list", "--no-trunc")
r := runCLI(t, "status-page", "list", "--no-trunc")
requireSuccess(t, r)
}

Expand Down
20 changes: 15 additions & 5 deletions e2e/resource_list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package e2e_test

import (
"strings"
"testing"
)

Expand Down Expand Up @@ -113,16 +114,25 @@ func TestChangeListJSON(t *testing.T) {
// StatusPage
// ---------------------------------------------------------------------------

// Test 248: statuspage list
// Test 248: status-page list
//
// `status-page list` is a generated command with no displayColumns entry, so the
// table columns are the reflective heuristic: the first 8 scalar fields of
// StatusPageItem, headed by their upper-cased JSON tag. Components/sections are
// nested arrays and are skipped by that heuristic, so there is no COMPONENTS
// column, and PAGE_ID/NAME/URL_NAME fall past the 8-column cut.
func TestStatusPageList(t *testing.T) {
r := runCLI(t, "statuspage", "list")
r := runCLI(t, "status-page", "list")
requireSuccess(t, r)
requireTableHeaders(t, r.Stdout, "ID", "NAME", "SLUG", "STATUS", "COMPONENTS")
if strings.HasPrefix(strings.TrimSpace(r.Stdout), "No results.") {
t.Skip("no status pages available")
}
requireTableHeaders(t, r.Stdout, "CONTACT_INFO", "CUSTOM_DOMAIN", "DATE_VIEW", "DISPLAY_UPTIME_MODE")
}

// Test 252: statuspage list JSON
// Test 252: status-page list JSON
func TestStatusPageListJSON(t *testing.T) {
r := runCLI(t, "statuspage", "list", "--json")
r := runCLI(t, "status-page", "list", "--json")
requireSuccess(t, r)
requireValidJSON(t, r.Stdout)
}
Loading