@@ -3038,7 +3038,8 @@ func RunWorkflowOnGitHub(workflowIdOrName string, verbose bool) error {
3038
3038
fmt .Printf ("Successfully triggered workflow: %s\n " , lockFileName )
3039
3039
3040
3040
// 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 != "" {
3042
3043
fmt .Printf ("\n 🔗 View workflow run: %s\n " , runURL )
3043
3044
} else if verbose && err != nil {
3044
3045
fmt .Printf ("Note: Could not get workflow run URL: %v\n " , err )
@@ -3130,33 +3131,111 @@ func findMatchingLockFile(workflowName string, verbose bool) string {
3130
3131
return ""
3131
3132
}
3132
3133
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
+
3135
3141
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 )
3137
3143
}
3138
3144
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
+ }
3143
3213
}
3144
3214
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
+ }
3148
3219
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 )
3152
3227
}
3153
3228
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
+
3154
3233
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 )
3156
3235
}
3157
3236
3158
3237
if len (output ) == 0 {
3159
- return "" , fmt .Errorf ("no runs found for workflow" )
3238
+ return "" , time. Time {}, fmt .Errorf ("no runs found for workflow" )
3160
3239
}
3161
3240
3162
3241
// Parse the JSON output
@@ -3165,22 +3244,34 @@ func getLatestWorkflowRunURL(lockFileName string, verbose bool) (string, error)
3165
3244
DatabaseID int64 `json:"databaseId"`
3166
3245
Status string `json:"status"`
3167
3246
Conclusion string `json:"conclusion"`
3247
+ CreatedAt string `json:"createdAt"`
3168
3248
}
3169
3249
3170
3250
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 )
3172
3252
}
3173
3253
3174
3254
if len (runs ) == 0 {
3175
- return "" , fmt .Errorf ("no runs found" )
3255
+ return "" , time. Time {}, fmt .Errorf ("no runs found" )
3176
3256
}
3177
3257
3178
3258
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
+
3179
3270
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 )
3181
3272
}
3182
3273
3183
- return run .URL , nil
3274
+ return run .URL , createdAt , nil
3184
3275
}
3185
3276
3186
3277
// checkCleanWorkingDirectory checks if there are uncommitted changes
0 commit comments