Skip to content

CI

CI #3746

Workflow file for this run

name: CI
run-name: "CI${{ github.event.inputs.client-libs-test-image-tag != '' && format(' using image: {0}', github.event.inputs.client-libs-test-image-tag) || '' }}"
on:
pull_request:
push:
branches: [ 'main', 'v3' ]
paths:
- '**'
- '!/docs/*' # Don't run workflow when files are only in the /docs directory
workflow_dispatch:
inputs:
client-libs-test-image-tag:
description: 'Tag of the client libs test image to use'
required: false
default: ''
windows-topology:
description: 'Servers to start on the Windows job: full (everything) or basic (primary/replica/secure/failover only)'
required: false
default: 'full'
type: choice
options:
- basic
- full
windows-redis-version:
description: 'Pin the apt redis version on the Windows job, e.g. 6:8.0.2-1rl1~jammy1 (blank = whatever is GA)'
required: false
default: ''
jobs:
main:
name: StackExchange.Redis (Ubuntu)
runs-on: ubuntu-latest
env:
DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION: "1" # Enable color output, even though the console output is redirected in Actions
TERM: xterm # Enable color output in GitHub Actions
steps:
- uses: actions/checkout@v6
- name: Run tests
uses: ./.github/actions/run-tests
with:
client-libs-test-image-tag: "${{ github.event.inputs.client-libs-test-image-tag }}"
windows:
name: StackExchange.Redis (Windows Server 2025)
if: github.event.inputs.client-libs-test-image-tag == ''
# Note: must not be windows-2022; setup-wsl runs `wsl --update` only on that image
# (ImageOS == win22), and that download intermittently 403s from Microsoft, failing the job.
runs-on: windows-2025
env:
NUGET_CERT_REVOCATION_MODE: offline # Disabling signing because of massive perf hit, see https://github.com/NuGet/Home/issues/11548
DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION: "1" # Note this doesn't work yet for Windows - see https://github.com/dotnet/runtime/issues/68340
TERM: xterm
DOCKER_BUILDKIT: 1
# This runner routinely fails the library's 5s defaults for reasons that are nothing to do
# with the code under test (fractional vCPU, nested virtualisation, thread-pool ramp). Raise
# the floor for connections that do not ask for a specific timeout; tests that deliberately
# pick a short timeout to exercise timeout behaviour are unaffected.
REDIS_TESTS_MIN_TIMEOUT_MS: "20000"
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0 # Fetch the full history
# Surfaced early, and before restore/build, so the version to release is visible even if
# something later in this (long) job fails. Cutting a release means creating a GitHub Release
# tagged with whatever this prints; release.yml then verifies the tag agrees with nbgv.
- name: Report computed version
run: |
dotnet tool install --global nbgv
$version = nbgv get-version --project src/StackExchange.Redis --variable NuGetPackageVersion
"### Computed package version: ``$version``" >> $env:GITHUB_STEP_SUMMARY
Write-Output "Computed package version: $version"
# Restore is ~80s of the build step on a cold runner, and the package set changes rarely.
#
# The key globs the whole workspace for **/*.csproj, so it must only ever be computed against a
# still tree. Two things disturb one: the sixteen redis processes writing through DrvFs once the
# servers are up - rdb files into RedisConfigs/Temp, nodes-*.conf into RedisConfigs/Cluster, and
# BGSAVE's temp-<pid>.rdb appearing and being renamed under the walk - and, later, every bin/ and
# obj/ the build produces. Enumerating a tree that is changing underneath you is how you get an
# intermittent "hashFiles(...) failed. Fail to hash files under directory ...".
#
# Hashing here, straight after checkout, keeps the restore safe; but actions/cache re-evaluates
# `key` in its post step to decide what to save, and that runs at the end of the job when the
# churn is at its worst. So resolve it *once*, into an output, and hand the cache action a plain
# string it can reuse at both ends.
- name: Compute NuGet cache key
id: nuget-cache-key
run: |
"value=${{ runner.os }}-nuget-${{ hashFiles('Directory.Packages.props', 'global.json', '**/*.csproj') }}" >> $env:GITHUB_OUTPUT
- name: Cache NuGet packages
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: ${{ steps.nuget-cache-key.outputs.value }}
restore-keys: ${{ runner.os }}-nuget-
- uses: Vampire/setup-wsl@v7 # v7 is the first version running on Node 24
with:
distribution: Ubuntu-22.04
- name: Install Redis
shell: wsl-bash {0}
working-directory: ./tests/RedisConfigs
run: |
apt-get update
apt-get install curl gpg lsb-release libgomp1 jq -y
curl -fsSL https://packages.redis.io/gpg | gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
chmod 644 /usr/share/keyrings/redis-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/redis.list
apt-get update
# Don't let dpkg start the packaged redis-server service: we start our own
# instances below, and the packaged unit would otherwise race them for
# 127.0.0.1:6379 on any run where WSL came up with systemd.
# This also sidesteps an install hang we hit with the 8.10.0 package, whose
# redis.conf lost "supervised auto" - redis then never sd_notify()s, so the
# Type=notify unit never leaves "activating" and the postinst's
# "systemctl start" blocks. That one's an upstream packaging slip being
# fixed there, but the guard above is worth keeping either way.
printf '#!/bin/sh\nexit 101\n' > /usr/sbin/policy-rc.d
chmod +x /usr/sbin/policy-rc.d
# A blank pin means "whatever is GA today", which is not reproducible: a run from last
# month and a run from today can test different servers. Supply windows-redis-version
# via workflow_dispatch to pin it; `apt-cache madison redis-server` lists the candidates.
PIN='${{ github.event.inputs.windows-redis-version }}'
if [ -n "$PIN" ]; then
case "$PIN" in
*[!A-Za-z0-9.:+~_-]*) echo "refusing suspicious version pin: $PIN" >&2; exit 1 ;;
esac
echo "pinning redis to $PIN"
apt-get install -y "redis-server=$PIN" "redis-tools=$PIN" \
|| { echo "pin not available; candidates:" >&2; apt-cache madison redis-server >&2; exit 1; }
else
apt-get install -y redis
fi
redis-server --version
mkdir redis
- name: Run redis-server
shell: wsl-bash {0}
working-directory: ./tests/RedisConfigs/redis
run: |
# Defaults to the whole topology: this job is the only coverage of .NET Framework and of
# Windows' own socket/TLS stacks, so we would rather keep it complete. `basic` starts only
# primary/replica/secure/failover, which is useful when bisecting whether cluster and
# sentinel background load is implicated in a failure - measured across three runs it was
# not, so it is opt-in rather than the default. Tests needing absent servers skip promptly
# either way (see Skip.IfNoCluster / Skip.IfNoSentinel).
TOPOLOGY='${{ github.event.inputs.windows-topology || 'full' }}'
echo "topology: $TOPOLOGY"
redis-server ../Basic/primary-6379.conf &
redis-server ../Basic/replica-6380.conf &
redis-server ../Basic/secure-6381.conf &
redis-server ../Failover/primary-6382.conf &
redis-server ../Failover/replica-6383.conf &
PORTS="6379 6380 6381 6382 6383"
if [ "$TOPOLOGY" = "full" ]; then
for p in 7000 7001 7002 7003 7004 7005; do
redis-server "../Cluster/cluster-$p.conf" --dir ../Cluster &
done
redis-server ../Sentinel/redis-7010.conf &
redis-server ../Sentinel/redis-7011.conf &
for p in 26379 26380 26381; do
redis-server "../Sentinel/sentinel-$p.conf" --sentinel &
done
PORTS="$PORTS 7000 7001 7002 7003 7004 7005 7010 7011 26379 26380 26381"
fi
# Poll rather than `sleep 5`: on a fast machine this returns almost immediately, and on a
# slow one it actually waits long enough. A fixed sleep gets both cases wrong.
#
# NB: this shell runs with -euo pipefail, so a bare `out=$(redis-cli ...)` aborts the whole
# step the instant a server is not yet accepting connections - i.e. precisely the state we
# are here to wait out. Every probe therefore tolerates failure explicitly.
missing=""
for p in $PORTS; do
ready=""
for _ in $(seq 1 30); do
out=$(redis-cli -p "$p" PING 2>&1 || true)
# secure-6381 answers NOAUTH without credentials, which still proves it is listening
case "$out" in
PONG|*NOAUTH*) ready="yes"; break ;;
esac
sleep 1
done
if [ -n "$ready" ]; then
echo "port $p ready: $(redis-cli -p "$p" INFO SERVER 2>/dev/null | grep redis_version || echo 'version unavailable')"
else
echo "port $p DID NOT COME UP"
missing="$missing $p"
fi
done
# Deliberately fatal, and deliberately not continue-on-error: running ~5000 tests against
# servers that never started produces thousands of confusing connection failures ten
# minutes later, instead of one clear message here.
if [ -n "$missing" ]; then
echo "::error::no server on:$missing"
exit 1
fi
- name: .NET Build
run: dotnet build Build.csproj -c Release /p:CI=true
# The analyzer and the props that configures it reach consumers only through the package, and a
# packaging regression is silent: no diagnostics, ever, for anybody, with a build that still succeeds.
# The real pack below runs only on pushes to main/v3, so check it here where PRs will see it too.
- name: Verify package contents
run: |
$out = "${env:GITHUB_WORKSPACE}\.packcheck"
dotnet pack src/StackExchange.Redis/StackExchange.Redis.csproj --no-build -c Release /p:PackageOutputPath=$out /p:CI=true
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
$nupkg = Get-ChildItem "$out\StackExchange.Redis.*.nupkg" | Select-Object -First 1
if (-not $nupkg) { Write-Error "no package was produced"; exit 1 }
Add-Type -AssemblyName System.IO.Compression.FileSystem
$zip = [System.IO.Compression.ZipFile]::OpenRead($nupkg.FullName)
try { $names = @($zip.Entries | ForEach-Object { $_.FullName }) } finally { $zip.Dispose() }
$missing = @()
foreach ($required in @("analyzers/dotnet/cs/StackExchange.Redis.Build.dll", "build/StackExchange.Redis.props")) {
if ($names -contains $required) { Write-Host "ok: $required" } else { $missing += $required }
}
if ($missing.Count) {
Write-Error "$($nupkg.Name) is missing: $($missing -join ', ')"
Write-Host "package contained:"; $names | Sort-Object | ForEach-Object { Write-Host " $_" }
exit 1
}
- name: StackExchange.Redis.Tests
run: |
$exitCode = 0
foreach ($tfm in @("net10.0", "net481")) {
dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj --no-build -c Release -f $tfm --logger trx --logger GitHubActions --results-directory ./test-results/ /p:CI=true
if ($LASTEXITCODE -ne 0) {
$exitCode = $LASTEXITCODE
}
}
exit $exitCode
- uses: dorny/test-reporter@v3
continue-on-error: true
if: success() || failure()
with:
name: Tests Results - Windows Server 2025
path: 'test-results/*.trx'
reporter: dotnet-trx
# list only failures: listing every test rendered ~3MB into $GITHUB_STEP_SUMMARY, which has a
# 1MB limit, so the summary was dropped entirely and failures had to be read from annotations
list-suites: failed
list-tests: failed
- name: Upload test results
uses: actions/upload-artifact@v4
if: success() || failure()
with:
# the trx carries per-test output, which the failure-only report above deliberately omits; that is
# the difference between diagnosing a CI-only failure and guessing at it
name: test-results-windows
path: test-results/*.trx
retention-days: 14