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
8 changes: 7 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup WSL
uses: Particular/setup-wsl-action@v1.0.0
- name: Run
uses: ./
with:
Expand Down Expand Up @@ -66,6 +68,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup WSL
uses: Particular/setup-wsl-action@v1.0.0
- name: Run with FTS
uses: ./
with:
Expand Down Expand Up @@ -105,6 +109,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup WSL
uses: Particular/setup-wsl-action@v1.0.0
- name: Run with distributed transactions
uses: ./
with:
Expand All @@ -118,7 +124,7 @@ jobs:
$ErrorActionPreference = 'Stop'

# Mirror the action's distribution resolution so the inspect commands reach the right WSL.
$distribution = $Env:WSL_DISTRIBUTION_OVERRIDE ?? 'Debian'
$distribution = $Env:WSL_DISTRIBUTION

if ($Env:RUNNER_OS -eq 'Windows') {
$envJson = (& wsl.exe --distribution $distribution -- docker inspect sqlserver --format '{{json .Config.Env}}') -join "`n"
Expand Down
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,36 @@ This action installs and runs SQL Server for a GitHub Actions workflow. Also [ad

1. Runs SQL Server in a Linux Docker container on every platform.
* On Linux, the container runs directly via Docker using the `mcr.microsoft.com/mssql/server:2022-latest` image.
* On Windows, the Linux container runs inside WSL2 (the action installs and starts Docker inside the WSL distribution).
* On Windows, the Linux container runs inside WSL2 provisioned by [setup-wsl-action](https://github.com/Particular/setup-wsl-action) — run it first, see [Prerequisites](#prerequisites).
* Sets collation to case sensitive `SQL_Latin1_General_CP1_CS_AS` note SQL Azure is insensitive `CI_AS`
1. Creates environment variables for a connection string and for `sqlcmd`.
1. Waits for the SQL instance to be accessible.
1. Creates a default database catalog.
1. Tears the container down in a post step (see [Cleanup](#cleanup)).

## Prerequisites

This action does **not** provision WSL or Docker itself. On **Windows** runners it requires [setup-wsl-action](https://github.com/Particular/setup-wsl-action) to run **first** in the same job — that action provisions WSL2 + Docker, keeps the instance alive, and exports the `WSL_DISTRIBUTION`, `WSL_IP`, and `WSL_TOOLS_MODULE_PATH` environment variables this action relies on. On **Linux** runners setup-wsl-action is a no-op but should still be included so the workflow is uniform.

If setup-wsl-action has not run, the action fails fast with a clear error.

## Usage

Install SQL Server 2022 with a default database of `nservicebus` and put the connection string in the environment variable `SQL_SERVER_CONNECTION_STRING`:

```yaml
steps:
- name: Setup WSL
uses: Particular/setup-wsl-action@v1
- name: Install SQL Server
uses: Particular/install-sql-server-action@v1.5.0 # Check if this is the latest version at https://github.com/Particular/install-sql-server-action/tags
with:
connection-string-env-var: SQL_SERVER_CONNECTION_STRING
catalog: nservicebus
```

> Every example below assumes a `Setup WSL` step runs first (as shown in the first example); it is omitted for brevity.

It is also possible to specify the SQl server major version to be installed

```yaml
Expand Down Expand Up @@ -111,7 +121,7 @@ On hosted runners this cleanup is harmless — the runner VM is destroyed at the
The generated connection string uses SQL authentication (`User Id=sa;Password=...;Encrypt=false;`):

- On **Linux** the data source is `localhost` (`Server=localhost;...`).
- On **Windows** the data source is the WSL2 VM IP address (`Server=<wsl-ip>;...`) so that the host can reach the container. The `WSL_DISTRIBUTION_OVERRIDE` and `WSL_MEMORY_OVERRIDE` environment variables can be set to override the distribution name (default `Debian`) and the `.wslconfig` memory limit (default `4GB`) respectively.
- On **Windows** the data source is the WSL2 VM IP address (`Server=<wsl-ip>;...`) so that the host can reach the container. The address comes from the `WSL_IP` environment variable set by setup-wsl-action.

## Using `sqlcmd`

Expand Down Expand Up @@ -172,3 +182,5 @@ To run setup.ps1 / cleanup.ps1 directly during local debugging, set the same `IN
pwsh -File setup.ps1 -ContainerName sqlserver -ConnectionStringName SQL_SERVER_CONNECTION_STRING -Catalog nservicebus
pwsh -File cleanup.ps1 -ContainerName sqlserver
```

> Running `setup.ps1`/`cleanup.ps1` directly on Windows requires `WSL_TOOLS_MODULE_PATH` to point at setup-wsl-action's `WslTools` module (set it by running setup-wsl-action first).
11 changes: 8 additions & 3 deletions cleanup.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ param (

$ErrorActionPreference = 'Continue'

# The Windows container removal runs through the WslTools module (Invoke-Wsl), which
# setup-wsl-action exports at WSL_TOOLS_MODULE_PATH. Import it here with a clear guard.
if (-not $Env:WSL_TOOLS_MODULE_PATH) {
throw "This action requires Particular/setup-wsl-action to run first — it provisions WSL/Docker and exports the WslTools module at WSL_TOOLS_MODULE_PATH."
}
Import-Module $Env:WSL_TOOLS_MODULE_PATH -Force

$runnerOs = $Env:RUNNER_OS ?? "Linux"
$enableDtc = $EnableDistributedTransactions -eq "true"

Expand All @@ -21,11 +28,9 @@ if ($runnerOs -eq "Linux") {
docker rm $ContainerName 2>$null
}
elseif ($runnerOs -eq "Windows") {
$wslDistribution = $Env:WSL_DISTRIBUTION_OVERRIDE ?? "Debian"

if ($ContainerName) {
Write-Output "Removing WSL Docker container $ContainerName"
wsl.exe --distribution $wslDistribution --user root -- bash -c "docker rm --force ${ContainerName} 2>/dev/null || true"
Invoke-Wsl -Command "docker rm --force ${ContainerName} 2>/dev/null || true"
}

if ($enableDtc) {
Expand Down
12 changes: 0 additions & 12 deletions modules/WslTools/WslTools.psd1

This file was deleted.

36 changes: 0 additions & 36 deletions modules/WslTools/WslTools.psm1

This file was deleted.

101 changes: 19 additions & 82 deletions setup.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ param (

$ErrorActionPreference = 'Stop'

$modulePath = Join-Path $PSScriptRoot 'modules' 'WslTools'
Import-Module $modulePath -Force
if (-not $Env:WSL_TOOLS_MODULE_PATH) {
throw "This action requires Particular/setup-wsl-action to run first — it provisions WSL/Docker and exports the WslTools module at WSL_TOOLS_MODULE_PATH."
}
Import-Module $Env:WSL_TOOLS_MODULE_PATH -Force

function Save-State {
param(
Expand Down Expand Up @@ -60,16 +62,13 @@ function Install-FullTextSearch {

$containerScriptPath = "/tmp/install-fts.sh"
if ($WslDistribution) {
# The host file must be visible to the WSL filesystem before `wsl.exe ... docker cp`
# can copy it into the container. WSL auto-mounts the Windows drive containing the
# temp directory under /mnt/<drive>/, so we translate the host path ourselves instead
# The host file must be visible to the WSL filesystem before Docker inside WSL can copy
# it into the container. WSL auto-mounts the Windows drive containing the temp directory
# under /mnt/<drive>/, so we translate the host path ourselves (ConvertTo-WslPath) instead
# of round-tripping through `wslpath` -- the interop arg parser mangles backslashes.
$wslRenderedPath = ConvertTo-WslPath -WindowsPath $renderedPath
& wsl.exe --distribution $WslDistribution -- docker cp $wslRenderedPath "${ContainerName}:${containerScriptPath}"
if ($LASTEXITCODE -ne 0) {
throw "Failed to copy FTS install script into the container"
}
& wsl.exe --distribution $WslDistribution -- docker exec -u 0 $ContainerName bash $containerScriptPath
Invoke-Wsl -Distribution $WslDistribution -CheckExitCode -Command "docker cp '$wslRenderedPath' '${ContainerName}:${containerScriptPath}'"
Invoke-Wsl -Distribution $WslDistribution -CheckExitCode -Command "docker exec -u 0 $ContainerName bash $containerScriptPath"
}
else {
docker cp $renderedPath "${ContainerName}:${containerScriptPath}"
Expand Down Expand Up @@ -261,76 +260,14 @@ if ($runnerOs -eq "Linux") {
elseif ($runnerOs -eq "Windows") {
Write-Output "Running SQL Server in container $ContainerName inside WSL"

$wslDistribution = $Env:WSL_DISTRIBUTION_OVERRIDE ?? "Debian"

# Constrain the WSL2 VM (memory) and keep it from being shut down when idle, so
# that Docker and the container are not torn down between this setup step and the
# later test steps. Only write when the file is absent so local development
# configurations are left untouched.
$wslConfigPath = Join-Path $Env:USERPROFILE ".wslconfig"
if (-not (Test-Path $wslConfigPath)) {
$wslMemory = $Env:WSL_MEMORY_OVERRIDE ?? "4GB"
Write-Output "Writing $wslConfigPath (memory=$wslMemory) to constrain the WSL2 VM"
Set-Content -Path $wslConfigPath -Value "[wsl2]`nmemory=$wslMemory`nvmIdleTimeout=-1" -Encoding ASCII
}

Write-Output "::group::Preparing WSL ($wslDistribution)"
# WSL and Docker were provisioned by setup-wsl-action. Read the distribution and the WSL
# VM IP from the environment it exported rather than provisioning or detecting them here.
$wslDistribution = $Env:WSL_DISTRIBUTION
$ipAddress = $Env:WSL_IP

wsl.exe --set-default-version 2 | Out-Null

# Install the distribution if it is not already registered.
$installedDistributions = ((wsl.exe --list --quiet) -replace "`0", "") |
ForEach-Object { $_.Trim() } |
Where-Object { $_ -ne "" }

if ($installedDistributions -notcontains $wslDistribution) {
Write-Output "Installing $wslDistribution in WSL"
wsl.exe --install $wslDistribution --web-download --no-launch
if ($LASTEXITCODE -ne 0) {
throw "Failed to install $wslDistribution in WSL"
}
if (-not $ipAddress) {
throw "WSL_IP is not set. Run Particular/setup-wsl-action before this action."
}
else {
Write-Output "$wslDistribution is already installed"
}

# Ensure Docker is installed inside the WSL distribution.
Write-Output "Ensuring Docker is installed inside $wslDistribution"
Invoke-Wsl -Distribution $wslDistribution -CheckExitCode -Command "command -v docker >/dev/null 2>&1 || { apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install --yes docker.io; }"

# Start the Docker daemon via systemd when available, otherwise via the SysV service.
Write-Output "Starting Docker daemon inside $wslDistribution"
Invoke-Wsl -Distribution $wslDistribution -CheckExitCode -Command "docker info >/dev/null 2>&1 || { if [ -d /run/systemd/system ]; then systemctl start docker; else service docker start; fi; }"

# Keep the WSL instance alive for the rest of the job. WSL terminates an instance when no
# processes remain under its init (PID 2); a plain background process (e.g. sleep) does not
# prevent this, but a D-Bus session bus launched through `wsl --exec` does. vmIdleTimeout
# above covers the VM-level idle timeout; this covers the separate instance-level shutdown.
# See https://github.com/microsoft/WSL/issues/10138 and
# https://blog.lecoteauverdoyant.co.uk/articles/wsl-keep-alive.html
Write-Output "Starting a D-Bus session to keep the WSL instance alive for the job"
# dbus-launch ships in the dbus-x11 package (not dbus). Verify it is present afterwards so a
# packaging change can never silently leave the instance unguarded again.
Invoke-Wsl -Distribution $wslDistribution -CheckExitCode -Command "command -v dbus-launch >/dev/null 2>&1 || { apt-get update && apt-get install -y dbus-x11; }; command -v dbus-launch >/dev/null 2>&1 || { echo 'dbus-launch is unavailable after installing dbus-x11' >&2; exit 1; }"
wsl.exe --distribution $wslDistribution --user root --exec /usr/bin/dbus-launch true
if ($LASTEXITCODE -ne 0) {
throw "dbus-launch keep-alive failed with exit code $LASTEXITCODE"
}

Write-Output "::endgroup::"

# Determine the WSL VM IPv4 address early -- it's needed for the container hostname (DTC)
# and for the connection string, and the VM's IP is stable once WSL is running.
$wslIp = ((wsl.exe --distribution $wslDistribution --user root -- hostname -I) -replace "`0", "").Trim().Split(" ", [System.StringSplitOptions]::RemoveEmptyEntries) |
Where-Object { $_ -match '^\d+\.\d+\.\d+\.\d+$' } |
Select-Object -First 1

if (-not $wslIp) {
throw "Could not determine the WSL IPv4 address"
}

$ipAddress = $wslIp
Write-Output "WSL address: $ipAddress"

if ($enableDtc) {
# The container's DTC puts the container's hostname in its "whereabouts" blob. The Windows
Expand All @@ -342,8 +279,8 @@ elseif ($runnerOs -eq "Windows") {
$hostsPath = "$Env:SystemRoot\System32\drivers\etc\hosts"
$hostsMarker = "sqlserver"
if (-not (Get-Content $hostsPath -ErrorAction SilentlyContinue | Select-String $hostsMarker)) {
Write-Output "Adding hosts entry: $wslIp sqlserver"
Add-Content -Path $hostsPath -Value "$wslIp sqlserver"
Write-Output "Adding hosts entry: $ipAddress sqlserver"
Add-Content -Path $hostsPath -Value "$ipAddress sqlserver"
}
}

Expand Down Expand Up @@ -385,7 +322,7 @@ elseif ($runnerOs -eq "Windows") {
if ($LASTEXITCODE -ne 0) {
throw "Failed to start SQL Server container in WSL"
}
& wsl.exe --distribution $wslDistribution -- docker ps --filter "name=$ContainerName"
Invoke-Wsl -Distribution $wslDistribution -Command "docker ps --filter name=$ContainerName"
Write-Output "::endgroup::"

if ($enableFts) {
Expand All @@ -403,7 +340,7 @@ elseif ($runnerOs -eq "Windows") {
Write-Output "::endgroup::"

Write-Output "Starting SQL Server process..."
& wsl.exe --distribution $wslDistribution -- docker exec -d $ContainerName /opt/mssql/bin/sqlservr
Invoke-Wsl -Distribution $wslDistribution -CheckExitCode -Command "docker exec -d $ContainerName /opt/mssql/bin/sqlservr"
}

# The Windows runner ships a native sqlcmd (MSSQL.CMDLnUtils). Wrap it so the self-signed
Expand Down