Skip to content

Commit 8c2d661

Browse files
committed
ci: more install retries for desktop, shared between global and worker
1 parent 520a684 commit 8c2d661

4 files changed

Lines changed: 81 additions & 46 deletions

File tree

packages/playwright-vscode-ext/src/config/downloadVSCode.ts

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77

8-
import { downloadAndUnzipVSCode } from '@vscode/test-electron';
9-
import * as Duration from 'effect/Duration';
108
import * as Effect from 'effect/Effect';
11-
import * as Schedule from 'effect/Schedule';
129
import * as path from 'node:path';
10+
import { downloadVSCodeWithRetry } from '../utils/downloadUtils';
1311
import { resolveRepoRoot } from '../utils/repoRoot';
1412

1513
/**
@@ -21,21 +19,5 @@ export default async (): Promise<void> => {
2119
const cachePath = path.join(repoRoot, '.vscode-test');
2220
const version = process.env.PLAYWRIGHT_DESKTOP_VSCODE_VERSION ?? undefined;
2321

24-
const download = Effect.fn('downloadVSCode')(function* () {
25-
return yield* Effect.tryPromise({
26-
try: () => downloadAndUnzipVSCode({ version, cachePath }),
27-
catch: error => (error instanceof Error ? error : new Error(String(error)))
28-
}).pipe(
29-
Effect.retry(
30-
Schedule.exponential(Duration.seconds(5)).pipe(
31-
Schedule.compose(Schedule.recurs(2)),
32-
Schedule.tapOutput(attempt =>
33-
Effect.logWarning(`⚠️ VS Code download attempt ${attempt + 1} failed, retrying...`)
34-
)
35-
)
36-
)
37-
);
38-
});
39-
40-
await Effect.runPromise(download());
22+
await Effect.runPromise(downloadVSCodeWithRetry(version, cachePath));
4123
};

packages/playwright-vscode-ext/src/fixtures/createDesktopTest.ts

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88
// This is Node.js test infrastructure, not extension code
99
import type { WorkerFixtures, TestFixtures } from './desktopFixtureTypes';
1010
import { test as base, _electron as electron } from '@playwright/test';
11-
import { downloadAndUnzipVSCode } from '@vscode/test-electron';
11+
import * as Duration from 'effect/Duration';
12+
import * as Effect from 'effect/Effect';
1213
import type { ChildProcess } from 'node:child_process';
1314
import * as fs from 'node:fs/promises';
1415
import * as path from 'node:path';
1516

17+
import { downloadVSCodeWithRetry } from '../utils/downloadUtils';
1618
import { filterErrors } from '../utils/helpers';
1719
import { resolveRepoRoot } from '../utils/repoRoot';
1820
import { createEmptyTestWorkspace, createTestWorkspace } from './desktopWorkspace';
@@ -30,7 +32,12 @@ const CLOSE_TIMEOUT_MS = 5000;
3032
const forceKillProcessGroup = (proc: ChildProcess): void => {
3133
const { pid } = proc;
3234
if (typeof pid !== 'number') return;
33-
try { process.kill(-pid, 'SIGKILL'); } catch {}
35+
36+
try {
37+
process.kill(-pid, 'SIGKILL');
38+
} catch {
39+
// ignore
40+
}
3441
proc.stdin?.destroy();
3542
proc.stdout?.destroy();
3643
proc.stderr?.destroy();
@@ -69,7 +76,8 @@ export const createDesktopTest = (options: CreateDesktopTestOptions) => {
6976
const repoRoot = resolveRepoRoot(fixturesDir);
7077
const cachePath = path.join(repoRoot, '.vscode-test');
7178
const version = process.env.PLAYWRIGHT_DESKTOP_VSCODE_VERSION ?? undefined;
72-
const executablePath = await downloadAndUnzipVSCode({ version, cachePath });
79+
80+
const executablePath = await Effect.runPromise(downloadVSCodeWithRetry(version, cachePath));
7381
await use(executablePath);
7482
},
7583
{ scope: 'worker' }
@@ -93,10 +101,7 @@ export const createDesktopTest = (options: CreateDesktopTestOptions) => {
93101
if (Object.keys(effectiveUserSettings).length > 0) {
94102
const userSettingsDir = path.join(userDataDir, 'User');
95103
await fs.mkdir(userSettingsDir, { recursive: true });
96-
await fs.writeFile(
97-
path.join(userSettingsDir, 'settings.json'),
98-
JSON.stringify(effectiveUserSettings, null, 2)
99-
);
104+
await fs.writeFile(path.join(userSettingsDir, 'settings.json'), JSON.stringify(effectiveUserSettings, null, 2));
100105
}
101106
const extensionsDir = path.join(workspaceDir, '.vscode-test-extensions');
102107
await fs.mkdir(extensionsDir, { recursive: true });
@@ -151,22 +156,29 @@ export const createDesktopTest = (options: CreateDesktopTestOptions) => {
151156
forceKillProcessGroup(proc);
152157
// Wait for Node.js to register the exit (pipes closed → 'close' event → 'exit' event)
153158
await new Promise<void>(resolve => {
154-
if (proc.exitCode !== null) { resolve(); return; }
159+
if (proc.exitCode !== null) {
160+
resolve();
161+
return;
162+
}
155163
proc.on('close', () => resolve());
156164
setTimeout(resolve, 10_000);
157165
});
158166
}
159167
console.log(`[teardown] exitCode=${proc?.exitCode} killed=${proc?.killed}`);
160168
} else {
161-
try {
162-
await Promise.race([
163-
electronApp.close(),
164-
new Promise<false>(resolve => setTimeout(() => resolve(false), CLOSE_TIMEOUT_MS))
165-
]);
166-
} catch {}
169+
await Effect.runPromise(
170+
Effect.tryPromise(() => electronApp.close()).pipe(
171+
Effect.timeout(Duration.millis(CLOSE_TIMEOUT_MS)),
172+
Effect.catchAll(() => Effect.void)
173+
)
174+
);
167175
// Force-kill if close didn't work (Windows timeout fallback)
168176
if (proc?.exitCode === null && process.platform === 'win32') {
169-
try { process.kill(proc.pid!, 'SIGKILL'); } catch {}
177+
try {
178+
process.kill(proc.pid!, 'SIGKILL');
179+
} catch {
180+
// ignore
181+
}
170182
}
171183
}
172184
console.log('[teardown] done');
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright (c) 2025, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
8+
import { downloadAndUnzipVSCode } from '@vscode/test-electron';
9+
import * as Duration from 'effect/Duration';
10+
import * as Effect from 'effect/Effect';
11+
import * as Schedule from 'effect/Schedule';
12+
13+
/**
14+
* Downloads and unzips VS Code with retries.
15+
* @param version VS Code version to download
16+
* @param cachePath Path to cache the download
17+
* @returns Effect that resolves to the executable path
18+
*/
19+
export const downloadVSCodeWithRetry = (
20+
version: string | undefined,
21+
cachePath: string
22+
): Effect.Effect<string, Error, never> => {
23+
const downloadTask = Effect.fn('downloadVSCode')(function* () {
24+
return yield* Effect.tryPromise({
25+
try: () => downloadAndUnzipVSCode({ version, cachePath }),
26+
catch: error => (error instanceof Error ? error : new Error(String(error)))
27+
});
28+
});
29+
30+
return downloadTask().pipe(
31+
Effect.retry(
32+
Schedule.exponential(Duration.seconds(5)).pipe(
33+
Schedule.compose(Schedule.recurs(4)),
34+
Schedule.tapOutput(attempt =>
35+
Effect.logWarning(`⚠️ VS Code download attempt ${attempt + 1} failed, retrying...`)
36+
)
37+
)
38+
),
39+
Effect.flatMap(executablePath =>
40+
executablePath
41+
? Effect.succeed(executablePath)
42+
: Effect.fail(new Error('VS Code download returned undefined executable path'))
43+
)
44+
);
45+
};

packages/playwright-vscode-ext/src/web/createHeadlessServer.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export const createHeadlessServer = async (options: HeadlessServerOptions): Prom
3434
const repoRoot = resolveRepoRoot(options.callerDirname);
3535
const testRunnerDataDir = path.join(repoRoot, '.vscode-test-web');
3636

37-
const openTask = Effect.fn('createHeadlessServer.open')(function* () {
37+
const openTask = Effect.fn('openHeadlessServer')(function* () {
3838
yield* Effect.log(`🌐 Starting VS Code Web (headless) for ${options.extensionName} tests...`, {
3939
extensionDevelopmentPath,
4040
extensionPaths,
@@ -68,11 +68,11 @@ export const createHeadlessServer = async (options: HeadlessServerOptions): Prom
6868
: [])
6969
]
7070
}),
71-
catch: error => error instanceof Error ? error : new Error(String(error))
71+
catch: error => (error instanceof Error ? error : new Error(String(error)))
7272
});
7373
});
7474

75-
const program = openTask().pipe(
75+
await openTask().pipe(
7676
Effect.retry(
7777
Schedule.exponential(Duration.seconds(5)).pipe(
7878
Schedule.compose(Schedule.recurs(2)),
@@ -83,15 +83,11 @@ export const createHeadlessServer = async (options: HeadlessServerOptions): Prom
8383
),
8484
Effect.catchAll(error =>
8585
Effect.logError('❌ Failed to start headless server after 3 attempts', {
86-
error: error.message
87-
}).pipe(
88-
Effect.andThen(() => process.exit(1)),
89-
Effect.asVoid
90-
)
91-
)
86+
error: error instanceof Error ? error.message : String(error)
87+
})
88+
),
89+
Effect.runPromiseExit
9290
);
93-
94-
await Effect.runPromise(program as Effect.Effect<void, Error, never>);
9591
};
9692

9793
/** Sets up signal handlers for graceful shutdown */

0 commit comments

Comments
 (0)