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
136 changes: 66 additions & 70 deletions .editorconfig

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# GitHub Copilot Instructions

## Primary instruction source

Use the repository root [`AGENTS.md`](../AGENTS.md) as the **primary** source of truth for behavior,
architecture context, testing standards, and completion criteria.

If this file and `AGENTS.md` appear to conflict, prefer `AGENTS.md` unless this file explicitly states a
GitHub Copilot-only exception.

## Copilot-specific guidance

This file should only contain **GitHub Copilot-specific** instruction details.
Keep product, architecture, and general engineering standards centralized in `AGENTS.md`.

## Operating expectations for Copilot

- Apply the `AGENTS.md` testing bar strictly: TUnit tests, `*.UnitTests`/`*.IntegrationTests` project
placement, and the rule that WSLC integration modules run serially.
- Treat work as incomplete until the relevant tests pass; unit tests need no WSLC host, integration tests do.
- Never weaken the WSLC session/store invariants (single shared session, serialised session mutations, the
shared-store fallback, `Secret` redaction) to make a test pass.
- Consult the repository `.agents/` folder for additional skills/workflows that may improve execution
quality. Skill content is delivered by NuGet packages and is read-only here.
- Keep edits minimal, focused, and aligned with existing SDK and repository conventions.
30 changes: 30 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: PR

on:
pull_request:
branches: [main]
# `ready_for_review` is not one of the default activity types, so a draft PR would never
# re-evaluate the required build gate once it is marked ready for review.
types: [opened, synchronize, reopened, ready_for_review]

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

jobs:
build:
name: Build and test
uses: purview-dev/build/.github/workflows/purview-build.yml@main
with:
# The shared workflow defaults to the 10.0.x SDK, but this repository targets net11.0 and pins
# the .NET 11 SDK in global.json. Without the matching SDK the runner cannot resolve global.json
# ("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.1.26425.128"
# 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.
test-filter: "/*/*/*/*[Category=Unit]"
# All eight library projects are packable, so pack and validate the produced packages.
run-pack: true
validate-pack: true
secrets: inherit
22 changes: 22 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Release

on:
push:
branches: [main]

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

jobs:
release:
name: Release packages
uses: purview-dev/build/.github/workflows/purview-release.yml@main
with:
# The shared workflow defaults to the 10.0.x SDK, but this repository targets net11.0 and pins
# the .NET 11 SDK in global.json; without it the release job cannot resolve global.json.
# Keep in sync with `sdk.version` in global.json (and with .github/workflows/pr.yml).
dotnet-version: "11.0.100-rc.1.26425.128"
release-mode: NuGet
release-branch: main
secrets: inherit
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -657,4 +657,4 @@ sketch

!scripts/*

./.tools/purview-build/**/*
.tools/purview-build/
134 changes: 134 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Agent Instructions

## Purpose and authority

This repository contains `Purview.WslContainers`, a **WSLC-native** Testcontainers-style library for .NET:
throwaway Linux containers for integration testing on **Microsoft WSL Containers (WSLC)**, with no Docker
installation, plus the service modules (`PostgreSql`, `Redis`, `MsSql`, `RabbitMq`, `Azurite`, `Nats`,
`MySql`).

- This file is the repository-wide source of truth for AI agents. A more-specific `AGENTS.md` in a subtree, if
one is ever added, takes precedence for that subtree.
- `.github/copilot-instructions.md` defers to this file.
- Follow explicit user instructions first, then the nearest applicable repository instructions, then
established code patterns.
- Operate only in this repository unless the user explicitly expands the scope.
- Never read, copy, log, or commit secrets from excluded files, environment variables, user profiles, local
configuration, test output, or registry credentials.

## Source of truth and layout

| Path | Purpose |
| --- | --- |
| `src/WSLTestContainers.slnx` | Canonical solution for restore, build, test and pack |
| `src/src/WslContainers` | Core runtime (`Purview.WslContainers`): `ContainerBuilder`, `WslContainer`, runtime, sessions, images, networking, mounts, wait strategies, diagnostics |
| `src/src/<Module>` | Service modules; each is a thin layer over the core and carries a bespoke `Sdk/README.md` |
| `src/src/<Project>/Sdk` | Package-only assets. `Sdk/README.md` is packed as the package README (suppressing the repo-root README); `Sdk/buildTransitive/**` would ship MSBuild assets |
| `src/tests` | TUnit unit (`*.UnitTests`) and WSLC integration (`*.IntegrationTests`) projects |
| `spikes/WslcSpikes` | Phase 0 investigation harness (`s1`..`s14` probes); not part of the test run |
| `docs/wiki` | User-facing documentation wiki, aggregated by the purview-dev website |
| `mkdocs.yml` | Wiki site configuration (`docs_dir: docs/wiki`) |
| `src/Directory.Build.props` / `src/Directory.Build.targets` | Solution-wide SDK import, package metadata and the shared package icon |
| `purview-build.json` | Shared `Purview.Build` pipeline configuration: solution, test discovery/filter and the **exhaustive** pack-validation manifest |
| `Justfile` | Supported local workflow commands |
| `.agents` | Package-delivered skills, agents and prompts (read-only here) |
| `.purview/agent-sync.cache` | SDK-managed manifest recording which `.agents` files are already mirrored |

## Standard workflow

1. Read this file, inspect the working tree, and locate the implementation, tests, documentation and existing
patterns relevant to the task.
2. Confirm behaviour from code and tests rather than relying on memory or documentation alone.
3. Make the smallest coherent change. Preserve public behaviour unless the task explicitly changes it.
4. Update tests for fixes and behaviour changes. Update documentation when public behaviour changes.
5. Run the narrowest meaningful validation first, then broader validation in proportion to risk.
6. Review the diff for unrelated edits, generated noise, compatibility risks, and missing docs or tests.

## Runtime invariants

- **One shared, process-wide session** (`WslContainerRuntime.Instance`), lazily started, named
`wslc-{pid}-{random8}`. Container isolation comes from unique names and unique host ports, not from extra
sessions.
- **Images are shared by default** (`StorageMode.Shared` → `%LOCALAPPDATA%\Purview\WslContainers\images`). A
session exclusively locks its `storage.vhdx`; the runtime verifies the store once and falls back to an
isolated per-process store on a sharing violation (`0x80070020`). Do not remove that fallback.
- **All session-mutating operations are serialised** through the runtime's gate (concurrent `Start` can fail
with `0x8000FFFF`). Keep new lifecycle paths inside it.
- **Random host ports are native** (`windowsPort=0`) and read back from the container's mapped ports — never
probe for a free port.
- **Default networking is `Bridged`**; only IPv4 loopback is mapped, and UDP is unsupported
(`WslContainerNotSupportedException`).
- **`DisposeAsync` is idempotent** and never terminates the shared session; a process-exit hook disposes the
session so its name is released.
- **Secrets use `Secret`** and must never reach logs, names, `ToString()` or diagnostics (they are redacted).

## Module rules

- A module supplies only defaults: image, ports, environment, module configuration (`WithXxx`), a readiness
strategy, and connection-string/endpoint accessors. It must not duplicate runtime infrastructure.
- New modules follow the shape in [Contributing Modules](docs/wiki/Contributing-Modules.md): a
`ContainerConfiguration`-derived record, a `ContainerBuilder<TBuilder, TContainer, TConfiguration>`
subclass, and a container exposing `GetConnectionString()`/endpoints built from `GetMappedPublicPort`.
- Prefer verifying the service for readiness (exec a readiness command or open a host client connection) over
a bare TCP check. Where an image reports readiness too early (MySQL), a log match is wrong.
- SQL Server requires an explicit `AcceptLicense()` call; never accept licensing terms on the caller's behalf.

## Packaging and validation

- Every project under `src/src` sets `<IsPackable>true</IsPackable>` **literally** in the `.csproj`: the SDK
resolves `IsPackable` by scanning the project file, and the package metadata/icon conditions in
`src/Directory.Build.props` depend on it.
- Each package ships `lib/$(TFM)/<Assembly>.{dll,xml}`, `README.md` (from `Sdk/README.md`) and
`purview-logo-light.png`. PDBs ship only in the `.snupkg`.
- `PackValidation.RequireExplicitContent` defaults to `true`, so `purview-build.json`'s `RequiredContent` is
the **exhaustive** manifest: a produced package with no rule, or a packed entry matched by no glob, fails
validation. Adding or removing packaged content means updating that map.
- Validate with `just pack` and `just pipeline-pack-validate`.

## Testing

- Test assemblies are categorised by the SDK: `*.UnitTests` → `Unit`, `*.IntegrationTests` → `Integration`.
- `purview-build.json` filters pipeline runs to `[Category=Unit]`, so the shared pipeline never needs a WSLC
host; keep it that way unless the task requires real containers.
- WSLC integration tests run **serially** (`--max-parallel-test-modules 1` in the `Justfile`): a session
exclusively locks its image-store VHD, so parallel modules fail with `0x80070020`.
- Use the shared `WslcTest` helper in `src/tests/SharedTestingFramework` for integration skip/availability
checks. Unit tests may use the modules' `BuildConfigurationForTesting()` internal hook.

## Local commands

```text
just build # dotnet build (Debug)
just test '/*/*/*/*[Category=Unit]' # unit tests only
just lint-check / just lint-fix # CSharpier check / format
just pack # build + dotnet pack into ./artifacts
just pipeline-pack-validate # restore, build, lint, test, pack, validate
just scrub # reset bin/obj, clean, forced restore, build-server shutdown
```

Commit messages follow Conventional Commits enforced by the `commit-msg` lefthook
(`.config/lefthook.yml` → `npx commitlint`); allowed types are in `commitlint.config.mts`.

## Versioning and releases

- `package.json` is the authoritative release and package version. Do not diverge project versions by hand.
- `.github/workflows/pr.yml` builds and tests pull requests through the shared
`purview-dev/build/.github/workflows/purview-build.yml`, with pack and validation enabled.
- `.github/workflows/release.yml` runs the shared release pipeline with `release-mode: NuGet` on a push to
`main`.
- Both workflows pin `dotnet-version` to `global.json`'s `sdk.version`; keep them in sync, and keep
`purview-build.json` pointing at `src/WSLTestContainers.slnx`.

## Completion checklist

Before handing work back:

- Confirm the requested behaviour and scope are satisfied, and only intended files changed.
- Review public API, package-content and dependency-direction implications.
- Update the affected package `Sdk/README.md`, the root `README.md`, `docs/wiki` (plus `_Sidebar.md` and
`mkdocs.yml` for new pages), `AGENTS.md` and `purview-build.json` when the change affects them.
- Watch for the packaging traps: a new packable project missing `<IsPackable>true</IsPackable>` or a
`RequiredContent` entry, and a new `Sdk/README.md` that does not describe its own package.
- Run the appropriate build, test, formatting and pack checks in proportion to risk.
- State exactly what validation ran. If a check was skipped or blocked, give the concrete reason and the
remaining risk.
14 changes: 0 additions & 14 deletions Directory.Build.props

This file was deleted.

11 changes: 10 additions & 1 deletion Justfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
set quiet

export TESTINGPLATFORM_EXITCODE_IGNORE := "8"
export DOTNET_CLI_TELEMETRY_OPTOUT := "1"
export DOTNET_SKIP_FIRST_TIME_EXPERIENCE := "1"
export DO_NOT_TRACK := "1"

root_folder := "./src/"
solution_file := root_folder + "WSLTestContainers.slnx"
test_solution := solution_file
Expand Down Expand Up @@ -84,10 +89,14 @@ restore *args:
dotnet restore {{ solution_file }} {{ args }}

# Runs tests for the solution with the specified configuration (default: Debug)
# Test modules run serially by default: a WSLC session exclusively locks its image-store VHD, so parallel
# test assemblies contend for the shared store and fail with 0x80070020 ("file is being used by another
# process"). Override by appending the flag, e.g.
# just test '/*/*/*/*/' --max-parallel-test-modules 4
[group('Build and Test')]
test filter="/*/*/*/*/" *args:
echo "Running tests for {{ BLUE }}{{ test_solution }}{{ NORMAL }} with {{ YELLOW }}{{ build_configuration }}{{ NORMAL }}..."
dotnet test --solution "{{ test_solution }}" --configuration "{{ build_configuration }}" --treenode-filter={{ filter }} {{ args }}
dotnet test --solution "{{ test_solution }}" --configuration "{{ build_configuration }}" --max-parallel-test-modules 1 --treenode-filter={{ filter }} {{ args }}

# Cleans the solution with the specified configuration (default: Debug)
[group('Build and Test')]
Expand Down
Loading
Loading