From 8fb7e4eeb4843abf9fcd3c19c0a345b32d477f31 Mon Sep 17 00:00:00 2001 From: Scott Olcott Date: Mon, 28 Sep 2026 09:54:31 -0600 Subject: [PATCH 1/2] Serialize webpack and karma tasks on CI to stop runner OOM kills Under org.gradle.parallel, the production webpack and browser-test tasks of several modules overlap alongside the Gradle and Kotlin daemons and exhaust ubuntu-latest's 16 GB. The runner is then killed mid-build, and the job ends as "The operation was canceled" or "The runner has received a shutdown signal" with no Gradle error. A shared build service with maxParallelUsages = 1 runs those tasks one at a time when CI is set; local builds stay parallel. Co-Authored-By: Claude Opus 5.5 --- build.gradle.kts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index 7fb9912..cd5cfc6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,7 @@ import com.ncorti.ktfmt.gradle.KtfmtExtension import org.gradle.buildconfiguration.tasks.UpdateDaemonJvm +import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest +import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpack // AGP 9.x has built-in Kotlin support, pinned to an older Kotlin Gradle Plugin (KGP) version. // Metro 1.3.2 needs Kotlin >= 2.3.20 and Compose needs a matching compiler plugin, so we override @@ -29,3 +31,21 @@ tasks.named("updateDaemonJvm") { languageVersion = JavaLanguageVersion.of(25) vendor.set(JvmVendorSpec.AMAZON) } + +// On CI, run the webpack and karma tasks one at a time. Each starts a Node process sized for a +// whole Compose bundle, and karma adds a Chrome on top. `build` has seven modules with web +// executables, so under org.gradle.parallel they overlap alongside the Gradle and Kotlin daemons, +// exhaust ubuntu-latest's 16 GB, and the runner is killed: the job ends as "The operation was +// canceled" or "The runner has received a shutdown signal", with no Gradle error at all. +// Locally there is enough memory, so they stay parallel. +abstract class WebBundleTaskLimiter : BuildService + +val webBundleTaskLimiter = + gradle.sharedServices.registerIfAbsent("webBundleTaskLimiter", WebBundleTaskLimiter::class) { + if (providers.environmentVariable("CI").isPresent) maxParallelUsages = 1 + } + +subprojects { + tasks.withType().configureEach { usesService(webBundleTaskLimiter) } + tasks.withType().configureEach { usesService(webBundleTaskLimiter) } +} From 34fcc11471023580c81d5e6c8c6ffe1f3c00325f Mon Sep 17 00:00:00 2001 From: Scott Olcott Date: Mon, 28 Sep 2026 10:19:43 -0600 Subject: [PATCH 2/2] Enlarge CI swap and log memory during the build Serializing webpack and karma was not enough: run 36447154005 was still killed, this time just after :web's production and test Kotlin/JS and Wasm compiles, which run in the Kotlin daemon rather than webpack. Give the runner a 12 GB swapfile so the peak pages out, and sample memory, disk and the largest processes into the Build step's own log so the next failure, if any, shows what was resident. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build.yml | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 915cd3f..30f1fe5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,5 +47,28 @@ jobs: - name: Check build-logic run: ./gradlew -p build-logic ktfmtCheck checkSortDependencies detekt + # `build` overlaps the Gradle and Kotlin daemons, Kotlin/Native, R8, webpack and karma's + # Chrome, and peaks above the runner's 16 GB. When it does, the runner itself is OOM-killed + # and the job ends as "The operation was canceled" with no Gradle error. The image ships a + # 4 GB swapfile; a larger one lets the peak page out instead. + - name: Enlarge swap + run: | + sudo swapoff -a + sudo rm -f /mnt/swapfile + sudo fallocate -l 12G /mnt/swapfile + sudo chmod 600 /mnt/swapfile + sudo mkswap /mnt/swapfile + sudo swapon /mnt/swapfile + free -h + + # The sampler prints into this step's own log so the numbers survive the runner being + # killed; a file or a later step would not. - name: Build - run: ./gradlew build + run: | + ( while sleep 30; do + echo "::group::resources $(date -u +%T)" + free -m; df -h / /mnt + ps -eo rss:10,args --sort=-rss | head -8 | cut -c1-160 + echo "::endgroup::" + done ) & + ./gradlew build