Stop the Gradle build cache from being invalidated on every release

Two independent defects, both found while a release sat at 8.83 GB of the
10 GB per-repo cache limit with entries being evicted.

1. set-version rewrote build.gradle.kts with sed on every run, and that file
   is hashed into the Gradle build-cache key — so each release produced a
   brand-new key and orphaned the previous one. The repo inventory showed the
   damage: windows-package-x64, macos-x64 and windows-package-arm64 each had
   two ~1.1 GB entries differing only in that hash. build.gradle.kts now reads
   -PversionName/-PversionCode (injected as ORG_GRADLE_PROJECT_* by
   set-version) and keeps its previous values as defaults, so local builds are
   unchanged. Types stay String/Integer for the `as Int`/`as String` consumers
   in app/android and app/shared. Side benefit: hash-release-build-inputs also
   hashes build.gradle.kts, so the "inputs unchanged" marker no longer churns
   per version either.

2. Every desktop scope archived the Android build dirs (never built there)
   plus the final installers, the exported ProGuard jars, the jpackage runtime
   image and the ProGuard temp output — all regenerated for each version, and
   together the bulk of a ~1.7 GB entry. Those paths are now excluded, which
   is what kept the repo over the limit and into eviction (GitHub evicts by
   oldest last-accessed once the limit is hit).

Restore and save path lists stay in sync: actions/cache silently skips the save
when they differ.

Cross-tag reuse is NOT addressed here and cannot be: per the docs, a run for one
tag cannot read another tag's cache, and the automatic retry against the default
branch is a no-op for us because main has no app/desktop module to warm it.
This commit is contained in:
2026-10-16 20:36:56 +03:00
Unverified
parent f98f0c217b
commit 0e7b1810fc
4 changed files with 46 additions and 20 deletions
+19 -3
View File
@@ -17,6 +17,15 @@ outputs:
runs:
using: composite
steps:
# Keep the path lists here and in save-gradle-build-cache identical: actions/cache refuses to
# save when the restore and save paths differ.
#
# Scope: caches are only visible to runs on the same ref (plus the default branch), so a run
# for a new release tag never sees the previous tag's cache — see "Restrictions for accessing
# a cache". This cache therefore pays off for re-runs of a tag and for warm branch runs, not
# across tags. That is why the key must stay stable across releases: everything cached here
# is a pure function of these files, and the release version is injected through Gradle
# properties (see set-version) rather than by editing a hashed source file.
- uses: actions/cache/restore@v5
if: inputs.scope == 'android'
id: cache-android
@@ -28,6 +37,7 @@ runs:
app/android/build
utils/shared/build
utils/android/build
!app/android/build/outputs
key: gradle-build-${{ runner.os }}-${{ runner.arch }}-v2-${{ inputs.scope }}-${{ hashFiles('gradle/libs.versions.toml', 'settings.gradle.kts', 'gradle.properties', 'build.gradle.kts', 'app/desktop/build.gradle.kts', 'app/desktop/proguard-rules.pro', 'app/shared/build.gradle.kts', 'app/android/build.gradle.kts', 'utils/shared/build.gradle.kts', 'utils/android/build.gradle.kts') }}
restore-keys: |
gradle-build-${{ runner.os }}-${{ runner.arch }}-v2-${{ inputs.scope }}-
@@ -37,14 +47,20 @@ runs:
if: inputs.scope != 'android'
id: cache
with:
# Desktop scopes never build Android, so the Android module dirs are not archived here.
# The negated paths are final artifacts and regenerated intermediates: they are rebuilt for
# every version anyway, and archiving them is what pushed this repo past the 10 GB cache
# limit and into eviction (see the size of the per-scope entries).
path: |
.gradle
build
app/desktop/build
app/shared/build
app/android/build
utils/shared/build
utils/android/build
app/desktop/build
!app/desktop/build/distributions
!app/desktop/build/ci
!app/desktop/build/compose/binaries
!app/desktop/build/compose/tmp
key: gradle-build-${{ runner.os }}-${{ runner.arch }}-v2-${{ inputs.scope }}-${{ hashFiles('gradle/libs.versions.toml', 'settings.gradle.kts', 'gradle.properties', 'build.gradle.kts', 'app/desktop/build.gradle.kts', 'app/desktop/proguard-rules.pro', 'app/shared/build.gradle.kts', 'app/android/build.gradle.kts', 'utils/shared/build.gradle.kts', 'utils/android/build.gradle.kts') }}
restore-keys: |
gradle-build-${{ runner.os }}-${{ runner.arch }}-v2-${{ inputs.scope }}-
@@ -9,6 +9,8 @@ inputs:
runs:
using: composite
steps:
# Path lists must stay identical to cache-gradle-build: actions/cache skips the save when the
# restore and save paths do not match.
- name: Save Android Gradle build cache
if: inputs.scope == 'android'
uses: actions/cache/save@v5
@@ -22,6 +24,7 @@ runs:
app/android/build
utils/shared/build
utils/android/build
!app/android/build/outputs
- name: Save Gradle build cache
if: inputs.scope != 'android'
uses: actions/cache/save@v5
@@ -31,8 +34,10 @@ runs:
path: |
.gradle
build
app/desktop/build
app/shared/build
app/android/build
utils/shared/build
utils/android/build
app/desktop/build
!app/desktop/build/distributions
!app/desktop/build/ci
!app/desktop/build/compose/binaries
!app/desktop/build/compose/tmp
+9 -11
View File
@@ -1,5 +1,5 @@
name: Set release version
description: Writes versionName/versionCode into build.gradle.kts for CI
description: Makes versionName/versionCode available to Gradle for CI
inputs:
version:
@@ -12,19 +12,17 @@ runs:
- name: Set version
shell: bash
run: |
set -euo pipefail
VERSION="${{ inputs.version }}"
VERSION_CODE="$(printf '%s' "$VERSION" | sed 's/[^0-9]//g')"
if [[ -z "$VERSION_CODE" ]]; then
VERSION_CODE=1
fi
GRADLE_FILE="build.gradle.kts"
CURRENT_NAME="$(sed -n 's/.*extra\["versionName"\] = "\([^"]*\)".*/\1/p' "$GRADLE_FILE" | head -n 1)"
CURRENT_CODE="$(sed -n 's/.*extra\["versionCode"\] = \([0-9]*\).*/\1/p' "$GRADLE_FILE" | head -n 1)"
if [[ "$CURRENT_NAME" == "$VERSION" && "$CURRENT_CODE" == "$VERSION_CODE" ]]; then
echo "versionName=$VERSION versionCode=$VERSION_CODE (unchanged)"
exit 0
fi
sed -i.bak "s/extra\\[\"versionName\"\\] = \"[^\"]*\"/extra[\"versionName\"] = \"$VERSION\"/" "$GRADLE_FILE"
sed -i.bak "s/extra\\[\"versionCode\"\\] = [0-9]*/extra[\"versionCode\"] = $VERSION_CODE/" "$GRADLE_FILE"
rm -f "$GRADLE_FILE.bak"
# Injected as Gradle project properties rather than rewriting build.gradle.kts: that file
# is hashed into the Gradle build-cache key, so editing it per release meant no release
# could ever reuse the previous one's cache.
{
echo "ORG_GRADLE_PROJECT_versionName=$VERSION"
echo "ORG_GRADLE_PROJECT_versionCode=$VERSION_CODE"
} >> "$GITHUB_ENV"
echo "versionName=$VERSION versionCode=$VERSION_CODE"
+10 -3
View File
@@ -8,9 +8,16 @@ plugins {
alias(libs.plugins.google.services) apply false
}
/** Single source of truth for app version (APK + generated [AppBuildInfo]). */
extra["versionName"] = "1.2.0"
extra["versionCode"] = 120
/**
* Single source of truth for app version (APK + generated [AppBuildInfo]).
*
* CI injects `-PversionName` / `-PversionCode` (exported as `ORG_GRADLE_PROJECT_*` by
* .github/actions/set-version) instead of rewriting this file. This file is hashed into the
* Gradle build-cache key, so a per-release `sed` here gave every release a brand-new cache key
* and orphaned the previous one — roughly a gigabyte of dead cache per release tag.
*/
extra["versionName"] = providers.gradleProperty("versionName").getOrElse("1.2.0")
extra["versionCode"] = providers.gradleProperty("versionCode").getOrElse("120").toInt()
buildscript {
repositories {