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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/flashcatcloud/flashduty-cli
go 1.25.1

require (
github.com/flashcatcloud/flashduty-sdk v0.7.0
github.com/flashcatcloud/flashduty-sdk v0.8.1-0.20260512032214-576d76bd987a
github.com/spf13/cobra v1.10.2
golang.org/x/term v0.42.0
gopkg.in/yaml.v3 v3.0.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/flashcatcloud/flashduty-sdk v0.7.0 h1:yPW8ghyHB60/34fz5sBITXhMWtbsm2mxYVFORgs+jpE=
github.com/flashcatcloud/flashduty-sdk v0.7.0/go.mod h1:dG4eJfdZaj4jNBMwEexbfK/3PmcIMhNeJ88L/DcZzUY=
github.com/flashcatcloud/flashduty-sdk v0.8.1-0.20260512032214-576d76bd987a h1:nnMflbhcqVskLh22MaUpfXesqNegZ0uWUFd2sbenwT8=
github.com/flashcatcloud/flashduty-sdk v0.8.1-0.20260512032214-576d76bd987a/go.mod h1:dG4eJfdZaj4jNBMwEexbfK/3PmcIMhNeJ88L/DcZzUY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
Expand Down
3 changes: 3 additions & 0 deletions internal/cli/status_page_migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func newStatusPageMigrateStructureCmd() *cobra.Command {
var source string
var sourcePageID string
var sourceAPIKey string
var urlName string

cmd := &cobra.Command{
Use: "structure",
Expand All @@ -37,6 +38,7 @@ func newStatusPageMigrateStructureCmd() *cobra.Command {
result, err := ctx.Client.StartStatusPageMigration(cmdContext(ctx.Cmd), &flashduty.StartStatusPageMigrationInput{
SourceAPIKey: sourceAPIKey,
SourcePageID: sourcePageID,
URLName: urlName,
})
if err != nil {
return err
Expand All @@ -50,6 +52,7 @@ func newStatusPageMigrateStructureCmd() *cobra.Command {
cmd.Flags().StringVar(&source, "from", "", "Migration source provider (required)")
cmd.Flags().StringVar(&sourcePageID, "source-page-id", "", "Source page ID in the provider (required)")
cmd.Flags().StringVar(&sourceAPIKey, "api-key", "", "Source provider API key (required)")
cmd.Flags().StringVar(&urlName, "url-name", "", "Optional URL name for a newly created Flashduty public status page; fails if the source page is already mapped to a different URL name")
_ = cmd.MarkFlagRequired("from")
_ = cmd.MarkFlagRequired("source-page-id")
_ = cmd.MarkFlagRequired("api-key")
Expand Down
50 changes: 50 additions & 0 deletions internal/cli/status_page_migrate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ func TestCommandStatusPageMigrateStructureSendsSDKInput(t *testing.T) {
if gotInput.SourcePageID != "src-1" {
t.Errorf("SourcePageID = %q, want src-1", gotInput.SourcePageID)
}
if gotInput.URLName != "" {
t.Errorf("URLName = %q, want empty", gotInput.URLName)
}
if !strings.Contains(out, "Job ID: job-1") {
t.Errorf("missing job id in output:\n%s", out)
}
Expand All @@ -85,6 +88,53 @@ func TestCommandStatusPageMigrateStructureSendsSDKInput(t *testing.T) {
}
}

func TestCommandStatusPageMigrateStructureSendsURLName(t *testing.T) {
saveAndResetGlobals(t)

var gotInput *flashduty.StartStatusPageMigrationInput
mock := &mockStatusPageMigrate{
startStructure: func(_ context.Context, input *flashduty.StartStatusPageMigrationInput) (*flashduty.StartStatusPageMigrationOutput, error) {
gotInput = input
return &flashduty.StartStatusPageMigrationOutput{JobID: "job-url"}, nil
},
}
newClientFn = func() (flashdutyClient, error) { return mock, nil }

_, err := execCommand("statuspage", "migrate", "structure",
"--from", "atlassian",
"--source-page-id", "src-1",
"--api-key", "atlassian-secret",
"--url-name", "customer-facing-status",
)
if err != nil {
t.Fatalf("execCommand: %v", err)
}

if gotInput == nil {
t.Fatal("expected input to be captured")
}
if gotInput.URLName != "customer-facing-status" {
t.Errorf("URLName = %q, want customer-facing-status", gotInput.URLName)
}
}

func TestCommandStatusPageMigrateStructureHelpDescribesURLNameBehavior(t *testing.T) {
cmd := newStatusPageMigrateStructureCmd()
flag := cmd.Flags().Lookup("url-name")
if flag == nil {
t.Fatal("expected --url-name flag to be registered")
}

for _, want := range []string{
"newly created Flashduty public status page",
"already mapped to a different URL name",
} {
if !strings.Contains(flag.Usage, want) {
t.Errorf("--url-name usage missing %q: %s", want, flag.Usage)
}
}
}

func TestCommandStatusPageMigrateStructureRejectsUnsupportedSource(t *testing.T) {
saveAndResetGlobals(t)

Expand Down
3 changes: 3 additions & 0 deletions skills/flashduty-statuspage/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ flashduty statuspage migrate structure --from atlassian --source-page-id <id> --
| `--from` | string | Source provider, currently only `atlassian` (**required**) |
| `--source-page-id` | string | Page ID in the source provider (**required**) |
| `--api-key` | string | Source provider API key (**required**) |
| `--url-name` | string | Optional URL name for the newly created Flashduty public page. It is normalized with the same slug rules as page creation and only applies when the source page is not already mapped. If the source page already maps to a different Flashduty URL name, the command fails instead of changing the existing page. |

Returns a job ID plus the command to poll its status. Human output shows the new Flashduty `target_page_id` once the job reaches the `completed` phase — capture that for the subscriber migration.

Expand Down Expand Up @@ -210,6 +211,7 @@ Import structure first, verify, then import subscribers.
flashduty statuspage migrate structure \
--from atlassian \
--source-page-id page_atl_123 \
--url-name customer-facing-status \
--api-key "$ATLASSIAN_STATUSPAGE_API_KEY"
# → captures Job ID: str_abc

Expand Down Expand Up @@ -250,6 +252,7 @@ flashduty statuspage migrate status --job-id str_abc
- **Page ID** (int) is the Flashduty status page primary key. **Change ID** (int) is the ID of an incident/maintenance within a page. Don't confuse them.
- **Migration is async.** `migrate structure` and `migrate email-subscribers` return immediately with a job ID; the actual work happens on the backend.
- **Two migration jobs, not one.** Structure + history run separately from subscribers. This is deliberate — subscriber import triggers verification emails, so operators verify content first.
- **`--url-name` is create-only.** Use it to choose the public URL slug for a newly created Flashduty page. It does not rename an existing mapped target page; if the Atlassian page has already been migrated to another URL name, retry without `--url-name` or use the mapped page.
- **Migration phases** for the structure job progress in order: `components` → `sections` → `history` (incidents + maintenances) → `templates`. The subscribers job has a single `subscribers` phase.
- **Terminal statuses:** `completed`, `failed`, `cancelled`. Stop polling once any of these appears.
- **`--notify` is subscriber-visible.** In `create-incident`, omit or set `--notify=false` for silent incidents; set `--notify` when you want an announcement.
Expand Down
Loading