Skip to content

Commit 6d8aa73

Browse files
authored
fix fetch of run (#221)
1 parent f9dcac3 commit 6d8aa73

File tree

2 files changed

+141
-20
lines changed

2 files changed

+141
-20
lines changed

pkg/cli/commands.go

Lines changed: 111 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3038,7 +3038,8 @@ func RunWorkflowOnGitHub(workflowIdOrName string, verbose bool) error {
30383038
fmt.Printf("Successfully triggered workflow: %s\n", lockFileName)
30393039

30403040
// Try to get the latest run for this workflow to show a direct link
3041-
if runURL, err := getLatestWorkflowRunURL(lockFileName, verbose); err == nil && runURL != "" {
3041+
// Add a delay to allow GitHub Actions time to register the new workflow run
3042+
if runURL, err := getLatestWorkflowRunURLWithRetry(lockFileName, verbose); err == nil && runURL != "" {
30423043
fmt.Printf("\n🔗 View workflow run: %s\n", runURL)
30433044
} else if verbose && err != nil {
30443045
fmt.Printf("Note: Could not get workflow run URL: %v\n", err)
@@ -3130,33 +3131,111 @@ func findMatchingLockFile(workflowName string, verbose bool) string {
31303131
return ""
31313132
}
31323133

3133-
// getLatestWorkflowRunURL gets the URL for the most recent run of the specified workflow
3134-
func getLatestWorkflowRunURL(lockFileName string, verbose bool) (string, error) {
3134+
// getLatestWorkflowRunURLWithRetry gets the URL for the most recent run of the specified workflow
3135+
// with retry logic to handle timing issues when a workflow has just been triggered
3136+
func getLatestWorkflowRunURLWithRetry(lockFileName string, verbose bool) (string, error) {
3137+
const maxRetries = 6
3138+
const initialDelay = 2 * time.Second
3139+
const maxDelay = 10 * time.Second
3140+
31353141
if verbose {
3136-
fmt.Printf("Getting latest run URL for workflow: %s\n", lockFileName)
3142+
fmt.Printf("Getting latest run URL for workflow: %s (with retry logic)\n", lockFileName)
31373143
}
31383144

3139-
// Start spinner for network operation
3140-
spinner := console.NewSpinner("Getting latest workflow run...")
3141-
if !verbose {
3142-
spinner.Start()
3145+
// Capture the current time before we start polling
3146+
// This helps us identify runs that were created after the workflow was triggered
3147+
startTime := time.Now().UTC()
3148+
3149+
var lastErr error
3150+
for attempt := 0; attempt < maxRetries; attempt++ {
3151+
if attempt > 0 {
3152+
// Calculate delay with exponential backoff, capped at maxDelay
3153+
delay := time.Duration(attempt) * initialDelay
3154+
if delay > maxDelay {
3155+
delay = maxDelay
3156+
}
3157+
3158+
if verbose {
3159+
fmt.Printf("Waiting %v before retry attempt %d/%d...\n", delay, attempt+1, maxRetries)
3160+
} else if attempt == 1 {
3161+
// Show spinner only starting from second attempt to avoid flickering
3162+
spinner := console.NewSpinner("Waiting for workflow run to appear...")
3163+
spinner.Start()
3164+
time.Sleep(delay)
3165+
spinner.Stop()
3166+
continue
3167+
}
3168+
time.Sleep(delay)
3169+
}
3170+
3171+
// Get recent runs for this workflow, including creation timestamps
3172+
runURL, runCreatedAt, err := getLatestWorkflowRunWithTimestamp(lockFileName, verbose)
3173+
if err != nil {
3174+
lastErr = err
3175+
if verbose {
3176+
fmt.Printf("Attempt %d/%d failed: %v\n", attempt+1, maxRetries, err)
3177+
}
3178+
continue
3179+
}
3180+
3181+
// If we found a run and it was created after we started (within 30 seconds tolerance),
3182+
// it's likely the run we just triggered
3183+
if !runCreatedAt.IsZero() && runCreatedAt.After(startTime.Add(-30*time.Second)) {
3184+
if verbose {
3185+
fmt.Printf("Found recent run created at %v (started polling at %v)\n",
3186+
runCreatedAt.Format(time.RFC3339), startTime.Format(time.RFC3339))
3187+
}
3188+
return runURL, nil
3189+
}
3190+
3191+
if verbose {
3192+
if runCreatedAt.IsZero() {
3193+
fmt.Printf("Attempt %d/%d: Found run but no creation timestamp available\n", attempt+1, maxRetries)
3194+
} else {
3195+
fmt.Printf("Attempt %d/%d: Found run but it was created at %v (too old)\n",
3196+
attempt+1, maxRetries, runCreatedAt.Format(time.RFC3339))
3197+
}
3198+
}
3199+
3200+
// For the first few attempts, if we have a run but it's too old, keep trying
3201+
if attempt < 3 {
3202+
lastErr = fmt.Errorf("workflow run appears to be from a previous execution")
3203+
continue
3204+
}
3205+
3206+
// For later attempts, return what we found even if timing is uncertain
3207+
if runURL != "" {
3208+
if verbose {
3209+
fmt.Printf("Returning workflow run URL after %d attempts (timing uncertain)\n", attempt+1)
3210+
}
3211+
return runURL, nil
3212+
}
31433213
}
31443214

3145-
// Get the most recent run for this workflow
3146-
cmd := exec.Command("gh", "run", "list", "--workflow", lockFileName, "--limit", "1", "--json", "url,databaseId,status,conclusion")
3147-
output, err := cmd.Output()
3215+
// If we exhausted all retries, return the last error
3216+
if lastErr != nil {
3217+
return "", fmt.Errorf("failed to get workflow run URL after %d attempts: %w", maxRetries, lastErr)
3218+
}
31483219

3149-
// Stop spinner
3150-
if !verbose {
3151-
spinner.Stop()
3220+
return "", fmt.Errorf("no workflow run found after %d attempts", maxRetries)
3221+
}
3222+
3223+
// getLatestWorkflowRunWithTimestamp gets the URL and creation time for the most recent run
3224+
func getLatestWorkflowRunWithTimestamp(lockFileName string, verbose bool) (string, time.Time, error) {
3225+
if verbose {
3226+
fmt.Printf("Fetching latest run with timestamp for workflow: %s\n", lockFileName)
31523227
}
31533228

3229+
// Get the most recent run for this workflow including creation timestamp
3230+
cmd := exec.Command("gh", "run", "list", "--workflow", lockFileName, "--limit", "1", "--json", "url,databaseId,status,conclusion,createdAt")
3231+
output, err := cmd.Output()
3232+
31543233
if err != nil {
3155-
return "", fmt.Errorf("failed to get workflow runs: %w", err)
3234+
return "", time.Time{}, fmt.Errorf("failed to get workflow runs: %w", err)
31563235
}
31573236

31583237
if len(output) == 0 {
3159-
return "", fmt.Errorf("no runs found for workflow")
3238+
return "", time.Time{}, fmt.Errorf("no runs found for workflow")
31603239
}
31613240

31623241
// Parse the JSON output
@@ -3165,22 +3244,34 @@ func getLatestWorkflowRunURL(lockFileName string, verbose bool) (string, error)
31653244
DatabaseID int64 `json:"databaseId"`
31663245
Status string `json:"status"`
31673246
Conclusion string `json:"conclusion"`
3247+
CreatedAt string `json:"createdAt"`
31683248
}
31693249

31703250
if err := json.Unmarshal(output, &runs); err != nil {
3171-
return "", fmt.Errorf("failed to parse workflow run data: %w", err)
3251+
return "", time.Time{}, fmt.Errorf("failed to parse workflow run data: %w", err)
31723252
}
31733253

31743254
if len(runs) == 0 {
3175-
return "", fmt.Errorf("no runs found")
3255+
return "", time.Time{}, fmt.Errorf("no runs found")
31763256
}
31773257

31783258
run := runs[0]
3259+
3260+
// Parse the creation timestamp
3261+
var createdAt time.Time
3262+
if run.CreatedAt != "" {
3263+
if parsedTime, err := time.Parse(time.RFC3339, run.CreatedAt); err == nil {
3264+
createdAt = parsedTime
3265+
} else if verbose {
3266+
fmt.Printf("Warning: Could not parse creation time '%s': %v\n", run.CreatedAt, err)
3267+
}
3268+
}
3269+
31793270
if verbose {
3180-
fmt.Printf("Found run %d with status: %s\n", run.DatabaseID, run.Status)
3271+
fmt.Printf("Found run %d with status: %s, created at: %s\n", run.DatabaseID, run.Status, run.CreatedAt)
31813272
}
31823273

3183-
return run.URL, nil
3274+
return run.URL, createdAt, nil
31843275
}
31853276

31863277
// checkCleanWorkingDirectory checks if there are uncommitted changes

pkg/cli/commands_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,36 @@ func TestRunWorkflowOnGitHub(t *testing.T) {
165165
}
166166
}
167167

168+
func TestGetLatestWorkflowRunWithTimestamp(t *testing.T) {
169+
// Test with non-existent workflow - should handle gracefully
170+
url, createdAt, err := getLatestWorkflowRunWithTimestamp("nonexistent-workflow.lock.yml", false)
171+
if err == nil {
172+
t.Error("getLatestWorkflowRunWithTimestamp should return error for non-existent workflow")
173+
}
174+
if url != "" {
175+
t.Error("getLatestWorkflowRunWithTimestamp should return empty URL for non-existent workflow")
176+
}
177+
if !createdAt.IsZero() {
178+
t.Error("getLatestWorkflowRunWithTimestamp should return zero time for non-existent workflow")
179+
}
180+
}
181+
182+
func TestGetLatestWorkflowRunURLWithRetry(t *testing.T) {
183+
// Test with non-existent workflow - should handle gracefully and return error after retries
184+
url, err := getLatestWorkflowRunURLWithRetry("nonexistent-workflow.lock.yml", false)
185+
if err == nil {
186+
t.Error("getLatestWorkflowRunURLWithRetry should return error for non-existent workflow")
187+
}
188+
if url != "" {
189+
t.Error("getLatestWorkflowRunURLWithRetry should return empty URL for non-existent workflow")
190+
}
191+
192+
// The error message should indicate multiple attempts were made
193+
if !strings.Contains(err.Error(), "attempts") {
194+
t.Errorf("Error message should mention retry attempts, got: %v", err)
195+
}
196+
}
197+
168198
func TestAllCommandsExist(t *testing.T) {
169199
defer os.RemoveAll(".github")
170200

0 commit comments

Comments
 (0)