Skip to content

Separate optimization capabilities by effect #2995

Separate optimization capabilities by effect

Separate optimization capabilities by effect #2995

Workflow file for this run

name: CI
on:
pull_request:
branches: [main, master]
# release.yml calls this via `uses: ./.github/workflows/ci.yml` so tag
# releases run the exact same test matrix as per-PR CI.
workflow_call: {}
workflow_dispatch: {}
# Pushing three times to a branch used to queue three full matrices and run all of them. A newer
# pull-request run supersedes the older one; a push to main is not cancelled, because main wants a
# recorded result for every commit rather than only for the newest.
concurrency:
group: ci-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
# Force JavaScript-based actions to run on Node 24 instead of the deprecated Node 20 ahead of
# the 2026-06-16 hard cutover, until we bump each action to a Node-24-native major version.
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
jobs:
test:
name: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 90
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # version.pl needs full history
# FileFormat.PngCrushAdapters has sibling-conditional ProjectReferences
# to ..\..\PNGCrushCS\FileFormat.{Png,Apng,Jpeg,...}. Without the sibling
# those types disappear and any test that imports them fails to compile.
# Clone the sibling alongside CompressionWorkbench so the conditional
# resolves to true and the full image-format surface is available on CI.
- name: Clone PNGCrushCS sibling
shell: bash
run: git clone --depth 1 https://github.com/Hawkynt/PNGCrushCS.git "$GITHUB_WORKSPACE/../PNGCrushCS"
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
dotnet-quality: 'preview'
# MSBuild does not reliably propagate $(Configuration) across solution-tree
# boundaries; without this pre-build the sibling builds to bin/Debug/net8.0/
# and the publish step then can't find Release/net8.0/ during bundling.
- name: Pre-build PNGCrushCS sibling (Release)
shell: bash
run: |
set -euo pipefail
cd "$GITHUB_WORKSPACE/../PNGCrushCS"
REQUIRED="Compression.Core FileFormat.Core FileFormat.Bmp FileFormat.Apng \
FileFormat.Tiff FileFormat.BigTiff FileFormat.Mng FileFormat.Fli \
FileFormat.Dcx FileFormat.Icns FileFormat.Mpo FileFormat.Jpeg \
FileFormat.Png"
# Sibling reorganised most format projects into a FileFormats/ subdir
# but kept Compression.Core / FileFormat.Core at the root. Probe both
# locations per project so the script tolerates the layout move; also
# tolerate transient sibling-mid-refactor commits where a project is
# missing entirely — FileFormat.PngCrushAdapters.csproj has a matching
# SiblingPresent gate that falls back to an empty assembly.
locate() {
local proj="$1"
if [ -f "$proj/$proj.csproj" ]; then echo "$proj"; return 0; fi
if [ -f "FileFormats/$proj/$proj.csproj" ]; then echo "FileFormats/$proj"; return 0; fi
return 1
}
MISSING=""
for proj in $REQUIRED; do
if ! locate "$proj" >/dev/null; then
MISSING="$MISSING $proj"
fi
done
if [ -n "$MISSING" ]; then
echo "::warning::PNGCrushCS sibling incomplete (missing:$MISSING). Skipping pre-build; consumer csproj SiblingPresent gate handles the fallback."
exit 0
fi
for proj in $REQUIRED; do
dir="$(locate "$proj")"
echo "::group::Building $proj ($dir)"
dotnet build "$dir/$proj.csproj" --configuration Release --nologo
echo "::endgroup::"
test -f "$dir/bin/Release/net8.0/$proj.dll" || {
echo "::error::Pre-build did not produce $dir/bin/Release/net8.0/$proj.dll"
exit 1
}
done
echo "All sibling Release DLLs verified."
- name: Restore
run: dotnet restore CompressionWorkbench.slnx
- name: Build
run: dotnet build CompressionWorkbench.slnx --configuration Release --no-restore
# Verify the four NuGet meta-packages still pack on every PR / push.
# This catches regressions in the bundling target (filter excludes for
# Compression.Core / Registry, ProjectReference path resolution, etc.)
# before they reach release / nightly. We don't push or upload — the
# _build.yml workflow does the real packing and artifact upload for the
# nightly / release workflows that consume them.
#
# NB: --no-build is intentionally omitted. The meta csprojs use a custom
# `_AddBundledAssembliesToPackage` target with `DependsOnTargets="ResolveReferences"`
# which the SDK refuses to invoke under --no-build (NETSDK1085). The
# prior `dotnet build` step already produced up-to-date outputs so pack's
# build phase no-ops.
- name: Verify NuGet meta-packages pack
if: matrix.os == 'ubuntu-latest'
run: |
rm -rf dist-nuget
dotnet pack Compression.Core/Hawkynt.Compression.Core.csproj --configuration Release --output dist-nuget -p:Version=0.0.0-ci
dotnet pack Hawkynt.FileFormats.Audio/Hawkynt.FileFormats.Audio.csproj --configuration Release --output dist-nuget -p:Version=0.0.0-ci
dotnet pack Hawkynt.FileFormats.Archives/Hawkynt.FileFormats.Archives.csproj --configuration Release --output dist-nuget -p:Version=0.0.0-ci
dotnet pack Hawkynt.FileFormats.FileSystems/Hawkynt.FileFormats.FileSystems.csproj --configuration Release --output dist-nuget -p:Version=0.0.0-ci
ls -la dist-nuget/
- name: Install interop tools (Linux)
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y p7zip-full gzip bzip2 xz-utils zstd lz4 tar cpio genisoimage mtools qemu-utils gfs2-utils bcachefs-tools xfsprogs
# cabextract and wimlib-imagex are the tools that OWN the CAB and WIM formats.
# Both reference tests fall back to 7-Zip when they are absent, so without these
# two packages the owning readers were never asked -- which is what #85 and #86
# each reported. Installed so those cases run instead of silently degrading.
sudo apt-get install -y cabextract wimtools
sudo apt-get install -y "linux-modules-extra-$(uname -r)" || true
sudo modprobe gfs2 || true
# Core tests -- always run (no external tools required).
#
# NOTE the two similar category names, which are NOT spellings of each other:
# End2End (95 uses) in-process create/extract round trips - Ace, Arj, Arc and friends.
# No external process, so it belongs here and GATES the pull request.
# EndToEnd (10 uses) oracle comparisons against real 7-Zip / RAR / SFX binaries. Needs
# the tools installed below, hence its own advisory step.
# Renaming one to the other would move 95 gating tests into an advisory tier that installs
# tools they never use. Leave them distinct.
- name: Core tests
run: >
dotnet test Compression.Tests
--configuration Release --no-build
--filter "TestCategory!=EndToEnd&TestCategory!=OsIntegration&TestCategory!=ExternalInterop&TestCategory!=ExternalFsInterop&TestCategory!=PolyglotInterop&TestCategory!=Performance"
--logger "trx;LogFileName=core-tests.trx"
# Interop tools for E2E tests. gfs2-utils is a mandatory oracle for the
# ExHash gate below; linux-modules-extra supplies gfs2.ko on Ubuntu runner
# kernels where it is not built in.
- name: Install 7-Zip (Windows)
if: matrix.os == 'windows-latest'
run: choco install 7zip -y
# Unlike the broad external tier below, this is a hard gate: real
# fsck.gfs2 must accept our ExHash metadata and the Linux driver must mount
# the image and resolve/read the emitted namespace.
- name: GFS2 ExHash oracle
if: matrix.os == 'ubuntu-latest'
run: >
dotnet test Compression.Tests
--configuration Release --no-build
--filter "FullyQualifiedName~Compression.Tests.Gfs2.Gfs2NestedDirectoryExternalTests"
--logger "trx;LogFileName=gfs2-exhash-tests.trx"
# Bucket generation handling is a cross-tree invariant: alloc_v4,
# bucket_gens, extent/b-tree pointers and backpointers must all carry the
# same generation. Keep the real checker as a hard gate rather than relying
# on our own reader to agree with our own writer.
#
# The tests gate themselves on the checker's version. Ubuntu's archive
# currently ships bcachefs-tools 1.3.4, which refuses the superblock this
# package writes (metadata version 1.38) before it reads any bucket, so on
# this runner they skip and say so. They run in full wherever the tool is
# new enough. A checker that does read the image and then objects fails the
# step; that is never downgraded to a skip.
- name: bcachefs bucket generation oracle
if: matrix.os == 'ubuntu-latest'
run: >
dotnet test Compression.Tests
--configuration Release --no-build
--filter "FullyQualifiedName~Compression.Tests.BcacheFs.BcacheFsBucketGenerationExternalTests"
--logger "trx;LogFileName=bcachefs-generation-tests.trx"
# python-msgpack is the one oracle below that is not already on both runner images. Advisory
# on its own: without it the MessagePack case skips and says so, and the rest still run.
- name: Install structured-format oracles
continue-on-error: true
shell: bash
run: |
python3 -m pip install --user --break-system-packages msgpack \
|| python -m pip install --user msgpack \
|| true
# The frozen vectors in the core tier pin bytes that CPython, Perl and reg.exe accepted once.
# This asks the tools themselves, against live output, and it is a hard gate rather than
# advisory because the tools it needs ship with both runner images: CPython with pickle, json
# and ElementTree, Perl with Storable, and reg.exe on Windows. A red here means our output
# stopped being readable by the implementation that owns the format.
#
# Individual cases still Assert.Ignore when their own tool is absent, so a runner without one
# reports "not validated" for that case instead of failing the build for it. That is the only
# thing a missing tool is allowed to do; a tool that IS there and rejects our bytes fails.
- name: Structured pseudo-archive writer oracles
run: >
dotnet test Compression.Tests
--configuration Release --no-build
--filter "FullyQualifiedName~Compression.Tests.StructuredPseudoArchives.StructuredPseudoArchiveExternalToolTests"
--logger "trx;LogFileName=structured-writer-tests.trx"
- name: End-to-end interop tests
continue-on-error: true
run: >
dotnet test Compression.Tests
--configuration Release --no-build
--filter "Category=EndToEnd"
--logger "trx;LogFileName=e2e-tests.trx"
# These exercise our formats against the real external tools installed
# above (7-Zip, zstd, lz4, flac) and the post-mutation filesystem checks.
# They were excluded from the core filter but had no step of their own, so
# nothing ran them at all; advisory like the other interop tiers, because a
# runner without one of the tools must not fail the build.
- name: External interop tests
continue-on-error: true
run: >
dotnet test Compression.Tests
--configuration Release --no-build
--filter "Category=ExternalInterop"
--logger "trx;LogFileName=external-tests.trx"
# mkfs.*, 7z and friends. Advisory for the same reason the neighbours are: a runner without
# one of the tools must not fail the build. These used to sit in the CORE tier, which runs
# BEFORE the tools above are installed -- so they were reached with nothing to interoperate
# with and skipped themselves, every run, invisibly.
- name: External filesystem interop tests
continue-on-error: true
run: >
dotnet test Compression.Tests
--configuration Release --no-build
--filter "Category=ExternalFsInterop"
--logger "trx;LogFileName=external-fs-tests.trx"
- name: OS integration tests
continue-on-error: true
run: >
dotnet test Compression.Tests
--configuration Release --no-build
--filter "Category=OsIntegration"
--logger "trx;LogFileName=os-tests.trx"
# Polyglot tests verify our formats round-trip through other language
# ecosystems. Tests skip individually if the language package isn't
# available; never call `pip install` etc. from inside a test.
- name: Install polyglot test dependencies (Linux)
if: matrix.os == 'ubuntu-latest'
continue-on-error: true
run: |
python3 -m pip install --user --break-system-packages py7zr zstandard brotli lz4 rarfile || true
sudo cpanm --notest Archive::Zip IO::Compress::Lzma IO::Compress::Lzop || true
gem install --user-install minitar rubyzip zstd-ruby || true
npm install --prefix /tmp/polyglot-node adm-zip tar-stream || true
echo "NODE_PATH=/tmp/polyglot-node/node_modules" >> $GITHUB_ENV
- name: Polyglot interop tests
continue-on-error: true
run: >
dotnet test Compression.Tests
--configuration Release --no-build
--filter "Category=PolyglotInterop"
--logger "trx;LogFileName=polyglot-tests.trx"
# Wall-clock performance tests are advisory — a slow shared runner must not
# fail the build on a timing assertion.
- name: Performance tests
continue-on-error: true
run: >
dotnet test Compression.Tests
--configuration Release --no-build
--filter "Category=Performance"
--logger "trx;LogFileName=perf-tests.trx"
# AFTER every test leg, and it runs even when one of them failed. Ordered before them it
# masks the suite completely: this step going red stopped the job, no test ran at all, and
# the pull request looked merely blocked rather than failing. That is how twenty-five failing
# tests once reached main. The tests are the primary signal and must always produce one.
#
# The package READMEs must follow the house template, and their API reference must still match
# the assemblies just built. The template itself lives in RepositoryTemplate -- nothing about
# it is copied into this repo.
- name: Check package READMEs
if: ${{ !cancelled() && matrix.os == 'ubuntu-latest' }}
uses: Hawkynt/RepositoryTemplate/package-readme@v1
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-${{ matrix.os }}
path: '**/TestResults/*.trx'
# A text lint over README.md against the house convention — no build, no toolchain — so it
# runs beside the rest rather than inside it and cannot mask them.
repo-readme:
name: repo README
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Hawkynt/RepositoryTemplate/repo-readme@v1
with:
gui: true