@@ -57,6 +57,60 @@ export function deployTargetArgument(workspaceRoot: string, target: string): str
5757 return relative ( workspaceRoot , target )
5858}
5959
60+ /** How many times the deploy is attempted before the build gives up. */
61+ export const DEPLOY_ATTEMPTS = 3
62+
63+ /**
64+ * Back-off before a retry, in milliseconds, indexed by the retry number.
65+ *
66+ * `pnpm deploy --legacy` re-resolves from the registry and ignores the
67+ * lockfile, so a package published in a partially-propagated state fails the
68+ * build even though every pin here is installable. That happened with
69+ * `@tanstack/react-query@5.102.3`, whose `query-core` peer of the same version
70+ * was not yet resolvable — the desktop packaging gate went red for a package
71+ * nothing in the shipped runtime uses.
72+ *
73+ * A deterministic failure still fails; it just costs the sum of these waits
74+ * first. That is the trade: about half a minute added to a genuinely broken
75+ * build, against a release gate that no longer turns red because npm was
76+ * mid-publish.
77+ * @param retry - 1 for the first retry, 2 for the second.
78+ * @returns Milliseconds to wait before that retry.
79+ */
80+ export function deployRetryDelayMs ( retry : number ) : number {
81+ return retry <= 1 ? 5_000 : 20_000
82+ }
83+
84+ /**
85+ * Run an operation, retrying a failure up to {@link DEPLOY_ATTEMPTS} times.
86+ *
87+ * `sleep` and `onRetry` are injected so the policy is testable without a real
88+ * wait or a real registry.
89+ * @param operation - Receives the 1-based attempt number.
90+ * @param options - Injected clock and retry reporter.
91+ * @returns Nothing; the last failure is rethrown when every attempt fails.
92+ */
93+ export async function withDeployRetries (
94+ operation : ( attempt : number ) => Promise < void > ,
95+ options : {
96+ readonly attempts ?: number
97+ readonly sleep : ( milliseconds : number ) => Promise < void >
98+ readonly onRetry ?: ( attempt : number , error : unknown ) => void
99+ } ,
100+ ) : Promise < void > {
101+ const attempts = options . attempts ?? DEPLOY_ATTEMPTS
102+ for ( let attempt = 1 ; ; attempt += 1 ) {
103+ try {
104+ await operation ( attempt )
105+ return
106+ } catch ( error ) {
107+ if ( attempt >= attempts ) throw error
108+ options . onRetry ?.( attempt , error )
109+ await options . sleep ( deployRetryDelayMs ( attempt ) )
110+ }
111+ }
112+ }
113+
60114async function run ( command : string , args : readonly string [ ] ) : Promise < void > {
61115 const invocation = packageManagerInvocation ( process . platform , command , args )
62116 await new Promise < void > ( ( accept , reject ) => {
@@ -109,12 +163,21 @@ async function materializeLinks(): Promise<void> {
109163async function deploy ( target : string ) : Promise < void > {
110164 const savedWorkspaceState = existsSync ( workspaceState ) ? await readFile ( workspaceState ) : undefined
111165 try {
112- await run ( 'pnpm' , [
113- '--config.verify-deps-before-run=false' , '--filter' , deployPackage , 'deploy' , '--legacy' , '--prod' ,
114- '--config.node-linker=hoisted' , '--config.auto-install-peers=false' , '--config.link-workspace-packages=true' ,
115- '--config.allow-unused-patches=true' ,
116- deployTargetArgument ( repositoryRoot , target ) ,
117- ] )
166+ await withDeployRetries (
167+ ( ) => run ( 'pnpm' , [
168+ '--config.verify-deps-before-run=false' , '--filter' , deployPackage , 'deploy' , '--legacy' , '--prod' ,
169+ '--config.node-linker=hoisted' , '--config.auto-install-peers=false' , '--config.link-workspace-packages=true' ,
170+ '--config.allow-unused-patches=true' ,
171+ deployTargetArgument ( repositoryRoot , target ) ,
172+ ] ) ,
173+ {
174+ sleep : ( milliseconds ) => new Promise ( resolve => { setTimeout ( resolve , milliseconds ) } ) ,
175+ onRetry : ( attempt , error ) => {
176+ const reason = error instanceof Error ? error . message : String ( error )
177+ console . warn ( `desktop runtime staging attempt ${ attempt } failed, retrying: ${ reason } ` )
178+ } ,
179+ } ,
180+ )
118181 } finally {
119182 if ( savedWorkspaceState === undefined ) await rm ( workspaceState , { force : true } )
120183 else await writeFile ( workspaceState , savedWorkspaceState )
0 commit comments