From 10ed23d6d13078c9348edea9dd522efbcf5e6fe4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 11:50:56 +0000 Subject: [PATCH 01/11] Bump com.android.application in /src/proguard-android Bumps com.android.application from 8.7.0 to 9.2.1. --- updated-dependencies: - dependency-name: com.android.application dependency-version: 9.2.1 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- src/proguard-android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/proguard-android/build.gradle b/src/proguard-android/build.gradle index b168ffa79e6..a2d50b9a10c 100644 --- a/src/proguard-android/build.gradle +++ b/src/proguard-android/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'com.android.application' version '8.7.0' + id 'com.android.application' version '9.2.1' } repositories { From 17fb72c0b46043531e483dce6dc87beb5f1a5c7b Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 23 Jun 2026 08:32:44 -0500 Subject: [PATCH 02/11] Bump Gradle wrapper to 9.4.1 The com.android.application 9.2.1 plugin requires Gradle 9.4.1+. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- build-tools/gradle/gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-tools/gradle/gradle/wrapper/gradle-wrapper.properties b/build-tools/gradle/gradle/wrapper/gradle-wrapper.properties index d6e308a6378..221c4f98228 100644 --- a/build-tools/gradle/gradle/wrapper/gradle-wrapper.properties +++ b/build-tools/gradle/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From a44f188512cc2c69fea29409b94d44c44de878ec Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 23 Jun 2026 08:51:50 -0500 Subject: [PATCH 03/11] Remove jcenter() from manifestmerger build.gradle jcenter() was removed in Gradle 9 and causes: Could not find method jcenter() for arguments [] on repository container Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/manifestmerger/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/src/manifestmerger/build.gradle b/src/manifestmerger/build.gradle index 61a060fa37d..125ad8f40d5 100644 --- a/src/manifestmerger/build.gradle +++ b/src/manifestmerger/build.gradle @@ -14,7 +14,6 @@ repositories { maven { url 'https://maven.google.com' } mavenCentral() maven { url 'https://kotlin.bintray.com/kotlinx' } - jcenter() } dependencies { From a2d422f6087f822a108f2ddf6c3ec79dc49d8b2a Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 24 Jun 2026 10:54:17 -0500 Subject: [PATCH 04/11] Document NUGET_CREDENTIALPROVIDER_VSTS_TOKENTYPE=SelfDescribing for mirroring The default Compact session token issued by the Azure Artifacts credential provider is rejected by the dnceng dotnet-public-maven feed when ingesting plugin markers (e.g. the AGP plugin from `pluginManagement`). Setting `NUGET_CREDENTIALPROVIDER_VSTS_TOKENTYPE=SelfDescribing` forces a broader-scope token that the feed accepts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/instructions/gradle.instructions.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/instructions/gradle.instructions.md b/.github/instructions/gradle.instructions.md index 7233969afe9..613a4edfead 100644 --- a/.github/instructions/gradle.instructions.md +++ b/.github/instructions/gradle.instructions.md @@ -37,9 +37,19 @@ Test the CI path locally: `$env:RunningOnCI='true'` (PowerShell) or `RunningOnCI 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/ build` — sign in via the device-flow prompt; the feed proxies + caches the package. +2. ```powershell + $env:RunningOnCI='true' + $env:NUGET_CREDENTIALPROVIDER_VSTS_TOKENTYPE='SelfDescribing' + ./build-tools/gradle/gradlew.bat --project-dir src/ build + ``` + Sign in via the popup; the feed proxies + caches the package. `SelfDescribing` is required — the default `Compact` token is rejected by the feed when ingesting plugin markers (e.g. AGP plugin from `pluginManagement`). 3. Re-run CI on the Dependabot PR. No PR edit needed. +If the popup never appears or auth keeps cancelling, clear the cached session token and try again: +```powershell +Remove-Item "$env:LOCALAPPDATA\MicrosoftCredentialProvider\SessionTokenCache.dat" -ErrorAction SilentlyContinue +``` + The credprovider plugin is a no-op when no AzDO repos are configured (i.e. local builds without `RunningOnCI`). ## Don'ts From aceb5c6b39bc6c7358bdb388292a7cd6cdee2086 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 24 Jun 2026 13:38:43 -0500 Subject: [PATCH 05/11] Bump AGP to 9.2.1 in LibraryProjectZip test fixture After bumping the shared build-tools/gradle wrapper to 9.4.1, the JavaLib gradle fixture under tests/CodeGen-Binding/Xamarin.Android. LibraryProjectZip-LibBinding/ failed with: A problem occurred evaluating project ':library'. > 'org.gradle.api.artifacts.Dependency org.gradle.api.artifacts.dsl.DependencyHandler.module(java.lang.Object)' That API was removed in Gradle 9 and is called by AGP 7.4.2. Bump the fixture to AGP 9.2.1 (the same version proguard-android now uses, which is already mirrored in the dnceng dotnet-public-maven feed) and apply the small set of changes AGP 9 requires: * Use `namespace` in build.gradle instead of `package` in the manifest. * Replace removed `compileSdkVersion`/`minSdkVersion` DSL with `compileSdk`/`minSdk` and bump to API 35 / API 21. * Drop `testInstrumentationRunner` referencing the removed `android.support.test` package. * Use `proguard-android-optimize.txt` instead of the no-longer supported `proguard-android.txt`. * Replace `rootProject.buildDir` with `rootProject.layout.buildDirectory`. Repositories now use the dnceng feed when `RunningOnCI` is set, to match the centralized scheme used by src/* projects. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../java/JavaLib/build.gradle | 26 ++++++++++++++----- .../java/JavaLib/library/build.gradle | 12 ++++----- .../library/src/main/AndroidManifest.xml | 2 +- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/build.gradle b/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/build.gradle index 8a2011238e7..a5cd07ffd42 100644 --- a/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/build.gradle +++ b/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/build.gradle @@ -2,11 +2,18 @@ buildscript { repositories { - google() - mavenCentral() + if (System.getenv('RunningOnCI') || System.getenv('RUNNINGONCI')) { + maven { + url 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-maven/maven/v1' + name 'dotnet-public-maven' + } + } else { + google() + mavenCentral() + } } dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' + classpath 'com.android.tools.build:gradle:9.2.1' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -15,12 +22,19 @@ buildscript { allprojects { repositories { - google() - mavenCentral() + if (System.getenv('RunningOnCI') || System.getenv('RUNNINGONCI')) { + maven { + url 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-maven/maven/v1' + name 'dotnet-public-maven' + } + } else { + google() + mavenCentral() + } } } task clean(type: Delete) { - delete rootProject.buildDir + delete rootProject.layout.buildDirectory } diff --git a/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/library/build.gradle b/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/library/build.gradle index b5cb8374ce7..56ef97eb1fe 100644 --- a/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/library/build.gradle +++ b/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/library/build.gradle @@ -1,21 +1,19 @@ apply plugin: 'com.android.library' android { - compileSdkVersion 25 + namespace 'com.example.javalib' + compileSdk 35 defaultConfig { - minSdkVersion 19 - targetSdkVersion 25 + minSdk 21 + targetSdk 35 versionCode 1 versionName "1.0" - - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - } buildTypes { release { minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } } diff --git a/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/library/src/main/AndroidManifest.xml b/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/library/src/main/AndroidManifest.xml index a2fb60b67f5..0a0938ae37e 100644 --- a/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/library/src/main/AndroidManifest.xml +++ b/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/library/src/main/AndroidManifest.xml @@ -1,3 +1,3 @@ - + From 6ef9fbc282a2e9138c396bcad7232f5ab9c7c475 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 24 Jun 2026 13:42:03 -0500 Subject: [PATCH 06/11] Use shared eng/gradle/*-repositories.gradle in JavaLib fixture Follow the convention from .github/instructions/gradle.instructions.md: all gradle projects share eng/gradle/plugin-repositories.gradle and eng/gradle/dependency-repositories.gradle, applied via settings.gradle. Replace the inlined buildscript/allprojects repositories blocks with `apply from:` of the shared files, switch to the modern plugins {} DSL, and load the credprovider plugin in settings.gradle just like the src/* projects do. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../java/JavaLib/build.gradle | 34 ++----------------- .../java/JavaLib/library/build.gradle | 4 ++- .../java/JavaLib/settings.gradle | 14 ++++++++ 3 files changed, 19 insertions(+), 33 deletions(-) diff --git a/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/build.gradle b/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/build.gradle index a5cd07ffd42..3e75fba8418 100644 --- a/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/build.gradle +++ b/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/build.gradle @@ -1,37 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. -buildscript { - repositories { - if (System.getenv('RunningOnCI') || System.getenv('RUNNINGONCI')) { - maven { - url 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-maven/maven/v1' - name 'dotnet-public-maven' - } - } else { - google() - mavenCentral() - } - } - dependencies { - classpath 'com.android.tools.build:gradle:9.2.1' - - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle files - } -} - -allprojects { - repositories { - if (System.getenv('RunningOnCI') || System.getenv('RUNNINGONCI')) { - maven { - url 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-maven/maven/v1' - name 'dotnet-public-maven' - } - } else { - google() - mavenCentral() - } - } +plugins { + id 'com.android.library' version '9.2.1' apply false } task clean(type: Delete) { diff --git a/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/library/build.gradle b/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/library/build.gradle index 56ef97eb1fe..15b216479f5 100644 --- a/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/library/build.gradle +++ b/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/library/build.gradle @@ -1,4 +1,6 @@ -apply plugin: 'com.android.library' +plugins { + id 'com.android.library' +} android { namespace 'com.example.javalib' diff --git a/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/settings.gradle b/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/settings.gradle index d8f14a134bf..571862a24e5 100644 --- a/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/settings.gradle +++ b/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib/settings.gradle @@ -1 +1,15 @@ +// 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 = 'JavaLib' include ':library' From ac8f1584275707330c22ceec081135f24bbb781f Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 24 Jun 2026 13:42:20 -0500 Subject: [PATCH 07/11] Have Dependabot track the JavaLib gradle test fixture So future AGP releases get auto-bumped here too, instead of silently diverging from src/proguard-android and breaking with the next Gradle wrapper upgrade. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/dependabot.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 747a4146e28..e55b60ded30 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -45,6 +45,7 @@ updates: - "/src/r8" - "/src/manifestmerger" - "/src/proguard-android" + - "/tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib" schedule: interval: "weekly" - package-ecosystem: "gitsubmodule" From eb24c843653a69dff27d6ef1b6410909f3cc0e77 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 25 Jun 2026 12:56:05 -0500 Subject: [PATCH 08/11] Document az-bearer-token fallback for credprovider 401s When the artifacts-credprovider's VssSessionToken gets 401-rejected by the dnceng feed despite being properly attached (observed with com.android.tools.lint:lint-gradle and its transitive deps for AGP 9.x), fall back to using an Azure DevOps OAuth token from `az account get-access-token` directly via the Authorization Bearer header. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/instructions/gradle.instructions.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/instructions/gradle.instructions.md b/.github/instructions/gradle.instructions.md index 613a4edfead..413fcb412dd 100644 --- a/.github/instructions/gradle.instructions.md +++ b/.github/instructions/gradle.instructions.md @@ -50,6 +50,25 @@ If the popup never appears or auth keeps cancelling, clear the cached session to Remove-Item "$env:LOCALAPPDATA\MicrosoftCredentialProvider\SessionTokenCache.dat" -ErrorAction SilentlyContinue ``` +### Fallback: mirror via `az` bearer token + +Some packages (observed with `com.android.tools.lint:lint-gradle` and its transitive deps) get 401-rejected even with a successfully attached credprovider `VssSessionToken`. When that happens, mirror the failing URLs directly using an Azure DevOps OAuth token from `az`: + +```powershell +$token = az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv +$h = @{ Authorization = "Bearer $token" } + +# Pull each failing URL from the gradle error output and re-request it with the bearer token: +$urls = Select-String -Path -Pattern "Could not GET 'https://pkgs\.dev\.azure\.com/dnceng/[^']+'" -AllMatches | + % { $_.Matches } | % { $_.Value -replace "^Could not GET '","" -replace "'$","" } | Sort-Object -Unique +foreach ($u in $urls) { Invoke-WebRequest -Uri $u -Headers $h -SkipHttpErrorCheck | % StatusCode } + +# Repeat the gradle build to discover the next layer of transitive deps; the feed will +# return 401 for each new uncached package. Loop build → mirror → build until clean. +``` + +The resource id `499b84ac-1321-427f-aa17-267ca6975798` is Azure DevOps. After successful ingestion, the package is anonymous-readable, so future CI runs pass without any auth. + The credprovider plugin is a no-op when no AzDO repos are configured (i.e. local builds without `RunningOnCI`). ## Don'ts From 1bd45d88b8235cad59fc1079fd7bb30920026609 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 25 Jun 2026 12:57:15 -0500 Subject: [PATCH 09/11] Restructure mirror-on-401 docs around az bearer token loop Make the proven flow primary: * az login + Authorization Bearer header is the reliable path; works for plugin markers and transitive deps the credprovider's VssSessionToken trips on. * Document the build -> mirror failed urls -> rebuild loop that converges in a handful of iterations as AGP's resolver walks the dep graph. * Note that ingestion must be triggered from the project that actually needs the new package -- a sibling project's build won't trigger a mirror for someone else's missing dep. * Demote the credprovider plugin path to a fallback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/instructions/gradle.instructions.md | 76 ++++++++++++++------- 1 file changed, 50 insertions(+), 26 deletions(-) diff --git a/.github/instructions/gradle.instructions.md b/.github/instructions/gradle.instructions.md index 413fcb412dd..9b4ee2fb32c 100644 --- a/.github/instructions/gradle.instructions.md +++ b/.github/instructions/gradle.instructions.md @@ -34,41 +34,65 @@ Test the CI path locally: `$env:RunningOnCI='true'` (PowerShell) or `RunningOnCI ## 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. ```powershell - $env:RunningOnCI='true' - $env:NUGET_CREDENTIALPROVIDER_VSTS_TOKENTYPE='SelfDescribing' - ./build-tools/gradle/gradlew.bat --project-dir src/ build - ``` - Sign in via the popup; the feed proxies + caches the package. `SelfDescribing` is required — the default `Compact` token is rejected by the feed when ingesting plugin markers (e.g. AGP plugin from `pluginManagement`). -3. Re-run CI on the Dependabot PR. No PR edit needed. - -If the popup never appears or auth keeps cancelling, clear the cached session token and try again: +The new package isn't cached in the dnceng `dotnet-public-maven` feed yet. The CFSClean-isolated CI agents only do anonymous reads, so someone has to authenticate once locally to make the feed pull the package (and all its transitive deps) from upstream. + +### Recommended: mirror via `az` bearer token + +This is the most reliable path. It uses your `az login` Azure DevOps OAuth token directly via an HTTP Bearer header, so it bypasses every credprovider/session-token edge case. + ```powershell -Remove-Item "$env:LOCALAPPDATA\MicrosoftCredentialProvider\SessionTokenCache.dat" -ErrorAction SilentlyContinue +# Make sure you're logged in (corp account, MFA-satisfied) +az login + +cd +$env:ANDROID_HOME = '' # e.g. D:\android-toolchain\sdk +$env:RunningOnCI = 'true' + +# Project that needs the new package — must be the SAME project, not a sibling. +# (A failing AGP transitive dep in /tests/.../JavaLib cannot be mirrored by +# running gradle in /src/manifestmerger; the feed wants the request to come +# from the configuration that actually needs it.) +cd +$projGradle = "..\..\..\..\..\build-tools\gradle\gradlew.bat" # adjust depth + +function Mirror-FailedUrls($logPath) { + $urls = Select-String -Path $logPath -Pattern "Could not GET 'https://pkgs\.dev\.azure\.com/dnceng/[^']+'" -AllMatches | + % { $_.Matches } | % { $_.Value -replace "^Could not GET '","" -replace "'$","" } | Sort-Object -Unique + if ($urls.Count -eq 0) { return 0 } + $token = az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv + $h = @{ Authorization = "Bearer $token" } + foreach ($u in $urls) { Invoke-WebRequest -Uri $u -Headers $h -SkipHttpErrorCheck | Out-Null } + Write-Host "Mirrored $($urls.Count) packages" + return $urls.Count +} + +# Loop: build → mirror everything that 401'd → build again. Maven resolves the +# graph breadth-first, so each iteration uncovers the next layer of transitive +# deps. Typically converges in 3-5 iterations. +for ($i = 1; $i -le 15; $i++) { + $log = "build-iter-$i.log" + & $projGradle --no-daemon --refresh-dependencies *>&1 | Tee-Object $log | Out-Null + if ((Get-Content $log -Tail 5) -match "BUILD SUCCESSFUL") { Write-Host "Done!"; break } + if ((Mirror-FailedUrls $log) -eq 0) { Get-Content $log -Tail 30; throw "Failed but no 401s — different problem" } +} ``` -### Fallback: mirror via `az` bearer token +The resource id `499b84ac-1321-427f-aa17-267ca6975798` is Azure DevOps. After successful ingestion each package is anonymous-readable, so future CI runs pass without any auth. -Some packages (observed with `com.android.tools.lint:lint-gradle` and its transitive deps) get 401-rejected even with a successfully attached credprovider `VssSessionToken`. When that happens, mirror the failing URLs directly using an Azure DevOps OAuth token from `az`: +After mirroring, no PR edit is needed — just re-run the failed CI job. -```powershell -$token = az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv -$h = @{ Authorization = "Bearer $token" } +### Older alternative: the artifacts-credprovider plugin -# Pull each failing URL from the gradle error output and re-request it with the bearer token: -$urls = Select-String -Path -Pattern "Could not GET 'https://pkgs\.dev\.azure\.com/dnceng/[^']+'" -AllMatches | - % { $_.Matches } | % { $_.Value -replace "^Could not GET '","" -replace "'$","" } | Sort-Object -Unique -foreach ($u in $urls) { Invoke-WebRequest -Uri $u -Headers $h -SkipHttpErrorCheck | % StatusCode } +This sometimes works for simple cases (e.g. a single plugin-marker pull) but has unresolved auth gaps for some packages — observed with `com.android.tools.lint:lint-gradle` and its transitive deps, which 401 even with a properly attached `VssSessionToken`. Prefer the `az` flow above; fall back to credprovider only if you can't get an `az login` working. -# Repeat the gradle build to discover the next layer of transitive deps; the feed will -# return 401 for each new uncached package. Loop build → mirror → build until clean. +```powershell +iex "& { $(irm https://aka.ms/install-artifacts-credprovider.ps1) }" # one-time +Remove-Item "$env:LOCALAPPDATA\MicrosoftCredentialProvider\SessionTokenCache.dat" -ErrorAction SilentlyContinue +$env:RunningOnCI = 'true' +$env:NUGET_CREDENTIALPROVIDER_VSTS_TOKENTYPE = 'SelfDescribing' # required for plugin markers +./build-tools/gradle/gradlew.bat --project-dir build --no-daemon ``` -The resource id `499b84ac-1321-427f-aa17-267ca6975798` is Azure DevOps. After successful ingestion, the package is anonymous-readable, so future CI runs pass without any auth. - The credprovider plugin is a no-op when no AzDO repos are configured (i.e. local builds without `RunningOnCI`). ## Don'ts From a3ec68f2fc250697d1f330207c9a8f842f20801b Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 25 Jun 2026 13:01:25 -0500 Subject: [PATCH 10/11] Add eng/gradle/mirror-dependencies.ps1 helper Wrap the build -> mirror failed URLs -> rebuild loop (which until now was documented as ~20 lines of inline PowerShell in .github/instructions/gradle.instructions.md) into a reusable cross-platform PowerShell script. The helper: * Reads the gradle build log for `Could not GET ''` lines. * Re-fetches each failing URL with an Azure DevOps OAuth bearer token from `az account get-access-token`, which causes the feed's upstream connector to pull the package and cache it for anonymous reads. * Loops until the build succeeds or no more 401s appear (capped at 15 iterations -- usually converges in 2-5). Replace the inline how-to in the instructions doc with a short pointer at this script. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/instructions/gradle.instructions.md | 52 ++----- eng/gradle/mirror-dependencies.ps1 | 142 ++++++++++++++++++++ 2 files changed, 153 insertions(+), 41 deletions(-) create mode 100644 eng/gradle/mirror-dependencies.ps1 diff --git a/.github/instructions/gradle.instructions.md b/.github/instructions/gradle.instructions.md index 9b4ee2fb32c..5f6dc8fa1e9 100644 --- a/.github/instructions/gradle.instructions.md +++ b/.github/instructions/gradle.instructions.md @@ -34,56 +34,26 @@ Test the CI path locally: `$env:RunningOnCI='true'` (PowerShell) or `RunningOnCI ## When CI fails 401 on a Dependabot bump -The new package isn't cached in the dnceng `dotnet-public-maven` feed yet. The CFSClean-isolated CI agents only do anonymous reads, so someone has to authenticate once locally to make the feed pull the package (and all its transitive deps) from upstream. +The new package isn't cached in the dnceng `dotnet-public-maven` feed yet. CI agents only do anonymous reads, so someone has to authenticate once locally to make the feed pull the package (and its transitive deps) from upstream. -### Recommended: mirror via `az` bearer token - -This is the most reliable path. It uses your `az login` Azure DevOps OAuth token directly via an HTTP Bearer header, so it bypasses every credprovider/session-token edge case. +Use the helper script — it runs the build, parses any 401 URLs out of the log, re-fetches each one with an Azure DevOps bearer token (so the feed mirrors it), and loops until the build succeeds: ```powershell -# Make sure you're logged in (corp account, MFA-satisfied) -az login - -cd -$env:ANDROID_HOME = '' # e.g. D:\android-toolchain\sdk -$env:RunningOnCI = 'true' - -# Project that needs the new package — must be the SAME project, not a sibling. -# (A failing AGP transitive dep in /tests/.../JavaLib cannot be mirrored by -# running gradle in /src/manifestmerger; the feed wants the request to come -# from the configuration that actually needs it.) -cd -$projGradle = "..\..\..\..\..\build-tools\gradle\gradlew.bat" # adjust depth - -function Mirror-FailedUrls($logPath) { - $urls = Select-String -Path $logPath -Pattern "Could not GET 'https://pkgs\.dev\.azure\.com/dnceng/[^']+'" -AllMatches | - % { $_.Matches } | % { $_.Value -replace "^Could not GET '","" -replace "'$","" } | Sort-Object -Unique - if ($urls.Count -eq 0) { return 0 } - $token = az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv - $h = @{ Authorization = "Bearer $token" } - foreach ($u in $urls) { Invoke-WebRequest -Uri $u -Headers $h -SkipHttpErrorCheck | Out-Null } - Write-Host "Mirrored $($urls.Count) packages" - return $urls.Count -} +az login # one-time, corp account with MFA satisfied -# Loop: build → mirror everything that 401'd → build again. Maven resolves the -# graph breadth-first, so each iteration uncovers the next layer of transitive -# deps. Typically converges in 3-5 iterations. -for ($i = 1; $i -le 15; $i++) { - $log = "build-iter-$i.log" - & $projGradle --no-daemon --refresh-dependencies *>&1 | Tee-Object $log | Out-Null - if ((Get-Content $log -Tail 5) -match "BUILD SUCCESSFUL") { Write-Host "Done!"; break } - if ((Mirror-FailedUrls $log) -eq 0) { Get-Content $log -Tail 30; throw "Failed but no 401s — different problem" } -} +pwsh ./eng/gradle/mirror-dependencies.ps1 ` + -ProjectDir ` + -Task ` + -AndroidHome # required for any com.android.* project ``` -The resource id `499b84ac-1321-427f-aa17-267ca6975798` is Azure DevOps. After successful ingestion each package is anonymous-readable, so future CI runs pass without any auth. +The mirror must run in the project that actually needs the new package — a sibling project's build won't trigger a mirror for someone else's deps. Typical convergence is 2-5 iterations as the resolver walks the dep graph breadth-first. -After mirroring, no PR edit is needed — just re-run the failed CI job. +After it succeeds, just re-run the failed CI job. No PR edits needed — the packages are now anonymous-readable forever. -### Older alternative: the artifacts-credprovider plugin +### Fallback: the artifacts-credprovider plugin -This sometimes works for simple cases (e.g. a single plugin-marker pull) but has unresolved auth gaps for some packages — observed with `com.android.tools.lint:lint-gradle` and its transitive deps, which 401 even with a properly attached `VssSessionToken`. Prefer the `az` flow above; fall back to credprovider only if you can't get an `az login` working. +If you can't `az login` (e.g. on a machine without the Azure CLI), the older `artifacts-credprovider` flow sometimes works for simple cases. It has unresolved auth gaps for some packages — observed with `com.android.tools.lint:lint-gradle` and its transitives, which 401 even with a properly attached `VssSessionToken` — so prefer the script above when you can. ```powershell iex "& { $(irm https://aka.ms/install-artifacts-credprovider.ps1) }" # one-time diff --git a/eng/gradle/mirror-dependencies.ps1 b/eng/gradle/mirror-dependencies.ps1 new file mode 100644 index 00000000000..48f21223a97 --- /dev/null +++ b/eng/gradle/mirror-dependencies.ps1 @@ -0,0 +1,142 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Mirrors a gradle project's dependencies into the dnceng dotnet-public-maven + Azure Artifacts feed so CI can resolve them anonymously. + +.DESCRIPTION + When Dependabot bumps a gradle dependency (or its transitive graph changes), + CI fails with 401 errors because the new package(s) haven't been pulled + from upstream into the dnceng feed yet. CI agents only do anonymous reads, + so a developer has to authenticate locally once to seed the feed. + + This script does that by running the requested gradle build in a loop: + 1. Run gradle with RunningOnCI=true so it points at the dnceng feed. + 2. Parse any 'Could not GET' URLs out of the build log. + 3. Re-fetch each failing URL with an Azure DevOps OAuth bearer token + (obtained via `az account get-access-token`). The feed's upstream + connector then pulls the package and caches it for anonymous reads. + 4. Repeat until the build succeeds or no more 401s appear. + + After the loop converges, no PR edits are needed — just re-run the failing + CI job, since the packages are now anonymous-readable. + +.PARAMETER ProjectDir + Path to the gradle project (the one containing the failing dependency). + Mirroring must run in the project that actually requires the package; + a sibling project's build won't trigger a mirror for someone else's deps. + +.PARAMETER Task + Gradle task(s) to run. Should be one that resolves the new dependency + graph (e.g. 'assembleDebug', 'build', 'extractProguardFiles'). + +.PARAMETER AndroidHome + Optional path to the Android SDK. Required when the gradle build needs it + (any project using the com.android.* plugins). Defaults to the value of + `$env:ANDROID_HOME` if set. + +.PARAMETER MaxIterations + Cap on build/mirror cycles. Default 15. Typical convergence is 2-5 + iterations as the resolver walks the dep graph breadth-first. + +.EXAMPLE + pwsh ./eng/gradle/mirror-dependencies.ps1 ` + -ProjectDir tests/CodeGen-Binding/Xamarin.Android.LibraryProjectZip-LibBinding/java/JavaLib ` + -Task assembleDebug ` + -AndroidHome D:\android-toolchain\sdk + +.EXAMPLE + pwsh ./eng/gradle/mirror-dependencies.ps1 -ProjectDir src/proguard-android -Task extractProguardFiles +#> +[CmdletBinding()] +param( + [Parameter(Mandatory=$true)] + [string] $ProjectDir, + + [Parameter(Mandatory=$true)] + [string] $Task, + + [string] $AndroidHome = $env:ANDROID_HOME, + + [int] $MaxIterations = 15 +) + +$ErrorActionPreference = 'Stop' +$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '../..') | Select-Object -ExpandProperty Path +$projectDirAbs = Resolve-Path (Join-Path $repoRoot $ProjectDir) -ErrorAction Stop | Select-Object -ExpandProperty Path +$gradlew = if ($IsWindows -or $env:OS -eq 'Windows_NT') { + Join-Path $repoRoot 'build-tools/gradle/gradlew.bat' +} else { + Join-Path $repoRoot 'build-tools/gradle/gradlew' +} +if (-not (Test-Path $gradlew)) { throw "gradlew not found at $gradlew" } + +# Azure DevOps resource id — same for every AzDO tenant. +$azDevOpsResource = '499b84ac-1321-427f-aa17-267ca6975798' + +function Get-AzDevOpsToken { + $token = az account get-access-token --resource $azDevOpsResource --query accessToken -o tsv 2>$null + if ([string]::IsNullOrEmpty($token)) { + throw "Could not get an Azure DevOps access token. Run 'az login' first." + } + return $token +} + +function Invoke-Mirror($logPath) { + $urls = Select-String -Path $logPath -Pattern "Could not GET 'https://pkgs\.dev\.azure\.com/dnceng/[^']+'" -AllMatches | + ForEach-Object { $_.Matches } | + ForEach-Object { $_.Value -replace "^Could not GET '", "" -replace "'$", "" } | + Sort-Object -Unique + if ($urls.Count -eq 0) { return 0 } + $token = Get-AzDevOpsToken + $headers = @{ Authorization = "Bearer $token" } + $ok = 0; $fail = 0 + foreach ($u in $urls) { + try { + $r = Invoke-WebRequest -Uri $u -Headers $headers -SkipHttpErrorCheck -ErrorAction Stop + if ($r.StatusCode -eq 200) { $ok++ } else { $fail++; Write-Host " $($r.StatusCode) $u" -ForegroundColor Yellow } + } catch { + $fail++ + Write-Host " ERR $u : $_" -ForegroundColor Yellow + } + } + Write-Host " -> mirrored OK=$ok, not-found=$fail (of $($urls.Count))" -ForegroundColor Cyan + return $urls.Count +} + +Write-Host "Repo root: $repoRoot" +Write-Host "Project: $projectDirAbs" +Write-Host "Task: $Task" +if ($AndroidHome) { Write-Host "ANDROID_HOME: $AndroidHome" } + +# Verify az is available and authenticated up front so we fail fast. +Get-AzDevOpsToken | Out-Null + +if ($AndroidHome) { $env:ANDROID_HOME = $AndroidHome } +$env:RunningOnCI = 'true' + +Push-Location $projectDirAbs +try { + for ($i = 1; $i -le $MaxIterations; $i++) { + Write-Host "`n=== iteration $i ===" -ForegroundColor Green + $log = Join-Path ([IO.Path]::GetTempPath()) "gradle-mirror-iter-$i.log" + & $gradlew $Task --no-daemon --refresh-dependencies *>&1 | Tee-Object -FilePath $log | Out-Null + $tail = Get-Content $log -Tail 5 + if ($tail -match 'BUILD SUCCESSFUL') { + Write-Host "`nBUILD SUCCESSFUL after $i iteration(s). The feed now has the packages CI needs." -ForegroundColor Green + return + } + $count = Invoke-Mirror $log + if ($count -eq 0) { + Write-Host "`nGradle failed but no 401s to mirror — see $log" -ForegroundColor Red + Get-Content $log -Tail 30 + exit 1 + } + } + Write-Host "`nExhausted $MaxIterations iterations without success. Last log:" -ForegroundColor Red + Get-Content $log -Tail 30 + exit 1 +} +finally { + Pop-Location +} From 88e2f3cb9ba8fbd87f77207b85d254824418cb43 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 25 Jun 2026 14:18:23 -0500 Subject: [PATCH 11/11] Remove credprovider fallback from gradle instructions The artifacts-credprovider flow has unresolved auth gaps for some packages (notably com.android.tools.lint:lint-gradle), so the az-bearer script is the only reliable path. Keeping the fallback documented just invites people to spin on it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/instructions/gradle.instructions.md | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/.github/instructions/gradle.instructions.md b/.github/instructions/gradle.instructions.md index 5f6dc8fa1e9..28629fe2e73 100644 --- a/.github/instructions/gradle.instructions.md +++ b/.github/instructions/gradle.instructions.md @@ -51,20 +51,6 @@ The mirror must run in the project that actually needs the new package — a sib After it succeeds, just re-run the failed CI job. No PR edits needed — the packages are now anonymous-readable forever. -### Fallback: the artifacts-credprovider plugin - -If you can't `az login` (e.g. on a machine without the Azure CLI), the older `artifacts-credprovider` flow sometimes works for simple cases. It has unresolved auth gaps for some packages — observed with `com.android.tools.lint:lint-gradle` and its transitives, which 401 even with a properly attached `VssSessionToken` — so prefer the script above when you can. - -```powershell -iex "& { $(irm https://aka.ms/install-artifacts-credprovider.ps1) }" # one-time -Remove-Item "$env:LOCALAPPDATA\MicrosoftCredentialProvider\SessionTokenCache.dat" -ErrorAction SilentlyContinue -$env:RunningOnCI = 'true' -$env:NUGET_CREDENTIALPROVIDER_VSTS_TOKENTYPE = 'SelfDescribing' # required for plugin markers -./build-tools/gradle/gradlew.bat --project-dir build --no-daemon -``` - -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.