Skip to content
49 changes: 49 additions & 0 deletions .github/instructions/gradle.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
applyTo: "**/*.gradle"
Comment thread
jonathanpeppers marked this conversation as resolved.
---

# Gradle conventions

All `src/*` Gradle projects share two repo config files: **`eng/gradle/plugin-repositories.gradle`** (for `pluginManagement.repositories`) and **`eng/gradle/dependency-repositories.gradle`** (for `dependencyResolutionManagement.repositories`). Never hard-code Maven URLs (`mavenCentral()`, `google()`, `pkgs.dev.azure.com/...`, etc.) in `build.gradle`/`settings.gradle`.

## settings.gradle template

```groovy
pluginManagement {
apply from: "${rootDir}/../../eng/gradle/plugin-repositories.gradle", to: pluginManagement
}
plugins {
id 'com.microsoft.azure.artifacts.credprovider' version '1.1.1'
}
dependencyResolutionManagement {
apply from: "${rootDir}/../../eng/gradle/dependency-repositories.gradle", to: dependencyResolutionManagement
}
rootProject.name = '<project>'
```

`build.gradle` files must not declare their own `repositories { ... }`.

## CI vs local

Both files switch on `System.getenv('RunningOnCI')` (or `RUNNINGONCI` — AzDO uppercases env vars on Linux/macOS agents):

- **`RunningOnCI=true`** (Azure DevOps, set in `build-tools/automation/yaml-templates/variables.yaml`) → dnceng `dotnet-public-maven` feed (CFSClean isolation, https://aka.ms/1es/netiso/CFS). Anonymous read of cached packages.
- **unset** (local, Dependabot, GitHub Actions) → `google()` + `mavenCentral()` + `gradlePluginPortal()` for plugins, `google()` + `mavenCentral()` for deps. No credentials needed.

Test the CI path locally: `$env:RunningOnCI='true'` (PowerShell) or `RunningOnCI=true ...` (bash).

## When CI fails 401 on a Dependabot bump

The new package isn't cached in the feed yet. One-time setup, then ingest:

1. `iex "& { $(irm https://aka.ms/install-artifacts-credprovider.ps1) }"` (or the `.sh` equivalent)
2. `$env:RunningOnCI='true'; ./build-tools/gradle/gradlew.bat --project-dir src/<project> build` — sign in via the device-flow prompt; the feed proxies + caches the package.
3. Re-run CI on the Dependabot PR. No PR edit needed.

The credprovider plugin is a no-op when no AzDO repos are configured (i.e. local builds without `RunningOnCI`).

## Don'ts

- Don't hard-code Maven repo URLs in `build.gradle` / `settings.gradle`; use the shared file.
- Don't wrap `plugins {}` in `if (...)` — Gradle rejects it.
- Don't use modern `plugins { id 'com.android.application' version '...' }` DSL without confirming the plugin is in `dotnet-public-maven`; prefer `buildscript { ... } / apply plugin: '...'` when in doubt.
23 changes: 23 additions & 0 deletions eng/gradle/dependency-repositories.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Shared Maven repository list for project DEPENDENCY resolution
// (dependencyResolutionManagement.repositories) across every settings.gradle
// in this repo. See plugin-repositories.gradle for plugin resolution.
//
// Switches on RunningOnCI for the same CFSClean reasons described there.
// AzureArtifacts is intentionally NOT included here — it only hosts the
// credprovider plugin, so listing it in this scope would add a 404 round-trip
// to every dependency lookup.

repositories {
// AzDO uppercases pipeline variables when exporting them as env vars on
// Linux/macOS agents, so check both spellings.
def runningOnCI = System.getenv('RunningOnCI') ?: System.getenv('RUNNINGONCI')
if (runningOnCI == 'true') {
maven {
url = 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-maven/maven/v1'
name = 'dotnet-public-maven'
}
} else {
google()
mavenCentral()
}
}
68 changes: 68 additions & 0 deletions eng/gradle/plugin-repositories.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Shared Maven repository list for PLUGIN resolution (pluginManagement.repositories)
// across every settings.gradle in this repo. See plugin-repositories.gradle's
// sibling, dependency-repositories.gradle, for project dependency resolution.
//
// In our Azure DevOps CI pipeline (RunningOnCI=true), plugins resolve through
// the dnceng Azure Artifacts feed (dotnet-public-maven) for CFSClean network
// isolation compliance (https://aka.ms/1es/netiso/CFS). Locally and from
// GitHub Actions (e.g. Dependabot), the standard Gradle Plugin Portal is used.
//
// AzureArtifacts (anonymous public feed) is always included because every
// settings.gradle loads the artifacts-credprovider plugin from there.
//
// The dnceng feed proxies public sources. Once any package has been pulled
// through the feed (an authenticated request), it is cached and anonymous
// reads work forever after. CI therefore does NOT need credentials — it just
// reads anonymously from packages already cached in the feed.
//
// =================== TESTING / INGESTING LOCALLY ===================
//
// To exercise the CI code path locally (or to ingest a new package that
// Dependabot brought in but isn't yet cached in the feed):
//
// 1. Install the Azure Artifacts credential provider (one-time):
//
// PowerShell: iex "& { $(irm https://aka.ms/install-artifacts-credprovider.ps1) }"
// bash: wget -qO- https://aka.ms/install-artifacts-credprovider.sh | bash
//
// 2. Flip the switch and run the gradle build that needs the package:
//
// PowerShell: $env:RunningOnCI='true'; ./build-tools/gradle/gradlew.bat --project-dir src/r8 build
// bash: RunningOnCI=true ./build-tools/gradle/gradlew --project-dir src/r8 build
//
// On first authenticated request, you'll get a device-flow login prompt
// pointing at https://aka.ms/devicelogin — sign in with your Microsoft
// account. The credprovider caches the token; the feed caches the
// package; future CI runs read it anonymously and pass.
//
// =================== WORKFLOW FOR DEPENDABOT PRs ===================
//
// 1. Dependabot opens a PR bumping a Gradle dep (uses public repos, so it
// always sees the latest upstream version).
// 2. CI runs with RunningOnCI=true, hits the feed, and fails with 401 if
// the new package version isn't ingested yet.
// 3. A maintainer follows the steps above to ingest the package, then
// re-runs CI. No PR edit is required.

repositories {
// Anonymous public Azure Artifacts feed that hosts the
// artifacts-credprovider Gradle plugin (loaded by every settings.gradle).
maven {
url = 'https://pkgs.dev.azure.com/artifacts-public/PublicTools/_packaging/AzureArtifacts/maven/v1'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 💡 Performance — This AzureArtifacts feed is declared first and unconditionally, so the shared script injects it into dependencyResolutionManagement too. It only hosts the artifacts-credprovider plugin, yet now every project-dependency lookup (manifest-merger, r8, and their transitives) queries it first and 404s before falling through to google()/mavenCentral() (or dotnet-public-maven in CI) — an extra round-trip per dependency on every build, including local and Dependabot. Since the credprovider is a plugin, consider scoping this feed to pluginManagement only. Functionally harmless (as the comment notes); this is purely an optimization.

Rule: Don't inject a repository into scopes that never resolve from it

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Confirmed and fixed. Split the shared file: eng/gradle/plugin-repositories.gradle still includes the AzureArtifacts feed (the credprovider plugin lives there), and the new eng/gradle/dependency-repositories.gradle omits it so project-dependency lookups skip that 404 round-trip. Each settings.gradle now applies the right file to each scope.

name = 'AzureArtifacts'
}

// AzDO uppercases pipeline variables when exporting them as env vars on
// Linux/macOS agents, so check both spellings.
def runningOnCI = System.getenv('RunningOnCI') ?: System.getenv('RUNNINGONCI')
if (runningOnCI == 'true') {
maven {
url = 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-maven/maven/v1'
name = 'dotnet-public-maven'
}
} else {
google()
mavenCentral()
gradlePluginPortal()
}
}
7 changes: 0 additions & 7 deletions src/manifestmerger/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,6 @@ java {
targetCompatibility = ext.javaTargetVer
}

repositories {
maven { url 'https://maven.google.com' }
mavenCentral()
maven { url 'https://kotlin.bintray.com/kotlinx' }
jcenter()
}

dependencies {
// https://mvnrepository.com/artifact/com.android.tools.build/manifest-merger
implementation 'com.android.tools.build:manifest-merger:32.2.1'
Expand Down
13 changes: 13 additions & 0 deletions src/manifestmerger/settings.gradle
Original file line number Diff line number Diff line change
@@ -1 +1,14 @@
// See: eng/gradle/plugin-repositories.gradle, eng/gradle/dependency-repositories.gradle
pluginManagement {
apply from: "${rootDir}/../../eng/gradle/plugin-repositories.gradle", to: pluginManagement
}

plugins {
id 'com.microsoft.azure.artifacts.credprovider' version '1.1.1'
}

dependencyResolutionManagement {
apply from: "${rootDir}/../../eng/gradle/dependency-repositories.gradle", to: dependencyResolutionManagement
}

rootProject.name = 'manifestmerger'
5 changes: 0 additions & 5 deletions src/proguard-android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@ plugins {
id 'com.android.application' version '8.7.0'
}

repositories {
google()
mavenCentral()
}

android {
namespace 'com.microsoft.proguard.android'
// Setting the minimum we support at the moment, might not matter
Expand Down
16 changes: 11 additions & 5 deletions src/proguard-android/settings.gradle
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
// See: eng/gradle/plugin-repositories.gradle, eng/gradle/dependency-repositories.gradle
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
apply from: "${rootDir}/../../eng/gradle/plugin-repositories.gradle", to: pluginManagement
}

plugins {
id 'com.microsoft.azure.artifacts.credprovider' version '1.1.1'
}

dependencyResolutionManagement {
apply from: "${rootDir}/../../eng/gradle/dependency-repositories.gradle", to: dependencyResolutionManagement
}

rootProject.name = 'proguard-android'
5 changes: 0 additions & 5 deletions src/r8/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,6 @@ java {
targetCompatibility = ext.javaTargetVer
}

repositories {
google()
mavenCentral()
}

dependencies {
implementation 'com.android.tools:r8:9.1.31'
}
Expand Down
14 changes: 14 additions & 0 deletions src/r8/settings.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// See: eng/gradle/plugin-repositories.gradle, eng/gradle/dependency-repositories.gradle
pluginManagement {
apply from: "${rootDir}/../../eng/gradle/plugin-repositories.gradle", to: pluginManagement
}

plugins {
id 'com.microsoft.azure.artifacts.credprovider' version '1.1.1'
}

dependencyResolutionManagement {
apply from: "${rootDir}/../../eng/gradle/dependency-repositories.gradle", to: dependencyResolutionManagement
}

rootProject.name = 'r8'