-
Notifications
You must be signed in to change notification settings - Fork 577
[release/10.0.1xx] Route Gradle dependencies through dotnet-public-maven #12397
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
jonathanpeppers
wants to merge
6
commits into
release/10.0.1xx
Choose a base branch
from
jonathanpeppers-public-maven-backport
base: release/10.0.1xx
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
358a156
Centralize Gradle Maven repositories via shared eng/gradle/repositori…
jonathanpeppers c5b89a1
Bump com.android.application from 8.7.0 to 9.2.1 in /src/proguard-and…
dependabot[bot] 6dccd76
[build] Restrict Maven authentication to mirror seeding (#12055)
jonathanpeppers cbda5f4
[tests] Route Maven and Gradle resolution through the dnceng mirror (…
jonathanpeppers 2e3c7ac
[tests] Route XASdk Maven dependency through public feed (#12368)
jonathanpeppers 830e1a0
[release/10.0.1xx] Preserve Gradle 8 compatibility
jonathanpeppers File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| --- | ||
| applyTo: "**/*.gradle,**/*.gradle.kts" | ||
| --- | ||
|
|
||
| # 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 | ||
| // See: eng/gradle/plugin-repositories.gradle, eng/gradle/dependency-repositories.gradle | ||
| pluginManagement { | ||
| apply from: "${rootDir}/../../eng/gradle/plugin-repositories.gradle", to: pluginManagement | ||
| } | ||
| dependencyResolutionManagement { | ||
| apply from: "${rootDir}/../../eng/gradle/dependency-repositories.gradle", to: dependencyResolutionManagement | ||
| } | ||
| rootProject.name = '<project>' | ||
| ``` | ||
|
|
||
| Adjust the `../..` depth to reach the repo root from that project; it is not | ||
| always two levels (e.g. `external/Java.Interop/tools/java-source-utils` uses | ||
| four). | ||
|
|
||
| Kotlin DSL (`settings.gradle.kts`) applies the same two Groovy files, but passes | ||
| the receiver as `to = this`: | ||
|
|
||
| ```kotlin | ||
| // See: eng/gradle/plugin-repositories.gradle, eng/gradle/dependency-repositories.gradle | ||
| pluginManagement { | ||
| apply(from = "$rootDir/../../eng/gradle/plugin-repositories.gradle", to = this) | ||
| } | ||
| dependencyResolutionManagement { | ||
| apply(from = "$rootDir/../../eng/gradle/dependency-repositories.gradle", to = this) | ||
| } | ||
| rootProject.name = "<project>" | ||
| ``` | ||
|
|
||
| `build.gradle` files must not declare their own `repositories { ... }`. | ||
|
|
||
| ## CI vs local | ||
|
|
||
| Both files switch on `System.getenv('RUNNINGONCI')`. Azure DevOps exports the | ||
| `RunningOnCI` pipeline variable under this normalized environment-variable name. | ||
|
|
||
| - **`RUNNINGONCI=true`** (Azure DevOps, sourced from `RunningOnCI` 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. | ||
|
|
||
| CI reads cached packages from the mirror anonymously. `mirror-dependencies.ps1` | ||
| runs the same anonymous Gradle resolution, then seeds each missing URL with an | ||
| authenticated HTTP request until the build succeeds. | ||
|
|
||
| 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 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. | ||
|
|
||
| Use the helper script — it runs the build, parses any 401 URLs out of the log, re-fetches each one with an Azure DevOps OAuth token using Basic authentication (so the feed mirrors it), and loops until the build succeeds: | ||
|
|
||
| ```powershell | ||
| az login # one-time, corp account with MFA satisfied | ||
|
|
||
| pwsh ./eng/gradle/mirror-dependencies.ps1 ` | ||
| -ProjectDir <path-to-failing-gradle-project> ` | ||
| -Task <gradle-task-CI-runs> ` | ||
| -AndroidHome <path-to-Android-SDK> # required for any com.android.* project | ||
| ``` | ||
|
|
||
| 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 it succeeds, just re-run the failed CI job. No PR edits needed — the packages are now anonymous-readable forever. | ||
|
|
||
| Tests that resolve Maven files without Gradle can seed coordinates directly: | ||
|
|
||
| ```powershell | ||
| pwsh ./eng/gradle/mirror-dependencies.ps1 ` | ||
| -MavenArtifact 'androidx.core:core:1.12.0' | ||
| ``` | ||
|
|
||
| This attempts the coordinate's POM, JAR, AAR, and Gradle module metadata. Append | ||
| the exact filename as a fourth segment for a nonstandard payload. | ||
|
|
||
| ## Tests | ||
|
|
||
| Tests must not reach the public internet on CI; everything routes through the | ||
| mirror. Two mechanisms in `Xamarin.ProjectTools` handle this, and both apply | ||
| unconditionally — local runs hit the same URLs as CI, so a package the mirror | ||
| lacks fails everywhere instead of only on CI: | ||
|
|
||
| - **Generated Gradle projects** — `AndroidGradleProject` writes a | ||
| `settings.gradle.kts` that applies the same two shared config files by | ||
| absolute path, and copies the repository wrapper from `build-tools/gradle` | ||
| instead of running `gradle init`. Don't reintroduce `google()` / | ||
| `mavenCentral()` into generated projects, and don't let a generated project | ||
| download its own Gradle distribution on CI. | ||
| - **Non-Gradle Maven downloads** — use `TestEnvironment.DotNetPublicMaven` as | ||
| the base URL, both for `WebContent` on a `BuildItem` and for `Repository` | ||
| metadata on an `<AndroidMavenLibrary>`. Don't write a `repo1.maven.org` or | ||
| `maven.google.com` URL into a test, and don't use the `"Central"` / `"Google"` | ||
| shorthands there — those are covered without network by | ||
| `MavenDownloadTests.KnownRepositoryShorthand`. | ||
|
|
||
| When a test needs a coordinate the feed hasn't cached, seed it with | ||
| `-MavenArtifact` above rather than pointing the test at a public repository. | ||
|
|
||
| ## Don'ts | ||
|
|
||
| - Don't hard-code Maven repo URLs in `build.gradle` / `settings.gradle`; use the shared file. | ||
| - 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. | ||
| - Don't add a Gradle credential provider or any authenticated repository to a | ||
| build. CI resolves anonymously; authentication belongs only in | ||
| `mirror-dependencies.ps1`, which seeds the feed over plain HTTP. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| // 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. | ||
|
|
||
| repositories { | ||
| if (System.getenv('RUNNINGONCI') == 'true') { | ||
| maven { | ||
| url = 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-maven/maven/v1' | ||
| name = 'dotnet-public-maven' | ||
| } | ||
| } else { | ||
| google() | ||
| mavenCentral() | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,224 @@ | ||
| #!/usr/bin/env pwsh | ||
| <# | ||
| .SYNOPSIS | ||
| Mirrors Gradle dependencies or explicit Maven artifacts 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/HEAD' URLs out of the build log. | ||
| 3. Re-fetch each failing URL with an Azure DevOps OAuth token using Basic | ||
| authentication (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 MavenArtifact | ||
| Maven coordinates to mirror directly, for tests that do not use Gradle. | ||
| Each value is group:artifact:version, which attempts the POM, JAR, AAR, and | ||
| Gradle module metadata files. Append an exact filename as a fourth segment | ||
| when a test requests a nonstandard payload. | ||
|
|
||
| .PARAMETER GradleWrapper | ||
| Optional path to the Gradle wrapper used by CI for this project, relative | ||
| to the repository root or absolute. Defaults to build-tools/gradle/gradlew. | ||
| Use this when a subproject has its own wrapper so Gradle resolves the same | ||
| dependency variants that CI requests. | ||
|
|
||
| .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 | ||
|
|
||
| .EXAMPLE | ||
| pwsh ./eng/gradle/mirror-dependencies.ps1 ` | ||
| -ProjectDir external/Java.Interop/tests/Xamarin.Android.Tools.Bytecode-Tests/kotlin-gradle ` | ||
| -Task classes ` | ||
| -GradleWrapper external/Java.Interop/build-tools/gradle/gradlew.bat | ||
|
|
||
| .EXAMPLE | ||
| pwsh ./eng/gradle/mirror-dependencies.ps1 ` | ||
| -MavenArtifact 'androidx.core:core:1.12.0', ` | ||
| 'com.facebook.react:react-android:0.76.1:react-android-0.76.1.module' | ||
| #> | ||
| [CmdletBinding(DefaultParameterSetName='Gradle')] | ||
| param( | ||
| [Parameter(Mandatory=$true, ParameterSetName='Gradle')] | ||
| [string] $ProjectDir = '.', | ||
|
|
||
| [Parameter(Mandatory=$true, ParameterSetName='Gradle')] | ||
| [string] $Task, | ||
|
|
||
| [Parameter(Mandatory=$true, ParameterSetName='MavenArtifact')] | ||
| [string[]] $MavenArtifact, | ||
|
|
||
| [Parameter(ParameterSetName='Gradle')] | ||
| [string] $GradleWrapper, | ||
|
|
||
| [Parameter(ParameterSetName='Gradle')] | ||
| [string] $AndroidHome = $env:ANDROID_HOME, | ||
|
|
||
| [Parameter(ParameterSetName='Gradle')] | ||
| [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 | ||
| $defaultGradleWrapper = if ($IsWindows -or $env:OS -eq 'Windows_NT') { | ||
| 'build-tools/gradle/gradlew.bat' | ||
| } else { | ||
| 'build-tools/gradle/gradlew' | ||
| } | ||
| $gradleWrapperPath = if ([string]::IsNullOrEmpty($GradleWrapper)) { | ||
| Join-Path $repoRoot $defaultGradleWrapper | ||
| } elseif ([IO.Path]::IsPathRooted($GradleWrapper)) { | ||
| $GradleWrapper | ||
| } else { | ||
| Join-Path $repoRoot $GradleWrapper | ||
| } | ||
| $gradlew = Resolve-Path $gradleWrapperPath -ErrorAction Stop | Select-Object -ExpandProperty Path | ||
|
|
||
| # 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|HEAD) '(https://pkgs\.dev\.azure\.com/dnceng/[^']+)'" -AllMatches | | ||
| ForEach-Object { $_.Matches } | | ||
| ForEach-Object { $_.Groups[1].Value } | | ||
| Sort-Object -Unique | ||
| if ($urls.Count -eq 0) { return 0 } | ||
| $token = Get-AzDevOpsToken | ||
| $basicCredential = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$token")) | ||
| $headers = @{ Authorization = "Basic $basicCredential" } | ||
| $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 | ||
| } | ||
|
|
||
| function Get-MavenArtifactUrls($artifacts) { | ||
| $feedBaseUrl = 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-maven/maven/v1' | ||
| foreach ($artifact in $artifacts) { | ||
| $parts = $artifact.Split(':', 4) | ||
| if ($parts.Count -lt 3) { | ||
| throw "Invalid Maven artifact '$artifact'. Expected group:artifact:version[:filename]." | ||
| } | ||
| $group = $parts[0].Replace('.', '/') | ||
| $name = $parts[1] | ||
| $version = $parts[2] | ||
| $filenames = if ($parts.Count -eq 4) { | ||
| @($parts[3]) | ||
| } else { | ||
| @( | ||
| "$name-$version.pom" | ||
| "$name-$version.jar" | ||
| "$name-$version.aar" | ||
| "$name-$version.module" | ||
| ) | ||
| } | ||
| foreach ($filename in $filenames) { | ||
| "$feedBaseUrl/$group/$name/$version/$filename" | ||
| } | ||
| } | ||
| } | ||
|
|
||
| # Verify az is available and authenticated up front so we fail fast. | ||
| Get-AzDevOpsToken | Out-Null | ||
|
|
||
| if ($PSCmdlet.ParameterSetName -eq 'MavenArtifact') { | ||
| Write-Host "Mirroring Maven artifacts directly:" | ||
| $MavenArtifact | ForEach-Object { Write-Host " $_" } | ||
| $log = Join-Path ([IO.Path]::GetTempPath()) 'maven-artifact-mirror.log' | ||
| try { | ||
| Get-MavenArtifactUrls $MavenArtifact | | ||
| ForEach-Object { "Could not GET '$_'" } | | ||
| Set-Content $log | ||
| Invoke-Mirror $log | Out-Null | ||
| } | ||
| finally { | ||
| Remove-Item $log -ErrorAction SilentlyContinue | ||
| } | ||
| return | ||
| } | ||
|
|
||
| Write-Host "Repo root: $repoRoot" | ||
| Write-Host "Project: $projectDirAbs" | ||
| Write-Host "Task: $Task" | ||
| Write-Host "Gradle: $gradlew" | ||
| if ($AndroidHome) { Write-Host "ANDROID_HOME: $AndroidHome" } | ||
|
|
||
| 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 | ||
| if (Select-String -Path $log -Pattern 'BUILD SUCCESSFUL' -SimpleMatch -Quiet) { | ||
| 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 | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.