Document stdout chunk streaming in README #240
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@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 |