Build1 publisher2 min readPublished
An Android release pipeline consumed several times GitHub's 500MB Actions quota in about ten days
The failure landed on artifact upload, weeks after the pipeline went stable. The audit found ten near-identical Gradle caches, one per active branch, a hashFiles key that never matched anything, and 30-day retention on 70MB builds.
The Engineer · Build desk

What happened
- A multi-app Android release pipeline that had been working and stable for weeks started failing on artifact upload, with an error that had nothing to do with the code under test.
- GitHub's free organisation plan allows 500MB of Actions storage in total across the whole org, and the pipeline had been consuming several times that in about ten days.
- A cache-by-cache audit found the Gradle dependency cache stored as ten separate, effectively identical entries, together the largest single share of the quota.
- Artifact storage alone had reached nearly two gigabytes over about two weeks, made up of signed release AABs at roughly 70MB each and internal test APKs at 20 to 40MB.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint The allowance is counted across the organisation, so one repository's branch churn stops artifact upload for every other repo, and usage is recalculated only every 6 to 12 hours, so a cleanup does not clear the block immediately.
- cost The broken key charged twice: the org paid runner minutes to re-download the full Gradle dependency set on every build, then paid storage for a per-branch copy that every restore missed.
- exposure Any team that left Actions' suggested 30-day retention on large signed build outputs is accumulating retention days the same way, and the storage page is the only place it shows before an upload fails.
- decision Deleting artifacts buys time and leaves the question open; the author treats dependence on GitHub's shared minutes and storage as the dependency to eliminate.
The line that did the damage is the cache key:
`key: ${{ runner.os }}-gradle-${{ hashFiles('android/gradle/wrapper/gradle-wrapper.properties', 'android/build.gradle') }}` [6]
Both paths live under an `android/` directory that is .gitignored and generated later, inside the build script's own prebuild step [7]. When the cache step runs, neither file is on disk yet. `hashFiles()` against paths that do not exist returns an empty string every time, so the key never varied with the real config [8].
That breaks the cache in both directions. Restores always miss, so every build re-downloaded every Gradle dependency [9]. Saves always write, and GitHub scopes Actions caches per branch by default, so a cache saved on one branch is not shared with another; it is a full new copy [10]. Ten active branches held ten copies of the same dependency set at once, and none of them aged out, because ongoing work on each branch kept refreshing its own [11]. The dev.to post describes the two bugs as one root: a cache system that looked like it was working, doing none of its actual job, on both counts [19].
The artifact side is simpler to add up. The allowance is 500MB, total, org-wide [2]. A signed release AAB is about 70MB [4]. Seven of them is 490MB, or 98 percent of what the free plan gives the entire organisation, and those files carried 30-day retention because that is the number Actions suggests and nobody changed it [1][13]. Artifacts had reached nearly two gigabytes in about two weeks, four times the quota without counting a single cache [3][2].
The error message points at artifacts. They were the smaller half of the problem. According to the write-up, ten duplicate caches never appear in any failure output, and finding them meant going past the summary usage figures to list every artifact and every cache for the repository through `gh api` with `--paginate` [16][15].
This reaches your pipeline only on a free org plan, with build outputs measured in tens of megabytes, retention left at the default, and enough concurrent branches to multiply the cache [2][10][13]. The rate you approach the 500MB is artifact size times retention days, plus branch count times cache size. A repo whose artifacts are a few megabytes takes far longer to reach the same wall, and the per-branch duplication is waiting there when it does.
Cleanup helped, per the post, though the real fix lies elsewhere; the conclusion the author draws is that this pipeline should never again depend on GitHub's shared minutes or storage at all [17]. Part 4 covers that redesign and the first real production ship [18].
What to watch
- Part 4's redesign, and whether the pipeline moves to self-hosted runners, its own artifact storage, or both.
- Whether fixing the key to hash committed files collapses the ten per-branch copies or only makes restores hit.
- Any change by GitHub to the free-plan 500MB org-wide allowance or to per-branch cache scoping as a default.