From 0044a5a7da953f2d58f5e7c81e791032b697aa98 Mon Sep 17 00:00:00 2001 From: James Rich Date: Sun, 2 Aug 2026 18:19:46 -0500 Subject: [PATCH 1/2] ci: publish KMP Build Scans and cache to the OSS Community Develocity instance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the self-hosted HttpBuildCache with Develocity's remote cache at community.develocity.cloud under project `meshtastic`, matching the Meshtastic-Android onboarding (meshtastic/Meshtastic-Android#6531). Covers the KMP leg of this repo only — the buf, deno and cargo builds are unaffected. - `packages/kmp/gradle/develocity.settings.gradle` replaces `packages/kmp/gradle/build-cache.settings.gradle`. - All four `setup-gradle` call sites across kmp-pull-request.yml, publish-kmp.yml (2) and snapshot-kmp.yml gain an optional develocity-access-key; the GRADLE_CACHE_* env blocks are removed. - Scans publish only from authenticated builds, so fork PRs and developers without a provisioned key are unaffected; only authenticated CI runs write to the cache. The GRADLE_CACHE_* secrets are no longer read but are deliberately left in place as the rollback path. --- .github/workflows/kmp-pull-request.yml | 7 +-- .github/workflows/publish-kmp.yml | 9 ++- .github/workflows/snapshot-kmp.yml | 7 +-- README.md | 1 + .../kmp/gradle/build-cache.settings.gradle | 62 ------------------- .../kmp/gradle/develocity.settings.gradle | 56 +++++++++++++++++ packages/kmp/settings.gradle.kts | 7 ++- 7 files changed, 71 insertions(+), 78 deletions(-) delete mode 100644 packages/kmp/gradle/build-cache.settings.gradle create mode 100644 packages/kmp/gradle/develocity.settings.gradle diff --git a/.github/workflows/kmp-pull-request.yml b/.github/workflows/kmp-pull-request.yml index 7b800c78..449de966 100644 --- a/.github/workflows/kmp-pull-request.yml +++ b/.github/workflows/kmp-pull-request.yml @@ -10,11 +10,6 @@ on: permissions: contents: read -env: - GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }} - GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }} - GRADLE_CACHE_PASSWORD: ${{ secrets.GRADLE_CACHE_PASSWORD }} - jobs: build-kmp: runs-on: macos-latest @@ -33,6 +28,8 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v6 + with: + develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - name: Cache Konan (Kotlin/Native toolchain) uses: actions/cache@v6 diff --git a/.github/workflows/publish-kmp.yml b/.github/workflows/publish-kmp.yml index 3fc67398..fd93681a 100644 --- a/.github/workflows/publish-kmp.yml +++ b/.github/workflows/publish-kmp.yml @@ -33,11 +33,6 @@ on: permissions: contents: read -env: - GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }} - GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }} - GRADLE_CACHE_PASSWORD: ${{ secrets.GRADLE_CACHE_PASSWORD }} - jobs: build-kmp: runs-on: macos-latest @@ -77,6 +72,8 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v6 + with: + develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - name: Cache Konan (Kotlin/Native toolchain) uses: actions/cache@v6 @@ -113,6 +110,8 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v6 + with: + develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - name: Cache Konan (Kotlin/Native toolchain) uses: actions/cache@v6 diff --git a/.github/workflows/snapshot-kmp.yml b/.github/workflows/snapshot-kmp.yml index 1b92a8f2..32f28a89 100644 --- a/.github/workflows/snapshot-kmp.yml +++ b/.github/workflows/snapshot-kmp.yml @@ -15,11 +15,6 @@ on: permissions: contents: read -env: - GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }} - GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }} - GRADLE_CACHE_PASSWORD: ${{ secrets.GRADLE_CACHE_PASSWORD }} - jobs: publish-snapshot: runs-on: macos-latest @@ -40,6 +35,8 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v6 + with: + develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - name: Cache Konan (Kotlin/Native toolchain) uses: actions/cache@v6 diff --git a/README.md b/README.md index 7de787ba..661c8730 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![CLA assistant](https://cla-assistant.io/readme/badge/meshtastic/protobufs)](https://cla-assistant.io/meshtastic/protobufs) [![Fiscal Contributors](https://opencollective.com/meshtastic/tiers/badge.svg?label=Fiscal%20Contributors&color=deeppink)](https://opencollective.com/meshtastic/) [![Vercel](https://img.shields.io/static/v1?label=Powered%20by&message=Vercel&style=flat&logo=vercel&color=000000)](https://vercel.com?utm_source=meshtastic&utm_campaign=oss) +[![Revved up by Develocity](https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A)](https://community.develocity.cloud/scans?search.rootProjectNames=protobufs) ## Overview diff --git a/packages/kmp/gradle/build-cache.settings.gradle b/packages/kmp/gradle/build-cache.settings.gradle deleted file mode 100644 index dd9bfc87..00000000 --- a/packages/kmp/gradle/build-cache.settings.gradle +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Shared remote HTTP Gradle build cache for Meshtastic KMP libraries. - * - * Credentials come from the GRADLE_CACHE_URL / GRADLE_CACHE_USERNAME / - * GRADLE_CACHE_PASSWORD environment variables (CI secrets), or a - * local.properties / config.properties entry for local use. Writes to the - * cache happen only from trusted events (local dev, push, merge_group) with - * credentials present, so pull-request runs (and credential-less fork PRs) - * stay pull-only and cannot poison the cache. - */ - -def getMeshProperty(String key) { - def env = System.getenv(key) - if (env) return env - def currentDir = settingsDir - while (currentDir != null) { - for (name in ["local.properties", "config.properties"]) { - def f = new File(currentDir, name) - if (f.exists()) { - def props = new Properties() - f.withInputStream { props.load(it) } - if (props.containsKey(key)) return props.getProperty(key) - } - } - currentDir = currentDir.parentFile - } - return null -} - -buildCache { - local { - enabled = true - } - remote(HttpBuildCache) { - // Some cache servers return 403 on "Expect: 100-continue". - useExpectContinue = false - def cacheUrl = getMeshProperty("GRADLE_CACHE_URL")?.trim() - def cacheUsername = getMeshProperty("GRADLE_CACHE_USERNAME")?.trim() - def cachePassword = getMeshProperty("GRADLE_CACHE_PASSWORD")?.trim() - if (cacheUrl) { - // HTTPS + valid TLS enforced (no allowInsecureProtocol / no - // allowUntrustedServer): the cache server must present a trusted - // certificate over TLS. - url = cacheUrl.endsWith("/") ? cacheUrl : "${cacheUrl}/" - if (cacheUsername && cachePassword) { - credentials { - username = cacheUsername - password = cachePassword - } - } - // Write only from trusted events (local dev, push to a protected - // branch, or the merge queue) with credentials — never from - // pull_request runs, so unmerged code can't poison the cache. - def eventName = System.getenv("GITHUB_EVENT_NAME") - def trustedForPush = eventName == null || eventName == "push" || eventName == "merge_group" - push = (cacheUsername && cachePassword && trustedForPush) - enabled = true - } else { - enabled = false - } - } -} diff --git a/packages/kmp/gradle/develocity.settings.gradle b/packages/kmp/gradle/develocity.settings.gradle new file mode 100644 index 00000000..969bf9bb --- /dev/null +++ b/packages/kmp/gradle/develocity.settings.gradle @@ -0,0 +1,56 @@ +/* + * Develocity — Build Scans and remote Build Cache on the OSS Community instance + * (https://community.develocity.cloud), project `meshtastic`. + * + * Covers the KMP leg of this repo only; the buf, deno and cargo builds are + * unaffected. + * + * Replaces the former self-hosted HttpBuildCache: GRADLE_CACHE_URL / _USERNAME / + * _PASSWORD are no longer read anywhere in this repo. + */ + +def isCI = System.getenv("CI") != null + +develocity { + server = "https://community.develocity.cloud" + projectId = "meshtastic" + buildScan { + uploadInBackground = !isCI + // Unauthenticated builds (fork PRs, developers who never provisioned a key) + // publish nothing rather than failing. + publishing.onlyIf { it.authenticated } + // Fingerprints power cache-miss comparison (CI debugging); skip the payload locally. + capture { fileFingerprints = isCI } + // Public instance: no machine identity. Constants on purpose — scans already + // record OS/CPU and CCUD adds CI metadata. Keep the `if` OUTSIDE the closures: + // capture-free closures are what the configuration cache can serialize. + obfuscation { + ipAddresses { addresses -> addresses.collect { "0.0.0.0" } } + externalProcessName { "external-process" } + if (isCI) { + username { "ci" } + hostname { "ci-runner" } + } else { + username { "local-dev" } + hostname { "local-machine" } + } + } + } +} + +// Resolved outside the buildCache block: inside it the closure delegate is +// BuildCacheConfiguration, which has no `develocity` property. +def develocityBuildCache = develocity.buildCache +def accessKey = System.getenv("DEVELOCITY_ACCESS_KEY")?.trim() + +buildCache { + // Off on CI: runners are ephemeral and every hit comes from the remote anyway. + local { + enabled = !isCI + } + remote(develocityBuildCache) { + enabled = true + // Only authenticated CI writes, so unmerged and fork code cannot poison the cache. + push = isCI && accessKey != null && !accessKey.isEmpty() + } +} diff --git a/packages/kmp/settings.gradle.kts b/packages/kmp/settings.gradle.kts index 6fb5e07b..1b988462 100644 --- a/packages/kmp/settings.gradle.kts +++ b/packages/kmp/settings.gradle.kts @@ -6,6 +6,11 @@ pluginManagement { } } +plugins { + id("com.gradle.develocity") version "4.5.0" + id("com.gradle.common-custom-user-data-gradle-plugin") version "2.8.0" +} + dependencyResolutionManagement { repositories { google() @@ -13,6 +18,6 @@ dependencyResolutionManagement { } } -apply(from = "gradle/build-cache.settings.gradle") +apply(from = "gradle/develocity.settings.gradle") rootProject.name = "protobufs" From c3b4e71e122fcf336b497121581824acf5ae9d33 Mon Sep 17 00:00:00 2001 From: James Rich Date: Sun, 2 Aug 2026 19:17:28 -0500 Subject: [PATCH 2/2] ci: restrict Develocity cache writes to trusted events Same-repository pull requests DO receive repository secrets, so gating cache writes on the presence of DEVELOCITY_ACCESS_KEY alone let a PR build write entries into the shared cache. The self-hosted HttpBuildCache this replaced gated on GITHUB_EVENT_NAME and excluded pull_request; that protection was lost in the port and is restored here. Verified against a real CI-shaped environment: CI=true GITHUB_EVENT_NAME=pull_request -> pull-only CI=true GITHUB_EVENT_NAME=push -> writes enabled CI=true GITHUB_EVENT_NAME=merge_group -> writes enabled Local builds are still excluded by isCI, and fork PRs have no key at all. --- packages/kmp/gradle/develocity.settings.gradle | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/kmp/gradle/develocity.settings.gradle b/packages/kmp/gradle/develocity.settings.gradle index 969bf9bb..3d9e0f0a 100644 --- a/packages/kmp/gradle/develocity.settings.gradle +++ b/packages/kmp/gradle/develocity.settings.gradle @@ -50,7 +50,13 @@ buildCache { } remote(develocityBuildCache) { enabled = true - // Only authenticated CI writes, so unmerged and fork code cannot poison the cache. - push = isCI && accessKey != null && !accessKey.isEmpty() + // Write only from trusted events. Local dev is excluded by isCI, and + // pull_request runs are excluded here: a same-repository PR DOES receive + // repository secrets, so gating on the access key alone would let + // unmerged code write entries into the shared cache. Fork PRs have no + // key and are excluded twice over. + def eventName = System.getenv("GITHUB_EVENT_NAME") + def trustedForPush = eventName == "push" || eventName == "merge_group" + push = isCI && trustedForPush && accessKey != null && !accessKey.isEmpty() } }