Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"rollForward": false
},
"purview.build": {
"version": "0.5.4",
"version": "0.7.1",
"commands": [
"purview-build"
],
Expand Down
8 changes: 7 additions & 1 deletion .config/purview-build.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@
"Build": {
"Solution": "src/Containers.slnx",
"TestRoot": "src/tests",
// Discovery stays unit-only so the shared Ubuntu pipeline never launches a Windows-targeted WSLC
// test app. `just test` is the all-categories path; the TestFilter stays /*/*/*/* so widening
// discovery runs every category.
"TestPatterns": "*UnitTests.csproj",
"TestProjects": "*Tests.csproj",
"TestFilter": "/*/*/*/*[Category=Unit]"
// Test projects run one at a time whenever discovery is widened: a WSLC session exclusively
// locks its image-store VHD, and a parallel second process falls back to a cold isolated store.
"MaxTestParallelism": 1,
"TestFilter": "/*/*/*/*"
},
"PackValidation": {
// Explicit because Purview.Build 0.5.3 defaults RequiredCompilerFlags to empty. Earlier
Expand Down
26 changes: 21 additions & 5 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
# Purview.BuildSdk 1.1.0 - managed file, do not edit
#
# This file is written by Purview.BuildSdk and is replaced whenever the SDK version changes. The version
# on the first line is how the SDK recognises a file it owns: a .editorconfig without that marker is
# yours, and is never touched.
#
# To add your own rules, create a second .editorconfig in a subdirectory - `src/.editorconfig` is the
# usual choice - without `root = true`. EditorConfig applies the nearest file last, so your settings win
# over these, and nothing you write is lost when this file is updated.
#
# To stop the SDK updating this file, set one of:
# <PurviewEditorConfigAutoUpdate>false</PurviewEditorConfigAutoUpdate> keep this file as it is
# <PurviewRepoBootstrapMode>Never</PurviewRepoBootstrapMode> opt out of all bootstrapping
# <BootstrapEditorConfigToRepoRoot>false</BootstrapEditorConfigToRepoRoot>
#
# Deleting the first line also works: without the marker the SDK treats the file as yours. You then own
# every rule in it, including the ones a future SDK version would have fixed.
#
# See https://github.com/purview-dev/build-sdk#editorconfig for the full description.

root = true

# Base Configuration - General Settings, File Formatting, and Naming Conventions
Expand Down Expand Up @@ -360,12 +380,8 @@ dotnet_style_require_accessibility_modifiers = omit_if_default:warning
# Code-block preferences
csharp_prefer_braces = when_multiline:suggestion
csharp_prefer_simple_using_statement = false:silent
csharp_prefer_system_threading_lock = true
dotnet_style_prefer_collection_expression = true:suggestion

# Enable foreach explicit cast preference
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed:warning

# Prefer System.Threading.Lock
csharp_prefer_system_threading_lock = true:warning

Expand Down Expand Up @@ -497,7 +513,7 @@ dotnet_style_prefer_auto_properties = true:warning
dotnet_style_prefer_compound_assignment = true:warning
dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
dotnet_style_prefer_conditional_expression_over_return = true:suggestion
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed:warning
dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning
dotnet_style_prefer_inferred_tuple_names = true:warning
dotnet_style_prefer_simplified_boolean_expressions = false:silent
Expand Down
130 changes: 118 additions & 12 deletions .github/workflows/integration-wsl.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
name: Integration (WSL Containers)

# Manual-only. The WSLC integration suites need a Windows host with WSL Containers installed, which no
# GitHub-hosted runner provides, so this job runs on a self-hosted Windows runner carrying the custom
# `wslc` label. It is deliberately not attached to pull_request or push: it is the deliberate, manual
# proof that the WSLC backend and every service module run on WSL Containers. The Docker half of the
# matrix runs on every pull request instead (see pr.yml, the `integration-docker` job) because it needs
# only a daemon and therefore works on a hosted Linux runner.
# The WSLC suites need Windows Server 2025 with nested virtualisation, which GitHub's `windows-2025`
# hosted runners provide. The pre-release WSL channel carries wslc and its directory is not on PATH.
#
# Coverage:
# - every pull request: the WSLC contract suite, from pr.yml's `integration-wsl` job (fast)
# - nightly: the full hosted matrix (contract + every service module), from the schedule below
# - manually: either the hosted matrix or a self-hosted Windows runner carrying the custom `wslc`
# label (useful when a warm image store matters), selected with the `runner` input
#
# Runner prerequisites are documented in docs/wiki/Testing.md.
on:
Expand All @@ -19,18 +21,119 @@ on:
description: TUnit treenode filter. Defaults to every test.
required: false
default: "/*/*/*/*"
runner:
description: Runner to use for the full matrix.
type: choice
options: [hosted, self-hosted]
default: hosted
scope:
description: Test scope.
type: choice
options: [full, contract]
default: full
schedule:
# Nightly full matrix on a hosted windows-2025 runner (UTC). The contract suite already runs on
# every pull request; this is the standing proof that every service module works on WSLC.
- cron: "0 3 * * *"

concurrency:
# Never cancel a WSLC run in flight: it owns a session and its image store.
group: ${{ github.workflow }}-${{ inputs.ref || github.ref }}
cancel-in-progress: false

jobs:
integration-wsl:
name: Integration (WSL Containers)
# A self-hosted Windows x64 runner with WSL Containers installed and the custom `wslc` label.
integration-wsl-hosted:
name: Integration (WSL Containers, hosted)
# On a schedule there are no inputs, so default to the hosted full matrix. On manual dispatch the
# `runner` input decides between this job and the self-hosted one.
if: github.event_name == 'schedule' || inputs.runner != 'self-hosted'
runs-on: windows-2025
timeout-minutes: 90
env:
PURVIEW_CONTAINERS_BACKEND: wsl
PURVIEW_CONTAINERS_REQUIRE_BACKEND: wsl
WSL_UTF8: "1"
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}

- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
# Keep in sync with `sdk.version` in global.json (and the other workflows).
dotnet-version: "11.0.100-rc.2.26504.105"

- name: Install WSL Containers
shell: pwsh
run: |
wsl --update --pre-release
$wslc = 'C:\Program Files\WSL\wslc.exe'
if (-not (Test-Path $wslc)) {
# The feature reached GA in some channels; fall back to the stable update before failing.
wsl --update
}
wsl --version
if (-not (Test-Path $wslc)) {
throw "wslc.exe was not installed at $wslc; the hosted runner did not get a WSL build with containers."
}
$env:PATH = "C:\Program Files\WSL;$env:PATH"
& $wslc version

- name: Restore
run: dotnet restore src/Containers.slnx

- name: Build
run: dotnet build src/Containers.slnx --configuration Release --no-restore

- name: WSLC backend contract
# Real containers on a shared WSLC session; the suite caps its own parallelism.
run: dotnet test src/tests/Wsl.IntegrationTests/Wsl.IntegrationTests.csproj --configuration Release --no-build --treenode-filter '${{ inputs.filter || '/*/*/*/*' }}' --retry-failed-tests 2 --retry-failed-tests-delay 5s --retry-failed-tests-max-percentage 50

- name: Service modules on WSLC
if: inputs.scope != 'contract'
# Each module pulls its image and starts a real service. One project at a time keeps the shared
# WSLC image store uncontended (parallel sessions fail with 0x80070020). Every project runs so a
# single failure does not hide the rest, and the step fails at the end if any of them did.
shell: pwsh
run: |
$projects = @(
'src/tests/AzureKeyVaultEmulator.IntegrationTests/AzureKeyVaultEmulator.IntegrationTests.csproj',
'src/tests/PostgreSql.IntegrationTests/PostgreSql.IntegrationTests.csproj',
'src/tests/Redis.IntegrationTests/Redis.IntegrationTests.csproj',
'src/tests/MsSql.IntegrationTests/MsSql.IntegrationTests.csproj',
'src/tests/MySql.IntegrationTests/MySql.IntegrationTests.csproj',
'src/tests/RabbitMq.IntegrationTests/RabbitMq.IntegrationTests.csproj',
'src/tests/Azurite.IntegrationTests/Azurite.IntegrationTests.csproj',
'src/tests/Nats.IntegrationTests/Nats.IntegrationTests.csproj'
)

$filter = '${{ inputs.filter || '/*/*/*/*' }}'
$failed = @()
foreach ($project in $projects) {
Write-Host "::group::$project"
dotnet test $project --configuration Release --no-build --treenode-filter $filter --retry-failed-tests 2 --retry-failed-tests-delay 5s --retry-failed-tests-max-percentage 50
if ($LASTEXITCODE -ne 0) {
$failed += $project
}
Write-Host "::endgroup::"
}

if ($failed.Count -gt 0) {
throw "WSLC integration suites failed: $($failed -join ', ')"
}

integration-wsl-self-hosted:
name: Integration (WSL Containers, self-hosted)
if: inputs.runner == 'self-hosted'
# A self-hosted Windows x64 runner with WSL Containers installed and the custom `wslc` label. It is
# only used on manual dispatch; a runner with a warm image store makes the full matrix much faster.
runs-on: [self-hosted, wslc]
timeout-minutes: 60
env:
PURVIEW_CONTAINERS_BACKEND: wsl
PURVIEW_CONTAINERS_REQUIRE_BACKEND: wsl
WSL_UTF8: "1"
steps:
- uses: actions/checkout@v4
with:
Expand All @@ -46,7 +149,8 @@ jobs:
shell: pwsh
run: |
wsl --version
wslc version
$env:PATH = "C:\Program Files\WSL;$env:PATH"
& 'C:\Program Files\WSL\wslc.exe' version

- name: Restore
run: dotnet restore src/Containers.slnx
Expand All @@ -56,9 +160,10 @@ jobs:

- name: WSLC backend contract
# Real containers on a shared WSLC session; ~27 tests and ~4 minutes.
run: dotnet test src/tests/Wsl.IntegrationTests/Wsl.IntegrationTests.csproj --configuration Release --no-build --treenode-filter '${{ inputs.filter }}'
run: dotnet test src/tests/Wsl.IntegrationTests/Wsl.IntegrationTests.csproj --configuration Release --no-build --treenode-filter '${{ inputs.filter || '/*/*/*/*' }}' --retry-failed-tests 2 --retry-failed-tests-delay 5s --retry-failed-tests-max-percentage 50

- name: Service modules on WSLC
if: inputs.scope != 'contract'
# Each module pulls its image and starts a real service. One project at a time keeps the shared
# WSLC image store uncontended (parallel sessions fail with 0x80070020). Every project runs so a
# single failure does not hide the rest, and the step fails at the end if any of them did.
Expand All @@ -75,10 +180,11 @@ jobs:
'src/tests/Nats.IntegrationTests/Nats.IntegrationTests.csproj'
)

$filter = '${{ inputs.filter || '/*/*/*/*' }}'
$failed = @()
foreach ($project in $projects) {
Write-Host "::group::$project"
dotnet test $project --configuration Release --no-build --treenode-filter '${{ inputs.filter }}'
dotnet test $project --configuration Release --no-build --treenode-filter $filter --retry-failed-tests 2 --retry-failed-tests-delay 5s --retry-failed-tests-max-percentage 50
if ($LASTEXITCODE -ne 0) {
$failed += $project
}
Expand Down
58 changes: 52 additions & 6 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ jobs:
# ("A compatible .NET SDK was not found") and every step fails.
# Keep in sync with `sdk.version` in global.json.
dotnet-version: "11.0.100-rc.2.26504.105"
# Mirrors Build:TestFilter in purview-build.json: the SDK categorises unit test projects as Unit,
# so the WSLC integration suites are never selected and the runner needs no WSLC host.
# The repository default filter is /*/*/*/*, but test discovery stays unit-only and the shared
# pipeline runs on ubuntu-latest, so it has no WSLC host. This job narrows the filter to Unit
# anyway. The Docker suites run in the explicit job below; the WSLC contract runs on
# windows-2025 in the integration-wsl job.
test-filter: "/*/*/*/*[Category=Unit]"
# All library projects are packable, so pack and validate the produced packages.
run-pack: true
Expand All @@ -32,18 +34,62 @@ jobs:
# The shared pipeline filters to [Category=Unit] because the WSLC integration suites need a Windows
# host. The Docker integration suites need only a Docker daemon, so they run here on the same Linux
# runner a consumer would use - the standing proof that the library and every service module work off
# Windows/WSL. The suites skip themselves when no daemon is reachable, so a non-zero result is a real
# failure, not a missing runtime.
# Windows/WSL. The job requires the daemon, so a runner without one fails instead of skipping.
integration-docker:
name: Integration (Docker)
runs-on: ubuntu-latest
env:
# Pin the backend and require it: a runner without a daemon must fail, not silently skip every test.
PURVIEW_CONTAINERS_BACKEND: docker
PURVIEW_CONTAINERS_REQUIRE_BACKEND: docker
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
# Keep in sync with `sdk.version` in global.json.
dotnet-version: "11.0.100-rc.2.26504.105"
- name: Docker backend
run: dotnet test src/tests/Docker.IntegrationTests/Docker.IntegrationTests.csproj --configuration Release --treenode-filter '/*/*/*/*'
run: dotnet test src/tests/Docker.IntegrationTests/Docker.IntegrationTests.csproj --configuration Release --treenode-filter '/*/*/*/*' --retry-failed-tests 2 --retry-failed-tests-delay 5s --retry-failed-tests-max-percentage 50
- name: Service modules on Docker
run: dotnet test src/tests/Modules.Docker.IntegrationTests/Modules.Docker.IntegrationTests.csproj --configuration Release --treenode-filter '/*/*/*/*'
run: dotnet test src/tests/Modules.Docker.IntegrationTests/Modules.Docker.IntegrationTests.csproj --configuration Release --treenode-filter '/*/*/*/*' --retry-failed-tests 2 --retry-failed-tests-delay 5s --retry-failed-tests-max-percentage 50

# WSLC needs a Windows host with nested virtualisation. windows-2025 provides it (Windows Server
# 2025); wslc ships in the pre-release WSL channel and its directory is not on PATH. This is the
# contract suite only: the full module matrix is too slow for every pull request and runs in the
# nightly integration-wsl workflow instead.
integration-wsl:
name: Integration (WSLC contract)
runs-on: windows-2025
timeout-minutes: 30
env:
PURVIEW_CONTAINERS_BACKEND: wsl
PURVIEW_CONTAINERS_REQUIRE_BACKEND: wsl
WSL_UTF8: "1"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
# Keep in sync with `sdk.version` in global.json.
dotnet-version: "11.0.100-rc.2.26504.105"
- name: Install WSL Containers
shell: pwsh
run: |
wsl --update --pre-release
$wslc = 'C:\Program Files\WSL\wslc.exe'
if (-not (Test-Path $wslc)) {
# The feature reached GA in some channels; fall back to the stable update before failing.
wsl --update
}
wsl --version
if (-not (Test-Path $wslc)) {
throw "wslc.exe was not installed at $wslc; the hosted runner did not get a WSL build with containers."
}
$env:PATH = "C:\Program Files\WSL;$env:PATH"
& $wslc version
- name: Restore
run: dotnet restore src/Containers.slnx
- name: Build
run: dotnet build src/Containers.slnx --configuration Release --no-restore
- name: WSLC backend contract
# Real containers on the shared WSLC session; the suite caps its own parallelism.
run: dotnet test src/tests/Wsl.IntegrationTests/Wsl.IntegrationTests.csproj --configuration Release --no-build --treenode-filter '/*/*/*/*' --retry-failed-tests 2 --retry-failed-tests-delay 5s --retry-failed-tests-max-percentage 50
7 changes: 7 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ on:
branches: ["release/**"]
workflow_dispatch:

permissions:
contents: write
# The shared release workflow pushes the same packages to nuget.org and, when
# PURVIEW_PUBLISH_GITHUB_PACKAGES=true, to GitHub Packages. A reusable workflow cannot grant
# more than its caller, so the permission is declared here.
packages: write

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
Expand Down
Loading
Loading