Skip to content

chore(ci): replace explicit tar packaging with direct directory artifact uploads #2090

Description

@jpapiez

Why

.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

  1. 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.
  2. tar.gz's compression win is only 3.96% (~144 MiB/run) — far smaller than the current design implies.
  3. 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.

Out of scope

Risk — must be validated before merge

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.

Squad-Author: parker

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:devopsCI/CD, deployment, infrastructure, and runtime opspriority:p2Next sprintsquad:parkerAssigned to ⚙️ Parker (DevOps & Deployment Engineer)type:choreMaintenance, refactoring, cleanup

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions