From 5d8666d4337be6a9ba8aebca88d1d482b2151463 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sat, 4 Apr 2026 23:17:59 +0100 Subject: [PATCH 1/8] Move Playwright install from CI workflow into pipeline module Adds InstallPlaywrightModule to TUnit.Pipeline so Playwright browser installation runs concurrently with other pipeline work rather than blocking the entire CI job upfront. RunPlaywrightTestsModule depends on it via [DependsOn]. --- .github/workflows/dotnet.yml | 21 ------------------- .../Modules/InstallPlaywrightModule.cs | 14 +++++++++++++ .../Modules/RunPlaywrightTestsModule.cs | 3 ++- 3 files changed, 16 insertions(+), 22 deletions(-) create mode 100644 TUnit.Pipeline/Modules/InstallPlaywrightModule.cs diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 8da6269d71..f7c67e1b49 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -89,27 +89,6 @@ jobs: if: matrix.os == 'ubuntu-latest' uses: docker/setup-docker-action@v5.0.0 - - name: Cache Playwright Browsers - uses: actions/cache@v5 - continue-on-error: true - id: playwright-cache - with: - path: | - ~/.cache/ms-playwright - ~/Library/Caches/ms-playwright - ~/AppData/Local/ms-playwright - key: playwright-${{ runner.os }}-${{ hashFiles('**/package-lock.json', '**/package.json', '**/Directory.Packages.props') }} - restore-keys: | - playwright-${{ runner.os }}- - - - name: Install Playwright - run: npx playwright install --with-deps - if: steps.playwright-cache.outputs.cache-hit != 'true' - - - name: Install Playwright (browsers only) - run: npx playwright install - if: steps.playwright-cache.outputs.cache-hit == 'true' - - name: Enable Long Paths on Windows if: matrix.os == 'windows-latest' run: | diff --git a/TUnit.Pipeline/Modules/InstallPlaywrightModule.cs b/TUnit.Pipeline/Modules/InstallPlaywrightModule.cs new file mode 100644 index 0000000000..281e08e227 --- /dev/null +++ b/TUnit.Pipeline/Modules/InstallPlaywrightModule.cs @@ -0,0 +1,14 @@ +using ModularPipelines.Context; +using ModularPipelines.Models; +using ModularPipelines.Modules; +using ModularPipelines.Options; + +namespace TUnit.Pipeline.Modules; + +public class InstallPlaywrightModule : Module +{ + protected override Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) => + context.Shell.Bash.Command( + new BashCommandOptions("npx playwright install --with-deps"), + cancellationToken); +} diff --git a/TUnit.Pipeline/Modules/RunPlaywrightTestsModule.cs b/TUnit.Pipeline/Modules/RunPlaywrightTestsModule.cs index 37cc00c82c..b7c796752d 100644 --- a/TUnit.Pipeline/Modules/RunPlaywrightTestsModule.cs +++ b/TUnit.Pipeline/Modules/RunPlaywrightTestsModule.cs @@ -6,7 +6,8 @@ namespace TUnit.Pipeline.Modules; -[NotInParallel("NetworkTests"), RunOnLinuxOnly, RunOnWindowsOnly] +[NotInParallel("NetworkTests")] +[DependsOn] public class RunPlaywrightTestsModule : TestBaseModule { protected override Task<(DotNetRunOptions Options, CommandExecutionOptions? ExecutionOptions)> GetTestOptions(IModuleContext context, string framework, CancellationToken cancellationToken) From 4c2c09439f66a462859d05d0b1f8215da0ec33d9 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sat, 4 Apr 2026 23:27:54 +0100 Subject: [PATCH 2/8] Restore Playwright browser caching and conditional install Re-adds actions/cache@v5 for Playwright browsers in CI. InstallPlaywrightModule now checks if the browser cache directory is populated and skips --with-deps when browsers are already restored from cache. --- .github/workflows/dotnet.yml | 12 +++++++++ .../Modules/InstallPlaywrightModule.cs | 26 ++++++++++++++++--- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index f7c67e1b49..6670399f90 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -85,6 +85,18 @@ jobs: restore-keys: | nuget-${{ runner.os }}- + - name: Cache Playwright Browsers + uses: actions/cache@v5 + continue-on-error: true + with: + path: | + ~/.cache/ms-playwright + ~/Library/Caches/ms-playwright + ~/AppData/Local/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('**/package-lock.json', '**/package.json', '**/Directory.Packages.props') }} + restore-keys: | + playwright-${{ runner.os }}- + - name: Docker Setup Docker if: matrix.os == 'ubuntu-latest' uses: docker/setup-docker-action@v5.0.0 diff --git a/TUnit.Pipeline/Modules/InstallPlaywrightModule.cs b/TUnit.Pipeline/Modules/InstallPlaywrightModule.cs index 281e08e227..48d16dd3b3 100644 --- a/TUnit.Pipeline/Modules/InstallPlaywrightModule.cs +++ b/TUnit.Pipeline/Modules/InstallPlaywrightModule.cs @@ -7,8 +7,28 @@ namespace TUnit.Pipeline.Modules; public class InstallPlaywrightModule : Module { - protected override Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) => - context.Shell.Bash.Command( - new BashCommandOptions("npx playwright install --with-deps"), + protected override Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) + { + // If browsers were restored from cache, skip system dependency installation + var command = IsBrowserCachePresent() + ? "npx playwright install" + : "npx playwright install --with-deps"; + + return context.Shell.Bash.Command( + new BashCommandOptions(command), cancellationToken); + } + + private static bool IsBrowserCachePresent() + { + var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + string[] cachePaths = + [ + Path.Combine(home, ".cache", "ms-playwright"), // Linux + Path.Combine(home, "Library", "Caches", "ms-playwright"), // macOS + Path.Combine(home, "AppData", "Local", "ms-playwright"), // Windows + ]; + + return cachePaths.Any(p => Directory.Exists(p) && Directory.EnumerateDirectories(p).Any()); + } } From 09f0180084253ef59b1801aa59f6b52659a74b76 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sat, 4 Apr 2026 23:30:23 +0100 Subject: [PATCH 3/8] Simplify InstallPlaywrightModule - remove redundant cache detection playwright install --with-deps is idempotent; it detects cached browsers and skips re-downloading. The IsBrowserCachePresent() check duplicated Playwright's own logic and introduced a TOCTOU anti-pattern. The YAML cache step still restores browser binaries between CI runs. --- .../Modules/InstallPlaywrightModule.cs | 26 +++---------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/TUnit.Pipeline/Modules/InstallPlaywrightModule.cs b/TUnit.Pipeline/Modules/InstallPlaywrightModule.cs index 48d16dd3b3..281e08e227 100644 --- a/TUnit.Pipeline/Modules/InstallPlaywrightModule.cs +++ b/TUnit.Pipeline/Modules/InstallPlaywrightModule.cs @@ -7,28 +7,8 @@ namespace TUnit.Pipeline.Modules; public class InstallPlaywrightModule : Module { - protected override Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) - { - // If browsers were restored from cache, skip system dependency installation - var command = IsBrowserCachePresent() - ? "npx playwright install" - : "npx playwright install --with-deps"; - - return context.Shell.Bash.Command( - new BashCommandOptions(command), + protected override Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) => + context.Shell.Bash.Command( + new BashCommandOptions("npx playwright install --with-deps"), cancellationToken); - } - - private static bool IsBrowserCachePresent() - { - var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - string[] cachePaths = - [ - Path.Combine(home, ".cache", "ms-playwright"), // Linux - Path.Combine(home, "Library", "Caches", "ms-playwright"), // macOS - Path.Combine(home, "AppData", "Local", "ms-playwright"), // Windows - ]; - - return cachePaths.Any(p => Directory.Exists(p) && Directory.EnumerateDirectories(p).Any()); - } } From a723767285b97d3ea036da35b5cbbf0115dc4f72 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sun, 5 Apr 2026 00:10:16 +0100 Subject: [PATCH 4/8] Fix RunPlaywrightTestsModule working directory --- TUnit.Pipeline/Modules/RunPlaywrightTestsModule.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TUnit.Pipeline/Modules/RunPlaywrightTestsModule.cs b/TUnit.Pipeline/Modules/RunPlaywrightTestsModule.cs index b7c796752d..80125a8fa2 100644 --- a/TUnit.Pipeline/Modules/RunPlaywrightTestsModule.cs +++ b/TUnit.Pipeline/Modules/RunPlaywrightTestsModule.cs @@ -17,12 +17,13 @@ public class RunPlaywrightTestsModule : TestBaseModule return Task.FromResult<(DotNetRunOptions, CommandExecutionOptions?)>(( new DotNetRunOptions { - Project = project.FullName, + Project = project.Name, NoBuild = true, Configuration = "Release", }, new CommandExecutionOptions { + WorkingDirectory = project.Folder!.Path, EnvironmentVariables = new Dictionary { ["DISABLE_GITHUB_REPORTER"] = "true", From b27a628c9c134b6e02169fef3ec264cb4761f9e0 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sun, 5 Apr 2026 00:23:26 +0100 Subject: [PATCH 5/8] Fix RunPlaywrightTestsModule working directory (use Directory.FullName) --- TUnit.Pipeline/Modules/RunPlaywrightTestsModule.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TUnit.Pipeline/Modules/RunPlaywrightTestsModule.cs b/TUnit.Pipeline/Modules/RunPlaywrightTestsModule.cs index 80125a8fa2..e98a92b8ec 100644 --- a/TUnit.Pipeline/Modules/RunPlaywrightTestsModule.cs +++ b/TUnit.Pipeline/Modules/RunPlaywrightTestsModule.cs @@ -23,7 +23,7 @@ public class RunPlaywrightTestsModule : TestBaseModule }, new CommandExecutionOptions { - WorkingDirectory = project.Folder!.Path, + WorkingDirectory = project.Directory!.FullName, EnvironmentVariables = new Dictionary { ["DISABLE_GITHUB_REPORTER"] = "true", From 4a8369a46b552a7bfa2965aab812badf26308567 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sun, 5 Apr 2026 00:40:52 +0100 Subject: [PATCH 6/8] Remove NoBuild from RunPlaywrightTestsModule - project not in solution build --- TUnit.Pipeline/Modules/RunPlaywrightTestsModule.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/TUnit.Pipeline/Modules/RunPlaywrightTestsModule.cs b/TUnit.Pipeline/Modules/RunPlaywrightTestsModule.cs index e98a92b8ec..71cc8d658d 100644 --- a/TUnit.Pipeline/Modules/RunPlaywrightTestsModule.cs +++ b/TUnit.Pipeline/Modules/RunPlaywrightTestsModule.cs @@ -18,7 +18,6 @@ public class RunPlaywrightTestsModule : TestBaseModule new DotNetRunOptions { Project = project.Name, - NoBuild = true, Configuration = "Release", }, new CommandExecutionOptions From 0bb8ff4ccd4972b483aaf5e3bfb220bbde5d2e75 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sun, 5 Apr 2026 00:45:23 +0100 Subject: [PATCH 7/8] Add Playwright TestProject to CI solution and restore NoBuild --- TUnit.CI.slnx | 3 ++- TUnit.Pipeline/Modules/RunPlaywrightTestsModule.cs | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/TUnit.CI.slnx b/TUnit.CI.slnx index 1cb546d87b..19f3b65391 100644 --- a/TUnit.CI.slnx +++ b/TUnit.CI.slnx @@ -100,8 +100,9 @@ - + + diff --git a/TUnit.Pipeline/Modules/RunPlaywrightTestsModule.cs b/TUnit.Pipeline/Modules/RunPlaywrightTestsModule.cs index 71cc8d658d..e98a92b8ec 100644 --- a/TUnit.Pipeline/Modules/RunPlaywrightTestsModule.cs +++ b/TUnit.Pipeline/Modules/RunPlaywrightTestsModule.cs @@ -18,6 +18,7 @@ public class RunPlaywrightTestsModule : TestBaseModule new DotNetRunOptions { Project = project.Name, + NoBuild = true, Configuration = "Release", }, new CommandExecutionOptions From 35503dbf04e11b1c75f873f7a5fc921cf78710e9 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sun, 5 Apr 2026 00:46:11 +0100 Subject: [PATCH 8/8] Fix nullability warning in InstallPlaywrightModule --- TUnit.Pipeline/Modules/InstallPlaywrightModule.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TUnit.Pipeline/Modules/InstallPlaywrightModule.cs b/TUnit.Pipeline/Modules/InstallPlaywrightModule.cs index 281e08e227..3a065e64de 100644 --- a/TUnit.Pipeline/Modules/InstallPlaywrightModule.cs +++ b/TUnit.Pipeline/Modules/InstallPlaywrightModule.cs @@ -7,8 +7,8 @@ namespace TUnit.Pipeline.Modules; public class InstallPlaywrightModule : Module { - protected override Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) => - context.Shell.Bash.Command( + protected override async Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) => + await context.Shell.Bash.Command( new BashCommandOptions("npx playwright install --with-deps"), cancellationToken); }