diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index fe467c1887c5..bc4f230aa1cc 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,8 +1,3 @@ - -> [!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! - diff --git a/.github/workflows/dogfood-comment.yml b/.github/workflows/dogfood-comment.yml new file mode 100644 index 000000000000..a64a5a5ed510 --- /dev/null +++ b/.github/workflows/dogfood-comment.yml @@ -0,0 +1,85 @@ +name: Add Dogfooding Comment + +on: + # Use pull_request_target to run in the context of the base branch + # This allows commenting on PRs from forks + pull_request_target: + types: [opened, reopened, synchronize] + branches: + - 'main' + - 'net*' + - 'release/**' + + # Allow manual triggering + workflow_dispatch: + inputs: + pr_number: + description: 'PR number to add dogfooding comment to' + required: true + type: number + +jobs: + add-dogfood-comment: + # Only run on the dotnet org to avoid running on forks + if: ${{ github.repository_owner == 'dotnet' }} + runs-on: ubuntu-latest + permissions: + pull-requests: write + + steps: + - name: Add dogfooding comment + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + // Get PR number from either the PR event or manual input + const prNumber = context.payload.number || context.payload.inputs?.pr_number; + + const bashScript = 'https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh'; + const psScript = 'https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1'; + + // Unique marker to identify dogfooding comments + const dogfoodMarker = ''; + + const comment = `${dogfoodMarker} + šŸš€ **Dogfood this PR with:** + + > **āš ļø WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.** + + \`\`\`bash + curl -fsSL ${bashScript} | bash -s -- ${prNumber} + \`\`\` + + Or + + - Run remotely in PowerShell: + + \`\`\`powershell + iex "& { $(irm ${psScript}) } ${prNumber}" + \`\`\``; + + // Check for existing dogfooding comment + const comments = await github.rest.issues.listComments({ + issue_number: prNumber, + owner: context.repo.owner, + repo: context.repo.repo, + }); + + const existingComment = comments.data.find(comment => comment.body.includes(dogfoodMarker)); + + if (existingComment) { + // Update existing comment + await github.rest.issues.updateComment({ + comment_id: existingComment.id, + owner: context.repo.owner, + repo: context.repo.repo, + body: comment + }); + } else { + // Create new comment + await github.rest.issues.createComment({ + issue_number: prNumber, + owner: context.repo.owner, + repo: context.repo.repo, + body: comment + }); + } diff --git a/eng/pipelines/ci-device-tests.yml b/eng/pipelines/ci-device-tests.yml index aa84f16e2a89..ff062fd51557 100644 --- a/eng/pipelines/ci-device-tests.yml +++ b/eng/pipelines/ci-device-tests.yml @@ -12,7 +12,7 @@ trigger: include: - '*' exclude: - - .github/* + - .github/** - docs/* - src/Templates/* - CODE-OF-CONDUCT.md @@ -33,7 +33,7 @@ pr: include: - '*' exclude: - - .github/* + - .github/** - docs/* - src/Templates/* - CODE-OF-CONDUCT.md diff --git a/eng/pipelines/ci-official.yml b/eng/pipelines/ci-official.yml index 8415fdcf9af2..52f2b9b9732a 100644 --- a/eng/pipelines/ci-official.yml +++ b/eng/pipelines/ci-official.yml @@ -12,7 +12,7 @@ trigger: include: - '*' exclude: - - .github/* + - .github/** - docs/* - CODE-OF-CONDUCT.md - CONTRIBUTING.md diff --git a/eng/pipelines/ci-uitests.yml b/eng/pipelines/ci-uitests.yml index aa88490e4851..cb180db4d720 100644 --- a/eng/pipelines/ci-uitests.yml +++ b/eng/pipelines/ci-uitests.yml @@ -11,7 +11,7 @@ trigger: include: - '*' exclude: - - .github/* + - .github/** - docs/* - src/Templates/* - CODE-OF-CONDUCT.md @@ -31,7 +31,7 @@ pr: include: - '*' exclude: - - .github/* + - .github/** - docs/* - src/Templates/* - CODE-OF-CONDUCT.md diff --git a/eng/pipelines/ci.yml b/eng/pipelines/ci.yml index 5323eec8c6d9..92f58d12e8c1 100644 --- a/eng/pipelines/ci.yml +++ b/eng/pipelines/ci.yml @@ -12,7 +12,7 @@ trigger: exclude: - '**.md' - eng/Version.Details.xml - - .github/* + - .github/** - docs/* - LICENSE.TXT - PATENTS.TXT @@ -31,7 +31,7 @@ pr: exclude: - '**.md' - eng/Version.Details.xml - - .github/* + - .github/** - docs/* - LICENSE.TXT - PATENTS.TXT diff --git a/eng/pipelines/device-tests.yml b/eng/pipelines/device-tests.yml index de2ec467dc0c..194eef01ba18 100644 --- a/eng/pipelines/device-tests.yml +++ b/eng/pipelines/device-tests.yml @@ -11,7 +11,7 @@ trigger: include: - '*' exclude: - - .github/* + - .github/** - docs/* - src/Templates/* - CODE-OF-CONDUCT.md @@ -31,7 +31,7 @@ pr: include: - '*' exclude: - - .github/* + - .github/** - docs/* - src/Templates/* - CODE-OF-CONDUCT.md diff --git a/eng/pipelines/handlers.yml b/eng/pipelines/handlers.yml index fb10a80367d6..33d3f3512207 100644 --- a/eng/pipelines/handlers.yml +++ b/eng/pipelines/handlers.yml @@ -11,7 +11,7 @@ trigger: include: - '*' exclude: - - .github/* + - .github/** - docs/* - CODE-OF-CONDUCT.md - CONTRIBUTING.md @@ -30,7 +30,7 @@ pr: include: - '*' exclude: - - .github/* + - .github/** - docs/* - CODE-OF-CONDUCT.md - CONTRIBUTING.md diff --git a/eng/pipelines/ui-tests.yml b/eng/pipelines/ui-tests.yml index 651827f61922..8adb532f3174 100644 --- a/eng/pipelines/ui-tests.yml +++ b/eng/pipelines/ui-tests.yml @@ -11,7 +11,7 @@ trigger: include: - '*' exclude: - - .github/* + - .github/** - docs/* - src/Templates/* - CODE-OF-CONDUCT.md @@ -31,7 +31,7 @@ pr: include: - '*' exclude: - - .github/* + - .github/** - docs/* - src/Templates/* - CODE-OF-CONDUCT.md diff --git a/eng/scripts/README.md b/eng/scripts/README.md new file mode 100644 index 000000000000..9eacffa4309c --- /dev/null +++ b/eng/scripts/README.md @@ -0,0 +1,128 @@ +# .NET MAUI PR Artifact Scripts + +Scripts to test .NET MAUI pull request builds in your own projects before they're merged. + +## Scripts + +- `get-maui-pr.sh` - Bash script for Unix-like systems (Linux, macOS) +- `get-maui-pr.ps1` - PowerShell script for cross-platform use (Windows, Linux, macOS) + +## Quick Start + +> **āš ļø WARNING:** Always review the PR code before running these scripts. Only test PRs you trust. + +**Bash:** + +```bash +curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- +``` + +**PowerShell:** + +```powershell +iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } " +``` + +## NuGet Hive Path + +Downloaded packages are stored in a hive directory: + +- **Unix (Linux/macOS)**: `~/.maui/hives/pr-/packages` +- **Windows**: `%USERPROFILE%\.maui\hives\pr-\packages` + +## Requirements + +- .NET SDK installed +- A .NET MAUI project (`.csproj` with `true`) +- Internet connection +- **Bash only**: `curl`, `jq`, and `unzip` + +## Parameters + +### Bash Script (`get-maui-pr.sh`) + +| Argument | Description | Required | +|----------|-------------|----------| +| 1st | PR number to test | Yes | +| 2nd | Path to .csproj file | No (auto-detects) | + +### PowerShell Script (`get-maui-pr.ps1`) + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `-PrNumber` | PR number to test | Required | +| `-ProjectPath` | Path to .csproj file | Auto-detect | + +## Usage Examples + +### Bash Script Examples + +```bash +# Test PR in current directory +./get-maui-pr.sh 33002 + +# Test PR with specific project +./get-maui-pr.sh 33002 ./MyApp/MyApp.csproj +``` + +### PowerShell Script Examples + +```powershell +# Test PR in current directory +.\get-maui-pr.ps1 33002 + +# Test PR with specific project +.\get-maui-pr.ps1 -PrNumber 33002 -ProjectPath ./MyApp/MyApp.csproj +``` + +## Repository Override + +You can point the scripts at a fork by setting the `MAUI_REPO` environment variable to `owner/name` before invoking the script (defaults to `dotnet/maui`). + +```bash +export MAUI_REPO=myfork/maui +./get-maui-pr.sh 1234 +``` + +```powershell +$env:MAUI_REPO = 'myfork/maui' +./get-maui-pr.ps1 1234 +``` + +## Reverting Changes + +**TIP:** Use a separate Git branch for testing! + +```bash +git checkout -b test-pr-33002 +# ... test the PR ... +git checkout main # Easy revert! +``` + +Or manually revert: + +1. Edit your `.csproj` - change package version back to stable (see [NuGet](https://www.nuget.org/packages/Microsoft.Maui.Controls)) +2. Remove the `maui-pr-build` source from `NuGet.config` +3. Run `dotnet restore --force` + +## Troubleshooting + +### Common Issues + +1. **"No build found for PR"**: The PR may not have a completed build yet. Check the PR on GitHub for build status. +2. **"No .NET MAUI project found"**: Ensure you're in a directory with a `.csproj` file that has `true`. +3. **"Failed to download artifacts"**: Check your internet connection. Artifacts may have expired for older PRs. + +### Getting Help + +Run the PowerShell script with the help flag to see detailed usage information: + +```powershell +Get-Help .\get-maui-pr.ps1 -Detailed +``` + +## More Information + +- [Testing PR Builds Wiki](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) +- [.NET MAUI Nightly Builds](https://github.com/dotnet/maui/wiki/Nightly-Builds) +- [Contributing Guide](https://github.com/dotnet/maui/blob/main/CONTRIBUTING.md) diff --git a/eng/scripts/get-maui-pr.ps1 b/eng/scripts/get-maui-pr.ps1 new file mode 100644 index 000000000000..edc3ddc3c989 --- /dev/null +++ b/eng/scripts/get-maui-pr.ps1 @@ -0,0 +1,580 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Apply a .NET MAUI PR build to your project. + +.DESCRIPTION + This script downloads and applies NuGet packages from a specific .NET MAUI pull request build + to your local project. It automatically detects your project's target framework and updates + the necessary package references. + + The script uses a hive-based approach, storing packages in: ~/.maui/hives/pr-/packages + +.PARAMETER PrNumber + The pull request number to apply. Required. Can be passed as positional parameter. + +.PARAMETER ProjectPath + The path to the .csproj file. If not specified, searches for a MAUI project in the current directory. + +.EXAMPLE + iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 33002" + +.EXAMPLE + ./get-maui-pr.ps1 33002 + +.EXAMPLE + ./get-maui-pr.ps1 -PrNumber 33002 -ProjectPath ./MyApp/MyApp.csproj + +.NOTES + This script requires: + - .NET SDK installed + - Internet connection to access GitHub and Azure DevOps APIs + - A valid .NET MAUI project + + Repository Override: + Set MAUI_REPO environment variable to point to a fork (e.g., 'myfork/maui') + + For more information about testing PR builds, visit: + https://github.com/dotnet/maui/wiki/Testing-PR-Builds +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true, Position = 0)] + [int]$PrNumber, + + [Parameter(Mandatory = $false)] + [string]$ProjectPath = "" +) + +$ErrorActionPreference = "Stop" +$ProgressPreference = "SilentlyContinue" + +# Configuration - Allow override via environment variable +$GitHubRepo = if ($env:MAUI_REPO) { $env:MAUI_REPO } else { "dotnet/maui" } +$AzureDevOpsOrg = "xamarin" +$AzureDevOpsProject = "public" +$PackageName = "Microsoft.Maui.Controls" + +# Color output functions +function Write-Info { + param([string]$Message) + Write-Host "ā„¹ļø $Message" -ForegroundColor Cyan +} + +function Write-Success { + param([string]$Message) + Write-Host "āœ… $Message" -ForegroundColor Green +} + +function Write-Warning { + param([string]$Message) + Write-Host "āš ļø $Message" -ForegroundColor Yellow +} + +function Write-Error { + param([string]$Message) + Write-Host "āŒ $Message" -ForegroundColor Red +} + +function Write-Step { + param([string]$Message) + Write-Host "`nā–¶ļø $Message" -ForegroundColor Blue +} + +# Find MAUI project +function Find-MauiProject { + param([string]$SearchPath) + + if ([string]::IsNullOrEmpty($SearchPath)) { + $SearchPath = Get-Location + } + + if (Test-Path $SearchPath -PathType Leaf) { + if ($SearchPath -match '\.csproj$') { + return $SearchPath + } + throw "The specified file is not a .csproj file: $SearchPath" + } + + $projects = Get-ChildItem -Path $SearchPath -Filter "*.csproj" -File + + foreach ($project in $projects) { + $content = Get-Content $project.FullName -Raw + if ($content -match 'true') { + return $project.FullName + } + } + + throw "No .NET MAUI project found in $SearchPath. Make sure you're in a directory containing a MAUI project (.csproj with true)." +} + +# Get PR information from GitHub +function Get-PullRequestInfo { + param([int]$PrNumber) + + Write-Info "Fetching PR #$PrNumber information from GitHub..." + + try { + $prUrl = "https://api.github.com/repos/$GitHubRepo/pulls/$PrNumber" + $pr = Invoke-RestMethod -Uri $prUrl -Headers @{ "User-Agent" = "MAUI-PR-Script" } + + return @{ + Number = $pr.number + Title = $pr.title + State = $pr.state + SHA = $pr.head.sha + Ref = $pr.head.ref + } + } + catch { + throw "Failed to fetch PR information. Make sure PR #$PrNumber exists. Error: $_" + } +} + +# Get build information from GitHub Checks API +function Get-BuildInfo { + param([string]$SHA) + + Write-Info "Looking for build artifacts for commit $($SHA.Substring(0, 7))..." + + try { + $checksUrl = "https://api.github.com/repos/$GitHubRepo/commits/$SHA/check-runs" + $response = Invoke-RestMethod -Uri $checksUrl -Headers @{ + "User-Agent" = "MAUI-PR-Script" + "Accept" = "application/vnd.github.v3+json" + } + + # Look for the main MAUI build check + $buildCheck = $response.check_runs | Where-Object { + $_.name -eq "MAUI-public" -and $_.status -eq "completed" + } | Select-Object -First 1 + + if (-not $buildCheck) { + throw "No completed build found for this PR. The build may still be in progress or may have failed." + } + + if ($buildCheck.conclusion -ne "success") { + Write-Warning "Build completed with status: $($buildCheck.conclusion)" + $continue = Read-Host "Do you want to continue anyway? (y/N)" + if ($continue -ne "y" -and $continue -ne "Y") { + throw "Build was not successful. Aborting." + } + } + + # Extract build ID from details URL + if ($buildCheck.details_url -match 'buildId=(\d+)') { + $buildId = $Matches[1] + Write-Success "Found build ID: $buildId" + return $buildId + } + + throw "Could not extract build ID from check run details." + } + catch { + throw "Failed to get build information: $_" + } +} + +# Get artifacts from Azure DevOps +function Get-BuildArtifacts { + param([string]$BuildId) + + Write-Info "Fetching artifacts from Azure DevOps build $BuildId..." + + try { + $artifactsUrl = "https://dev.azure.com/$AzureDevOpsOrg/$AzureDevOpsProject/_apis/build/builds/$BuildId/artifacts?api-version=7.1" + $response = Invoke-RestMethod -Uri $artifactsUrl -Headers @{ "User-Agent" = "MAUI-PR-Script" } + + # Look for nuget artifact + $artifact = $response.value | Where-Object { $_.name -eq "nuget" } | Select-Object -First 1 + + if (-not $artifact) { + throw "No 'nuget' artifact found in build $BuildId" + } + + return $artifact.resource.downloadUrl + } + catch { + throw "Failed to get artifact information: $_" + } +} + +# Download and extract artifacts +function Get-Artifacts { + param([string]$DownloadUrl, [string]$BuildId) + + # Use hive directory pattern like Aspire CLI + $hiveDir = if ($IsWindows -or $env:OS -eq "Windows_NT") { + Join-Path $env:USERPROFILE ".maui\hives\pr-$PrNumber" + } else { + Join-Path $env:HOME ".maui/hives/pr-$PrNumber" + } + $packagesDir = Join-Path $hiveDir "packages" + $tempDir = $hiveDir + $zipFile = Join-Path $tempDir "artifacts.zip" + $extractDir = $packagesDir + + if (Test-Path $tempDir) { + Write-Info "Cleaning up previous download..." + Remove-Item $tempDir -Recurse -Force + } + + New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + New-Item -ItemType Directory -Path $extractDir -Force | Out-Null + + Write-Info "Downloading artifacts (this may take a moment)..." + try { + Invoke-WebRequest -Uri $DownloadUrl -OutFile $zipFile -UseBasicParsing + Write-Success "Downloaded artifacts" + + Write-Info "Extracting artifacts..." + Expand-Archive -Path $zipFile -DestinationPath $extractDir -Force + + # Find the NuGet packages directory + $nupkgDir = Get-ChildItem -Path $extractDir -Recurse -Directory | + Where-Object { (Get-ChildItem -Path $_.FullName -Filter "*.nupkg" -File).Count -gt 0 } | + Select-Object -First 1 + + if (-not $nupkgDir) { + throw "Could not find NuGet packages in the extracted artifacts" + } + + return $nupkgDir.FullName + } + catch { + throw "Failed to download or extract artifacts: $_" + } +} + +# Get package version from directory +function Get-PackageVersion { + param([string]$PackagesDir) + + $package = Get-ChildItem -Path $PackagesDir -Filter "$PackageName.*.nupkg" -File | + Where-Object { $_.Name -notmatch '\.symbols\.nupkg$' } | + Select-Object -First 1 + + if (-not $package) { + throw "Could not find $PackageName package in artifacts" + } + + if ($package.Name -match "$PackageName\.(.+)\.nupkg") { + return $Matches[1] + } + + throw "Could not extract version from package filename: $($package.Name)" +} + +# Detect target framework version +function Get-TargetFrameworkVersion { + param([string]$ProjectPath) + + $content = Get-Content $ProjectPath -Raw + + # Look for TargetFramework or TargetFrameworks + if ($content -match '([^<]+)') { + $tfms = $Matches[1] + + # Extract .NET version (e.g., net9.0, net10.0) + if ($tfms -match 'net(\d+)\.0') { + $netVersion = [int]$Matches[1] + return $netVersion + } + } + + throw "Could not determine target framework version from project file" +} + +# Check if version matches target framework +function Test-VersionCompatibility { + param([string]$Version, [int]$TargetNetVersion, [int]$PackageNetVersion) + + # PR builds are typically for the latest .NET version + # Check if the package targets a newer .NET version than the project + + if ($Version -match 'preview' -or $Version -match 'ci\.') { + if ($TargetNetVersion -lt $PackageNetVersion) { + return $false + } + } + + return $true +} + +# Extract .NET version from package version +function Get-PackageDotNetVersion { + param([string]$Version) + + # Extract major version from package (e.g., "10.0.20-ci..." -> 10) + if ($Version -match '^(\d+)\.') { + return [int]$Matches[1] + } + + # Default to current stable if can't determine + return 9 +} + +# Update target frameworks +function Update-TargetFrameworks { + param([string]$ProjectPath, [int]$NewNetVersion) + + $content = Get-Content $ProjectPath -Raw + + # Update all netX.0-* references (including in conditional TargetFrameworks) + $content = $content -replace 'net\d+\.0-', "net$NewNetVersion.0-" + + Set-Content -Path $ProjectPath -Value $content -NoNewline + Write-Success "Updated target frameworks to .NET $NewNetVersion.0" + Write-Warning "You may need to update other package dependencies to match .NET $NewNetVersion.0" +} + +# Create or update NuGet.config +function Update-NuGetConfig { + param([string]$ProjectDir, [string]$PackagesDir) + + $nugetConfigPath = Join-Path $ProjectDir "NuGet.config" + $sourceName = "maui-pr-build" + + if (Test-Path $nugetConfigPath) { + Write-Info "Updating existing NuGet.config..." + [xml]$config = Get-Content $nugetConfigPath + + # Ensure packageSources exists + if (-not $config.configuration.packageSources) { + $packageSources = $config.CreateElement("packageSources") + $config.configuration.AppendChild($packageSources) | Out-Null + } + + # Remove existing source with same name + $existingSource = $config.configuration.packageSources.add | Where-Object { $_.key -eq $sourceName } + if ($existingSource) { + $config.configuration.packageSources.RemoveChild($existingSource) | Out-Null + } + + # Add new source + $add = $config.CreateElement("add") + $add.SetAttribute("key", $sourceName) + $add.SetAttribute("value", $PackagesDir) + $config.configuration.packageSources.AppendChild($add) | Out-Null + + $config.Save($nugetConfigPath) + } + else { + Write-Info "Creating new NuGet.config..." + $config = @" + + + + + + + + +"@ + Set-Content -Path $nugetConfigPath -Value $config + } + + Write-Success "NuGet.config configured with local package source" +} + +# Update project package reference +function Update-PackageReference { + param([string]$ProjectPath, [string]$Version) + + $content = Get-Content $ProjectPath -Raw + + # Replace the version in PackageReference, handling both explicit version and $(MauiVersion) + $pattern = "( 10) + $packageDotNetVersion = $null + if ($version -match '^(\d+)\.') { + $packageDotNetVersion = $Matches[1] + } + + if ($willUpdateTfm) { + $targetVersionForDisplay = if ($packageDotNetVersion) { "$packageDotNetVersion.0" } else { "$packageNetVersion.0" } + Write-Host " • Target framework: Will be updated to .NET $targetVersionForDisplay" -ForegroundColor Gray + } + Write-Host "" + + $response = Read-Host "Do you want to continue? (y/N)" + if ($response -ne "y" -and $response -ne "Y") { + Write-Warning "Operation cancelled by user" + exit 0 + } + Write-Host "" + + if ($willUpdateTfm) { + $targetNetVersionToApply = if ($packageDotNetVersion) { [int]$packageDotNetVersion } else { 10 } + Update-TargetFrameworks -ProjectPath $projectPath -NewNetVersion $targetNetVersionToApply + $targetNetVersion = $targetNetVersionToApply + } + + Write-Step "Configuring NuGet sources" + Update-NuGetConfig -ProjectDir $projectDir -PackagesDir $packagesDir + + Write-Step "Updating package reference" + Update-PackageReference -ProjectPath $projectPath -Version $version + + # Get latest stable version for revert instructions + try { + $nugetResponse = Invoke-RestMethod -Uri "https://api.nuget.org/v3-flatcontainer/microsoft.maui.controls/index.json" -UseBasicParsing + $stableVersions = $nugetResponse.versions | Where-Object { $_ -notmatch '-' } | Sort-Object -Descending + $latestStable = $stableVersions[0] + } + catch { + $latestStable = "X.Y.Z" + } + + Write-Host @" + +╔═══════════════════════════════════════════════════════════╗ +ā•‘ ā•‘ +ā•‘ āœ… Successfully applied PR #$PrNumber! ā•‘ +ā•‘ ā•‘ +ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā• + +"@ -ForegroundColor Green + + Write-Info "Next steps:" + Write-Host " 1. Run 'dotnet restore' to download the packages" -ForegroundColor White + Write-Host " 2. Build and test your project with the PR changes" -ForegroundColor White + Write-Host " 3. Report your findings on: https://github.com/dotnet/maui/pull/$PrNumber" -ForegroundColor Cyan + Write-Host "" + Write-Info "Package: $PackageName $version" + Write-Info "Local package source: $packagesDir" + Write-Host "" + + Write-Host "═══════════════════════════════════════════════════════════" -ForegroundColor Yellow + Write-Host " TO REVERT TO PRODUCTION VERSION" -ForegroundColor Yellow + Write-Host "═══════════════════════════════════════════════════════════" -ForegroundColor Yellow + Write-Host "" + Write-Host "1. Edit $projectName and change the version:" -ForegroundColor White + Write-Host " From: Version=`"$version`"" -ForegroundColor Gray + Write-Host " To: Version=`"X.Y.Z`"" -ForegroundColor Gray + Write-Host " (Check https://www.nuget.org/packages/$PackageName for latest)" -ForegroundColor DarkGray + Write-Host "" + Write-Host "2. In NuGet.config, remove or comment out the 'maui-pr-build' source" -ForegroundColor White + Write-Host "" + Write-Host "3. Run: dotnet restore --force" -ForegroundColor White + Write-Host "" + Write-Host "TIP: Use a separate Git branch for testing PR builds!" -ForegroundColor Cyan + Write-Host " Then you can easily revert: git checkout main" -ForegroundColor Cyan + Write-Host "" + +} +catch { + Write-Error "Failed to apply PR build: $_" + Write-Host "" + Write-Info "Troubleshooting tips:" + Write-Host " • Make sure you're in a directory containing a .NET MAUI project" -ForegroundColor Gray + Write-Host " • Verify that PR #$PrNumber exists: https://github.com/dotnet/maui/pull/$PrNumber" -ForegroundColor Gray + Write-Host " • Check if there's a completed build for this PR (look for green checkmarks)" -ForegroundColor Gray + Write-Host " • Check your internet connection" -ForegroundColor Gray + Write-Host " • Visit: https://github.com/dotnet/maui/wiki/Testing-PR-Builds" -ForegroundColor Gray + + exit 1 +} diff --git a/eng/scripts/get-maui-pr.sh b/eng/scripts/get-maui-pr.sh new file mode 100644 index 000000000000..d01ad1b7f01c --- /dev/null +++ b/eng/scripts/get-maui-pr.sh @@ -0,0 +1,597 @@ +#!/usr/bin/env bash + +# .NET MAUI PR Build Applicator (Bash version) +# +# This script downloads and applies NuGet packages from a specific .NET MAUI pull request build +# to your local project. It automatically detects your project's target framework and updates +# the necessary package references. +# +# The script uses a hive-based approach, storing packages in: ~/.maui/hives/pr-/packages +# +# Usage: +# curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 33002 +# ./get-maui-pr.sh [PROJECT_PATH] +# +# Examples: +# ./get-maui-pr.sh 33002 +# ./get-maui-pr.sh 33002 ./MyApp/MyApp.csproj +# +# Requirements: +# - .NET SDK installed +# - curl and jq installed +# - unzip installed +# - Internet connection to access GitHub and Azure DevOps APIs +# - A valid .NET MAUI project +# +# Repository Override: +# Set MAUI_REPO environment variable to point to a fork (e.g., 'myfork/maui') +# +# For more information about testing PR builds, visit: +# https://github.com/dotnet/maui/wiki/Testing-PR-Builds + +set -e + +# Error handler +trap 'handle_error $? $LINENO' ERR + +handle_error() { + local exit_code=$1 + local line_num=$2 + echo "" + error "Failed to apply PR build (exit code: $exit_code at line $line_num)" + echo "" + info "Troubleshooting tips:" + echo " • Make sure you're in a directory containing a .NET MAUI project" + echo " • Verify that PR #${pr_number:-NUMBER} exists: https://github.com/dotnet/maui/pull/${pr_number:-NUMBER}" + echo " • Check if there's a completed build for this PR (look for green checkmarks)" + echo " • Check your internet connection" + echo " • Visit: https://github.com/dotnet/maui/wiki/Testing-PR-Builds" + exit $exit_code +} + +# Configuration - Allow override via environment variable +GITHUB_REPO="${MAUI_REPO:-dotnet/maui}" +AZURE_DEVOPS_ORG="xamarin" +AZURE_DEVOPS_PROJECT="public" +PACKAGE_NAME="Microsoft.Maui.Controls" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +MAGENTA='\033[0;35m' +WHITE='\033[1;37m' +GRAY='\033[0;37m' +DGRAY='\033[0;90m' +NC='\033[0m' # No Color + +# Output functions +info() { + echo -e "${CYAN}ā„¹ļø $1${NC}" +} + +success() { + echo -e "${GREEN}āœ… $1${NC}" +} + +warning() { + echo -e "${YELLOW}āš ļø $1${NC}" +} + +error() { + echo -e "${RED}āŒ $1${NC}" +} + +step() { + echo -e "\n${BLUE}ā–¶ļø $1${NC}" +} + +# Check dependencies +check_dependencies() { + local missing_deps=() + + if ! command -v curl &> /dev/null; then + missing_deps+=("curl") + fi + + if ! command -v jq &> /dev/null; then + missing_deps+=("jq") + fi + + if ! command -v unzip &> /dev/null; then + missing_deps+=("unzip") + fi + + if ! command -v dotnet &> /dev/null; then + missing_deps+=("dotnet") + fi + + if [ ${#missing_deps[@]} -gt 0 ]; then + error "Missing required dependencies: ${missing_deps[*]}" + echo "" + info "Please install the missing dependencies:" + for dep in "${missing_deps[@]}"; do + echo " - $dep" + done + exit 1 + fi +} + +# Find MAUI project +find_maui_project() { + local search_path="$1" + + if [ -z "$search_path" ]; then + search_path="." + fi + + # If it's a file and ends with .csproj + if [ -f "$search_path" ] && [[ "$search_path" == *.csproj ]]; then + echo "$search_path" + return 0 + fi + + # Search for .csproj files with UseMaui + for proj in "$search_path"/*.csproj; do + if [ -f "$proj" ]; then + if grep -q 'true' "$proj"; then + echo "$proj" + return 0 + fi + fi + done + + error "No .NET MAUI project found in $search_path" + info "Make sure you're in a directory containing a MAUI project (.csproj with true)" + exit 1 +} + +# Get PR information from GitHub +get_pr_info() { + local pr_number="$1" + + info "Fetching PR #$pr_number information from GitHub..." + + local pr_url="https://api.github.com/repos/$GITHUB_REPO/pulls/$pr_number" + local pr_json=$(curl -s -H "User-Agent: MAUI-PR-Script" "$pr_url") + + if [ -z "$pr_json" ] || echo "$pr_json" | jq -e '.message' > /dev/null 2>&1; then + error "Failed to fetch PR information. Make sure PR #$pr_number exists." + exit 1 + fi + + echo "$pr_json" +} + +# Get build information from GitHub Checks API +get_build_info() { + local sha="$1" + + info "Looking for build artifacts for commit ${sha:0:7}..." + + local checks_url="https://api.github.com/repos/$GITHUB_REPO/commits/$sha/check-runs" + local checks_json=$(curl -s -H "User-Agent: MAUI-PR-Script" -H "Accept: application/vnd.github.v3+json" "$checks_url") + + # Find the main MAUI build check + local build_check=$(echo "$checks_json" | jq -r '.check_runs[] | select(.name == "MAUI-public" and .status == "completed") | @json' | head -n 1) + + if [ -z "$build_check" ] || [ "$build_check" == "null" ]; then + error "No completed build found for this PR" + info "The build may still be in progress or may have failed." + exit 1 + fi + + local conclusion=$(echo "$build_check" | jq -r '.conclusion') + if [ "$conclusion" != "success" ]; then + warning "Build completed with status: $conclusion" + read -p "Do you want to continue anyway? (y/N) " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + error "Build was not successful. Aborting." + exit 1 + fi + fi + + # Extract build ID from details URL + local details_url=$(echo "$build_check" | jq -r '.details_url') + if [[ "$details_url" =~ buildId=([0-9]+) ]]; then + local build_id="${BASH_REMATCH[1]}" + success "Found build ID: $build_id" + echo "$build_id" + return 0 + fi + + error "Could not extract build ID from check run details" + exit 1 +} + +# Get artifacts from Azure DevOps +get_build_artifacts() { + local build_id="$1" + + info "Fetching artifacts from Azure DevOps build $build_id..." + + local artifacts_url="https://dev.azure.com/$AZURE_DEVOPS_ORG/$AZURE_DEVOPS_PROJECT/_apis/build/builds/$build_id/artifacts?api-version=7.1" + local artifacts_json=$(curl -s -H "User-Agent: MAUI-PR-Script" "$artifacts_url") + + # Look for nuget artifact + local download_url=$(echo "$artifacts_json" | jq -r '.value[] | select(.name == "nuget") | .resource.downloadUrl' | head -n 1) + + if [ -z "$download_url" ] || [ "$download_url" == "null" ]; then + error "No 'nuget' artifact found in build $build_id" + exit 1 + fi + + echo "$download_url" +} + +# Download and extract artifacts +get_artifacts() { + local download_url="$1" + local build_id="$2" + + # Use hive directory pattern like Aspire CLI + local hive_dir="$HOME/.maui/hives/pr-$pr_number" + local packages_dir="$hive_dir/packages" + local temp_dir="$hive_dir" + local zip_file="$temp_dir/artifacts.zip" + local extract_dir="$packages_dir" + + if [ -d "$temp_dir" ]; then + info "Cleaning up previous download..." + rm -rf "$temp_dir" + fi + + mkdir -p "$temp_dir" + mkdir -p "$extract_dir" + + info "Downloading artifacts (this may take a moment)..." + curl -L -o "$zip_file" "$download_url" 2>/dev/null + success "Downloaded artifacts" + + info "Extracting artifacts..." + unzip -q "$zip_file" -d "$extract_dir" + + # Find the NuGet packages directory + local nupkg_dir=$(find "$extract_dir" -type f -name "*.nupkg" -not -name "*.symbols.nupkg" | head -n 1 | xargs dirname) + + if [ -z "$nupkg_dir" ]; then + error "Could not find NuGet packages in the extracted artifacts" + exit 1 + fi + + echo "$nupkg_dir" +} + +# Get package version from directory +get_package_version() { + local packages_dir="$1" + + local package_file=$(find "$packages_dir" -type f -name "$PACKAGE_NAME.*.nupkg" -not -name "*.symbols.nupkg" | head -n 1) + + if [ -z "$package_file" ]; then + error "Could not find $PACKAGE_NAME package in artifacts" + exit 1 + fi + + local filename=$(basename "$package_file") + if [[ "$filename" =~ $PACKAGE_NAME\.(.+)\.nupkg ]]; then + echo "${BASH_REMATCH[1]}" + return 0 + fi + + error "Could not extract version from package filename: $filename" + exit 1 +} + +# Detect target framework version +get_target_framework_version() { + local project_path="$1" + + local content=$(cat "$project_path") + + if [[ "$content" =~ \([^\<]+)\ ]]; then + local tfms="${BASH_REMATCH[1]}" + + if [[ "$tfms" =~ net([0-9]+)\.0 ]]; then + echo "${BASH_REMATCH[1]}" + return 0 + fi + fi + + error "Could not determine target framework version from project file" + exit 1 +} + +# Extract .NET version from package version +get_package_dotnet_version() { + local version="$1" + + # Extract major version from package (e.g., "10.0.20-ci..." -> 10) + if [[ "$version" =~ ^([0-9]+)\. ]]; then + echo "${BASH_REMATCH[1]}" + return 0 + fi + + # Default to current stable if can't determine + echo "9" +} + +# Check if version matches target framework +test_version_compatibility() { + local version="$1" + local target_net_version="$2" + local package_net_version="$3" + + if [[ "$version" =~ preview|ci\. ]]; then + if [ "$target_net_version" -lt "$package_net_version" ]; then + return 1 + fi + fi + + return 0 +} + +# Update target frameworks +update_target_frameworks() { + local project_path="$1" + local new_net_version="$2" + + # Create a backup + cp "$project_path" "$project_path.bak" + + # Update all netX.0-* references (including in conditional TargetFrameworks) + sed -i.tmp "s/net[0-9]\+\.0-/net$new_net_version.0-/g" "$project_path" + rm -f "$project_path.tmp" + + # Cleanup backup file on success + rm -f "$project_path.bak" + + success "Updated target frameworks to .NET $new_net_version.0" + warning "You may need to update other package dependencies to match .NET $new_net_version.0" +} + +# Create or update NuGet.config +update_nuget_config() { + local project_dir="$1" + local packages_dir="$2" + + local nuget_config="$project_dir/NuGet.config" + local source_name="maui-pr-build" + + if [ -f "$nuget_config" ]; then + info "Updating existing NuGet.config..." + + # Remove existing source with same name if it exists + sed -i.tmp "/| \n |" "$nuget_config" + + rm -f "$nuget_config.tmp" + else + info "Creating new NuGet.config..." + cat > "$nuget_config" < + + + + + + + +EOF + fi + + success "NuGet.config configured with local package source" +} + +# Update project package reference +update_package_reference() { + local project_path="$1" + local version="$2" + + # Create a backup + cp "$project_path" "$project_path.bak" + + local content=$(cat "$project_path") + + # Check if using $(MauiVersion) variable + if [[ "$content" =~ \\)|\1$version\2|g" "$project_path" + rm -f "$project_path.tmp" + + # Check if content was actually modified + if ! diff -q "$project_path" "$project_path.bak" > /dev/null 2>&1; then + # Cleanup backup file on success + rm -f "$project_path.bak" + success "Updated $PACKAGE_NAME to version $version" + else + # Restore backup and report error + mv "$project_path.bak" "$project_path" + error "Could not find $PACKAGE_NAME package reference in project file" + exit 1 + fi +} + +# Main execution +main() { + # Check arguments + if [ $# -lt 1 ]; then + error "Usage: $0 [PROJECT_PATH]" + exit 1 + fi + + pr_number="$1" # Global for error handler + local project_path_arg="${2:-}" + + # Check dependencies + check_dependencies + + # Display banner + echo -e "${MAGENTA}" + cat << "EOF" + +╔═══════════════════════════════════════════════════════════╗ +ā•‘ ā•‘ +ā•‘ .NET MAUI PR Build Applicator ā•‘ +ā•‘ ā•‘ +ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā• + +EOF + echo -e "${NC}" + + step "Finding MAUI project" + local project_path=$(find_maui_project "$project_path_arg") + local project_dir=$(dirname "$project_path") + local project_name=$(basename "$project_path") + success "Found project: $project_name" + + step "Fetching PR information" + local pr_json=$(get_pr_info "$pr_number") + local pr_title=$(echo "$pr_json" | jq -r '.title') + local pr_state=$(echo "$pr_json" | jq -r '.state') + local pr_sha=$(echo "$pr_json" | jq -r '.head.sha') + + info "PR #$pr_number: $pr_title" + info "State: $pr_state" + + step "Detecting target framework" + local target_net_version=$(get_target_framework_version "$project_path") + info "Current target framework: .NET $target_net_version.0" + + step "Finding build artifacts" + local build_id=$(get_build_info "$pr_sha") + + step "Downloading artifacts" + local download_url=$(get_build_artifacts "$build_id") + local packages_dir=$(get_artifacts "$download_url" "$build_id") + + step "Extracting package information" + local version=$(get_package_version "$packages_dir") + success "Found package version: $version" + + # Extract .NET version from package version (e.g., 10.0.20-ci.main.25607.5 -> 10) + local package_dotnet_version + package_dotnet_version=$(get_package_dotnet_version "$version") + + # Check compatibility + local will_update_tfm=false + local target_version="$package_dotnet_version.0" + if ! test_version_compatibility "$version" "$target_net_version" "$package_dotnet_version"; then + warning "This PR build may target a newer .NET version than your project" + info "Your project targets: .NET $target_net_version.0" + info "This PR build targets: .NET $package_dotnet_version.0" + + read -p "Do you want to update your project to .NET $target_version? (y/N) " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + will_update_tfm=true + warning "Note: You may need to manually update other package dependencies to versions compatible with .NET $target_version" + else + warning "Continuing without updating target framework. The package may not be compatible." + fi + fi + + # Confirmation prompt + echo "" + echo -e "${YELLOW}═══════════════════════════════════════════════════════════${NC}" + echo -e "${YELLOW} CONFIRMATION${NC}" + echo -e "${YELLOW}═══════════════════════════════════════════════════════════${NC}" + echo "" + echo -e "${CYAN}By continuing, you will apply the PR artifacts to your project.${NC}" + echo "" + warning "This should NOT be used in production and is for testing purposes only." + echo "" + echo -e "${CYAN}TIP: Create a separate Git branch for testing!${NC}" + echo -e "${GRAY} git checkout -b test-pr-$pr_number${NC}" + echo "" + echo -e "${WHITE}Please test the changes you are looking for, check for any side-effects,${NC}" + echo -e "${WHITE}and report your findings on:${NC}" + echo -e "${BLUE} https://github.com/dotnet/maui/pull/$pr_number${NC}" + echo "" + echo -e "${WHITE}Changes to be applied:${NC}" + echo -e "${GRAY} • Project: $project_name${NC}" + echo -e "${GRAY} • Package version: $version${NC}" + if [ "$will_update_tfm" = true ]; then + echo -e "${GRAY} • Target framework: Will be updated to .NET $target_version${NC}" + fi + echo "" + + read -p "Do you want to continue? (y/N) " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + warning "Operation cancelled by user" + exit 0 + fi + echo "" + + if [ "$will_update_tfm" = true ]; then + local target_net_version_to_apply=10 + if [[ -n "$package_dotnet_version" ]]; then + target_net_version_to_apply="$package_dotnet_version" + fi + update_target_frameworks "$project_path" "$target_net_version_to_apply" + target_net_version="$target_net_version_to_apply" + fi + + step "Configuring NuGet sources" + update_nuget_config "$project_dir" "$packages_dir" + + step "Updating package reference" + update_package_reference "$project_path" "$version" + + # Get latest stable version for revert instructions + local latest_stable=$(curl -s "https://api.nuget.org/v3-flatcontainer/microsoft.maui.controls/index.json" | \ + jq -r '.versions[]' | grep -v '-' | tail -1) + if [ -z "$latest_stable" ]; then + latest_stable="X.Y.Z" + fi + + echo -e "${GREEN}" + cat << EOF + +╔═══════════════════════════════════════════════════════════╗ +ā•‘ ā•‘ +ā•‘ āœ… Successfully applied PR #$pr_number! ā•‘ +ā•‘ ā•‘ +ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā• + +EOF + echo -e "${NC}" + + info "Next steps:" + echo " 1. Run 'dotnet restore' to download the packages" + echo " 2. Build and test your project with the PR changes" + echo -e " 3. Report your findings on: ${CYAN}https://github.com/dotnet/maui/pull/$pr_number${NC}" + echo "" + info "Package: $PACKAGE_NAME $version" + info "Local package source: $packages_dir" + echo "" + + echo -e "${YELLOW}═══════════════════════════════════════════════════════════${NC}" + echo -e "${YELLOW} TO REVERT TO PRODUCTION VERSION${NC}" + echo -e "${YELLOW}═══════════════════════════════════════════════════════════${NC}" + echo "" + echo -e "${WHITE}1. Edit $project_name and change the version:${NC}" + echo -e "${GRAY} From: Version=\"$version\"${NC}" + echo -e "${GRAY} To: Version=\"X.Y.Z\"${NC}" + echo -e "${DGRAY} (Check https://www.nuget.org/packages/$PACKAGE_NAME for latest)${NC}" + echo "" + echo -e "${WHITE}2. In NuGet.config, remove or comment out the 'maui-pr-build' source${NC}" + echo "" + echo -e "${WHITE}3. Run: dotnet restore --force${NC}" + echo "" + echo -e "${CYAN}TIP: Use a separate Git branch for testing PR builds!${NC}" + echo -e "${CYAN} Then you can easily revert: git checkout main${NC}" + echo "" +} + +# Run main function +main "$@"