Skip to content

Cache the Robolectric Android SDK jars in CI - #6854

Merged
AndyScherzinger merged 1 commit into
masterfrom
ci/noid/cache-robolectric-sdks
Oct 15, 2026
Merged

AndyScherzinger merged 1 commit into
masterfrom
ci/noid/cache-robolectric-sdks

Conversation

@AndyScherzinger

@AndyScherzinger AndyScherzinger commented Oct 11, 2026 •

Copy link
Copy Markdown
Member

Merge order: #6855 first, then this one. #6855 is what turns the unit test job green again; this PR stops the download happening at all. It cannot take effect before #6855 is in, because actions/cache only writes its cache on a successful job — so merged on its own, this does nothing.

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, and nothing in CI caches that: setup-gradle covers ~/.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:

DisplayCutoutPaddingTest > initializationError FAILED
  java.lang.AssertionError at MavenArtifactFetcher.java:129
    Caused by: javax.net.ssl.SSLException at Alert.java:132
      Caused by: java.lang.RuntimeException at XDHKeyExchange.java:109
        Caused by: java.security.InvalidAlgorithmParameterException at OpenSSLXDHKeyPairGenerator.java:58

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/cache runs 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, where Post Cache the Robolectric Android SDK jars was reported as skipped and nothing was stored.

There is deliberately no restore-keys fallback: 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

  • ⛑️ Tests (unit and/or integration) are included or not needed
  • 🔖 Capability is checked or not needed
  • 🔙 Backport requests are created or not needed: /backport to stable-xx.x
  • 📅 Milestone is set
  • 🌸 PR title is meaningful (if it should be in the changelog: is it meaningful to users?)

🤖 AI (if applicable)

  • The content of this PR was partly or fully generated using AI

@AndyScherzinger AndyScherzinger added the 3. to review Waiting for reviews label Oct 11, 2026
@AndyScherzinger AndyScherzinger self-assigned this Oct 11, 2026
@AndyScherzinger AndyScherzinger added this to the 25.1.0 milestone Oct 11, 2026
@coderabbitai

coderabbitai Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 8642207f-395c-4710-ab4d-23c2755abe4d
📥 Commits

Reviewing files that changed from the base of the PR and between 5067dbb and 515afb6.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 6c094263-f628-4a10-b531-edd90a3a8e36
📥 Commits

Reviewing files that changed from the base of the PR and between 507861b and 43e7fbb.

📒 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; 3 remain after this review.

📝 Walkthrough

The unit-test workflow extracts the Robolectric dependency version from app/build.gradle.kts. It uses the runner OS and extracted version in the cache key for ~/.m2/repository/org/robolectric. It runs the unit-test task between cache restoration and saving. The save step runs on failure when the restore missed.

Priority: ➖ Normal

Merge Risk: 🔵 Low · up to 43e7f

The workflow change caches Robolectric SDK jars in CI. If a download fails partway through, an incomplete cache could be saved and later runs would keep re-downloading the missing jars. This is a minor CI efficiency risk and does not block merging.

Pre-merge checks: Passed 5
Passed Passed checks: 5 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: caching Robolectric Android SDK jars in CI.
Description check Passed The description explains the problem, implementation, cache-key behavior, dependency on PR #6855, known limitation, and checklist status. It is complete and directly related to the changes.
Docstring Coverage Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

📱 QA build

Download app-qa-debug.apk
QR code Open the QR code for this download
Commit 515afb6
Version 6854
Available until 7 days after this build

The QA build installs alongside a released Nextcloud app, so you can keep
using your existing install while testing.

Downloading the file requires a GitHub account, so open this link on the
device you want to test on, or transfer the APK to it.

@AndyScherzinger
AndyScherzinger force-pushed the ci/noid/cache-robolectric-sdks branch from 3a1f696 to 740caa8 Compare October 11, 2026 21:13

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: f79b9257-30c9-4987-a253-a6ac80ca1359
📥 Commits

Reviewing files that changed from the base of the PR and between 3a1f696 and 740caa8.

📒 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.

Comment on lines +53 to +55
# 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' }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.properties

Repository: 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' | sort

Repository: 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.kt

Repository: 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.kt

Repository: 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.

@AndyScherzinger
AndyScherzinger force-pushed the ci/noid/cache-robolectric-sdks branch from 740caa8 to 507861b Compare October 12, 2026 06:38
@nextcloud nextcloud deleted a comment from coderabbitai Bot Oct 12, 2026
@AndyScherzinger
AndyScherzinger force-pushed the ci/noid/cache-robolectric-sdks branch 3 times, most recently from f3ee1d4 to 5067dbb Compare October 14, 2026 16:49
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>
@AndyScherzinger
AndyScherzinger force-pushed the ci/noid/cache-robolectric-sdks branch from 5067dbb to 515afb6 Compare October 14, 2026 17:07
@github-actions

Copy link
Copy Markdown
Contributor

Codacy

Lint

TypemasterPR
Warnings138138
Errors1515

SpotBugs

CategoryBaseNew
Bad practice77
Correctness88
Dodgy code4040
Internationalization33
Malicious code vulnerability33
Performance88
Security1111
Total8080

@AndyScherzinger
AndyScherzinger merged commit 9124472 into master Oct 15, 2026
20 checks passed
@AndyScherzinger
AndyScherzinger deleted the ci/noid/cache-robolectric-sdks branch October 15, 2026 08:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3. to review Waiting for reviews AI assisted

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant