diff --git a/cmd/rwx/results.go b/cmd/rwx/results.go index d3a9b97..b2cb2a8 100644 --- a/cmd/rwx/results.go +++ b/cmd/rwx/results.go @@ -4,7 +4,10 @@ import ( "encoding/json" "fmt" + "github.com/rwx-cloud/cli/internal/api" "github.com/rwx-cloud/cli/internal/cli" + "github.com/rwx-cloud/cli/internal/errors" + "github.com/rwx-cloud/cli/internal/git" "github.com/spf13/cobra" ) @@ -14,25 +17,49 @@ var ( resultsCmd = &cobra.Command{ GroupID: "outputs", - Use: "results ", + Use: "results [run-id]", Short: "Get results for a run", - Args: cobra.ExactArgs(1), + Args: cobra.MaximumNArgs(1), PreRunE: func(cmd *cobra.Command, args []string) error { return requireAccessToken() }, RunE: func(cmd *cobra.Command, args []string) error { - runID := args[0] useJson := useJsonOutput() + var runID, branchName, repositoryName string + if len(args) > 0 { + runID = args[0] + } else { + branchName = service.GitClient.GetBranch() + repositoryName = git.RepoNameFromOriginUrl(service.GitClient.GetOriginUrl()) + + if branchName == "" || repositoryName == "" { + return fmt.Errorf("unable to determine the current branch and repository from git; please provide a run ID") + } + + if !useJson { + fmt.Printf("Fetching the latest run for %s repository on branch %s...\n", repositoryName, branchName) + } + } + result, err := service.GetRunStatus(cli.GetRunStatusConfig{ - RunID: runID, - Wait: ResultsWait, - Json: useJson, + RunID: runID, + BranchName: branchName, + RepositoryName: repositoryName, + Wait: ResultsWait, + Json: useJson, }) if err != nil { + if runID == "" && errors.Is(err, api.ErrNotFound) { + return fmt.Errorf("no run found for %s repository on branch %s", repositoryName, branchName) + } return err } + if result.RunID == "" { + return fmt.Errorf("no run found for %s repository on branch %s", repositoryName, branchName) + } + if useJson { jsonOutput := struct { RunID string @@ -49,13 +76,16 @@ var ( } fmt.Println(string(resultJson)) } else { + if result.RunURL != "" { + fmt.Printf("Run URL: %s\n", result.RunURL) + } if result.Completed { fmt.Printf("Run result status: %s\n", result.ResultStatus) } else { fmt.Printf("Run status: %s (in progress)\n", result.ResultStatus) } - promptResult, err := service.GetRunPrompt(runID) + promptResult, err := service.GetRunPrompt(result.RunID) if err == nil { fmt.Printf("\n%s", promptResult.Prompt) } diff --git a/internal/api/client.go b/internal/api/client.go index e598fc2..75b085c 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -713,7 +713,16 @@ func (c Client) TaskIDStatus(cfg TaskIDStatusConfig) (TaskStatusResult, error) { } func (c Client) RunStatus(cfg RunStatusConfig) (RunStatusResult, error) { - endpoint := fmt.Sprintf("/mint/api/runs/%s?fail_fast=true", url.PathEscape(cfg.RunID)) + var endpoint string + if cfg.RunID != "" { + endpoint = fmt.Sprintf("/mint/api/runs/%s?fail_fast=true", url.PathEscape(cfg.RunID)) + } else { + params := url.Values{} + params.Set("fail_fast", "true") + params.Set("branch_name", cfg.BranchName) + params.Set("repository_name", cfg.RepositoryName) + endpoint = "/mint/api/runs/latest?" + params.Encode() + } result := RunStatusResult{} req, err := http.NewRequest(http.MethodGet, endpoint, nil) diff --git a/internal/api/config.go b/internal/api/config.go index 425ad89..ce3d326 100644 --- a/internal/api/config.go +++ b/internal/api/config.go @@ -262,7 +262,9 @@ type ArtifactDownloadRequestResult struct { } type RunStatusConfig struct { - RunID string + RunID string + BranchName string + RepositoryName string } type RunStatus struct { @@ -272,6 +274,7 @@ type RunStatus struct { type RunStatusResult struct { Status *RunStatus `json:"run_status,omitempty"` RunID string `json:"run_id,omitempty"` + RunURL string `json:"run_url,omitempty"` Polling PollingResult `json:"polling"` } diff --git a/internal/cli/service_wait.go b/internal/cli/service_wait.go index 6bfd101..54b34cf 100644 --- a/internal/cli/service_wait.go +++ b/internal/cli/service_wait.go @@ -8,13 +8,16 @@ import ( ) type GetRunStatusConfig struct { - RunID string - Wait bool - Json bool + RunID string + BranchName string + RepositoryName string + Wait bool + Json bool } type GetRunStatusResult struct { RunID string + RunURL string ResultStatus string Completed bool } @@ -26,7 +29,11 @@ func (s Service) GetRunStatus(cfg GetRunStatusConfig) (*GetRunStatusResult, erro } for { - statusResult, err := s.APIClient.RunStatus(api.RunStatusConfig{RunID: cfg.RunID}) + statusResult, err := s.APIClient.RunStatus(api.RunStatusConfig{ + RunID: cfg.RunID, + BranchName: cfg.BranchName, + RepositoryName: cfg.RepositoryName, + }) if err != nil { if stopSpinner != nil { stopSpinner() @@ -43,8 +50,13 @@ func (s Service) GetRunStatus(cfg GetRunStatusConfig) (*GetRunStatusResult, erro if stopSpinner != nil { stopSpinner() } + runID := cfg.RunID + if statusResult.RunID != "" { + runID = statusResult.RunID + } return &GetRunStatusResult{ - RunID: cfg.RunID, + RunID: runID, + RunURL: statusResult.RunURL, ResultStatus: status, Completed: statusResult.Polling.Completed, }, nil diff --git a/internal/cli/service_wait_test.go b/internal/cli/service_wait_test.go index 8e4e99c..1fe661b 100644 --- a/internal/cli/service_wait_test.go +++ b/internal/cli/service_wait_test.go @@ -160,6 +160,112 @@ func TestService_GetRunStatus(t *testing.T) { require.Contains(t, err.Error(), "unable to get run status") }) + t.Run("resolves run by branch and repository name", func(t *testing.T) { + setup := setupTest(t) + + setup.mockAPI.MockRunStatus = func(cfg api.RunStatusConfig) (api.RunStatusResult, error) { + require.Equal(t, "", cfg.RunID) + require.Equal(t, "main", cfg.BranchName) + require.Equal(t, "cli", cfg.RepositoryName) + return api.RunStatusResult{ + Status: &api.RunStatus{Result: "succeeded"}, + RunID: "resolved-run-id", + RunURL: "https://cloud.rwx.com/mint/org/runs/resolved-run-id", + Polling: api.PollingResult{Completed: true}, + }, nil + } + + result, err := setup.service.GetRunStatus(cli.GetRunStatusConfig{ + BranchName: "main", + RepositoryName: "cli", + Wait: false, + Json: false, + }) + + require.NoError(t, err) + require.Equal(t, "resolved-run-id", result.RunID) + require.Equal(t, "https://cloud.rwx.com/mint/org/runs/resolved-run-id", result.RunURL) + require.Equal(t, "succeeded", result.ResultStatus) + require.True(t, result.Completed) + }) + + t.Run("returns ErrNotFound when API returns 404 for branch lookup", func(t *testing.T) { + setup := setupTest(t) + + setup.mockAPI.MockRunStatus = func(cfg api.RunStatusConfig) (api.RunStatusResult, error) { + return api.RunStatusResult{}, api.ErrNotFound + } + + _, err := setup.service.GetRunStatus(cli.GetRunStatusConfig{ + BranchName: "no-runs-here", + RepositoryName: "cli", + Wait: false, + Json: false, + }) + + require.Error(t, err) + require.ErrorIs(t, err, api.ErrNotFound) + }) + + t.Run("returns empty run ID when no run found for branch", func(t *testing.T) { + setup := setupTest(t) + + setup.mockAPI.MockRunStatus = func(cfg api.RunStatusConfig) (api.RunStatusResult, error) { + return api.RunStatusResult{ + Status: nil, + RunID: "", + Polling: api.PollingResult{Completed: true}, + }, nil + } + + result, err := setup.service.GetRunStatus(cli.GetRunStatusConfig{ + BranchName: "no-runs-here", + RepositoryName: "cli", + Wait: false, + Json: false, + }) + + require.NoError(t, err) + require.Equal(t, "", result.RunID) + }) + + t.Run("polls with branch lookup until run completes when Wait is true", func(t *testing.T) { + setup := setupTest(t) + + callCount := 0 + backoffMs := 0 + setup.mockAPI.MockRunStatus = func(cfg api.RunStatusConfig) (api.RunStatusResult, error) { + require.Equal(t, "main", cfg.BranchName) + require.Equal(t, "cli", cfg.RepositoryName) + callCount++ + if callCount < 2 { + return api.RunStatusResult{ + Status: &api.RunStatus{Result: "no_result"}, + RunID: "resolved-run-id", + Polling: api.PollingResult{Completed: false, BackoffMs: &backoffMs}, + }, nil + } + return api.RunStatusResult{ + Status: &api.RunStatus{Result: "succeeded"}, + RunID: "resolved-run-id", + Polling: api.PollingResult{Completed: true}, + }, nil + } + + result, err := setup.service.GetRunStatus(cli.GetRunStatusConfig{ + BranchName: "main", + RepositoryName: "cli", + Wait: true, + Json: false, + }) + + require.NoError(t, err) + require.Equal(t, 2, callCount) + require.Equal(t, "resolved-run-id", result.RunID) + require.Equal(t, "succeeded", result.ResultStatus) + require.True(t, result.Completed) + }) + t.Run("returns current status without waiting when Wait is false", func(t *testing.T) { setup := setupTest(t) diff --git a/internal/git/client.go b/internal/git/client.go index 73defc6..8319af2 100644 --- a/internal/git/client.go +++ b/internal/git/client.go @@ -118,6 +118,22 @@ func (c *Client) GetOriginUrl() string { return c.GetRemoteUrl(getRemote()) } +// RepoNameFromOriginUrl extracts the repository name from a git remote URL. +// For example, "git@github.com:rwx-cloud/cli.git" returns "cli". +func RepoNameFromOriginUrl(originUrl string) string { + // Handle SSH-style URLs (git@github.com:rwx-cloud/cli.git) + if idx := strings.LastIndex(originUrl, ":"); idx != -1 && !strings.Contains(originUrl, "://") { + originUrl = originUrl[idx+1:] + } + + // Handle HTTPS-style URLs (https://github.com/rwx-cloud/cli.git) + if idx := strings.LastIndex(originUrl, "/"); idx != -1 { + originUrl = originUrl[idx+1:] + } + + return strings.TrimSuffix(originUrl, ".git") +} + func (c *Client) GetRemoteUrl(remote string) string { cmd := exec.Command(c.Binary, "remote", "get-url", remote) cmd.Dir = c.Dir diff --git a/internal/git/client_test.go b/internal/git/client_test.go index 09ef2fa..f716c95 100644 --- a/internal/git/client_test.go +++ b/internal/git/client_test.go @@ -444,3 +444,23 @@ func TestGeneratePatchFile(t *testing.T) { }) }) } + +func TestRepoNameFromOriginUrl(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + {"SSH URL", "git@github.com:rwx-cloud/cli.git", "cli"}, + {"HTTPS URL", "https://github.com/rwx-cloud/cli.git", "cli"}, + {"SSH URL without .git suffix", "git@github.com:rwx-cloud/cli", "cli"}, + {"HTTPS URL without .git suffix", "https://github.com/rwx-cloud/cli", "cli"}, + {"empty string", "", ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.expected, git.RepoNameFromOriginUrl(tt.input)) + }) + } +}