Skip to content

Document stdout chunk streaming in README #240

Document stdout chunk streaming in README

Document stdout chunk streaming in README #240

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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.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 .
typos:
if: github.event_name != 'schedule'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Check spelling
uses: crate-ci/typos@bee27e3a4fd1ea2111cf90ab89cd076c870fce14 # v1.48.0
with:
config: ./typos.toml
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
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 benchmarks docs/snippets/DocSnippets.FSharp/Fixtures.fs
analyze:
if: github.event_name != 'schedule'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
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, so the tool manifest deliberately pins that
# compatible version. Resolve the restored version from the manifest instead of repeating it
# here, so a future coordinated update has only one version to change.
#
# fsharp-analyzers 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_version="$(dotnet tool list --local | awk '$1 == "fsharp-analyzers" { print $2 }')"
if [ -z "$tool_version" ]; then
echo "::error::fsharp-analyzers was not restored from .config/dotnet-tools.json"
exit 1
fi
tool_dll="$nuget_packages/fsharp-analyzers/$tool_version/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
# NuGet Package Validation (ApiCompat) gate. Packs all four packable packages and fails the workflow
# on any breaking public-API change vs. the last published release (baseline 2.4.2), unless it is the
# one documented narrow suppression (src/ProcessKit.Testing/CompatibilitySuppressions.xml — only the
# CassetteEntry positional-constructor arity change). This makes the SemVer promise in ROADMAP.md
# ("breaking changes land only in a new major version") a machine check, complementing the public-API
# snapshot test (which only guards vs. the current tree, not vs. the shipped release). ApiCompat also
# cross-checks the net8.0/net10.0 surfaces inside each package. EnablePackageValidation /
# PackageValidationBaselineVersion live in Directory.Build.props (packable projects only).
#
# Packs PER-PROJECT (not `dotnet pack ProcessKit.slnx`), matching .github/workflows/release.yml: the
# core is resolved as an assembly <Reference> (AGENTS.md), and the .slnx BuildDependency that orders a
# solution build injects a synthetic project-reference to the multi-targeted ProcessKit, which a
# solution-level `dotnet pack` (BuildProjectReferences=false) resolves via `GetTargetPath` on the
# cross-targeting outer project — an MSB4057 unrelated to validation. A per-project pack has no such
# synthetic reference: it resolves the real references and runs ApiCompat for both TFMs. Core is packed
# first so the companions' assembly <Reference> to ProcessKit.dll resolves from its build output.
pack:
if: github.event_name != 'schedule'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
# The 10.0.x SDK builds and packs both TFMs; the net8.0 reference assemblies are restored as
# NuGet packages (no net8.0 runtime needed — a package is only built and packed here, not run).
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-
- name: Restore
run: dotnet restore
# Any ApiCompat baseline violation fails `dotnet pack` (non-zero exit); `set -e` then fails the job.
- name: Pack all four packages with package validation (ApiCompat vs baseline 2.4.2)
run: |
set -euo pipefail
for proj in \
src/ProcessKit/ProcessKit.fsproj \
src/ProcessKit.Testing/ProcessKit.Testing.fsproj \
src/ProcessKit.Extensions.DependencyInjection/ProcessKit.Extensions.DependencyInjection.fsproj \
src/ProcessKit.Extensions.Hosting/ProcessKit.Extensions.Hosting.fsproj; do
echo "::group::pack $proj"
dotnet pack "$proj" --configuration Release
echo "::endgroup::"
done
test:
if: github.event_name != 'schedule'
# Read by the coverage ratchet in `coverage-summary`, which has to tell "a leg did not deliver
# its coverage artifact" (skip the gate) from "the matrix simply has fewer legs now" (gate as
# usual). Taken from the matrix itself so that adding or removing an OS leg needs no edit here;
# `matrixLegs` in the baseline file is only the fallback for a run where this value is missing.
outputs:
matrix-legs: ${{ steps.matrix-size.outputs.legs }}
strategy:
fail-fast: false
matrix:
# Matrix includes Windows ARM64 (windows-11-arm) to verify .NET SDK 10 resolves via
# actions/setup-dotnet on ARM64, and to ensure no x64-specific test assumptions break
# on ARM64 (native P/Invoke marshalling for Job Objects, named pipes, etc.). Pending
# first real CI run on this leg after merge; no live-run evidence yet.
os: [ubuntu-latest, ubuntu-24.04-arm, windows-11-arm, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
# Every leg would write the same `strategy.job-total`, so one leg publishing it is enough;
# pinning that to the Linux leg keeps the step on a shell that is guaranteed to be there. It
# runs before checkout so the value still reaches `coverage-summary` when this leg fails
# later on. The matrix shares one output slot, so if this leg is cancelled -- or another
# leg's empty value wins the slot -- the ratchet falls back to the leg count recorded in the
# baseline file, which is the same number until someone changes the matrix.
- name: Publish matrix size
id: matrix-size
if: matrix.os == 'ubuntu-latest'
shell: bash
run: echo "legs=${{ strategy.job-total }}" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
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 and Interleaving categories are flaky-sensitive concurrency suites (hundreds of
# concurrent spawns / randomized lifecycle interleavings, thread-pool/handle/memory baselines)
# that would slow down and add noise to every ordinary PR/push run; they have their own
# scheduled/workflow_dispatch stages below instead (`stress`, `interleaving`).
# Coverage is collected via the coverlet.collector data collector. No threshold is enforced
# per leg: the gate lives in `coverage-summary`, which compares the coverage merged across the
# whole matrix with the committed baseline. The Cobertura report lands under
# ./TestResults/<guid>/coverage.cobertura.xml.
- name: Test
run: >-
dotnet test --no-build --configuration Release --filter "Category!=Stress&Category!=Interleaving"
--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; `coverage-summary`
# merges them and gates on the merged number. A leg that produced none is tolerated on
# purpose (`if-no-files-found: ignore`), and the gate skips rather than fails when it sees
# fewer legs than the matrix has.
- 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
coverage-summary:
name: Coverage summary
if: ${{ always() && github.event_name != 'schedule' }}
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: '10.0.x'
# Pattern download succeeds when one or more matrix legs did not publish coverage. Keeping
# artifacts in separate directories avoids overwriting their identically named Cobertura files.
- name: Download available coverage reports
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: coverage-*
path: coverage
- name: Install ReportGenerator
run: >-
dotnet tool install --tool-path ./.tools
dotnet-reportgenerator-globaltool --version 5.5.10
# Renders the merged matrix coverage for a human and, in the same invocation, writes the
# machine readable JsonSummary that the ratchet step below reads. One merge with one set of
# assembly filters: the number that gates and the number a human reads cannot drift apart.
- name: Publish merged coverage summary
shell: bash
run: |
set -euo pipefail
mkdir -p coverage
mapfile -d '' coverage_reports < <(
find coverage -type f -name 'coverage.cobertura.xml' -print0
)
{
echo "## Cross-platform coverage"
echo
echo "Merged ${#coverage_reports[@]} available Cobertura report(s) from the test matrix."
echo
} >> "$GITHUB_STEP_SUMMARY"
if (( ${#coverage_reports[@]} == 0 )); then
echo "> No coverage reports were produced; see the matrix test jobs for details." \
>> "$GITHUB_STEP_SUMMARY"
exit 0
fi
assembly_filters="+ProcessKit;+ProcessKit.Testing"
assembly_filters+=";+ProcessKit.Extensions.DependencyInjection"
assembly_filters+=";+ProcessKit.Extensions.Hosting"
report_files=$(printf '%s;' "${coverage_reports[@]}")
report_files=${report_files%;}
./.tools/reportgenerator \
"-reports:$report_files" \
"-targetdir:coverage-report" \
"-reporttypes:MarkdownSummaryGithub;JsonSummary" \
"-assemblyfilters:$assembly_filters" \
"-title:ProcessKit cross-platform coverage"
cat coverage-report/SummaryGithub.md >> "$GITHUB_STEP_SUMMARY"
# Coverage ratchet. The note that used to sit above the summary step deferred this ("remains
# visualization only ... until a follow up can define and validate its main branch comparison
# semantics"); the semantics it was waiting for are written out here.
#
# Baseline source: `coverage-baseline.json`, committed in this repository, holding the merged
# line coverage to hold, the tolerance, and the number of matrix legs it was recorded from.
# Reading the last green run on main instead (through `gh api` or a retained artifact) was
# rejected on purpose: it needs a wider token scope, it depends on artifact retention and on
# that run still existing, it cannot work the same way on a fork, and it makes the verdict for
# a pull request depend on state living outside that pull request. A committed file is
# deterministic, and it turns every deliberate move of the baseline into a reviewable one line
# diff inside the change that causes it.
#
# Comparison: the merged line coverage of this run must be at least `lineCoverage` minus
# `toleranceLinePoints`, otherwise this job fails. The number comes from the JsonSummary the
# merge above already wrote, so the gated number and the published number are the same number.
#
# Tolerance: 0.5 line coverage points (authoritative value: `toleranceLinePoints` in the
# baseline file). Measured rather than guessed: two identical local runs of the same gated
# suite differed by 2 covered lines out of 9569 coverable ones, i.e. 0.02 points, because a
# few tests spawn real processes and take a timing dependent branch. At today's size 0.5
# points is roughly 48 lines, some twenty times that jitter, and still small enough to fail on
# a regression worth failing on: a new module of about 100 lines landing untested moves the
# number by about 0.75 points.
#
# Incomplete matrix: merged coverage is a union over the OS legs, and platform specific code
# (Job Objects, cgroups, POSIX signals) is covered only by its own leg, so a run that lost a
# leg reports several points less for reasons that have nothing to do with the change. Losing
# a leg is a normal outcome here -- artifacts are downloaded by pattern and a leg is allowed
# to publish none. The gate therefore compares how many legs delivered coverage with the size
# of the matrix and, if any leg is missing, degrades to a warning in the job summary instead
# of failing. One skipped check costs less than one false failure.
#
# Empty reports: a collector that instruments nothing still writes a valid Cobertura file, just
# one with an empty `packages` element, so a whole matrix can deliver the expected number of
# artifacts that hold no coverage at all. The merge of those files reports zero assemblies and
# no percentage, which says this run measured nothing -- the same class of event as a missing
# leg -- so the gate skips there too. Reading the absent percentage as 0.00 % would fail the
# job with a total loss of coverage that never happened. Coverage that is genuinely zero looks
# different (assemblies and coverable lines are reported, the percentage is 0) and is gated.
#
# Updating the baseline: take the number from the Coverage ratchet table of a full matrix run
# and write it into `coverage-baseline.json` in the same change that moves coverage, with the
# reason in the description. `"lineCoverage": null` disarms the gate and makes it record the
# observed value instead. CONTRIBUTING.md, section "Coverage baseline", has the procedure.
#
# The `needs.test.result` guard keeps a red matrix from producing a second, misleading
# failure: coverage collected from a run whose tests failed does not measure a working commit.
- name: Check coverage ratchet
if: needs.test.result == 'success'
shell: pwsh
run: >-
./scripts/check-coverage-ratchet.ps1
-CoverageRoot coverage
-SummaryJson coverage-report/Summary.json
-ExpectedLegs '${{ needs.test.outputs.matrix-legs }}'
# 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- 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&Category!=Interleaving" \
--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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
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
# Randomized lifecycle interleaving search (`[<Category("Interleaving")>]` in
# tests/ProcessKit.Tests/InterleavingTests.fs): each seed drives a random sequence of CONCURRENT
# public RunningProcess/ProcessGroup operations against a real child and asserts the interleaving
# upheld its invariants (no unexpected exceptions, no zombies, no unobserved task faults, telemetry
# back to zero). Like `stress`, it is flaky-sensitive and adds real wall-clock time, so it is kept off
# the ordinary PR/push `test` job and run on the same weekly schedule / on demand. The generated plans
# are seeded (a failure replays from its logged seed); the concurrency timing varies each run, so this
# keeps exploring new interleavings. ubuntu-latest covers the cross-platform teardown/reap path (the
# POSIX zombie check is Linux-only); add more OSes here if a platform-specific interleaving shows up.
interleaving:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
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: Interleaving test
run: >-
dotnet test --no-build --configuration Release --filter "Category=Interleaving"
--logger "trx;LogFileName=interleaving-results.trx" --results-directory ./TestResults
- name: Upload interleaving test results
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: interleaving-test-results
path: ./TestResults/*.trx
if-no-files-found: ignore