Skip to content

Commit da39c9a

Browse files
committed
Merge remote-tracking branch 'origin/main' into sync/main-into-workspace-team-20260805
2 parents 5f7766d + d87ba8e commit da39c9a

8 files changed

Lines changed: 144 additions & 49 deletions

File tree

.github/actions/configure-ci-parallelism/action.yml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: Configure CI parallelism
2-
description: Export CI worker counts derived from nproc/2
2+
description: Export build worker counts derived from nproc/2 with a memory-safe Playwright cap
33

44
runs:
55
using: composite
@@ -14,14 +14,18 @@ runs:
1414
if [ "$workers" -lt 1 ]; then
1515
workers=1
1616
fi
17+
playwright_workers="$workers"
18+
if [ "$playwright_workers" -gt 2 ]; then
19+
playwright_workers=2
20+
fi
1721
1822
{
1923
echo "OPEN_DESIGN_POSTINSTALL_CONCURRENCY=$workers"
2024
echo "OPEN_DESIGN_WORKSPACE_CONCURRENCY=$workers"
2125
echo "OD_CI_DAEMON_MAX_WORKERS=$workers"
2226
echo "OD_E2E_VITEST_MAX_WORKERS=$workers"
23-
echo "OD_PLAYWRIGHT_WORKERS=$workers"
27+
echo "OD_PLAYWRIGHT_WORKERS=$playwright_workers"
2428
} >> "$GITHUB_ENV"
2529
2630
echo "workers=$workers" >> "$GITHUB_OUTPUT"
27-
echo "nproc=$cpu_count workers=$workers"
31+
echo "nproc=$cpu_count workers=$workers playwright_workers=$playwright_workers"

.github/actions/setup-playwright/action.yml

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: Setup Playwright
2-
description: Restore Playwright browser cache and install browsers
2+
description: Use a preseeded Nexu browser when available, otherwise restore the browser cache and install browsers
33

44
inputs:
55
package-json-path:
@@ -16,8 +16,40 @@ inputs:
1616
runs:
1717
using: composite
1818
steps:
19+
- name: Detect preinstalled Playwright
20+
id: preinstalled-playwright
21+
shell: bash
22+
env:
23+
RUNNER_LABELS_JSON: ${{ inputs.runner-labels }}
24+
run: |
25+
preinstalled_enabled=false
26+
27+
# The ARC tools image carries both the OS dependencies and Chromium.
28+
# Keep this opt-in to the named Nexu profiles so a hosted runner with
29+
# a restored browser cache still receives its OS dependencies.
30+
case "$RUNNER_LABELS_JSON" in
31+
*'"nexu-runners-'*)
32+
if [ -n "${PLAYWRIGHT_BROWSERS_PATH:-}" ] && [ -d "$PLAYWRIGHT_BROWSERS_PATH" ]; then
33+
chromium_bin="$(find "$PLAYWRIGHT_BROWSERS_PATH" -type f \
34+
\( -name chrome -o -name chromium -o -name chrome-headless-shell \) \
35+
-perm -111 -print -quit 2>/dev/null || true)"
36+
if [ -n "$chromium_bin" ]; then
37+
preinstalled_enabled=true
38+
fi
39+
fi
40+
;;
41+
esac
42+
43+
echo "enabled=$preinstalled_enabled" >> "$GITHUB_OUTPUT"
44+
if [ "$preinstalled_enabled" = "true" ]; then
45+
echo "Using the Playwright browser preseeded in the Nexu runner image."
46+
else
47+
echo "No preseeded Playwright browser detected; using the workflow install path."
48+
fi
49+
1950
- name: Detect persistent Playwright cache
2051
id: persistent-playwright-cache
52+
if: ${{ steps.preinstalled-playwright.outputs.enabled != 'true' }}
2153
shell: bash
2254
env:
2355
RUNNER_LABELS_JSON: ${{ inputs.runner-labels }}
@@ -41,6 +73,7 @@ runs:
4173
4274
- name: Resolve Playwright version
4375
id: playwright-version
76+
if: ${{ steps.preinstalled-playwright.outputs.enabled != 'true' }}
4477
shell: bash
4578
run: |
4679
version=$(node -p "const pkg = require('./${{ inputs.package-json-path }}'); const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) }; (deps['@playwright/test'] || deps.playwright || '').replace(/[^0-9.]/g,'')")
@@ -51,12 +84,22 @@ runs:
5184
echo "version=$version" >> "$GITHUB_OUTPUT"
5285
5386
- name: Cache Playwright browser binaries
54-
if: ${{ steps.persistent-playwright-cache.outputs.enabled != 'true' }}
87+
if: ${{ steps.preinstalled-playwright.outputs.enabled != 'true' && steps.persistent-playwright-cache.outputs.enabled != 'true' }}
5588
uses: actions/cache@v5.0.5
5689
with:
5790
path: ~/.cache/ms-playwright
5891
key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
5992

93+
- name: Ensure preinstalled Playwright browser revision
94+
if: ${{ steps.preinstalled-playwright.outputs.enabled == 'true' }}
95+
shell: bash
96+
env:
97+
PACKAGE_JSON_PATH: ${{ inputs.package-json-path }}
98+
run: |
99+
package_dir="${PACKAGE_JSON_PATH%/*}"
100+
pnpm -C "$package_dir" exec playwright install chromium
101+
60102
- name: Install Playwright browsers
103+
if: ${{ steps.preinstalled-playwright.outputs.enabled != 'true' }}
61104
shell: bash
62105
run: ${{ inputs.install-command }}

.github/scripts/runners.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,35 +7,36 @@
77

88
GITHUB_HOSTED = ["ubuntu-24.04"]
99
WINDOWS_HOSTED = ["windows-latest"]
10-
CONTABO_CONTROL = ["self-hosted", "Linux", "X64", "od-persistent-ci", "od-ci-hot-poc"]
11-
BLACKSMITH_4V = ["blacksmith-4vcpu-ubuntu-2404"]
10+
NEXU_SMALL = ["nexu-runners-small"]
11+
NEXU_MEDIUM = ["nexu-runners-medium"]
12+
NEXU_LARGE = ["nexu-runners-large"]
1213

1314

1415
def compact_json(value):
1516
return json.dumps(value, separators=(",", ":"))
1617

1718

1819
def normalize_mode(raw_mode):
19-
mode = (raw_mode or "default").strip().lower()
20+
mode = raw_mode or "default"
2021
if mode in {"default", "performance", "economic"}:
2122
return mode
2223
return "default"
2324

2425

2526
def resolve_contract(mode):
26-
general_medium = BLACKSMITH_4V if mode == "performance" else GITHUB_HOSTED
27-
hot_path = GITHUB_HOSTED if mode == "economic" else BLACKSMITH_4V
28-
control = CONTABO_CONTROL if mode == "default" else GITHUB_HOSTED
27+
control = GITHUB_HOSTED if mode == "economic" else NEXU_SMALL
28+
workload = GITHUB_HOSTED if mode == "economic" else NEXU_MEDIUM
29+
browser_workload = GITHUB_HOSTED if mode == "economic" else NEXU_LARGE
2930

3031
return {
3132
"runs_on": {
3233
"control": control,
33-
"general_medium": general_medium,
34-
"workspace_unit": GITHUB_HOSTED,
34+
"general_medium": workload,
35+
"workspace_unit": workload,
3536
"windows_tools": WINDOWS_HOSTED,
36-
"js_hot": hot_path,
37-
"ui_hot": hot_path,
38-
"visual_hot": hot_path,
37+
"js_hot": workload,
38+
"ui_hot": browser_workload,
39+
"visual_hot": browser_workload,
3940
},
4041
"decision": {
4142
"schema_version": 1,

.github/workflows/ci.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,14 @@ concurrency:
3131
jobs:
3232
runners:
3333
name: Resolve runner profiles
34-
runs-on: ubuntu-24.04
34+
# Resolve on the Nexu ARC fleet for trusted branches, but preserve the
35+
# hosted bootstrap path for external forks and explicit economic mode.
36+
runs-on: >-
37+
${{ ((github.event_name == 'pull_request'
38+
&& github.event.pull_request.head.repo.full_name != github.repository)
39+
|| vars.OD_CI_RUNNER_MODE == 'economic')
40+
&& 'ubuntu-24.04'
41+
|| 'nexu-runners-small' }}
3542
outputs:
3643
runs_on: ${{ steps.runners.outputs.runs_on }}
3744
decision: ${{ steps.runners.outputs.decision }}

apps/desktop/tests/main/updater.test.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ import {
3232
import { installerObservationSummaryPath } from "../../src/main/installer-observations.js";
3333

3434
type FixtureServer = {
35-
artifactRanges: () => string[];
3635
artifactRequests: () => number;
3736
close: () => Promise<void>;
3837
metadataRequests: () => number;
@@ -117,7 +116,6 @@ async function createUpdaterFixture(options: {
117116
const payloadPath = platform === "win" ? "/payload.7z" : "/payload.zip";
118117
const payloadBody = Buffer.from(options.payloadBody ?? "open design updater payload fixture");
119118
const payloadDigest = createHash("sha256").update(payloadBody).digest("hex");
120-
const artifactRanges: string[] = [];
121119
let artifactRequests = 0;
122120
let metadataRequests = 0;
123121
const server = createServer((request, response) => {
@@ -173,7 +171,6 @@ async function createUpdaterFixture(options: {
173171
artifactRequests += 1;
174172
const failArtifactAttempts = options.failArtifactAttempts ?? (options.failFirstArtifactWithTerminated === true ? 1 : 0);
175173
const range = typeof request.headers.range === "string" ? request.headers.range : undefined;
176-
if (range != null) artifactRanges.push(range);
177174
const match = range == null ? null : /^bytes=(\d+)-$/.exec(range);
178175
const start = match?.[1] == null ? 0 : Number(match[1]);
179176
const ranged = range != null && Number.isInteger(start) && start >= 0 && start < artifactBody.byteLength;
@@ -217,7 +214,6 @@ async function createUpdaterFixture(options: {
217214
});
218215
const address = serverAddress(server);
219216
return {
220-
artifactRanges: () => artifactRanges,
221217
artifactRequests: () => artifactRequests,
222218
close: async () => {
223219
await new Promise<void>((resolveClose, rejectClose) => {
@@ -2472,7 +2468,7 @@ describe("desktop updater", () => {
24722468
}
24732469
});
24742470

2475-
it("resumes an interrupted artifact download before surfacing an error", async () => {
2471+
it("recovers from an interrupted artifact download without surfacing an error", async () => {
24762472
const root = makeRoot();
24772473
const fixture = await createUpdaterFixture({
24782474
artifactBody: "open design updater fixture with retry",
@@ -2496,7 +2492,9 @@ describe("desktop updater", () => {
24962492
expect(checked.state).toBe(DESKTOP_UPDATE_STATES.DOWNLOADED);
24972493
expect(checked.error).toBeUndefined();
24982494
expect(fixture.artifactRequests()).toBe(2);
2499-
expect(fixture.artifactRanges()).toEqual([expect.stringMatching(/^bytes=\d+-$/)]);
2495+
// Byte-range resumption is covered by @open-design/download. At this
2496+
// integration boundary, a full retry is also valid when the interrupted
2497+
// response did not persist any partial bytes before the stream failed.
25002498
expect(logger.warn).not.toHaveBeenCalled();
25012499
} finally {
25022500
await fixture.close();

apps/web/tests/components/App.connectors.test.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,14 @@ const baseConfig: AppConfig = {
237237
agentCliEnv: {},
238238
};
239239

240+
async function clickCurrentPrivacyChoice(name: string) {
241+
// App bootstrap can rerender the banner while the async findByRole call is
242+
// resolving. Re-query synchronously before dispatching the event so the
243+
// click lands on the currently mounted button.
244+
await screen.findByRole('button', { name });
245+
fireEvent.click(screen.getByRole('button', { name }));
246+
}
247+
240248
describe('App connectors settings flows', () => {
241249
beforeEach(() => {
242250
useRouteMock.mockReturnValue(homeRouteMock);
@@ -400,7 +408,7 @@ describe('App connectors settings flows', () => {
400408
expect(mockedSyncConfigToDaemon).toHaveBeenCalled();
401409
});
402410
mockedSyncConfigToDaemon.mockClear();
403-
fireEvent.click(await screen.findByRole('button', { name: 'Share' }));
411+
await clickCurrentPrivacyChoice('Share');
404412

405413
await waitFor(() => {
406414
expect(mockedSyncConfigToDaemon).toHaveBeenCalledWith(
@@ -428,7 +436,7 @@ describe('App connectors settings flows', () => {
428436
expect(mockedSyncConfigToDaemon).toHaveBeenCalled();
429437
});
430438
mockedSyncConfigToDaemon.mockClear();
431-
fireEvent.click(await screen.findByRole('button', { name: 'Share' }));
439+
await clickCurrentPrivacyChoice('Share');
432440

433441
await waitFor(() => {
434442
expect(mockedSyncConfigToDaemon).toHaveBeenCalledWith(
@@ -456,7 +464,7 @@ describe('App connectors settings flows', () => {
456464
expect(mockedSyncConfigToDaemon).toHaveBeenCalled();
457465
});
458466
mockedSyncConfigToDaemon.mockClear();
459-
fireEvent.click(await screen.findByRole('button', { name: 'Share' }));
467+
await clickCurrentPrivacyChoice('Share');
460468

461469
await waitFor(() => {
462470
expect(mockedSyncConfigToDaemon).toHaveBeenCalledWith(
@@ -477,7 +485,7 @@ describe('App connectors settings flows', () => {
477485
expect(mockedSyncConfigToDaemon).toHaveBeenCalled();
478486
});
479487
mockedSyncConfigToDaemon.mockClear();
480-
fireEvent.click(await screen.findByRole('button', { name: "Don't share" }));
488+
await clickCurrentPrivacyChoice("Don't share");
481489

482490
await waitFor(() => {
483491
expect(mockedSyncConfigToDaemon).toHaveBeenCalledWith(
@@ -504,7 +512,7 @@ describe('App connectors settings flows', () => {
504512
expect(mockedSyncConfigToDaemon).toHaveBeenCalled();
505513
});
506514
mockedSyncConfigToDaemon.mockClear();
507-
fireEvent.click(await screen.findByRole('button', { name: "Don't share" }));
515+
await clickCurrentPrivacyChoice("Don't share");
508516

509517
await waitFor(() => {
510518
expect(mockedSyncConfigToDaemon).toHaveBeenCalledWith(

e2e/tests/actions-cache-workflows.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { readFile } from "node:fs/promises";
33
import { describe, expect, it } from "vitest";
44

55
const setupWorkspaceAction = new URL("../../.github/actions/setup-workspace/action.yml", import.meta.url);
6+
const setupPlaywrightAction = new URL("../../.github/actions/setup-playwright/action.yml", import.meta.url);
67
const cacheMaintenanceWorkflow = new URL("../../.github/workflows/cache-maintenance.yml", import.meta.url);
78
const landingPageCiWorkflow = new URL("../../.github/workflows/landing-page-ci.yml", import.meta.url);
89
const visualBaselineWorkflow = new URL("../../.github/workflows/visual-baseline.yml", import.meta.url);
@@ -16,6 +17,17 @@ function sectionBetween(content: string, start: string, end: string): string {
1617
}
1718

1819
describe("GitHub Actions cache workflows", () => {
20+
it("[P1] uses preseeded Playwright only for a ready Nexu runner image", async () => {
21+
const action = await readFile(setupPlaywrightAction, "utf8");
22+
23+
expect(action).toContain("*'\"nexu-runners-'*");
24+
expect(action).toContain("PLAYWRIGHT_BROWSERS_PATH");
25+
expect(action).toContain("steps.preinstalled-playwright.outputs.enabled == 'true'");
26+
expect(action).toContain("steps.preinstalled-playwright.outputs.enabled != 'true'");
27+
expect(action).toContain('pnpm -C "$package_dir" exec playwright install chromium');
28+
expect(action).toContain("run: ${{ inputs.install-command }}");
29+
});
30+
1931
it("[P1] keeps pnpm cache writes on explicit trusted main seed jobs", async () => {
2032
const action = await readFile(setupWorkspaceAction, "utf8");
2133

0 commit comments

Comments
 (0)