Skip to content

Commit bf61568

Browse files
PureWeengithub-actions[bot]Copilotmmitche
authored
Use Azure Artifacts Maven feed for CFSClean network isolation compliance (#35089)
<!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ## Problem The official build pipeline (`dotnet-maui`, def 1095) fails because [CFSClean network isolation](#34540) blocks direct access to `repo.maven.apache.org`. This breaks two separate Gradle invocations: 1. **`src/Core/AndroidNative` build** — our own Gradle project 2. **`Microsoft.Android.Sdk.Bindings.Gradle.targets`** — Android SDK binding generator in `Core.csproj` Per [1ES CFS guidance](https://aka.ms/1es/netiso/CFS), the fix is to route all Maven dependency resolution through an Azure Artifacts feed with upstream sources. ## Fix ### Gradle configuration changes - **`settings.gradle`** — Replace `mavenCentral()`, `google()`, `gradlePluginPortal()` with the `dotnet-public-maven` Azure Artifacts feed. Add the [Azure Artifacts Gradle credential provider](https://pkgs.dev.azure.com/artifacts-public/PublicTools/_packaging/AzureArtifacts/maven/v1) plugin (v1.1.1) for local authentication. - **`build.gradle`** — Point `buildscript.repositories` to the same feed for AGP classpath resolution. - **`eng/init.gradle`** — Global Gradle init script that redirects any remaining Maven Central/Google Maven references (e.g. from `Microsoft.Android.Sdk.Bindings.Gradle.targets`) to the feed. Installed into `GRADLE_USER_HOME` by the pipeline. ### Pipeline changes - **`cache-gradle.yml`** — Copy `init.gradle` into `GRADLE_USER_HOME` **after** cache restore to prevent stale cached copies. Uses `$(GRADLE_USER_HOME)` variable for the destination path. ### Why the ingestion script (`eng/ingest-maven-deps.sh`) is needed The `dotnet-public-maven` feed proxies Maven Central, but new packages require an **authenticated first-time pull** to be saved. The Gradle credential provider plugin has two limitations that prevent `dotnet build` from self-ingesting: 1. **Skips entirely in CI** — when `TF_BUILD=True` (Azure Pipelines), the plugin is a no-op 2. **Doesn't cover all Gradle scopes** — the plugin injects auth into `pluginManagement.repositories` and `project.repositories`, but NOT `buildscript.repositories` or AGP's internal `detachedConfiguration` scopes. This means `dotnet build` locally cannot ingest new packages through the Android SDK binding targets even with correct credentials. We verified this by adding an un-ingested package (`io.coil-kt:coil:2.7.0`) — `dotnet build` fails with 401 despite the credential provider authenticating successfully. **Upstream issue:** [microsoft/artifacts-credprovider#671](microsoft/artifacts-credprovider#671) The script works around these gaps by: 1. Acquiring an auth token via the .NET credential provider (MSAL) 2. Pre-ingesting platform-specific artifacts (aapt2) for all OS variants (macOS/Linux/Windows) 3. Running Gradle with `--refresh-dependencies` to bypass local cache 4. Falling back to `curl` with Bearer token for unreachable scopes **Run `./eng/ingest-maven-deps.sh` after adding or updating any Maven/Gradle dependency.** ### Documentation updates - `settings.gradle` — explains the feed setup and when to run the script - `gradle-wrapper.properties` — warning not to upgrade Gradle past 8.x (`dotnet/android#10738`) - `copilot-instructions.md` — always-on guidance for Gradle 401 failures - `azdo-build-investigator/SKILL.md` — error signatures and DO NOTs for CI investigation - `android.instructions.md` — quick reference for Android developers ## Verified - ✅ Internal official build [2961149](https://dev.azure.com/dnceng/internal/_build/results?buildId=2961149) passed — Pack macOS + Pack Windows both green - ✅ Same pattern used by dotnet/aspnetcore ([PR #64962](dotnet/aspnetcore#64962)) - ✅ Feed is public — no auth needed to read already-ingested packages, external contributors can build without credentials - ✅ Locally verified: `dotnet build` works for already-ingested packages, fails for new ones (confirming script is needed) --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Matt Mitchell (.NET) <mmitche@microsoft.com>
1 parent 6c514cc commit bf61568

9 files changed

Lines changed: 241 additions & 7 deletions

File tree

.github/copilot-instructions.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,14 @@ When referencing or triggering CI pipelines, use these current pipeline names:
102102

103103
**⚠️ Old pipeline names** (e.g., `MAUI-UITests-public`, `MAUI-public`) are **outdated** and should NOT be used. Always use the names above.
104104

105+
### Gradle / Maven Dependency Failures (CFSClean)
106+
107+
The official CI build uses CFSClean network isolation which blocks `repo.maven.apache.org`. All Gradle/Maven dependencies resolve through the `dotnet-public-maven` Azure Artifacts feed.
108+
109+
**If CI fails with Gradle 401 errors** like `"No local versions of package"` or `"Please provide authentication to save package from upstream"`, it means a Maven package hasn't been ingested into the feed yet. **Fix:** run `./eng/ingest-maven-deps.sh` locally to pre-populate the feed. See `src/Core/AndroidNative/settings.gradle` for details.
110+
111+
**Do NOT upgrade Gradle past 8.x** — the Android SDK's `net.android.init.gradle.kts` is incompatible with Gradle 9.x (`dotnet/android#10738`).
112+
105113
### Code Formatting
106114

107115
Always format code before committing:

.github/instructions/android.instructions.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ applyTo:
44
- "**/Android/**/*.cs"
55
- "**/Platforms/Android/**/*.cs"
66
- "**/Platform/Android/**/*.cs"
7+
- "**/AndroidNative/**"
8+
- "eng/init.gradle"
9+
- "eng/ingest-maven-deps.sh"
710
---
811

912
# Android Platform Development Guidelines
@@ -123,3 +126,8 @@ protected override void DisconnectHandler(RecyclerView platformView)
123126
| Listener not working | Check lifecycle (register/unregister) |
124127
| Memory leak | Ensure Dispose() called on Java.Lang.Object |
125128
| Threading error | Use `platformView.Post()` for UI thread |
129+
| Gradle 401 / Maven dependency failure | Run `./eng/ingest-maven-deps.sh` — see `copilot-instructions.md` |
130+
131+
## Gradle / Maven Dependency Failures
132+
133+
CI uses CFSClean which blocks Maven Central. All deps go through the `dotnet-public-maven` Azure Artifacts feed. If a new package hasn't been ingested, CI fails with `XAGRDL0000` / 401. Run `./eng/ingest-maven-deps.sh` locally to fix. Do NOT upgrade Gradle past 8.x (`dotnet/android#10738`).

.github/skills/azdo-build-investigator/SKILL.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,26 @@ If available, use the `mcp-binlog-tool` MCP server to analyze downloaded `.binlo
6666
| `error CS####` | `maui-pr` | C# compiler error — check file/line |
6767
| `error XA####` | `maui-pr` | Android build error |
6868
| `XamlC` | `maui-pr` | XAML compiler — usually missing type or bad binding |
69+
| `error XAGRDL0000` / `401` / `No local versions` | `maui-pr` or official build | Gradle/Maven feed issue — see below |
6970
| `XHarness timeout` | `maui-pr-devicetests` Helix logs | Test killed by infrastructure; may be transient |
7071
| `No test result files found` | `maui-pr-devicetests` Helix logs | Tests never ran or app crashed on launch |
7172
| UI test screenshot diff | `maui-pr-uitests` | Visual regression; check baseline images |
73+
74+
### Gradle / Maven / CFSClean Failures
75+
76+
**Error signatures:**
77+
```
78+
error XAGRDL0000: Could not resolve com.android.tools.build:gradle:8.11.1
79+
> Received status code 401: Unauthorized - No local versions of package
80+
```
81+
```
82+
error XAGRDL0000: Could not GET '...pkgs.dev.azure.com/.../maven/v1/...'
83+
> Unauthorized - Please provide authentication to save package from upstream
84+
```
85+
86+
**Fix:** Tell the user to run `./eng/ingest-maven-deps.sh` locally to pre-ingest packages into the feed.
87+
88+
**Do NOT:**
89+
- Remove CFSClean from `ci-official.yml` — security compliance requirement
90+
- Upgrade Gradle past 8.x — `dotnet/android#10738`
91+
- Add `mavenCentral()` or `google()` back — use the Azure Artifacts feed

eng/ingest-maven-deps.sh

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#!/bin/bash
2+
# Ingest Maven/Gradle dependencies into the dotnet-public-maven Azure Artifacts feed.
3+
#
4+
# WHY THIS IS NEEDED:
5+
# CI builds run under CFSClean network isolation which blocks direct access to
6+
# Maven Central (repo.maven.apache.org). All Maven dependencies are resolved
7+
# through the dotnet-public-maven Azure Artifacts feed instead. However, this
8+
# feed requires an authenticated request the FIRST time a package is pulled
9+
# from upstream Maven Central — after that, anyone can read it anonymously.
10+
#
11+
# The CI pipeline's credential provider plugin (com.microsoft.azure.artifacts.
12+
# credprovider) skips authentication in Azure Pipelines (TF_BUILD=True), so
13+
# new packages MUST be pre-ingested locally before CI can use them.
14+
#
15+
# WHEN TO RUN:
16+
# After adding or updating any Maven/Gradle dependency in
17+
# src/Core/AndroidNative/build.gradle or settings.gradle.
18+
#
19+
# HOW IT WORKS:
20+
# 1. Acquires an auth token via the .NET Azure Artifacts credential provider
21+
# 2. Pre-ingests platform-specific artifacts (e.g. aapt2) for all OS variants
22+
# (macOS/Linux/Windows) since Gradle only resolves the local OS classifier
23+
# 3. Runs the Gradle build with --refresh-dependencies to bypass local cache
24+
# and force actual downloads through the feed (which triggers ingestion)
25+
# 4. For packages that Gradle's credential provider can't reach (e.g. AGP's
26+
# internal detachedConfiguration scopes), falls back to curl with Bearer
27+
# token to force-ingest the specific package URLs
28+
#
29+
# COMMON PITFALL:
30+
# Running ./gradlew build without --refresh-dependencies may appear to succeed
31+
# but actually resolves from ~/.gradle/caches/ (local cache from prior builds
32+
# that used mavenCentral() directly). This does NOT ingest into the feed.
33+
#
34+
# Prerequisites:
35+
# - JDK 17+
36+
# - .NET Azure Artifacts credential provider installed
37+
# (https://github.com/microsoft/artifacts-credprovider#installation)
38+
#
39+
# Usage:
40+
# ./eng/ingest-maven-deps.sh
41+
42+
set -euo pipefail
43+
44+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
45+
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
46+
ANDROID_DIR="$REPO_ROOT/src/Core/AndroidNative"
47+
FEED_URL="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-maven/maven/v1"
48+
CRED_PROVIDER="$HOME/.nuget/plugins/netcore/CredentialProvider.Microsoft/CredentialProvider.Microsoft.dll"
49+
50+
echo "=== Maven Dependency Ingestion for dotnet-public-maven ==="
51+
echo ""
52+
53+
# Step 1: Get auth token
54+
echo "Acquiring auth token..."
55+
if [ ! -f "$CRED_PROVIDER" ]; then
56+
echo "ERROR: Azure Artifacts credential provider not found at $CRED_PROVIDER"
57+
echo "Install it from: https://github.com/microsoft/artifacts-credprovider#installation"
58+
exit 1
59+
fi
60+
61+
TOKEN=$(dotnet "$CRED_PROVIDER" -U "$FEED_URL" -F Json -N true -I true 2>/dev/null \
62+
| python3 -c "import json,sys; print(json.load(sys.stdin).get('Password',''))" 2>/dev/null)
63+
64+
if [ -z "$TOKEN" ]; then
65+
echo "ERROR: Failed to acquire auth token. Make sure you're signed in to Azure DevOps."
66+
exit 1
67+
fi
68+
echo "Token acquired."
69+
70+
# Step 2: Ingest platform-specific artifacts for all OS variants
71+
# Gradle only resolves the classifier for the current OS (e.g. aapt2-osx.jar on macOS).
72+
# CI builds on Windows/Linux need their variants pre-ingested too.
73+
echo ""
74+
echo "Step 1/3: Ingesting cross-platform artifacts..."
75+
AAPT2_VERSION="8.11.1-12782657"
76+
for classifier in osx linux windows; do
77+
for ext in jar pom; do
78+
url="$FEED_URL/com/android/tools/build/aapt2/$AAPT2_VERSION/aapt2-$AAPT2_VERSION-$classifier.$ext"
79+
code=$(curl -s -o /dev/null -w "%{http_code}" --oauth2-bearer "$TOKEN" "$url" 2>/dev/null)
80+
echo " aapt2-$AAPT2_VERSION-$classifier.$ext: $code"
81+
done
82+
done
83+
84+
# Step 3: Run Gradle build with refresh to ingest via credential provider
85+
echo ""
86+
echo "Step 2/3: Running Gradle build with --refresh-dependencies..."
87+
cd "$ANDROID_DIR"
88+
if ! ./gradlew build --no-daemon --refresh-dependencies \
89+
-Dazure.artifacts.credprovider.nonInteractive=true \
90+
-Dazure.artifacts.credprovider.isRetry=true 2>&1 | tail -20; then
91+
echo "WARNING: Initial Gradle build failed (expected if packages need ingestion). Continuing..."
92+
fi
93+
94+
# Step 4: Loop — build, find missing packages, curl-ingest them
95+
echo ""
96+
echo "Step 3/3: Ingesting any remaining packages via REST API..."
97+
for i in $(seq 1 30); do
98+
result=$(./gradlew build --no-daemon \
99+
-Dazure.artifacts.credprovider.nonInteractive=true 2>&1 || true)
100+
101+
if echo "$result" | grep -q "BUILD SUCCESSFUL"; then
102+
echo "All dependencies ingested successfully! ✅"
103+
exit 0
104+
fi
105+
106+
# Extract failed URLs and curl them with auth
107+
urls=$(echo "$result" | grep "Could not GET\|Could not HEAD" \
108+
| sed "s/.*'\(https:[^']*\)'.*/\1/" | sort -u | grep "pkgs.dev.azure.com" || true)
109+
count=$(echo "$urls" | grep -c "https" 2>/dev/null || echo "0")
110+
111+
if [ "$count" = "0" ]; then
112+
# No feed URLs failing — might be a different error
113+
echo "Build failed but not due to feed issues. Check build output."
114+
echo "$result" | grep -i "error" | grep -v "warning" | head -5
115+
exit 1
116+
fi
117+
118+
echo " Run $i: ingesting $count packages..."
119+
echo "$urls" | while read url; do
120+
[ -z "$url" ] && continue
121+
code=$(curl -s -o /dev/null -w "%{http_code}" --oauth2-bearer "$TOKEN" "$url" 2>/dev/null)
122+
if [ "$code" = "200" ]; then
123+
echo "$(basename "$url")"
124+
else
125+
echo " ❌ ($code) $(basename "$url")"
126+
fi
127+
done
128+
done
129+
130+
echo "WARNING: Reached max iterations. Some packages may still need ingestion."
131+
exit 1

eng/init.gradle

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Redirect Maven Central and Google Maven to Azure Artifacts feed
2+
// for CFSClean network isolation compliance.
3+
// See: https://aka.ms/1es/netiso/CFS
4+
allprojects {
5+
repositories {
6+
def azureFeed = findByName("dotnet-public-maven") ?: maven {
7+
url "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-maven/maven/v1"
8+
name "dotnet-public-maven"
9+
}
10+
11+
all { ArtifactRepository repo ->
12+
if (repo != azureFeed &&
13+
repo instanceof MavenArtifactRepository &&
14+
(repo.url.toString().contains(".maven.org") ||
15+
repo.url.toString().contains("maven.apache.org") ||
16+
repo.url.toString().contains("maven.google.com") ||
17+
repo.url.toString().contains("dl.google.com") ||
18+
repo.url.toString().contains("plugins.gradle.org"))) {
19+
project.logger.warn "Replacing repository ${repo.url} with Azure Artifacts feed ${azureFeed.url}."
20+
remove repo
21+
}
22+
}
23+
}
24+
}

eng/pipelines/common/cache-gradle.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,14 @@ steps:
3939
"gradle" | "v1" | "$(Agent.OS)" | ${{ parameters.checkoutDirectory }}/src/Core/AndroidNative/gradle/wrapper/gradle-wrapper.properties
4040
"gradle" | "v1" | "$(Agent.OS)"
4141
path: $(GRADLE_USER_HOME)
42+
43+
# Copy init.gradle AFTER cache restore so it is not overwritten by a stale cached copy
44+
- script: |
45+
cp "${{ parameters.checkoutDirectory }}/eng/init.gradle" "$(GRADLE_USER_HOME)/init.gradle"
46+
displayName: install Gradle init script (Linux/macOS)
47+
condition: ne(variables['Agent.OS'], 'Windows_NT')
48+
49+
- pwsh: |
50+
Copy-Item "${{ parameters.checkoutDirectory }}/eng/init.gradle" (Join-Path "$(GRADLE_USER_HOME)" "init.gradle")
51+
displayName: install Gradle init script (Windows)
52+
condition: eq(variables['Agent.OS'], 'Windows_NT')

src/Core/AndroidNative/build.gradle

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
// Top-level build file where you can add configuration options common to all sub-projects/modules.
22
buildscript {
33
repositories {
4-
google()
5-
mavenCentral()
4+
maven {
5+
url = 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-maven/maven/v1'
6+
name = 'dotnet-public-maven'
7+
}
68
}
79
dependencies {
810
classpath "com.android.tools.build:gradle:8.11.1"

src/Core/AndroidNative/gradle/wrapper/gradle-wrapper.properties

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# DO NOT upgrade Gradle beyond 8.x without verifying that the Microsoft.Android.Sdk
2+
# version used by this branch supports it. The Android SDK generates a
3+
# net.android.init.gradle.kts script that has a Kotlin type mismatch (String? vs Any)
4+
# incompatible with Gradle 9.x's stricter type checking. The fix is merged upstream
5+
# (dotnet/android#10738) but must ship in the Android SDK version referenced by this
6+
# branch before Gradle can be upgraded.
17
distributionBase=GRADLE_USER_HOME
28
distributionPath=wrapper/dists
39
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip

src/Core/AndroidNative/settings.gradle

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,40 @@
1+
// All Maven dependencies are resolved through the dnceng Azure Artifacts feed
2+
// (dotnet-public-maven) for CFSClean network isolation compliance. The feed
3+
// proxies Maven Central, Google Maven, and Gradle Plugin Portal.
4+
//
5+
// IMPORTANT: New packages must be ingested into the feed before CI can use them.
6+
// The CI credential provider plugin skips auth in Azure Pipelines, so packages
7+
// that aren't already in the feed will fail with 401. After adding or updating
8+
// dependencies, run:
9+
//
10+
// ./eng/ingest-maven-deps.sh
11+
//
12+
// See: https://aka.ms/1es/netiso/CFS
13+
114
pluginManagement {
215
repositories {
3-
google()
4-
mavenCentral()
5-
gradlePluginPortal()
16+
maven {
17+
url = 'https://pkgs.dev.azure.com/artifacts-public/PublicTools/_packaging/AzureArtifacts/maven/v1'
18+
name = 'AzureArtifacts'
19+
}
20+
maven {
21+
url = 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-maven/maven/v1'
22+
name = 'dotnet-public-maven'
23+
}
624
}
725
}
826

27+
plugins {
28+
id 'com.microsoft.azure.artifacts.credprovider' version '1.1.1'
29+
}
30+
931
dependencyResolutionManagement {
1032
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
1133
repositories {
12-
google()
13-
mavenCentral()
34+
maven {
35+
url = 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-maven/maven/v1'
36+
name = 'dotnet-public-maven'
37+
}
1438
}
1539
}
1640

0 commit comments

Comments
 (0)