From ec9e0c4ec1b5c072986fffd4fedd85c4700f2689 Mon Sep 17 00:00:00 2001 From: Jason Robinaugh Date: Sat, 28 Feb 2026 14:15:52 -0500 Subject: [PATCH] Allow rwx results to infer run from current git branch and repository When no run ID positional argument is provided, the CLI now reads the current branch and repository name from git and queries the /mint/api/runs/latest endpoint with branch_name and repository_name query params. The resolved run ID is then used for the prompt lookup. Also prints the run URL (when returned by the API) before the status output, and handles the case where no matching run is found. --- cmd/rwx/results.go | 44 +++++++++++-- internal/api/client.go | 11 +++- internal/api/config.go | 5 +- internal/cli/service_wait.go | 22 +++++-- internal/cli/service_wait_test.go | 106 ++++++++++++++++++++++++++++++ internal/git/client.go | 16 +++++ internal/git/client_test.go | 20 ++++++ 7 files changed, 210 insertions(+), 14 deletions(-) diff --git a/cmd/rwx/results.go b/cmd/rwx/results.go index d3a9b979..b2cb2a8b 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 e598fc25..75b085ca 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 425ad89e..ce3d3267 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 6bfd1016..54b34cf7 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 8e4e99cd..1fe661b2 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 73defc69..8319af26 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 09ef2fac..f716c958 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)) + }) + } +}