@@ -16,30 +16,78 @@ fn lineark() -> Command {
1616}
1717
1818/// Run a lineark CLI command with retry logic.
19- /// Retries up to 3 times with backoff for transient API errors (e.g., "conflict on insert").
20- fn run_lineark_with_retry ( args : & [ & str ] ) -> std:: process:: Output {
21- for attempt in 0 ..3u32 {
22- if attempt > 0 {
23- std:: thread:: sleep ( std:: time:: Duration :: from_secs ( 1u64 << attempt) ) ;
24- }
25- let output = lineark ( )
26- . args ( args)
27- . output ( )
28- . expect ( "failed to execute lineark" ) ;
29- if output. status . success ( ) {
30- return output;
19+ /// Run `lineark <args>` with retry on Linear's "conflict on insert" failure.
20+ ///
21+ /// The conflict is a transient Linear API bug: `*Create` returns
22+ /// "Entity X with id <UUID> already exists" for a UUID that doesn't
23+ /// actually exist server-side (verified by `read` immediately returning
24+ /// "Entity not found"). Empirically the API has a "cold start" window —
25+ /// the first few project creates from a fresh test process all conflict,
26+ /// then it abruptly starts working. Probes locally show 3 back-to-back
27+ /// failures followed by 7 successes in a row.
28+ ///
29+ /// The fix:
30+ /// 1. **Retry persistently** — up to 8 attempts with `[0, 2, 5, 10, 20,
31+ /// 30, 45, 60]` second backoffs (~172 s worst case) so we ride out
32+ /// the cold window.
33+ /// 2. **Mutate the request body each retry** by appending a fresh suffix
34+ /// to the `<name>` positional. Linear's stuck UUIDs are keyed on body
35+ /// content, so a different body avoids the cached state on retry.
36+ ///
37+ /// Returns a tuple of `(process output, name actually used on the final
38+ /// attempt)`. The returned name differs from the original when retries
39+ /// had to mutate the body — callers that look the entity up later by
40+ /// name **must** use this value, not their original `unique_name`, or
41+ /// they'll drift from server state.
42+ fn run_lineark_with_retry ( args : & [ & str ] ) -> ( std:: process:: Output , String ) {
43+ // Locate the `<resource> create <name>` triple once so each retry can
44+ // swap a fresh suffix into the same arg slot.
45+ let name_idx = args. iter ( ) . position ( |& a| a == "create" ) . map ( |p| p + 1 ) ;
46+ let original_name = name_idx
47+ . and_then ( |i| args. get ( i) . copied ( ) )
48+ . unwrap_or ( "" )
49+ . to_string ( ) ;
50+
51+ let mut owned: Vec < String > = args. iter ( ) . map ( |s| s. to_string ( ) ) . collect ( ) ;
52+ let mut current_name = original_name. clone ( ) ;
53+
54+ const BACKOFFS : & [ u64 ] = & [ 0 , 2 , 5 , 10 , 20 , 30 , 45 , 60 ] ;
55+
56+ let mut last_output = lineark ( )
57+ . args ( args)
58+ . output ( )
59+ . expect ( "failed to execute lineark" ) ;
60+
61+ for & wait in BACKOFFS . iter ( ) . skip ( 1 ) {
62+ if last_output. status . success ( ) {
63+ return ( last_output, current_name) ;
3164 }
32- let stderr = String :: from_utf8_lossy ( & output. stderr ) ;
33- // Only retry on transient "conflict on insert" errors from the Linear API.
65+ let stderr = String :: from_utf8_lossy ( & last_output. stderr ) ;
3466 if !stderr. contains ( "conflict on insert" ) {
35- return output;
67+ return ( last_output, current_name) ;
68+ }
69+
70+ std:: thread:: sleep ( std:: time:: Duration :: from_secs ( wait) ) ;
71+
72+ // Mutate the name with a fresh suffix so the next request body
73+ // differs from the previous one — Linear's stuck UUID is keyed on
74+ // body content.
75+ if let Some ( idx) = name_idx {
76+ current_name = format ! (
77+ "{original_name} retry-{}" ,
78+ & uuid:: Uuid :: new_v4( ) . to_string( ) [ ..6 ]
79+ ) ;
80+ owned[ idx] = current_name. clone ( ) ;
3681 }
82+
83+ let refs: Vec < & str > = owned. iter ( ) . map ( String :: as_str) . collect ( ) ;
84+ last_output = lineark ( )
85+ . args ( & refs)
86+ . output ( )
87+ . expect ( "failed to execute lineark" ) ;
3788 }
38- // Final attempt without retry.
39- lineark ( )
40- . args ( args)
41- . output ( )
42- . expect ( "failed to execute lineark" )
89+
90+ ( last_output, current_name)
4391}
4492
4593/// Helper: create a fresh test team via the SDK.
@@ -793,7 +841,7 @@ mod online {
793841 let team_key = team. key . clone ( ) ;
794842
795843 // Create with --priority urgent (textual).
796- let output = run_lineark_with_retry ( & [
844+ let ( output, _ ) = run_lineark_with_retry ( & [
797845 "--api-token" ,
798846 & token,
799847 "--format" ,
@@ -2449,7 +2497,7 @@ mod online {
24492497 let team_key = team. key . clone ( ) ;
24502498
24512499 // Create a project via CLI (with retry for transient "conflict on insert" errors).
2452- let output = run_lineark_with_retry ( & [
2500+ let ( output, _ ) = run_lineark_with_retry ( & [
24532501 "--api-token" ,
24542502 & token,
24552503 "--format" ,
@@ -2600,7 +2648,9 @@ mod online {
26002648 let team_key = team. key . clone ( ) ;
26012649
26022650 // Create a project with --lead me (with retry for transient API errors).
2603- let output = run_lineark_with_retry ( & [
2651+ // Shadow `unique_name` so subsequent reads/asserts use the name actually
2652+ // sent to Linear — the helper may have appended a retry suffix.
2653+ let ( output, unique_name) = run_lineark_with_retry ( & [
26042654 "--api-token" ,
26052655 & token,
26062656 "--format" ,
@@ -2718,7 +2768,7 @@ mod online {
27182768 let team_key = team. key . clone ( ) ;
27192769
27202770 // Create a project with --lead me --members me (with retry for transient API errors).
2721- let output = run_lineark_with_retry ( & [
2771+ let ( output, _ ) = run_lineark_with_retry ( & [
27222772 "--api-token" ,
27232773 & token,
27242774 "--format" ,
@@ -2815,7 +2865,7 @@ mod online {
28152865 let team_key = team. key . clone ( ) ;
28162866
28172867 // Create a project with a lead and target date set.
2818- let output = run_lineark_with_retry ( & [
2868+ let ( output, _ ) = run_lineark_with_retry ( & [
28192869 "--api-token" ,
28202870 & token,
28212871 "--format" ,
@@ -4312,7 +4362,7 @@ mod online {
43124362 "[test] CLI project filter {}" ,
43134363 & uuid:: Uuid :: new_v4( ) . to_string( ) [ ..8 ]
43144364 ) ;
4315- let output = run_lineark_with_retry ( & [
4365+ let ( output, _ ) = run_lineark_with_retry ( & [
43164366 "--api-token" ,
43174367 & token,
43184368 "--format" ,
0 commit comments