Repository navigation
Cache the Robolectric Android SDK jars in CI - #6854
Conversation
|
Important Review skippedReview was skipped as selected files did not have any reviewable changes. ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Priority: ➖ Normal Merge Risk: 🔵 Low · up to
|
📱 QA build
The QA build installs alongside a released Nextcloud app, so you can keep Downloading the file requires a GitHub account, so open this link on the |
3a1f696 to
740caa8
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Organization UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
f79b9257-30c9-4987-a253-a6ac80ca1359
📒 Files selected for processing (1)
.github/workflows/unit-tests.yml
Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 2 remain after this review.
| # Saved even when the suite fails, so a red run still leaves the jars behind. | ||
| - name: Save the Robolectric Android SDK jars | ||
| if: ${{ always() && steps.robolectric-cache.outputs.cache-hit != 'true' }} |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
sed -n '1,115p' .github/workflows/unit-tests.yml
rg -n 'robolectric|mavenCentral|mavenLocal|dependencyResolutionManagement|repositories' app/build.gradle.kts settings.gradle.kts build.gradle.kts gradle.propertiesRepository: nextcloud/talk-android
Length of output: 2967
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- PR diff ---'
git diff --no-ext-diff --unified=30 cd6970664f6e79404bf41b4610fc5a60e04a556a 740caa8bf8d86ead37f22330732136c738069152 -- .github/workflows/unit-tests.yml app/build.gradle.kts
printf '%s\n' '--- Robolectric and SDK references ---'
rg -n -i --glob '!**/build/**' --glob '!**/.gradle/**' 'robolectric|androidsdk|sdk[-_ ]?level|mavenCentral|repositories' . | head -250
printf '%s\n' '--- Gradle files ---'
rg --files -g '*.gradle' -g '*.gradle.kts' -g 'gradle.properties' | sortRepository: nextcloud/talk-android
Length of output: 32084
🌐 Web query:
official actions/cache restore save exact key cache-hit existing cache behavior
💡 Result:
For **`actions/cache/restore` + `actions/cache/save`**, the key point is that **restore doesn’t save anything; save doesn’t overwrite an existing cache**.
- **Restore:** An exact match for `key` restores that cache and sets `cache-hit` to `true`. A match via `restore-keys` is only a partial match, so `cache-hit` is `false`; a miss also means no exact hit. ([github.com](https://github.com/actions/cache?utm_source=openai))
- **Save:** If a cache already exists for the same key, version, and scope, it cannot be replaced. To save changed contents, use a new key. If the key is new, save can create a cache, assuming the run has write access. ([github.com](https://github.com/actions/cache?utm_source=openai))
- **Typical pattern:** Reuse the restore action’s primary key in the save step. If you want to save only after an exact miss, condition the save step on `cache-hit != 'true'`; otherwise an existing cache may make the save redundant or unsuccessful. ([github.com](https://github.com/osy/actions-cache/blob/main/restore/README.md?utm_source=openai))
```yaml
- uses: actions/cache/restore@v6
id: cache
with:
path: build
key: build-${{ hashFiles('**/lockfile') }}
# Build/update files in build...
- uses: actions/cache/save@v6
if: steps.cache.outputs.cache-hit != 'true'
with:
path: build
key: ${{ steps.cache.outputs.cache-primary-key }}
```
An exact hit is scoped not just by key, but also by cache version and branch/scope, so the same text key alone doesn’t guarantee a usable match. ([github.com](https://github.com/actions/cache?utm_source=openai))
Citations:
- 1: https://github.com/actions/cache?utm_source=openai
- 2: https://github.com/actions/cache?utm_source=openai
- 3: https://github.com/osy/actions-cache/blob/main/restore/README.md?utm_source=openai
- 4: https://github.com/actions/cache?utm_source=openai
🏁 Script executed:
printf '%s\n' '--- repository build and Robolectric configuration ---'
sed -n '1,80p' build.gradle
sed -n '1,90p' settings.gradle
cat app/src/test/resources/robolectric.properties 2>/dev/null || true
printf '%s\n' '--- representative Robolectric test declarations ---'
sed -n '1,90p' app/src/test/java/com/nextcloud/talk/utils/DisplayCutoutPaddingTest.kt
sed -n '1,95p' app/src/test/java/com/nextcloud/talk/utils/ssl/TrustManagerTest.ktRepository: nextcloud/talk-android
Length of output: 9059
🌐 Web query:
Robolectric 4.16.1 MavenDependencyResolver Android SDK download local repository partial download
💡 Result:
For **Robolectric 4.16.1**, the SDK JARs are **Maven artifacts**, not Android Studio SDK-platform downloads. `DefaultSdkProvider` asks its `DependencyResolver` for the required `android-all-instrumented` artifact by Maven coordinate. ([github.com](https://github.com/robolectric/robolectric/blob/master/robolectric/src/main/java/org/robolectric/plugins/DefaultSdkProvider.java?utm_source=openai))
If you’re seeing a **partial or failed download**, the relevant cache is typically Robolectric’s own temporary cache—not just Maven’s or Gradle’s dependency cache. Robolectric’s runner source shows it wrapping `MavenDependencyResolver` in a cached resolver under `${java.io.tmpdir}/robolectric`; in offline mode it instead uses `robolectric.dependency.dir`. ([chromium.googlesource.com](https://chromium.googlesource.com/external/robolectric/%2B/shadow_context_wrapper/robolectric/src/main/java/org/robolectric/RobolectricTestRunner.java?utm_source=openai))
Try removing the incomplete artifact/cache and rerunning the test with network access. On Unix-like systems, for example:
```sh
rm -rf "${TMPDIR:-/tmp}/robolectric"
```
If using offline mode, ensure the required SDK JAR is fully present in the configured dependency directory. **Caveat:** the cache-path source found here is not pinned to the 4.16.1 tag, so confirm the exact path against your runtime/configuration before deleting anything. Robolectric 4.16.1 was released on January 21, 2026. ([github.com](https://github.com/robolectric/robolectric/releases?after=robolectric-4.4-beta-1&utm_source=openai))
If you share the exact error and the partially downloaded filename/path, I can narrow down whether it’s Robolectric’s cache, Gradle’s cache, or a repository/proxy issue.
Citations:
- 1: https://github.com/robolectric/robolectric/blob/master/robolectric/src/main/java/org/robolectric/plugins/DefaultSdkProvider.java?utm_source=openai
- 2: https://chromium.googlesource.com/external/robolectric/%2B/shadow_context_wrapper/robolectric/src/main/java/org/robolectric/RobolectricTestRunner.java?utm_source=openai
- 3: https://github.com/robolectric/robolectric/releases?after=robolectric-4.4-beta-1&utm_source=openai
🏁 Script executed:
printf '%s\n' '--- repository build and Robolectric configuration ---'
sed -n '1,80p' build.gradle
sed -n '1,90p' settings.gradle
if test -f app/src/test/resources/robolectric.properties; then cat app/src/test/resources/robolectric.properties; fi
printf '%s\n' '--- representative Robolectric test declarations ---'
sed -n '1,90p' app/src/test/java/com/nextcloud/talk/utils/DisplayCutoutPaddingTest.ktRepository: nextcloud/talk-android
Length of output: 5157
🌐 Web query:
site:github.com/robolectric/robolectric tag robolectric-4.16.1 DefaultSdkProvider MavenDependencyResolver CachingResolver
💡 Result:
At tag **`robolectric-4.16.1`**:
- `DefaultSdkProvider` hard-codes the supported SDK entries and uses its injected `DependencyResolver` to obtain each SDK jar. It builds an `android-all-instrumented` dependency by default (or `android-all` when preinstrumented jars are disabled). ([github.com](https://github.com/robolectric/robolectric/blob/robolectric-4.16.1/robolectric/src/main/java/org/robolectric/plugins/DefaultSdkProvider.java))
- `MavenDependencyResolver` fetches dependencies into a local Maven repository and uses a file lock while fetching. The resolver implementation shown does **not** contain `CachingResolver`. ([github.com](https://github.com/robolectric/robolectric/blob/robolectric-4.16.1/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenDependencyResolver.java))
- So, for this tag, the source supports describing the Maven resolver’s local-repository/fetch behavior, but not attributing a `CachingResolver` class or wrapper to it. That class’s presence elsewhere in the repository remains unverified here.
Citations:
- 1: https://github.com/robolectric/robolectric/blob/robolectric-4.16.1/robolectric/src/main/java/org/robolectric/plugins/DefaultSdkProvider.java
- 2: https://github.com/robolectric/robolectric/blob/robolectric-4.16.1/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenDependencyResolver.java
Do not save an incomplete SDK cache after a download failure.
Robolectric 4.16.1 resolves each requested android-all-instrumented SDK jar through MavenDependencyResolver into the local Maven repository. If initialization fetches some SDK levels before a TLS failure, the save step can publish that partial directory.
A later exact-key hit sets cache-hit to true, so this workflow skips the save step. Robolectric can still download missing jars during that run, but those jars cannot refresh the existing cache. This creates repeated network downloads, not a permanently nonfunctional test suite.
Keep saving after failed tests when the required SDK jars are complete, or use a cache key that can be renewed after the directory is augmented.
740caa8 to
507861b
Compare
f3ee1d4 to
5067dbb
Compare
Robolectric does not take its `android-all` jars through Gradle. It fetches them itself from Maven Central the first time a test needs a given SDK level, so every unit test run depends on a TLS handshake to a remote repository that nothing caches: `setup-gradle` covers `~/.gradle`, while Robolectric writes to `~/.m2/repository/org/robolectric`. When that handshake fails the whole suite dies in `initializationError` before a single assertion runs, which reads like a broken pull request rather than the infrastructure hiccup it is. Cache the directory Robolectric downloads into, keyed on the Robolectric version so that an upgrade fetches the new jars once and caches those instead of serving stale ones. Restore and save are separate steps because `actions/cache` only saves on a green job, and the runs worth caching are precisely the ones where a download succeeded but a later test failed. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
5067dbb to
515afb6
Compare
Robolectric does not take its
android-alljars through Gradle. It fetches them itself from Maven Central the first time a test needs a given SDK level, and nothing in CI caches that:setup-gradlecovers~/.gradle, while Robolectric writes to~/.m2/repository/org/robolectric. So every single unit test run depends on a live TLS handshake to a remote repository.When that handshake fails, the whole suite dies before a single assertion runs and the pull request looks broken:
That happened twice today, on #6763 and #6749, in unrelated pull requests that touch none of the code involved.
This caches the directory Robolectric downloads into. The key is the Robolectric version, read out of
app/build.gradle.kts, so an upgrade fetches the new jars once and caches those rather than serving stale ones — and an unrelated edit to the Gradle file does not churn a ~500 MB cache entry.Restore and save are separate steps on purpose.
actions/cacheruns its save only on a green job (post-if: success()), which is backwards here: the runs worth caching are exactly the ones where the download succeeded and a later test failed. Verified on this PR's own first run, wherePost Cache the Robolectric Android SDK jarswas reported asskippedand nothing was stored.There is deliberately no
restore-keysfallback: a partial hit would write a second copy of the same jars under every new key, and a full miss after an upgrade is exactly the behaviour we have today, self-healing on the next run.Known limitation, not worth more machinery: adding a new
@Config(sdk = [...])level without upgrading Robolectric hits the key exactly, so the cache is not rewritten and that one new jar is downloaded on each run until the next version bump.🏁 Checklist
/backport to stable-xx.x🤖 AI (if applicable)