Repository navigation
Cut the artifact footprint that is actually there: retention on -out/-records, ship #75's workflow side, and stop leaking a write token to Pkg.test #81
Description
Replaces #80, whose premise was a measurement error (see its body). This is the small version, ordered by measured effect per line changed. Nothing here changes an input default, a permission, or the fork-PR path.
What the fleet actually stores
Live bytes only — filtering expired == false, because an expired artifact keeps its size_in_bytes metadata after GitHub deletes the bytes, and summing without that filter overcounts by every artifact ever created. Across the four largest consumers (86 of the account's 90.7 MB live):
| family | live files | live bytes | retention | line |
|---|---|---|---|---|
-out-* |
7457 | 58.73 MB | 7 days | :649 |
-records |
750 | 16.93 MB | 30 days | :844 |
-lcov |
908 | 10.33 MB | 7 days | :794 |
-timings |
98 | 0.11 MB | 1 day | :336 |
-coverage-* |
0 | 0 MB | 1 day | :636 |
-prebuild |
0 | 0 MB | 1 day | :429 |
Two families hold 84 % of it, and both are the ones whose retention is set for forensics rather than for the run.
1. Retention on -out-* and -records
Nothing in a run depends on either past its own run: collect consumes -out-* in the same run, timings consumes it in the same run, and -records is a 30-day merged archive nobody reads programmatically. At ~97 runs/day a 7-day window is ~680 live copies of data whose useful life is minutes.
Proposal: -out-* 7 → 2 days, -records 30 → 7 days. Both become inputs so a consumer who does want a forensic window can say so — defaults are the shortest value that keeps in-run consumption intact, which is what the other four families already do.
Cost: a shorter window for reading a failed shard's raw section timings after the fact. That is the whole cost, and it is a knob.
2. Ship the workflow side of #75
counter_index / restore_counters are merged, tested (test/core/test_coverage.jl) and not referenced anywhere in .github/ — the shard step at :611-624 still plain-cps the raw .cov. Measured on a real 48-file shard payload: 965,565 → ~15 KB, ~62×, round-trip byte-exact on 48/48 files, and restore_counters already accepts .cov.idx so old and new parts/ trees both restore.
This does not reduce live bytes (-coverage-* expires in a day and is already 0 MB live) — it reduces the bytes an in-flight run has to create, which is the thing a quota gate blocks on. ~5 lines.
3. Automate the prune
lab-sotashimozono/.github#32 already carries this. Demoted from "the fix" to what it is: a backstop. It cannot restore service inside a quota event, because usage is recalculated only every 6–12 hours — which is the one piece of #78's reasoning that survives, and the reason the recent outage lasted 18 hours after the underlying number was already fixed.
4. Move the labels → shards history onto a job output
needs.labels.outputs.timings already exists (:267, :482), added by #77 so the matrix cannot split into "some shards have history, some don't". timings.tsv is under 1 KB. Passing the content base64 through a job output instead of upload-artifact/download-artifact (:330-337 / :481-485):
- removes the artifact whose failure gated the whole matrix during the outage
- needs no new mechanism, no permission change, and works on fork PRs
- hardens the invariant it carries: every shard provably reads the identical history, rather than each fetching
ci-timingsand hoping. That is the failure recorded at:290-293— seven shards loaded 23 rows, the eighth loaded none, "two units ran twice, two ran nowhere, and only the completeness gate noticed" — made structurally impossible instead of merely detected.
Note the asymmetry that makes the other direction hard, and that #80 got wrong: GitHub does not merge job outputs across matrix legs. The last leg to finish overwrites, order unspecified. So shards → collect genuinely needs a transport, and the completeness evidence genuinely has to move. actions/cache is the candidate worth measuring (separate quota, LRU eviction rather than hard failure, one key per run_id/sid, restorable by exact key) — the #72 objection was about a multi-GB shared depot key, not about a 2 KB TSV under a unique one.
5. :595 hands a write-capable token to arbitrary test code unconditionally
Independent of everything above:
594 TESTSHARDS_CLAIM: ${{ inputs.steal && '1' || '' }}
595 TESTSHARDS_CLAIM_TOKEN: ${{ secrets.GITHUB_TOKEN }}:594 is gated on inputs.steal. :595 is not. Since callers must grant contents: write (:249-252), that token is write-capable and sits in the environment of julia-actions/julia-runtest on every run of every consumer — readable by the suite and by every dependency it loads — whether claiming is enabled or not.
Fix: ${{ inputs.steal && secrets.GITHUB_TOKEN || '' }}, the same form :594 already uses. Note :582-584 documents in this file why the operand order matters (an empty string is falsy, so cond && '' || value sets it anyway).
Explicitly NOT proposed
- Removing the artifact path. 90.7 MB live against a documented 500 MB pool; the storage is not structurally unusable, it was unmanaged. See [design] Artifact-free sharding on a free plan: move the completeness check to the shard instead of moving its data #80.
- Per-shard Codecov uploads. Re-adds the lcov conversion
:601-606centralised on measured grounds (3–25 s per shard), multiplies upload count by 8 against another metered free-plan resource, and leaves a tokenless or fork-PR user with per-shard percentages — which do not average, and which this repo already recorded as "54.5 % for a suite that covered 94.8 %". - Weakening
Every unit ran, exactly once. Failing closed when it cannot verify is correct for a gate. If it should survive an artifact failure, it needs an evidence path, not a lower bar.
Acceptance
- live artifact bytes for a consumer measured before and after (1), filtering
expired == false - a shard's
.cov.idxrestores byte-exact incollect, with aparts/tree containing both formats -
-timingsno longer appears in the artifact list for a run, and all N shards report the same history row count -
TESTSHARDS_CLAIM_TOKENis empty in asteal: falserun, asserted from the shard's own environment - a fork pull request still gets: tests, a completeness verdict, and a coverage number
-
shards: 1andsteal: trueboth still pass
Activity
sotashimozono commentedon Aug 19, 2026
Item 2 (shipping the workflow side of #75) is tracked in #74, which already carries the payload measurement. Treat #74 as authoritative for that item; the storage numbers in #74 are corrected in a comment there.
sotashimozono commentedon Aug 19, 2026
Framing correction: nothing in this issue lifts the current block.
Actions storageis metered in GigabyteHours — an accrual over the billing period. Deleting artifacts or shortening retention stops further accrual but does not un-accrue what the period has already counted, and August stands at 374.04 GB·h against an allowance of0.5 GB × 730 h = 365 GB·h. So:-out-*/-records) lowers next period's accrual rate. It is recurrence prevention.collectalready has checked out #74 / Coverage counters can travel without the source they are printed against (#74) #75's wiring) lowers the bytes an in-flight run creates — which matters at the margin but not while creation is refused outright.lab-sotashimozono/.github#32is closed because it deletes onlyexpired == truerecords, whose bytes GitHub has already reclaimed. Zero benefit, one REST call each.TESTSHARDS_CLAIM_TOKENat:595) is independent of all of this and stands on its own.For a run that survives the condition rather than avoiding it next month, see #82 — cache and job outputs instead of artifact storage, with the mode derived from the caller's visibility.
Quantified, so item 1's value is not overstated: at the current 90.7 MB live the fleet accrues ≈
0.0907 × 730 ≈ 66 GB·hper month, comfortably inside 365. The 374 came from the pre-prune period when live was over 1 GB. So retention is worth doing, and it is worth doing for the right reason.sotashimozono commentedon Aug 20, 2026
Closing — keeping the current behaviour. Superseded in substance by #82, which is also closed.
Two things in this thread are worth keeping regardless of what is or is not built:
expired == false. An expired artifact keeps itssize_in_bytesmetadata after GitHub deletes the bytes, so an unfiltered sum counts phantoms — measured 4894 expired records, all with a nonzero size, on one repository. The fleet's live total is 90.7 MB across 25 private repos, and the families that hold it are-out-*(7-day retention) and-records(30-day), not-coverage-*(1-day, 0 MB live).:595hands a write-capable token to arbitrary test code unconditionally.:594gatesTESTSHARDS_CLAIMoninputs.steal;:595gatesTESTSHARDS_CLAIM_TOKEN: ${{ secrets.GITHUB_TOKEN }}on nothing. Since callers are required to grantcontents: write(:249-252), that token sits in the environment ofjulia-actions/julia-runteston every run of every consumer, claiming enabled or not. Independent of the transport question and unaddressed.