Skip to content

Add cover image, badge row, and button links to the docs Overview page #146

Add cover image, badge row, and button links to the docs Overview page

Add cover image, badge row, and button links to the docs Overview page #146

Workflow file for this run

name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]
workflow_dispatch:
# Weekly, off the ordinary PR/push path — feeds only the `stress` job below (see its own
# `if:` guard); the other jobs skip a scheduled run so this doesn't re-run the whole matrix.
schedule:
- cron: '0 6 * * 1'
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
yaml-lint:
if: github.event_name != 'schedule'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: '3.x'
- name: Install yamllint
run: pip install yamllint
# Config is .yamllint.yml — tuned for Actions YAML. Real defects (tabs,
# duplicate keys, bad indentation) fail; cosmetic line-length stays a
# non-failing warning.
- name: Lint YAML
run: yamllint .
format:
if: github.event_name != 'schedule'
runs-on: ubuntu-latest
steps:
# Actions are pinned to a full commit SHA (supply-chain hardening); the
# trailing comment records the human-readable version. Dependabot bumps the
# SHA and updates the comment on its weekly run.
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5
with:
dotnet-version: '10.0.x'
# Fantomas is the F# formatter and this repo's style authority — the F#
# compiler does not enforce .editorconfig style the way Roslyn does for C#.
- name: Restore tools
run: dotnet tool restore
- name: Check formatting
run: dotnet fantomas --check src tests samples
analyze:
if: github.event_name != 'schedule'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5
with:
dotnet-version: '10.0.x'
- name: Restore tools
run: dotnet tool restore
- name: Restore packages
run: dotnet restore
# The CLI does not discover MSBuild PackageReference analyzers on its own.
# Ionide.Analyzers 0.15.0 is built for fsharp-analyzers 0.36.0; newer
# versions may break compatibility, so keep the package/tool pins aligned.
#
# fsharp-analyzers 0.36.0 ships only a net8.0 executable (see .config/dotnet-tools.json
# and the `tools/net8.0/any` layout of the restored package) — it relies on its
# runtimeconfig.json's `"rollForward": "LatestMajor"` to run on newer runtimes. That
# roll-forward only kicks in when no exact framework match is found. GitHub-hosted
# ubuntu-latest runners ship a net8.0 runtime pre-installed regardless of this job's own
# `dotnet-version: '10.0.x'`, so the tool's own apphost resolves an exact net8.0 match and
# runs natively on net8.0 -- but Ionide.ProjInfo then loads the MSBuild toolset from the
# only SDK actually installed here (net10.0, pinned by global.json), and loading a
# net10.0-targeted MSBuild toolset into a net8.0 host process fails with "could not find
# System.Runtime, Version=10.0.0.0" (confirmed by reproducing both the failure and the fix
# in a clean ubuntu container with a net8.0 runtime added alongside net10.0). Invoking the
# tool DLL directly via `dotnet exec --fx-version` forces it to run natively on the
# installed net10.0 runtime instead, so the host and the MSBuild toolset it loads agree.
- name: Analyze F# source
run: |
set -euo pipefail
analyzers_path="${NUGET_PACKAGES:-$HOME/.nuget/packages}/ionide.analyzers/0.15.0/analyzers/dotnet/fs"
if [ ! -d "$analyzers_path" ]; then
echo "::error::Ionide.Analyzers package was not restored under $analyzers_path"
exit 1
fi
nuget_packages="${NUGET_PACKAGES:-$HOME/.nuget/packages}"
tool_dll="$nuget_packages/fsharp-analyzers/0.36.0/tools/net8.0/any/FSharp.Analyzers.Cli.dll"
if [ ! -f "$tool_dll" ]; then
echo "::error::fsharp-analyzers tool assembly was not restored under $tool_dll"
exit 1
fi
fx_version="$(dotnet --list-runtimes | awk '/^Microsoft\.NETCore\.App 10\./ { print $2 }' | sort -V | tail -1)"
if [ -z "$fx_version" ]; then
echo "::error::No installed net10.0 Microsoft.NETCore.App runtime found"
exit 1
fi
dotnet exec --fx-version "$fx_version" "$tool_dll" --project src/ProcessKit/ProcessKit.fsproj \
--analyzers-path "$analyzers_path" \
--exclude-analyzers PostfixGenericsAnalyzer StructDiscriminatedUnionAnalyzer \
--treat-as-error IONIDE-001 IONIDE-003 IONIDE-006 \
--output-format github
test:
if: github.event_name != 'schedule'
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5
with:
# The 10.0.x SDK builds (pinned by global.json); the 8.0.x runtime is needed to
# run the net8.0 test leg of the multi-targeted (net8.0;net10.0) test project.
dotnet-version: |
8.0.x
10.0.x
# Cache the global NuGet package folder across runs. Keyed on the central
# version files so the cache invalidates only when dependencies change.
- name: Cache NuGet packages
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('Directory.Packages.props', 'nuget.config', 'global.json') }}
restore-keys: ${{ runner.os }}-nuget-
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build --no-restore --configuration Release
- name: Restore samples
run: dotnet restore samples/Samples.slnx
- name: Build samples
run: dotnet build samples/Samples.slnx --no-restore --configuration Release
# The Stress category is a flaky-sensitive soak/leak suite (hundreds of concurrent spawns,
# thread-pool/handle/memory baselines) that would slow down and add noise to every ordinary
# PR/push run; it has its own scheduled/workflow_dispatch stage below instead (`stress`).
# Coverage is collected via the coverlet.collector data collector (visibility only —
# no threshold gate is enforced here). The Cobertura report lands under
# ./TestResults/<guid>/coverage.cobertura.xml.
- name: Test
run: >-
dotnet test --no-build --configuration Release --filter "Category!=Stress"
--logger "trx;LogFileName=test-results.trx" --results-directory ./TestResults
--collect:"XPlat Code Coverage"
# Test results are uploaded even when the Test step fails, so a red CI run
# can be diagnosed from the .trx without re-running locally.
- name: Upload test results
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: test-results-${{ matrix.os }}
path: ./TestResults/*.trx
if-no-files-found: ignore
# Coverage reports are published as a downloadable artifact per OS leg — no gate,
# just visibility (see the task that introduced this step for rationale).
- name: Upload coverage report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: coverage-${{ matrix.os }}
path: ./TestResults/**/coverage.cobertura.xml
if-no-files-found: ignore
# The Linux cgroup v2 `limits` backend can only enable controllers at the real cgroup root, so
# it never engages under the unprivileged matrix legs (a systemd scope / private cgroup
# namespace). This leg runs the limits tests in a privileged container with the host cgroup
# namespace and moves the test process to the real root, so cgroup enforcement is actually
# exercised (PROCESSKIT_EXPECT_CGROUP makes the tests require the cgroup path, not the fallback).
test-cgroup-limits:
if: github.event_name != 'schedule'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Run cgroup v2 limits tests (privileged)
run: |
docker run --rm --privileged --cgroupns=host \
-v "$PWD:/src" -w /src \
-e DOTNET_CLI_TELEMETRY_OPTOUT=1 -e DOTNET_NOLOGO=1 \
mcr.microsoft.com/dotnet/sdk:10.0 \
bash -c '
set -e
if echo $$ > /sys/fs/cgroup/cgroup.procs 2>/dev/null; then
export PROCESSKIT_EXPECT_CGROUP=1
fi
echo "PROCESSKIT_EXPECT_CGROUP=$PROCESSKIT_EXPECT_CGROUP"
# The sdk:10.0 image has only the net10 runtime; build and test just net10.0 here
# (the cgroup limits backend is runtime-version-independent, so one TFM suffices).
dotnet build --configuration Release -p:TargetFrameworks=net10.0
dotnet test --no-build --configuration Release \
--framework net10.0 \
--filter "FullyQualifiedName~LimitsTests"
'
# musl/Alpine smoke: the native layer (posix_spawn, AF_UNIX socketpair stdio, direct pidfd/epoll
# syscalls via `syscall(2)`, setpriv privilege drop) is otherwise exercised only against glibc (the
# `test` job's ubuntu-latest leg). Alpine is the de facto standard base for containerized .NET
# deployments and differs in libc (musl) and available utilities, so it gets its own leg — by the
# same raw `docker run` pattern as `test-cgroup-limits` above, rather than the matrix in `test`,
# because it needs a pre-test package install step the matrix legs don't. Runs the full suite
# (minus Stress, same filter as `test`) at net10.0 only: the sdk:10.0-alpine image, like sdk:10.0,
# carries just the net10 runtime. BusyBox (Alpine's base) ships its own `setpriv` applet — same
# name as util-linux's, but missing the --reuid/--regid/--clear-groups flags Native.Posix.fs's
# setprivCommand relies on for the Uid/Gid privilege-drop path — so the real util-linux package is
# installed first to shadow it; everything else in the suite runs unmodified (musl needed no other
# accommodation — confirmed green against this exact leg definition before it was added here).
test-alpine:
if: github.event_name != 'schedule'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Run full test suite on musl/Alpine (excluding Stress)
run: |
docker run --rm \
-v "$PWD:/src" -w /src \
-e DOTNET_CLI_TELEMETRY_OPTOUT=1 -e DOTNET_NOLOGO=1 \
mcr.microsoft.com/dotnet/sdk:10.0-alpine \
sh -c '
set -e
apk add --no-cache util-linux
dotnet build --configuration Release -p:TargetFrameworks=net10.0
dotnet test --no-build --configuration Release --framework net10.0 \
tests/ProcessKit.Tests/ProcessKit.Tests.fsproj \
--filter "Category!=Stress" \
--logger "trx;LogFileName=test-results-alpine.trx" --results-directory ./TestResults
'
- name: Upload test results
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: test-results-alpine
path: ./TestResults/*.trx
if-no-files-found: ignore
# NativeAOT/trimming smoke: publishes a minimal ProcessKit consumer (samples/FSharp.NativeAot) with
# PublishAot and RUNS the produced native binary, so the packages' IsTrimmable/IsAotCompatible claims
# are validated in a real ahead-of-time-compiled image rather than only asserted in metadata. The run
# exercises spawn + honest capture + containment (a child inside a kill-on-dispose ProcessGroup), so a
# trimmed-away code path that breaks at runtime fails the job. Both OS legs are covered because each
# AOT-compiles and runs a different containment backend: win-x64 the Windows Job Object (struct
# marshalling), linux-x64 the POSIX process group. FSharp.Core (the F# runtime, an immovable dependency)
# emits unavoidable trim/AOT baseline warnings, so ilc's treat-warnings-as-errors is off in the sample;
# the grep gate below re-imposes a hard failure if ilc ever attributes a warning to a ProcessKit* assembly
# (i.e. if our own code regresses its trim/AOT cleanliness). See docs/platform-support.md.
aot-smoke:
if: github.event_name != 'schedule'
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
rid: linux-x64
- os: windows-latest
rid: win-x64
runs-on: ${{ matrix.os }}
defaults:
run:
# bash on both OSes (GitHub's bash uses `-eo pipefail`, so a failed publish in a `| tee` pipe still
# fails the step) so the publish/gate/run commands are written once.
shell: bash
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5
with:
dotnet-version: '10.0.x'
- name: Cache NuGet packages
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('Directory.Packages.props', 'nuget.config', 'global.json') }}
restore-keys: ${{ runner.os }}-nuget-
# NativeAOT on Linux links with clang against zlib; the Windows runner already has the MSVC C++
# toolchain the ilc link step needs.
- name: Install NativeAOT prerequisites (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y clang zlib1g-dev
- name: Build the referenced libraries (Release, net10.0)
# The sample resolves ProcessKit and ProcessKit.Extensions.DependencyInjection as assembly
# <Reference>s from their src/**/bin/Release/net10.0 outputs (the ilc input IL for the native image).
# Core is built first so the DI project's assembly reference to it resolves. Building just these two
# for the single TFM the AOT sample targets is all this smoke needs — no test/benchmark projects,
# no net8.0 leg.
run: |
dotnet build src/ProcessKit/ProcessKit.fsproj --configuration Release --framework net10.0
dotnet build src/ProcessKit.Extensions.DependencyInjection/ProcessKit.Extensions.DependencyInjection.fsproj \
--configuration Release --framework net10.0
- name: Publish the NativeAOT smoke consumer
run: >-
dotnet publish samples/FSharp.NativeAot/FSharp.NativeAot.fsproj
--configuration Release -r ${{ matrix.rid }}
-o "$PWD/samples/FSharp.NativeAot/publish-aot" 2>&1 | tee aot-publish.log
- name: Gate — no ProcessKit assembly may produce trim/AOT warnings
run: |
if grep -E "Assembly 'ProcessKit" aot-publish.log; then
echo "::error::A ProcessKit assembly produced trim/AOT warnings under NativeAOT publish (see log)."
exit 1
fi
echo "No ProcessKit trim/AOT warnings — the packages stayed AOT-clean."
- name: Run the NativeAOT smoke (spawn + capture + containment)
run: |
if [ "$RUNNER_OS" = "Windows" ]; then
./samples/FSharp.NativeAot/publish-aot/FSharp.NativeAot.exe
else
./samples/FSharp.NativeAot/publish-aot/FSharp.NativeAot
fi
# Stress/soak suite (`[<Category("Stress")>]` in tests/ProcessKit.Tests/StressTests.fs): hundreds
# of concurrent runs against thread-pool/managed-memory/handle baselines. Deliberately kept off the
# ordinary PR/push `test` job (flaky-sensitive, adds real wall-clock time) and run instead on a
# weekly schedule or on demand via workflow_dispatch. ubuntu-latest is enough to cover the
# cross-platform teardown/reap path (ProcessGroup + POSIX zombie reaping) that most past fixes in
# CHANGELOG.md have landed in; add more OSes here if a platform-specific stress regression shows up.
stress:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5
with:
dotnet-version: |
8.0.x
10.0.x
- name: Cache NuGet packages
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('Directory.Packages.props', 'nuget.config', 'global.json') }}
restore-keys: ${{ runner.os }}-nuget-
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build --no-restore --configuration Release
- name: Stress test
run: >-
dotnet test --no-build --configuration Release --filter "Category=Stress"
--logger "trx;LogFileName=stress-results.trx" --results-directory ./TestResults
- name: Upload stress test results
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: stress-test-results
path: ./TestResults/*.trx
if-no-files-found: ignore