You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
.github/workflows/ci.yml job dotnet-build packages each selected project's bin/Debug/net10.0 into a per-project .tgz using an inline Python tarfile step (~lines 337–386), uploads each with actions/upload-artifact@v7 + archive: false, and consumers download with skip-decompress: true then tar -xzf to extract.
That pattern came from #2071, and at the time it was the right call — it is the workaround actions/upload-artifact itself documents under Limitations → Permission Loss. But upload-artifact natively supports directory uploads and a compression-level input, so the packaging step may be unnecessary complexity.
This was measured rather than assumed. The measurement partly contradicts the rationale recorded in #2071.
Measurement
Debug build of farm-web.sln, 20 CI-declared artifacts, 19 present on disk (Farm.Web.IntegrationTests not built). 7,549 files, 4,078.6 MiB raw.
Compression compared using Python zipfile (same zlib DEFLATE model as Node archiver, so byte comparison is faithful; timing is directional only).
Approach
Upload
+ Downloads
Total / full-safe run
Pack time
tar.gz (current)
1,609.3 MiB
2,025.7 MiB
3.55 GiB
138.7 s
ZIP-6 (action default)
1,673.0 MiB
2,106.3 MiB
3.69 GiB
57.6 s
ZIP-0 (no compression)
4,079.8 MiB
5,185.2 MiB
9.05 GiB
3.0 s
Download counts derived from ci.yml: 25 download events total (API tests ×4 — three shards plus the provider job; app migrations ×2 each; all others ×1).
Findings
compression-level: 0 is a hard no.bin/ output compresses ~2.5:1. Storing uncompressed costs 2.53× upload bytes and +5.5 GiB per full-safe run. The action's README recommends 0 only for already-compressed or random data.
tar.gz's compression win is only 3.96% (~144 MiB/run) — far smaller than the current design implies.
The current packaging step is 2.4× slower than zipping (138.7 s vs 57.6 s). Note ci.yml uses Python tarfile, not GNU tar. This cost sits inside dotnet-build, which ci: build .NET once and shard the API test leg #2071 deliberately made a fan-out barrier — every test leg waits on it. ci: build .NET once and shard the API test leg #2071 accepted a 60–90 s wall-clock regression to centralize the build; most of that appears recoverable here.
Net: trading ~144 MiB/run for ~80 s off the critical-path barrier plus ~91 lines of deleted YAML/script.
Scope
In scope
Delete the inline Python tarfile packaging step in dotnet-build.
Convert all 20 upload-artifact steps from archive: false tarballs to direct directory uploads with an explicit name: (currently omitted, because archive: false derives the artifact name from the file basename).
Drop skip-decompress: true from the 5 download-artifact steps.
Delete the 3 tar -xzf extract steps.
Use the default compression level — do not set compression-level at all.
upload-artifact's zip path does not preserve Unix file permissions: all files become 0644, directories 0755. On ubuntu-latest the per-project apphosts are extensionless ELF executables that would lose +x.
Current evidence says this is inert: consumers invoke dotnet test <dll> (assembly mode) and dotnet ef … --no-build, so apphosts are never executed. Native assets present — SQLite, libsodium, Assimp, qpdf, QuestPDF Skia, lib3mf — are all .so loaded via dlopen, which does not require the exec bit.
However, the measurement was taken on Windows, where apphosts are .exe. Confidence: high on compression ratios, moderate on Linux permission safety. This needs one real ubuntu-latest run to confirm, not a post-merge discovery.
Acceptance criteria
Python packaging step and all 3 tar -xzf extract steps removed
All 20 upload steps use direct directory paths with explicit name:
compression-level is not set to 0 anywhere
Verified on ubuntu-latest that no consumer requires an exec bit on an extracted file
Full-safe CI run green: all dotnet-test shards, migration-drift, dotnet-test-providers
Zero test loss — still 6,564 discovered tests (1,903 + 674 + 3,689 sharded + 298 provider)
bash scripts/ci/tests/test-select-dotnet-tests.sh passes (canonical job snapshots may need updating)
yamllint line-length violations unchanged from baseline (34)
Required status-context names unchanged
Notes
Relates to #2071 and #2069. Per repo policy, changes under .github/workflows/** always take the full three-reviewer gate — the documentation-only exemption does not apply.
Why
.github/workflows/ci.ymljobdotnet-buildpackages each selected project'sbin/Debug/net10.0into a per-project.tgzusing an inline Pythontarfilestep (~lines 337–386), uploads each withactions/upload-artifact@v7+archive: false, and consumers download withskip-decompress: truethentar -xzfto extract.That pattern came from #2071, and at the time it was the right call — it is the workaround
actions/upload-artifactitself documents under Limitations → Permission Loss. Butupload-artifactnatively supports directory uploads and acompression-levelinput, so the packaging step may be unnecessary complexity.This was measured rather than assumed. The measurement partly contradicts the rationale recorded in #2071.
Measurement
Debug build of
farm-web.sln, 20 CI-declared artifacts, 19 present on disk (Farm.Web.IntegrationTestsnot built). 7,549 files, 4,078.6 MiB raw.Compression compared using Python
zipfile(same zlib DEFLATE model as Nodearchiver, so byte comparison is faithful; timing is directional only).Download counts derived from
ci.yml: 25 download events total (API tests ×4 — three shards plus the provider job; app migrations ×2 each; all others ×1).Findings
compression-level: 0is a hard no.bin/output compresses ~2.5:1. Storing uncompressed costs 2.53× upload bytes and +5.5 GiB per full-safe run. The action's README recommends0only for already-compressed or random data.ci.ymluses Pythontarfile, not GNUtar. This cost sits insidedotnet-build, which ci: build .NET once and shard the API test leg #2071 deliberately made a fan-out barrier — every test leg waits on it. ci: build .NET once and shard the API test leg #2071 accepted a 60–90 s wall-clock regression to centralize the build; most of that appears recoverable here.Net: trading ~144 MiB/run for ~80 s off the critical-path barrier plus ~91 lines of deleted YAML/script.
Scope
In scope
tarfilepackaging step indotnet-build.upload-artifactsteps fromarchive: falsetarballs to direct directory uploads with an explicitname:(currently omitted, becausearchive: falsederives the artifact name from the file basename).skip-decompress: truefrom the 5download-artifactsteps.tar -xzfextract steps.compression-levelat all.Out of scope
Risk — must be validated before merge
upload-artifact's zip path does not preserve Unix file permissions: all files become0644, directories0755. Onubuntu-latestthe per-project apphosts are extensionless ELF executables that would lose+x.Current evidence says this is inert: consumers invoke
dotnet test <dll>(assembly mode) anddotnet ef … --no-build, so apphosts are never executed. Native assets present — SQLite, libsodium, Assimp, qpdf, QuestPDF Skia, lib3mf — are all.soloaded viadlopen, which does not require the exec bit.However, the measurement was taken on Windows, where apphosts are
.exe. Confidence: high on compression ratios, moderate on Linux permission safety. This needs one realubuntu-latestrun to confirm, not a post-merge discovery.Acceptance criteria
tar -xzfextract steps removedname:compression-levelis not set to0anywhereubuntu-latestthat no consumer requires an exec bit on an extracted filedotnet-testshards,migration-drift,dotnet-test-providersbash scripts/ci/tests/test-select-dotnet-tests.shpasses (canonical job snapshots may need updating)line-lengthviolations unchanged from baseline (34)Notes
Relates to #2071 and #2069. Per repo policy, changes under
.github/workflows/**always take the full three-reviewer gate — the documentation-only exemption does not apply.Squad-Author: parker