Add cover image, badge row, and button links to the docs Overview page #146
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |