Skip to content
Draft
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
44 changes: 37 additions & 7 deletions cmd/rwx/results.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -14,25 +17,49 @@ var (

resultsCmd = &cobra.Command{
GroupID: "outputs",
Use: "results <run-id>",
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
Expand All @@ -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)
}
Expand Down
11 changes: 10 additions & 1 deletion internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 4 additions & 1 deletion internal/api/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,9 @@ type ArtifactDownloadRequestResult struct {
}

type RunStatusConfig struct {
RunID string
RunID string
BranchName string
RepositoryName string
}

type RunStatus struct {
Expand All @@ -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"`
}

Expand Down
22 changes: 17 additions & 5 deletions internal/cli/service_wait.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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()
Expand All @@ -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
Expand Down
106 changes: 106 additions & 0 deletions internal/cli/service_wait_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
16 changes: 16 additions & 0 deletions internal/git/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions internal/git/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
})
}
}