From 933f98586e5b4f74e95c814aae85a7d5ee2b7fc9 Mon Sep 17 00:00:00 2001 From: Quentin Rousseau Date: Thu, 4 Jun 2026 02:34:52 -0700 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=90=9B=20fix:=20resolve=20incidents?= =?UTF-8?q?=20get/update/delete=20with=20sequential=20IDs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Rootly API accepts UUID, slug, or bare numeric sequential ID but not the "INC-123" display format. NormalizeIncidentID strips the prefix so `rootly incidents get INC-31` hits /v1/incidents/31 instead of /v1/incidents/INC-31 which 404'd. Closes #28 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- internal/api/client.go | 12 ++++++++ internal/api/client_test.go | 25 +++++++++++++++++ internal/cmd/incidents/cmd_test.go | 45 ++++++++++++++++++++++++++++++ internal/cmd/incidents/delete.go | 9 ++++-- internal/cmd/incidents/get.go | 3 +- internal/cmd/incidents/update.go | 6 ++-- 6 files changed, 93 insertions(+), 7 deletions(-) diff --git a/internal/api/client.go b/internal/api/client.go index 6cfaa6e..68f3481 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -8,6 +8,7 @@ import ( "net/http" "net/http/httputil" "os" + "regexp" "strings" "time" @@ -1008,6 +1009,17 @@ func (c *Client) ListIncidentsCLI(ctx context.Context, page, pageSize int, sort }, nil } +var incidentSeqIDPattern = regexp.MustCompile(`(?i)^INC-(\d+)$`) + +// NormalizeIncidentID strips the "INC-" prefix from sequential IDs so the +// bare number can be passed to the API (which accepts UUID, slug, or numeric sequential ID). +func NormalizeIncidentID(id string) string { + if m := incidentSeqIDPattern.FindStringSubmatch(id); m != nil { + return m[1] + } + return id +} + // GetIncidentByID fetches incident detail by ID without requiring updatedAt parameter. // Does not use cache (stateless). func (c *Client) GetIncidentByID(ctx context.Context, id string) (*Incident, error) { diff --git a/internal/api/client_test.go b/internal/api/client_test.go index 500ca40..e46a526 100644 --- a/internal/api/client_test.go +++ b/internal/api/client_test.go @@ -55,6 +55,31 @@ func TestNewClientWithHTTPS(t *testing.T) { } } +func TestNormalizeIncidentID(t *testing.T) { + tests := []struct { + input string + want string + }{ + {"INC-42", "42"}, + {"INC-1", "1"}, + {"inc-99", "99"}, + {"Inc-123", "123"}, + {"e5923856-6fe8-4a2c-b0eb-cb783e811d06", "e5923856-6fe8-4a2c-b0eb-cb783e811d06"}, + {"some-slug", "some-slug"}, + {"INC-", "INC-"}, + {"INC-abc", "INC-abc"}, + {"42", "42"}, + } + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + got := NormalizeIncidentID(tt.input) + if got != tt.want { + t.Errorf("NormalizeIncidentID(%q) = %q, want %q", tt.input, got, tt.want) + } + }) + } +} + func TestUserAgentHeader(t *testing.T) { Version = "1.2.3" diff --git a/internal/cmd/incidents/cmd_test.go b/internal/cmd/incidents/cmd_test.go index b1edbc7..ea42d0e 100644 --- a/internal/cmd/incidents/cmd_test.go +++ b/internal/cmd/incidents/cmd_test.go @@ -403,6 +403,51 @@ func TestRunListNoToken(t *testing.T) { } } +func TestRunGetNormalizesSequentialID(t *testing.T) { + setupTestServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v1/incidents/42" { + t.Errorf("expected path /v1/incidents/42, got %s", r.URL.Path) + } + w.Header().Set("Content-Type", "application/vnd.api+json") + w.Write([]byte(getResponse())) + }) + + viper.Set("format", "table") + cmd := newTestCmd() + + output := captureStdout(t, func() { + err := runGet(cmd, []string{"INC-42"}) + if err != nil { + t.Fatalf("runGet returned error: %v", err) + } + }) + + if !strings.Contains(output, "DB outage") { + t.Errorf("expected output to contain 'DB outage', got: %s", output) + } +} + +func TestRunGetAcceptsUUID(t *testing.T) { + uuid := "e5923856-6fe8-4a2c-b0eb-cb783e811d06" + setupTestServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v1/incidents/"+uuid { + t.Errorf("expected UUID in path, got %s", r.URL.Path) + } + w.Header().Set("Content-Type", "application/vnd.api+json") + w.Write([]byte(getResponse())) + }) + + viper.Set("format", "table") + cmd := newTestCmd() + + captureStdout(t, func() { + err := runGet(cmd, []string{uuid}) + if err != nil { + t.Fatalf("runGet returned error: %v", err) + } + }) +} + func TestRunGetAPIError(t *testing.T) { setupTestServer(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) diff --git a/internal/cmd/incidents/delete.go b/internal/cmd/incidents/delete.go index dd5a899..fd5cc4d 100644 --- a/internal/cmd/incidents/delete.go +++ b/internal/cmd/incidents/delete.go @@ -8,6 +8,8 @@ import ( "github.com/mattn/go-isatty" "github.com/spf13/cobra" + + "github.com/rootlyhq/rootly-cli/internal/api" ) var deleteCmd = &cobra.Command{ @@ -55,7 +57,8 @@ func confirmDelete(prompt string, skipConfirm bool) error { } func runDelete(cmd *cobra.Command, args []string) error { - incidentID := args[0] + displayID := args[0] + incidentID := api.NormalizeIncidentID(displayID) // Get API client apiClient, err := getAPIClient() @@ -65,7 +68,7 @@ func runDelete(cmd *cobra.Command, args []string) error { // Handle confirmation skipConfirm, _ := cmd.Flags().GetBool("yes") - prompt := fmt.Sprintf("Delete incident %s? This cannot be undone", incidentID) + prompt := fmt.Sprintf("Delete incident %s? This cannot be undone", displayID) if err := confirmDelete(prompt, skipConfirm); err != nil { if err.Error() == "aborted" { fmt.Fprintln(os.Stderr, "Aborted.") @@ -80,7 +83,7 @@ func runDelete(cmd *cobra.Command, args []string) error { } // Print success message to stdout - _, _ = fmt.Fprintf(os.Stdout, "Deleted incident %s\n", incidentID) + _, _ = fmt.Fprintf(os.Stdout, "Deleted incident %s\n", displayID) return nil } diff --git a/internal/cmd/incidents/get.go b/internal/cmd/incidents/get.go index f46d16c..c6c82fe 100644 --- a/internal/cmd/incidents/get.go +++ b/internal/cmd/incidents/get.go @@ -37,8 +37,7 @@ func init() { } func runGet(cmd *cobra.Command, args []string) error { - // Get incident ID from args - id := args[0] + id := api.NormalizeIncidentID(args[0]) // Get API client apiClient, err := getAPIClient() diff --git a/internal/cmd/incidents/update.go b/internal/cmd/incidents/update.go index 2c572a3..7b7d530 100644 --- a/internal/cmd/incidents/update.go +++ b/internal/cmd/incidents/update.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" + "github.com/rootlyhq/rootly-cli/internal/api" "github.com/rootlyhq/rootly-cli/internal/printer" ) @@ -39,7 +40,8 @@ func init() { } func runUpdate(cmd *cobra.Command, args []string) error { - incidentID := args[0] + displayID := args[0] + incidentID := api.NormalizeIncidentID(displayID) // Get API client apiClient, err := getAPIClient() @@ -78,7 +80,7 @@ func runUpdate(cmd *cobra.Command, args []string) error { } // Print success message to stderr - fmt.Fprintf(os.Stderr, "Updated incident %s\n", incidentID) + fmt.Fprintf(os.Stderr, "Updated incident %s\n", displayID) // Print incident to stdout using configured format format := viper.GetString("format") From bed7cd301edc60c33303b7818190b36ea02d9ac4 Mon Sep 17 00:00:00 2001 From: Quentin Rousseau Date: Thu, 4 Jun 2026 04:57:55 -0700 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=A9=B9=20fix:=20preserve=20original?= =?UTF-8?q?=20INC-xxx=20ID=20in=20get=20error=20messages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keeps displayID separate from normalized ID in runGet, consistent with update and delete commands. Error now shows "failed to get incident INC-999" instead of bare "999". 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- internal/cmd/incidents/get.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/cmd/incidents/get.go b/internal/cmd/incidents/get.go index c6c82fe..5c4d37c 100644 --- a/internal/cmd/incidents/get.go +++ b/internal/cmd/incidents/get.go @@ -37,7 +37,8 @@ func init() { } func runGet(cmd *cobra.Command, args []string) error { - id := api.NormalizeIncidentID(args[0]) + displayID := args[0] + id := api.NormalizeIncidentID(displayID) // Get API client apiClient, err := getAPIClient() @@ -48,7 +49,7 @@ func runGet(cmd *cobra.Command, args []string) error { // Call API incident, err := apiClient.GetIncidentByID(cmd.Context(), id) if err != nil { - return fmt.Errorf("failed to get incident: %w", err) + return fmt.Errorf("failed to get incident %s: %w", displayID, err) } // Get format from viper