From f1da3ce57e62a11251eaad1fa4de2148b11a839a Mon Sep 17 00:00:00 2001 From: Ahmed Muhsin Date: Wed, 5 Aug 2026 13:51:40 -0500 Subject: [PATCH 1/2] Onboard Maven restores to CFS --- .gitattributes | 2 + .gitignore | 6 +- README.md | 21 ++++ build.ps1 | 20 ++-- eng/ci/templates/jobs/build.yml | 13 ++ .../official/jobs/build-and-test.yml | 13 ++ .../Install-MavenCredentialProvider.ps1 | 113 ++++++++++++++++++ eng/scripts/install-maven-credprovider.sh | 88 ++++++++++++++ pom.xml | 32 ++++- settings.xml | 35 ++++++ 10 files changed, 327 insertions(+), 16 deletions(-) create mode 100644 .gitattributes create mode 100644 eng/scripts/Install-MavenCredentialProvider.ps1 create mode 100644 eng/scripts/install-maven-credprovider.sh create mode 100644 settings.xml diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..567e212 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Shell scripts must retain LF endings on every platform. +*.sh text eol=lf \ No newline at end of file diff --git a/.gitignore b/.gitignore index 044af7d..f254aa6 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,8 @@ hs_err_pid* *.iml #OSX -.DS_Store \ No newline at end of file +.DS_Store + +# The Maven credential provider is an opt-in for ingesting uncached packages. CI authenticates +# with MavenAuthenticate@0, and committing the extension would break anonymous restores. +.mvn/ \ No newline at end of file diff --git a/README.md b/README.md index 4037c6c..c23b382 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,27 @@ This repo contains library for building Azure Java Functions. Visit the [complet ## Prerequisites * Java 8 +* [Apache Maven](https://maven.apache.org/) 3.0 or later + +## Package feed + +Maven packages and plugins are restored through the `upstream-public` Azure Artifacts feed. The +root `pom.xml` overrides Maven's `central` repository, and `settings.xml` mirrors early plugin and +extension requests to the same feed. + +Packages already cached in the feed can be restored anonymously. When a new package version has not +been cached yet, a Microsoft developer can install the Azure Artifacts credential provider: + +```powershell +./eng/scripts/Install-MavenCredentialProvider.ps1 +``` + +```bash +./eng/scripts/install-maven-credprovider.sh +``` + +The helper creates a local `.mvn/extensions.xml`, which is intentionally ignored by Git. CI installs +the repository `settings.xml` and authenticates with `MavenAuthenticate@0` before running Maven. ## Parent POM diff --git a/build.ps1 b/build.ps1 index 6f098bd..9c7e1ff 100644 --- a/build.ps1 +++ b/build.ps1 @@ -135,9 +135,6 @@ Pop-Location -StackName "libraryDir" $ApplicationInsightsAgentVersion = '3.5.2' $ApplicationInsightsAgentFilename = "applicationinsights-agent-${ApplicationInsightsAgentVersion}.jar" -$ApplicationInsightsAgentUrl = "https://repo1.maven.org/maven2/com/microsoft/azure/applicationinsights-agent/${ApplicationInsightsAgentVersion}/${ApplicationInsightsAgentFilename}" - -# Download application insights agent from maven central $ApplicationInsightsAgentFile = "$currDir/$ApplicationInsightsAgentFilename" # local testing cleanup @@ -157,14 +154,15 @@ if (Test-Path -Path $oldExtract) { Remove-Item -Path $oldExtract -Recurse } -echo "Start downloading '$ApplicationInsightsAgentUrl' to '$currDir'" -try { - Invoke-WebRequest -Uri $ApplicationInsightsAgentUrl -OutFile $ApplicationInsightsAgentFile -} catch { - echo "An error occurred. Download fails" $ApplicationInsightsAgentFile - echo "Exiting" - exit 1 -} +Write-Host "Restoring '$ApplicationInsightsAgentFilename' through Maven" +$mavenArguments = @( + '--batch-mode' + 'org.apache.maven.plugins:maven-dependency-plugin:3.8.1:copy' + "-Dartifact=com.microsoft.azure:applicationinsights-agent:${ApplicationInsightsAgentVersion}:jar" + "-DoutputDirectory=$currDir" +) +& mvn @mavenArguments +StopOnFailedExecution if (-not(Test-Path -Path $ApplicationInsightsAgentFile)) { echo "$ApplicationInsightsAgentFile do not exist." diff --git a/eng/ci/templates/jobs/build.yml b/eng/ci/templates/jobs/build.yml index 8ec1b2d..a3da951 100644 --- a/eng/ci/templates/jobs/build.yml +++ b/eng/ci/templates/jobs/build.yml @@ -13,6 +13,19 @@ jobs: inputs: workingFile: .npmrc + # Maven resolves plugins and extensions before a pom's repositories are honored. Install the + # mirror before MavenAuthenticate@0, which adds credentials to the same settings file. + - pwsh: | + $m2 = Join-Path $HOME '.m2' + New-Item -ItemType Directory -Path $m2 -Force | Out-Null + Copy-Item '$(Build.SourcesDirectory)/settings.xml' (Join-Path $m2 'settings.xml') -Force + displayName: 'Install Maven settings.xml' + + - task: MavenAuthenticate@0 + displayName: 'Authenticate Maven to CFS' + inputs: + artifactsFeeds: upstream-public + - pwsh: | Write-Host "Java_HOME: $JAVA_HOME" Get-Command mvn diff --git a/eng/ci/templates/official/jobs/build-and-test.yml b/eng/ci/templates/official/jobs/build-and-test.yml index 294e8f4..412f9f2 100644 --- a/eng/ci/templates/official/jobs/build-and-test.yml +++ b/eng/ci/templates/official/jobs/build-and-test.yml @@ -24,6 +24,19 @@ jobs: - task: NuGetAuthenticate@1 displayName: 'Authenticate NuGet to CFS' + # Maven resolves plugins and extensions before a pom's repositories are honored. Install the + # mirror before MavenAuthenticate@0, which adds credentials to the same settings file. + - pwsh: | + $m2 = Join-Path $HOME '.m2' + New-Item -ItemType Directory -Path $m2 -Force | Out-Null + Copy-Item '$(Build.SourcesDirectory)/settings.xml' (Join-Path $m2 'settings.xml') -Force + displayName: 'Install Maven settings.xml' + + - task: MavenAuthenticate@0 + displayName: 'Authenticate Maven to CFS' + inputs: + artifactsFeeds: upstream-public + - pwsh: | Write-Host "Java_HOME: $env:JAVA_HOME" Get-Command mvn diff --git a/eng/scripts/Install-MavenCredentialProvider.ps1 b/eng/scripts/Install-MavenCredentialProvider.ps1 new file mode 100644 index 0000000..ca71763 --- /dev/null +++ b/eng/scripts/Install-MavenCredentialProvider.ps1 @@ -0,0 +1,113 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS + Installs the Azure Artifacts Maven credential provider for local development. + +.DESCRIPTION + Anonymous restores work for packages already cached in the CFS feed. Microsoft developers can + run this script to authenticate and ingest a package version that has not been cached yet. + +.PARAMETER Version + Credential provider version to install. + +.PARAMETER LocalRepositoryPath + Maven local repository path. Defaults to ~/.m2/repository. + +.PARAMETER Force + Reinstalls the provider and overwrites the generated .mvn/extensions.xml. +#> + +[CmdletBinding()] +param( + [string] $Version = '3.2.1', + [string] $LocalRepositoryPath, + [switch] $Force +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$groupId = 'com.microsoft.azure' +$artifactId = 'artifacts-maven-credprovider' +$bootstrapFeed = 'https://pkgs.dev.azure.com/artifacts-public/PublicTools/_packaging/AzureArtifacts/maven/v1' +$repositoryId = 'central' +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..' '..')).Path + +if (-not (Get-Command mvn -ErrorAction SilentlyContinue)) { + throw "Maven ('mvn') was not found on PATH." +} + +$customLocalRepository = -not [string]::IsNullOrWhiteSpace($LocalRepositoryPath) +if (-not $customLocalRepository) { + $LocalRepositoryPath = Join-Path $HOME '.m2' 'repository' +} + +$artifactDirectory = $LocalRepositoryPath +foreach ($segment in ($groupId.Split('.') + @($artifactId, $Version))) { + $artifactDirectory = Join-Path $artifactDirectory $segment +} +$artifactPath = Join-Path $artifactDirectory "$artifactId-$Version.jar" + +if ($Force -or -not (Test-Path $artifactPath)) { + $workingDirectory = Join-Path ([IO.Path]::GetTempPath()) ('maven-credprovider-' + [Guid]::NewGuid().ToString('n')) + New-Item -ItemType Directory -Path $workingDirectory -Force | Out-Null + try { + Push-Location $workingDirectory + try { + $arguments = @( + '--batch-mode' + 'dependency:get' + "-Dartifact=${groupId}:${artifactId}:${Version}" + "-DremoteRepositories=${repositoryId}::::${bootstrapFeed}" + ) + if ($customLocalRepository) { + $arguments += "-Dmaven.repo.local=$LocalRepositoryPath" + } + & mvn @arguments + if ($LASTEXITCODE -ne 0) { + throw "Maven failed with exit code $LASTEXITCODE." + } + } + finally { + Pop-Location + } + } + finally { + Remove-Item $workingDirectory -Recurse -Force -ErrorAction SilentlyContinue + } +} + +if (-not (Test-Path $artifactPath)) { + throw "Credential provider was not found at '$artifactPath' after installation." +} + +$extensionsDirectory = Join-Path $repoRoot '.mvn' +$extensionsPath = Join-Path $extensionsDirectory 'extensions.xml' +if ((Test-Path $extensionsPath) -and -not $Force) { + $existing = Get-Content $extensionsPath -Raw + if ($existing -notmatch [regex]::Escape($artifactId)) { + throw "'$extensionsPath' contains an unmanaged Maven extension. Use -Force to overwrite it." + } + if ($existing -match "\s*$([regex]::Escape($Version))\s*") { + Write-Host "Maven credential provider $Version is already configured." + return + } +} + +$extensions = @" + + + + $groupId + $artifactId + $Version + + +"@ + +New-Item -ItemType Directory -Path $extensionsDirectory -Force | Out-Null +Set-Content -Path $extensionsPath -Value $extensions -Encoding utf8 +Write-Host "Configured Maven credential provider $Version in '$extensionsPath'." \ No newline at end of file diff --git a/eng/scripts/install-maven-credprovider.sh b/eng/scripts/install-maven-credprovider.sh new file mode 100644 index 0000000..626b77e --- /dev/null +++ b/eng/scripts/install-maven-credprovider.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash + +set -euo pipefail + +group_id='com.microsoft.azure' +artifact_id='artifacts-maven-credprovider' +bootstrap_feed='https://pkgs.dev.azure.com/artifacts-public/PublicTools/_packaging/AzureArtifacts/maven/v1' +repository_id='central' +version='3.2.1' +local_repository_path='' +force=false + +usage() { + cat <<'EOF' +Usage: install-maven-credprovider.sh [options] + +Options: + -v, --version Credential provider version to install. + -l, --local-repository Maven local repository path. + -f, --force Reinstall and overwrite .mvn/extensions.xml. + -h, --help Show this help text. +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + -v|--version) [[ $# -ge 2 ]] || { echo "error: $1 requires a value" >&2; exit 1; }; version="$2"; shift 2 ;; + -l|--local-repository) [[ $# -ge 2 ]] || { echo "error: $1 requires a value" >&2; exit 1; }; local_repository_path="$2"; shift 2 ;; + -f|--force) force=true; shift ;; + -h|--help) usage; exit 0 ;; + *) echo "error: unknown argument '$1'" >&2; usage >&2; exit 1 ;; + esac +done + +command -v mvn >/dev/null 2>&1 || { echo "error: Maven ('mvn') was not found on PATH." >&2; exit 1; } + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd -- "$script_dir/../.." && pwd)" +custom_local_repository=true +if [[ -z "$local_repository_path" ]]; then + custom_local_repository=false + local_repository_path="$HOME/.m2/repository" +fi + +group_path="${group_id//./\/}" +artifact_path="$local_repository_path/$group_path/$artifact_id/$version/$artifact_id-$version.jar" +if [[ "$force" == true || ! -f "$artifact_path" ]]; then + working_directory="$(mktemp -d)" + trap 'rm -rf "$working_directory"' EXIT + arguments=( + --batch-mode + dependency:get + "-Dartifact=${group_id}:${artifact_id}:${version}" + "-DremoteRepositories=${repository_id}::::${bootstrap_feed}" + ) + if [[ "$custom_local_repository" == true ]]; then + arguments+=("-Dmaven.repo.local=$local_repository_path") + fi + (cd "$working_directory" && mvn "${arguments[@]}") +fi + +[[ -f "$artifact_path" ]] || { echo "error: credential provider was not found at '$artifact_path'." >&2; exit 1; } + +extensions_directory="$repo_root/.mvn" +extensions_path="$extensions_directory/extensions.xml" +if [[ -f "$extensions_path" && "$force" != true ]]; then + grep -q "$artifact_id" "$extensions_path" || { echo "error: '$extensions_path' contains an unmanaged Maven extension." >&2; exit 1; } + if grep -qE "[[:space:]]*${version//./\.}[[:space:]]*" "$extensions_path"; then + echo "Maven credential provider $version is already configured." + exit 0 + fi +fi + +mkdir -p "$extensions_directory" +cat >"$extensions_path" < + + + $group_id + $artifact_id + $version + + +EOF + +echo "Configured Maven credential provider $version in '$extensions_path'." \ No newline at end of file diff --git a/pom.xml b/pom.xml index 8a0e26c..bf51a44 100644 --- a/pom.xml +++ b/pom.xml @@ -59,12 +59,12 @@ + - maven.snapshots - Maven Central Snapshot Repository - https://oss.sonatype.org/content/repositories/snapshots/ + central + https://pkgs.dev.azure.com/azfunc/public/_packaging/upstream-public/maven/v1 - false + true true @@ -72,6 +72,30 @@ + + + central + https://pkgs.dev.azure.com/azfunc/public/_packaging/upstream-public/maven/v1 + + true + + + true + + + + + sonatype-nexus-snapshots + https://pkgs.dev.azure.com/azfunc/public/_packaging/upstream-public/maven/v1 + + false + + + true + + + + com.microsoft.azure.functions diff --git a/settings.xml b/settings.xml new file mode 100644 index 0000000..d4ed171 --- /dev/null +++ b/settings.xml @@ -0,0 +1,35 @@ + + + + + + upstream-public + Azure Functions public upstream feed + https://pkgs.dev.azure.com/azfunc/public/_packaging/upstream-public/maven/v1 + central + + + \ No newline at end of file From 99552dbd79e2d5f5635aeb5620203b69d5686a7d Mon Sep 17 00:00:00 2001 From: Ahmed Muhsin Date: Wed, 5 Aug 2026 21:41:59 -0500 Subject: [PATCH 2/2] Align CFS tooling with reference --- .gitattributes | 2 +- .gitignore | 2 +- README.md | 100 +++++++++- build.ps1 | 2 +- .../official/jobs/build-and-test.yml | 2 +- .../Install-MavenCredentialProvider.ps1 | 97 +++++++--- eng/scripts/install-maven-credprovider.sh | 171 +++++++++++++----- settings.xml | 2 +- 8 files changed, 290 insertions(+), 88 deletions(-) diff --git a/.gitattributes b/.gitattributes index 567e212..10212bc 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,2 @@ # Shell scripts must retain LF endings on every platform. -*.sh text eol=lf \ No newline at end of file +*.sh text eol=lf diff --git a/.gitignore b/.gitignore index f254aa6..1e34738 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,4 @@ hs_err_pid* # The Maven credential provider is an opt-in for ingesting uncached packages. CI authenticates # with MavenAuthenticate@0, and committing the extension would break anonymous restores. -.mvn/ \ No newline at end of file +.mvn/ diff --git a/README.md b/README.md index c23b382..f2168fd 100644 --- a/README.md +++ b/README.md @@ -21,12 +21,47 @@ This repo contains library for building Azure Java Functions. Visit the [complet ## Package feed -Maven packages and plugins are restored through the `upstream-public` Azure Artifacts feed. The -root `pom.xml` overrides Maven's `central` repository, and `settings.xml` mirrors early plugin and -extension requests to the same feed. +All Maven packages and plugins are restored from the `upstream-public` Azure Artifacts feed +(`https://pkgs.dev.azure.com/azfunc/public/_packaging/upstream-public/maven/v1`), which is configured +as the `central` repository in every `pom.xml` in this repository. -Packages already cached in the feed can be restored anonymously. When a new package version has not -been cached yet, a Microsoft developer can install the Azure Artifacts credential provider: +The repository root also has a [`settings.xml`](settings.xml) that mirrors `central` to the same +feed. It exists because a `pom.xml` cannot cover everything: + +- Maven resolves build extensions and plugin prefixes *before* a pom's `` are honored, + so those requests would otherwise go straight to Maven Central. +- `MavenAuthenticate@0` and the credential provider key credentials off the Azure Artifacts *feed + name* (`upstream-public`), while the pom repository id must be `central` in order to override the + id Maven inherits from the Super POM. The mirror id bridges the two. + +CI installs this file to `~/.m2/settings.xml`. Locally you only need it when pulling a package or +version the feed has not cached yet, in which case pass it explicitly with `mvn -s settings.xml`. + +### Anonymous restore (default) + +The feed allows anonymous reads, so no credentials are required to build once a package version has +been saved to the feed. External contributors and fresh clones need no setup. `mvn` just works. +Never commit credentials or a `` entry to `settings.xml` in this repository because doing so +would force authentication on everyone. + +### Authenticating (Microsoft developers only) + +Authentication is only needed to *ingest* a package version that the feed has not cached yet. The +first restore of any new or upgraded dependency will fail anonymously with: + +> No local versions of package '...'; please provide authentication to access versions from upstream +> that have not yet been saved to your feed. + +When that happens, a Microsoft developer with access to the `azfunc/public` project must run the +restore once with credentials, which pulls the version from upstream and saves it to the feed. Every +subsequent anonymous restore then succeeds. + +The recommended way to authenticate is the `artifacts-maven-credprovider`, which acquires a token via +Entra ID so you do not have to manage a PAT. + +Run the helper script for your shell from the root of your clone. It installs the credential provider +into your local Maven repository if it is missing, then writes `.mvn/extensions.xml`. Both scripts +are idempotent, so re-running them is safe: ```powershell ./eng/scripts/Install-MavenCredentialProvider.ps1 @@ -36,8 +71,59 @@ been cached yet, a Microsoft developer can install the Azure Artifacts credentia ./eng/scripts/install-maven-credprovider.sh ``` -The helper creates a local `.mvn/extensions.xml`, which is intentionally ignored by Git. CI installs -the repository `settings.xml` and authenticates with `MavenAuthenticate@0` before running Maven. +Pass `-Version` / `--version` to install a different release, and `-Force` / `--force` to reinstall or +to overwrite an `.mvn/extensions.xml` the script does not manage. + +If you would rather do it by hand, the equivalent steps are: + +1. Bootstrap the credential provider once per machine. Run this from a directory outside any Maven + project, such as your home directory. It downloads the extension from the public `AzureArtifacts` + tools feed, which needs no authentication: + + ```powershell + mvn dependency:get "-Dartifact=com.microsoft.azure:artifacts-maven-credprovider:3.2.1" "-DremoteRepositories=central::::https://pkgs.dev.azure.com/artifacts-public/PublicTools/_packaging/AzureArtifacts/maven/v1" + ``` + + Using the repository id `central` matters. Maven records the extension as having come from + `central`, which is the same id this repository's `pom.xml` files declare, so the cached copy + validates during later builds. + +2. Create `.mvn/extensions.xml` at the root of your clone: + + ```xml + + + com.microsoft.azure + artifacts-maven-credprovider + 3.2.1 + + + ``` + +`.mvn/` is deliberately listed in `.gitignore`. Do not commit it. The extension exits when it +detects a build context, and committing it would break anonymous restores for everyone else. + +If you would rather not use the credential provider, you can instead add a `` entry to your +user-level `~/.m2/settings.xml` (never to a file inside this repository), using an Azure DevOps +personal access token with Packaging read and write scope: + +```xml + + + + + central + azfunc + [PERSONAL_ACCESS_TOKEN] + + + +``` + +CI covers this automatically. The `MavenAuthenticate@0` task in the build templates authenticates the +`central` repository, so merged changes to dependency versions are ingested by the pipeline. The +credential provider is not used in pipelines. ## Parent POM diff --git a/build.ps1 b/build.ps1 index 9c7e1ff..a1701d8 100644 --- a/build.ps1 +++ b/build.ps1 @@ -212,4 +212,4 @@ Write-Host "Creating the functions.codeless file" New-Item -path $currDir\agent -type file -name "functions.codeless" Write-Host "Copying the unsigned Application Insights Agent to worker directory" -Copy-Item "$currDir/agent" "$currDir/azure-functions-java-worker/Azure.Functions.Cli/workers/java" -Recurse -Verbose -Force \ No newline at end of file +Copy-Item "$currDir/agent" "$currDir/azure-functions-java-worker/Azure.Functions.Cli/workers/java" -Recurse -Verbose -Force diff --git a/eng/ci/templates/official/jobs/build-and-test.yml b/eng/ci/templates/official/jobs/build-and-test.yml index 412f9f2..5eef6f5 100644 --- a/eng/ci/templates/official/jobs/build-and-test.yml +++ b/eng/ci/templates/official/jobs/build-and-test.yml @@ -95,4 +95,4 @@ jobs: JAVA_HOME: $(JAVA_HOME_8_X64) displayName: 'Build & Run tests for java 8' condition: eq(${{ parameters.runEndToEndTests }}, true) - \ No newline at end of file + diff --git a/eng/scripts/Install-MavenCredentialProvider.ps1 b/eng/scripts/Install-MavenCredentialProvider.ps1 index ca71763..254508f 100644 --- a/eng/scripts/Install-MavenCredentialProvider.ps1 +++ b/eng/scripts/Install-MavenCredentialProvider.ps1 @@ -2,20 +2,37 @@ <# .SYNOPSIS - Installs the Azure Artifacts Maven credential provider for local development. + Bootstraps the Azure Artifacts Maven credential provider for local development. .DESCRIPTION - Anonymous restores work for packages already cached in the CFS feed. Microsoft developers can - run this script to authenticate and ingest a package version that has not been cached yet. + Maven packages for this repository are restored from an Azure Artifacts feed. Reads are + anonymous, so this script is only needed by Microsoft developers who have to ingest a package + version that the feed has not cached yet. + + The script: + 1. Verifies the credential provider is present in the local Maven repository, and downloads it + from the public AzureArtifacts tools feed if it is not. + 2. Writes '.mvn/extensions.xml' at the root of the repository so Maven loads the provider. + + '.mvn/' is intentionally listed in .gitignore. The extension exits when it detects a build + context, and committing it would force an authenticated restore on anonymous consumers. Azure + Pipelines uses the MavenAuthenticate@0 task instead. .PARAMETER Version - Credential provider version to install. + Version of the credential provider to install. Defaults to the version pinned by this script. .PARAMETER LocalRepositoryPath - Maven local repository path. Defaults to ~/.m2/repository. + Path to the local Maven repository. Defaults to '~/.m2/repository'. .PARAMETER Force - Reinstalls the provider and overwrites the generated .mvn/extensions.xml. + Overwrite an existing '.mvn/extensions.xml' even if it declares extensions this script does not + manage, and re-download the credential provider even when it is already installed. + +.EXAMPLE + ./eng/scripts/Install-MavenCredentialProvider.ps1 + +.LINK + https://eng.ms/docs/coreai/devdiv/one-engineering-system-1es/1es-docs/azure-artifacts/maven-credprovider #> [CmdletBinding()] @@ -31,15 +48,18 @@ $ErrorActionPreference = 'Stop' $groupId = 'com.microsoft.azure' $artifactId = 'artifacts-maven-credprovider' $bootstrapFeed = 'https://pkgs.dev.azure.com/artifacts-public/PublicTools/_packaging/AzureArtifacts/maven/v1' + +# Maven records the extension against this repository id. It must match the of the repositories +# declared in this repository's pom.xml files, otherwise resolution fails validation later. $repositoryId = 'central' + $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..' '..')).Path if (-not (Get-Command mvn -ErrorAction SilentlyContinue)) { - throw "Maven ('mvn') was not found on PATH." + throw "Maven ('mvn') was not found on PATH. Install Apache Maven 3.0 or above and try again." } -$customLocalRepository = -not [string]::IsNullOrWhiteSpace($LocalRepositoryPath) -if (-not $customLocalRepository) { +if (-not $LocalRepositoryPath) { $LocalRepositoryPath = Join-Path $HOME '.m2' 'repository' } @@ -47,26 +67,37 @@ $artifactDirectory = $LocalRepositoryPath foreach ($segment in ($groupId.Split('.') + @($artifactId, $Version))) { $artifactDirectory = Join-Path $artifactDirectory $segment } + $artifactPath = Join-Path $artifactDirectory "$artifactId-$Version.jar" -if ($Force -or -not (Test-Path $artifactPath)) { - $workingDirectory = Join-Path ([IO.Path]::GetTempPath()) ('maven-credprovider-' + [Guid]::NewGuid().ToString('n')) +if ((Test-Path $artifactPath) -and -not $Force) { + Write-Host "Credential provider $Version is already installed at '$artifactPath'." +} +else { + Write-Host "Installing credential provider $Version from the public tools feed..." + + # The bootstrap must run outside of any Maven project so that this repository's own repository + # and extension configuration does not take part in resolving the extension itself. + $workingDirectory = Join-Path ([IO.Path]::GetTempPath()) ('credprovider-bootstrap-' + [Guid]::NewGuid().ToString('n')) New-Item -ItemType Directory -Path $workingDirectory -Force | Out-Null + try { Push-Location $workingDirectory try { - $arguments = @( + $mvnArgs = @( '--batch-mode' 'dependency:get' "-Dartifact=${groupId}:${artifactId}:${Version}" "-DremoteRepositories=${repositoryId}::::${bootstrapFeed}" ) - if ($customLocalRepository) { - $arguments += "-Dmaven.repo.local=$LocalRepositoryPath" + + if ($PSBoundParameters.ContainsKey('LocalRepositoryPath')) { + $mvnArgs += "-Dmaven.repo.local=$LocalRepositoryPath" } - & mvn @arguments + + & mvn @mvnArgs if ($LASTEXITCODE -ne 0) { - throw "Maven failed with exit code $LASTEXITCODE." + throw "'mvn dependency:get' failed with exit code $LASTEXITCODE." } } finally { @@ -76,30 +107,40 @@ if ($Force -or -not (Test-Path $artifactPath)) { finally { Remove-Item $workingDirectory -Recurse -Force -ErrorAction SilentlyContinue } -} -if (-not (Test-Path $artifactPath)) { - throw "Credential provider was not found at '$artifactPath' after installation." + if (-not (Test-Path $artifactPath)) { + throw "Bootstrap reported success but '$artifactPath' was not found. If a mirror is configured in your settings.xml, temporarily disable it and retry." + } + + Write-Host "Installed credential provider to '$artifactPath'." } $extensionsDirectory = Join-Path $repoRoot '.mvn' $extensionsPath = Join-Path $extensionsDirectory 'extensions.xml' + if ((Test-Path $extensionsPath) -and -not $Force) { $existing = Get-Content $extensionsPath -Raw + if ($existing -notmatch [regex]::Escape($artifactId)) { - throw "'$extensionsPath' contains an unmanaged Maven extension. Use -Force to overwrite it." + throw "'$extensionsPath' already exists and declares extensions this script does not manage. Review it manually, or re-run with -Force to overwrite it." } + if ($existing -match "\s*$([regex]::Escape($Version))\s*") { - Write-Host "Maven credential provider $Version is already configured." + Write-Host "'$extensionsPath' is already configured for version $Version." + Write-Host 'Done.' return } } -$extensions = @" +$extensionsContent = @" - + + $groupId $artifactId @@ -109,5 +150,7 @@ $extensions = @" "@ New-Item -ItemType Directory -Path $extensionsDirectory -Force | Out-Null -Set-Content -Path $extensionsPath -Value $extensions -Encoding utf8 -Write-Host "Configured Maven credential provider $Version in '$extensionsPath'." \ No newline at end of file +Set-Content -Path $extensionsPath -Value $extensionsContent -Encoding utf8 + +Write-Host "Wrote '$extensionsPath' for version $Version." +Write-Host 'Done.' diff --git a/eng/scripts/install-maven-credprovider.sh b/eng/scripts/install-maven-credprovider.sh index 626b77e..00135d4 100644 --- a/eng/scripts/install-maven-credprovider.sh +++ b/eng/scripts/install-maven-credprovider.sh @@ -1,88 +1,161 @@ #!/usr/bin/env bash +# +# Bootstraps the Azure Artifacts Maven credential provider for local development. +# +# Maven packages for this repository are restored from an Azure Artifacts feed. Reads are anonymous, +# so this script is only needed by Microsoft developers who have to ingest a package version that +# the feed has not cached yet. +# +# The script: +# 1. Verifies the credential provider is present in the local Maven repository, and downloads it +# from the public AzureArtifacts tools feed if it is not. +# 2. Writes '.mvn/extensions.xml' at the root of the repository so Maven loads the provider. +# +# '.mvn/' is intentionally listed in .gitignore. The extension exits when it detects a build context, +# and committing it would force an authenticated restore on anonymous consumers. Azure Pipelines +# uses the MavenAuthenticate@0 task instead. +# +# See https://eng.ms/docs/coreai/devdiv/one-engineering-system-1es/1es-docs/azure-artifacts/maven-credprovider set -euo pipefail -group_id='com.microsoft.azure' -artifact_id='artifacts-maven-credprovider' -bootstrap_feed='https://pkgs.dev.azure.com/artifacts-public/PublicTools/_packaging/AzureArtifacts/maven/v1' -repository_id='central' +GROUP_ID='com.microsoft.azure' +ARTIFACT_ID='artifacts-maven-credprovider' +BOOTSTRAP_FEED='https://pkgs.dev.azure.com/artifacts-public/PublicTools/_packaging/AzureArtifacts/maven/v1' + +# Maven records the extension against this repository id. It must match the of the repositories +# declared in this repository's pom.xml files, otherwise resolution fails validation later. +REPOSITORY_ID='central' + version='3.2.1' local_repository_path='' force=false usage() { - cat <<'EOF' + cat <<'EOF' Usage: install-maven-credprovider.sh [options] Options: - -v, --version Credential provider version to install. - -l, --local-repository Maven local repository path. - -f, --force Reinstall and overwrite .mvn/extensions.xml. - -h, --help Show this help text. + -v, --version Version of the credential provider to install. + -l, --local-repository Path to the local Maven repository. Defaults to ~/.m2/repository. + -f, --force Overwrite an unmanaged .mvn/extensions.xml and re-download the + credential provider even when it is already installed. + -h, --help Show this help text. EOF } while [[ $# -gt 0 ]]; do - case "$1" in - -v|--version) [[ $# -ge 2 ]] || { echo "error: $1 requires a value" >&2; exit 1; }; version="$2"; shift 2 ;; - -l|--local-repository) [[ $# -ge 2 ]] || { echo "error: $1 requires a value" >&2; exit 1; }; local_repository_path="$2"; shift 2 ;; - -f|--force) force=true; shift ;; - -h|--help) usage; exit 0 ;; - *) echo "error: unknown argument '$1'" >&2; usage >&2; exit 1 ;; - esac + case "$1" in + -v|--version) + [[ $# -ge 2 ]] || { echo "error: $1 requires a value" >&2; exit 1; } + version="$2" + shift 2 + ;; + -l|--local-repository) + [[ $# -ge 2 ]] || { echo "error: $1 requires a value" >&2; exit 1; } + local_repository_path="$2" + shift 2 + ;; + -f|--force) + force=true + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "error: unknown argument '$1'" >&2 + usage >&2 + exit 1 + ;; + esac done -command -v mvn >/dev/null 2>&1 || { echo "error: Maven ('mvn') was not found on PATH." >&2; exit 1; } - script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" repo_root="$(cd -- "$script_dir/../.." && pwd)" -custom_local_repository=true -if [[ -z "$local_repository_path" ]]; then - custom_local_repository=false - local_repository_path="$HOME/.m2/repository" + +if ! command -v mvn >/dev/null 2>&1; then + echo "error: Maven ('mvn') was not found on PATH. Install Apache Maven 3.0 or above and try again." >&2 + exit 1 fi -group_path="${group_id//./\/}" -artifact_path="$local_repository_path/$group_path/$artifact_id/$version/$artifact_id-$version.jar" -if [[ "$force" == true || ! -f "$artifact_path" ]]; then - working_directory="$(mktemp -d)" - trap 'rm -rf "$working_directory"' EXIT - arguments=( - --batch-mode - dependency:get - "-Dartifact=${group_id}:${artifact_id}:${version}" - "-DremoteRepositories=${repository_id}::::${bootstrap_feed}" - ) - if [[ "$custom_local_repository" == true ]]; then - arguments+=("-Dmaven.repo.local=$local_repository_path") - fi - (cd "$working_directory" && mvn "${arguments[@]}") +local_repository_specified=true +if [[ -z "$local_repository_path" ]]; then + local_repository_specified=false + local_repository_path="$HOME/.m2/repository" fi -[[ -f "$artifact_path" ]] || { echo "error: credential provider was not found at '$artifact_path'." >&2; exit 1; } +group_path="${GROUP_ID//./\/}" +artifact_path="$local_repository_path/$group_path/$ARTIFACT_ID/$version/$ARTIFACT_ID-$version.jar" + +if [[ -f "$artifact_path" && "$force" != true ]]; then + echo "Credential provider $version is already installed at '$artifact_path'." +else + echo "Installing credential provider $version from the public tools feed..." + + # The bootstrap must run outside of any Maven project so that this repository's own repository + # and extension configuration does not take part in resolving the extension itself. + working_directory="$(mktemp -d)" + cleanup() { rm -rf "$working_directory"; } + trap cleanup EXIT + + mvn_args=( + --batch-mode + dependency:get + "-Dartifact=${GROUP_ID}:${ARTIFACT_ID}:${version}" + "-DremoteRepositories=${REPOSITORY_ID}::::${BOOTSTRAP_FEED}" + ) + + if [[ "$local_repository_specified" == true ]]; then + mvn_args+=("-Dmaven.repo.local=$local_repository_path") + fi + + (cd "$working_directory" && mvn "${mvn_args[@]}") + + if [[ ! -f "$artifact_path" ]]; then + echo "error: bootstrap reported success but '$artifact_path' was not found." >&2 + echo "If a mirror is configured in your settings.xml, temporarily disable it and retry." >&2 + exit 1 + fi + + echo "Installed credential provider to '$artifact_path'." +fi extensions_directory="$repo_root/.mvn" extensions_path="$extensions_directory/extensions.xml" + if [[ -f "$extensions_path" && "$force" != true ]]; then - grep -q "$artifact_id" "$extensions_path" || { echo "error: '$extensions_path' contains an unmanaged Maven extension." >&2; exit 1; } - if grep -qE "[[:space:]]*${version//./\.}[[:space:]]*" "$extensions_path"; then - echo "Maven credential provider $version is already configured." - exit 0 - fi + if ! grep -q "$ARTIFACT_ID" "$extensions_path"; then + echo "error: '$extensions_path' already exists and declares extensions this script does not manage." >&2 + echo "Review it manually, or re-run with --force to overwrite it." >&2 + exit 1 + fi + + if grep -qE "[[:space:]]*${version//./\\.}[[:space:]]*" "$extensions_path"; then + echo "'$extensions_path' is already configured for version $version." + echo 'Done.' + exit 0 + fi fi mkdir -p "$extensions_directory" cat >"$extensions_path" < - + + - $group_id - $artifact_id + $GROUP_ID + $ARTIFACT_ID $version EOF -echo "Configured Maven credential provider $version in '$extensions_path'." \ No newline at end of file +echo "Wrote '$extensions_path' for version $version." +echo 'Done.' diff --git a/settings.xml b/settings.xml index d4ed171..23e083d 100644 --- a/settings.xml +++ b/settings.xml @@ -32,4 +32,4 @@ central - \ No newline at end of file +