Artifacts

Maven and Gradle with Azure Artifacts

A practical guide to configuring Maven and Gradle projects to publish and consume Java packages from Azure Artifacts feeds, including authentication, upstream sources, and CI/CD pipeline integration.

Maven and Gradle with Azure Artifacts

Overview

Azure Artifacts supports Maven and Gradle as first-class package types, giving Java teams a private artifact repository that integrates directly with Azure DevOps pipelines and permissions. If you are building Java libraries internally and distributing them across teams, Azure Artifacts replaces the need to run your own Nexus or Artifactory instance. You get feed management, upstream proxying of Maven Central, and authentication that ties into your Azure DevOps identity -- all without maintaining additional infrastructure.

I have migrated several organizations from self-hosted Nexus repositories to Azure Artifacts, and the experience is consistently smoother once you get the authentication configuration right. Maven and Gradle each have their own way of handling credentials, and Azure DevOps adds a layer on top with Personal Access Tokens and service connections. This article walks through the complete setup for both build tools, including publishing, consuming, upstream sources, and pipeline automation.

Prerequisites

  • An Azure DevOps organization with an active project
  • An Azure Artifacts feed created (organization-scoped or project-scoped)
  • Java JDK 11 or later installed
  • Maven 3.8+ or Gradle 7.0+ installed locally
  • An Azure DevOps Personal Access Token (PAT) with Packaging (Read & Write) scope
  • Node.js 18+ for the automation scripts
  • Basic familiarity with pom.xml and Gradle build files

Understanding Azure Artifacts Feed URLs for Maven

Every Azure Artifacts feed exposes Maven-compatible repository URLs. You need two URLs:

  • Repository URL (for consuming packages): https://pkgs.dev.azure.com/{org}/{project}/_packaging/{feed}/maven/v1
  • Snapshot Repository URL (for snapshot versions): same URL, Azure Artifacts handles snapshots and releases in the same feed

You can find these URLs in the Azure DevOps portal under Artifacts > Connect to Feed > Maven.

Maven Configuration

settings.xml Setup

Maven reads credentials from ~/.m2/settings.xml, not from pom.xml. This is important -- never put credentials in your POM file because it gets committed to source control.

Create or update your ~/.m2/settings.xml:

<?xml version="1.0" encoding="utf-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
                              https://maven.apache.org/xsd/settings-1.0.0.xsd">
  <servers>
    <server>
      <id>azure-artifacts</id>
      <username>azure</username>
      <password>${env.AZURE_DEVOPS_PAT}</password>
    </server>
  </servers>

  <profiles>
    <profile>
      <id>azure-artifacts</id>
      <repositories>
        <repository>
          <id>azure-artifacts</id>
          <url>https://pkgs.dev.azure.com/my-organization/my-project/_packaging/java-packages/maven/v1</url>
          <releases>
            <enabled>true</enabled>
          </releases>
          <snapshots>
            <enabled>true</enabled>
          </snapshots>
        </repository>
      </repositories>
    </profile>
  </profiles>

  <activeProfiles>
    <activeProfile>azure-artifacts</activeProfile>
  </activeProfiles>
</settings>

The critical detail here is that the <id> in <server> must match the <id> in <repository>. Maven uses this ID to link the credentials to the repository. If they do not match, you get 401 errors that are frustrating to debug because Maven does not tell you it could not find matching credentials.

The ${env.AZURE_DEVOPS_PAT} syntax reads the PAT from an environment variable. Set it:

# Linux/macOS
export AZURE_DEVOPS_PAT="your-pat-here"

# Windows
set AZURE_DEVOPS_PAT=your-pat-here

pom.xml Distribution Management

Add the distribution management section to your library's pom.xml to enable publishing:

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.mycompany</groupId>
  <artifactId>shared-utilities</artifactId>
  <version>1.0.0</version>
  <packaging>jar</packaging>

  <name>Shared Utilities</name>
  <description>Internal utility library for platform services</description>

  <distributionManagement>
    <repository>
      <id>azure-artifacts</id>
      <url>https://pkgs.dev.azure.com/my-organization/my-project/_packaging/java-packages/maven/v1</url>
    </repository>
    <snapshotRepository>
      <id>azure-artifacts</id>
      <url>https://pkgs.dev.azure.com/my-organization/my-project/_packaging/java-packages/maven/v1</url>
    </snapshotRepository>
  </distributionManagement>

  <dependencies>
    <dependency>
      <groupId>com.google.guava</groupId>
      <artifactId>guava</artifactId>
      <version>32.1.3-jre</version>
    </dependency>
  </dependencies>
</project>

Again, the <id>azure-artifacts</id> in <repository> must match the server ID in settings.xml.

Publishing with Maven

# Deploy a release version
mvn deploy

# Deploy with a specific version
mvn versions:set -DnewVersion=1.1.0
mvn deploy

# Deploy a snapshot
mvn versions:set -DnewVersion=1.1.0-SNAPSHOT
mvn deploy

Output from a successful deploy:

[INFO] --- maven-deploy-plugin:3.1.1:deploy (default-deploy) @ shared-utilities ---
Uploading to azure-artifacts: https://pkgs.dev.azure.com/my-organization/my-project/_packaging/java-packages/maven/v1/com/mycompany/shared-utilities/1.0.0/shared-utilities-1.0.0.jar
Uploaded to azure-artifacts: https://pkgs.dev.azure.com/my-organization/my-project/_packaging/java-packages/maven/v1/com/mycompany/shared-utilities/1.0.0/shared-utilities-1.0.0.jar (15 kB at 12 kB/s)
Uploading to azure-artifacts: https://pkgs.dev.azure.com/my-organization/my-project/_packaging/java-packages/maven/v1/com/mycompany/shared-utilities/1.0.0/shared-utilities-1.0.0.pom
Uploaded to azure-artifacts: https://pkgs.dev.azure.com/my-organization/my-project/_packaging/java-packages/maven/v1/com/mycompany/shared-utilities/1.0.0/shared-utilities-1.0.0.pom (1.2 kB at 980 B/s)
[INFO] BUILD SUCCESS

Snapshots vs Releases

Azure Artifacts handles snapshots differently than a traditional Nexus server. When you publish a SNAPSHOT version (e.g., 1.1.0-SNAPSHOT), Azure Artifacts stores each upload as a unique timestamped version. Consuming projects resolve the latest snapshot automatically.

I recommend using snapshots only during active development within a team. For cross-team consumption, always publish release versions. Snapshot dependencies make builds non-reproducible, and debugging an issue where "it worked yesterday" because a snapshot changed overnight is a miserable experience.

Gradle Configuration

build.gradle Setup

Gradle handles repositories and credentials differently from Maven. Here is a complete build.gradle for a library project:

plugins {
    id 'java-library'
    id 'maven-publish'
}

group = 'com.mycompany'
version = '1.0.0'

java {
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
    withSourcesJar()
    withJavadocJar()
}

repositories {
    maven {
        name 'AzureArtifacts'
        url 'https://pkgs.dev.azure.com/my-organization/my-project/_packaging/java-packages/maven/v1'
        credentials {
            username 'azure'
            password System.getenv('AZURE_DEVOPS_PAT')
        }
    }
    mavenCentral()
}

dependencies {
    implementation 'com.google.guava:guava:32.1.3-jre'
    testImplementation 'junit:junit:4.13.2'
}

publishing {
    publications {
        mavenJava(MavenPublication) {
            from components.java

            pom {
                name = 'Shared Utilities'
                description = 'Internal utility library for platform services'
            }
        }
    }

    repositories {
        maven {
            name 'AzureArtifacts'
            url 'https://pkgs.dev.azure.com/my-organization/my-project/_packaging/java-packages/maven/v1'
            credentials {
                username 'azure'
                password System.getenv('AZURE_DEVOPS_PAT')
            }
        }
    }
}

build.gradle.kts (Kotlin DSL)

If you use the Kotlin DSL for Gradle, the configuration looks like this:

plugins {
    `java-library`
    `maven-publish`
}

group = "com.mycompany"
version = "1.0.0"

java {
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
    withSourcesJar()
    withJavadocJar()
}

repositories {
    maven {
        name = "AzureArtifacts"
        url = uri("https://pkgs.dev.azure.com/my-organization/my-project/_packaging/java-packages/maven/v1")
        credentials {
            username = "azure"
            password = System.getenv("AZURE_DEVOPS_PAT")
        }
    }
    mavenCentral()
}

dependencies {
    implementation("com.google.guava:guava:32.1.3-jre")
    testImplementation("junit:junit:4.13.2")
}

publishing {
    publications {
        create<MavenPublication>("mavenJava") {
            from(components["java"])

            pom {
                name.set("Shared Utilities")
                description.set("Internal utility library for platform services")
            }
        }
    }

    repositories {
        maven {
            name = "AzureArtifacts"
            url = uri("https://pkgs.dev.azure.com/my-organization/my-project/_packaging/java-packages/maven/v1")
            credentials {
                username = "azure"
                password = System.getenv("AZURE_DEVOPS_PAT")
            }
        }
    }
}

Publishing with Gradle

# Publish to Azure Artifacts
gradle publish

# Publish with a custom version
gradle publish -Pversion=1.1.0

# Publish a snapshot
gradle publish -Pversion=1.1.0-SNAPSHOT

Output:

> Task :compileJava
> Task :processResources NO-SOURCE
> Task :classes
> Task :jar
> Task :sourcesJar
> Task :javadocJar
> Task :generateMetadataFileForMavenJavaPublication
> Task :generatePomFileForMavenJavaPublication
> Task :publishMavenJavaPublicationToAzureArtifactsRepository

BUILD SUCCESSFUL in 8s
7 actionable tasks: 7 executed

Upstream Sources: Proxying Maven Central

When you enable Maven Central as an upstream source on your Azure Artifacts feed, your feed acts as a proxy. Developers configure only your feed URL, and requests for packages not found in your feed are automatically forwarded to Maven Central. Once fetched, the package is cached in your feed.

To add Maven Central as an upstream source, navigate to Artifacts > Feed Settings > Upstream Sources and add:

  • Name: Maven Central
  • Type: Public
  • URL: https://repo.maven.apache.org/maven2

Or configure it via the REST API:

// add-maven-upstream.js
var https = require("https");

var org = "my-organization";
var project = "my-project";
var feedId = "java-packages";
var pat = process.env.AZURE_DEVOPS_PAT;
var auth = Buffer.from(":" + pat).toString("base64");

function getFeed(callback) {
  var options = {
    hostname: "feeds.dev.azure.com",
    path: "/" + org + "/" + project + "/_apis/packaging/feeds/" + feedId + "?api-version=7.1",
    method: "GET",
    headers: { "Authorization": "Basic " + auth }
  };

  var req = https.request(options, function(res) {
    var data = "";
    res.on("data", function(chunk) { data += chunk; });
    res.on("end", function() { callback(null, JSON.parse(data)); });
  });
  req.on("error", callback);
  req.end();
}

function addUpstream(feed) {
  var hasUpstream = feed.upstreamSources.some(function(s) {
    return s.name === "Maven Central";
  });

  if (hasUpstream) {
    console.log("Maven Central upstream already configured");
    return;
  }

  feed.upstreamSources.push({
    name: "Maven Central",
    protocol: "maven",
    location: "https://repo.maven.apache.org/maven2",
    upstreamSourceType: "public"
  });

  var body = JSON.stringify(feed);
  var options = {
    hostname: "feeds.dev.azure.com",
    path: "/" + org + "/" + project + "/_apis/packaging/feeds/" + feedId + "?api-version=7.1",
    method: "PATCH",
    headers: {
      "Content-Type": "application/json",
      "Authorization": "Basic " + auth,
      "Content-Length": Buffer.byteLength(body)
    }
  };

  var req = https.request(options, function(res) {
    var data = "";
    res.on("data", function(chunk) { data += chunk; });
    res.on("end", function() {
      if (res.statusCode === 200) {
        console.log("Maven Central upstream added successfully");
      } else {
        console.error("Failed (" + res.statusCode + "):", data);
      }
    });
  });

  req.write(body);
  req.end();
}

getFeed(function(err, feed) {
  if (err) return console.error("Error:", err.message);
  addUpstream(feed);
});

The upstream proxy has one important behavior to understand: package resolution order. Azure Artifacts checks the local feed first, then upstream sources in the order they are configured. If you have a local package with the same group ID and artifact ID as a public package, the local version takes precedence. This is by design and lets you override public packages when needed, but it can also cause confusion if you accidentally publish a package with a conflicting name.

Pipeline Integration

Maven Pipeline

trigger:
  branches:
    include:
      - main
      - develop

pool:
  vmImage: ubuntu-latest

variables:
  mavenFeed: java-packages
  MAVEN_CACHE_FOLDER: $(Pipeline.Workspace)/.m2/repository
  MAVEN_OPTS: -Dmaven.repo.local=$(MAVEN_CACHE_FOLDER)

steps:
  - task: Cache@2
    inputs:
      key: 'maven | "$(Agent.OS)" | **/pom.xml'
      restoreKeys: |
        maven | "$(Agent.OS)"
      path: $(MAVEN_CACHE_FOLDER)
    displayName: Cache Maven dependencies

  - task: MavenAuthenticate@0
    inputs:
      artifactsFeeds: $(mavenFeed)
    displayName: Authenticate with Azure Artifacts

  - task: Maven@4
    inputs:
      mavenPomFile: pom.xml
      goals: clean verify
      options: $(MAVEN_OPTS)
    displayName: Build and test

  - task: Maven@4
    inputs:
      mavenPomFile: pom.xml
      goals: deploy
      options: $(MAVEN_OPTS) -DskipTests
    displayName: Publish to Azure Artifacts
    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))

The MavenAuthenticate@0 task injects credentials into the Maven settings at runtime. You do not need to manage settings.xml in your pipeline -- the task handles it. This is the recommended approach for CI/CD.

Gradle Pipeline

trigger:
  branches:
    include:
      - main
      - develop

pool:
  vmImage: ubuntu-latest

variables:
  mavenFeed: java-packages
  GRADLE_USER_HOME: $(Pipeline.Workspace)/.gradle

steps:
  - task: Cache@2
    inputs:
      key: 'gradle | "$(Agent.OS)" | **/*.gradle* | **/gradle-wrapper.properties'
      restoreKeys: |
        gradle | "$(Agent.OS)"
      path: $(GRADLE_USER_HOME)
    displayName: Cache Gradle dependencies

  - task: Gradle@3
    inputs:
      gradleWrapperFile: gradlew
      tasks: clean build
      javaHomeOption: JDKVersion
      jdkVersionOption: '1.11'
    displayName: Build and test
    env:
      AZURE_DEVOPS_PAT: $(System.AccessToken)

  - task: Gradle@3
    inputs:
      gradleWrapperFile: gradlew
      tasks: publish
      javaHomeOption: JDKVersion
      jdkVersionOption: '1.11'
    displayName: Publish to Azure Artifacts
    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    env:
      AZURE_DEVOPS_PAT: $(System.AccessToken)

Note the $(System.AccessToken) -- this is the pipeline's built-in OAuth token. It avoids the need for a separate PAT. However, the build service identity needs Contributor permission on the feed.

Complete Working Example

This example demonstrates a multi-module Maven project with a shared library and a consuming service, automated through an Azure Pipeline.

Project Structure

my-java-project/
  pom.xml (parent POM)
  shared-lib/
    pom.xml
    src/main/java/com/mycompany/shared/DateUtils.java
  api-service/
    pom.xml
    src/main/java/com/mycompany/api/Application.java
  azure-pipelines.yml

Parent POM

<?xml version="1.0" encoding="utf-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.mycompany</groupId>
  <artifactId>my-java-project</artifactId>
  <version>1.0.0</version>
  <packaging>pom</packaging>

  <modules>
    <module>shared-lib</module>
    <module>api-service</module>
  </modules>

  <properties>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <repositories>
    <repository>
      <id>azure-artifacts</id>
      <url>https://pkgs.dev.azure.com/my-organization/my-project/_packaging/java-packages/maven/v1</url>
      <releases>
        <enabled>true</enabled>
      </releases>
      <snapshots>
        <enabled>true</enabled>
      </snapshots>
    </repository>
  </repositories>

  <distributionManagement>
    <repository>
      <id>azure-artifacts</id>
      <url>https://pkgs.dev.azure.com/my-organization/my-project/_packaging/java-packages/maven/v1</url>
    </repository>
    <snapshotRepository>
      <id>azure-artifacts</id>
      <url>https://pkgs.dev.azure.com/my-organization/my-project/_packaging/java-packages/maven/v1</url>
    </snapshotRepository>
  </distributionManagement>
</project>

Shared Library

<!-- shared-lib/pom.xml -->
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <parent>
    <groupId>com.mycompany</groupId>
    <artifactId>my-java-project</artifactId>
    <version>1.0.0</version>
  </parent>

  <artifactId>shared-utilities</artifactId>
  <version>1.0.0</version>
  <packaging>jar</packaging>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.13.2</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>
// shared-lib/src/main/java/com/mycompany/shared/DateUtils.java
package com.mycompany.shared;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.ZoneOffset;

public class DateUtils {

    private static final DateTimeFormatter ISO_FORMAT =
        DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");

    public static String toISOString(LocalDateTime dateTime) {
        return dateTime.format(ISO_FORMAT);
    }

    public static long toEpochMillis(LocalDateTime dateTime) {
        return dateTime.toInstant(ZoneOffset.UTC).toEpochMilli();
    }

    public static LocalDateTime fromEpochMillis(long epochMillis) {
        return LocalDateTime.ofEpochSecond(
            epochMillis / 1000, 0, ZoneOffset.UTC);
    }
}

Pipeline YAML

trigger:
  branches:
    include:
      - main

pool:
  vmImage: ubuntu-latest

variables:
  mavenFeed: java-packages
  MAVEN_CACHE_FOLDER: $(Pipeline.Workspace)/.m2/repository
  MAVEN_OPTS: -Dmaven.repo.local=$(MAVEN_CACHE_FOLDER)

stages:
  - stage: Build
    jobs:
      - job: BuildAndTest
        steps:
          - task: Cache@2
            inputs:
              key: 'maven | "$(Agent.OS)" | **/pom.xml'
              restoreKeys: maven | "$(Agent.OS)"
              path: $(MAVEN_CACHE_FOLDER)
            displayName: Cache Maven local repo

          - task: MavenAuthenticate@0
            inputs:
              artifactsFeeds: $(mavenFeed)
            displayName: Authenticate with feed

          - task: Maven@4
            inputs:
              mavenPomFile: pom.xml
              goals: clean verify
              options: $(MAVEN_OPTS)
            displayName: Build and run tests

          - task: PublishTestResults@2
            inputs:
              testResultsFormat: JUnit
              testResultsFiles: '**/surefire-reports/TEST-*.xml'
            condition: always()
            displayName: Publish test results

  - stage: Publish
    dependsOn: Build
    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    jobs:
      - job: PublishLibrary
        steps:
          - task: Cache@2
            inputs:
              key: 'maven | "$(Agent.OS)" | **/pom.xml'
              restoreKeys: maven | "$(Agent.OS)"
              path: $(MAVEN_CACHE_FOLDER)
            displayName: Cache Maven local repo

          - task: MavenAuthenticate@0
            inputs:
              artifactsFeeds: $(mavenFeed)
            displayName: Authenticate with feed

          - task: Maven@4
            inputs:
              mavenPomFile: shared-lib/pom.xml
              goals: deploy
              options: $(MAVEN_OPTS) -DskipTests
            displayName: Publish shared-lib to Azure Artifacts

Common Issues and Troubleshooting

1. 401 Unauthorized During Maven Deploy

Error:

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-deploy-plugin:3.1.1:deploy
  Return code is: 401, ReasonPhrase: Unauthorized.

The server ID in settings.xml does not match the repository ID in your pom.xml. Check that both use the exact same ID string. Also verify your PAT has not expired and has the Packaging (Read & Write) scope. A common mistake is creating a PAT with only the Read scope.

2. Gradle Cannot Resolve Dependencies from Feed

Error:

> Could not resolve com.mycompany:shared-utilities:1.0.0.
  > Could not get resource 'https://pkgs.dev.azure.com/.../shared-utilities-1.0.0.pom'.
    > Could not GET 'https://pkgs.dev.azure.com/.../shared-utilities-1.0.0.pom'. Received status code 401

Gradle reads credentials from the build.gradle file directly. Make sure the AZURE_DEVOPS_PAT environment variable is set. In CI pipelines, pass it as an environment variable to the Gradle task using env: AZURE_DEVOPS_PAT: $(System.AccessToken).

3. Snapshot Not Updating to Latest Version

Error: Your consuming project keeps resolving an old snapshot even though you just published a new one.

Maven and Gradle cache snapshots locally. For Maven, delete the local cache or force an update:

mvn dependency:resolve -U

The -U flag forces Maven to check for updated snapshots. For Gradle:

gradle build --refresh-dependencies

4. MavenAuthenticate Task Not Found in Pipeline

Error:

##[error]Task MavenAuthenticate@0 not found.

This task requires the Azure Artifacts extension to be installed in your Azure DevOps organization. Navigate to Organization Settings > Extensions and search for "Azure Artifacts". If your organization is on the hosted service, this should already be available.

5. POM Validation Errors During Deploy

Error:

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-deploy-plugin:3.1.1:deploy
  The POM for com.mycompany:shared-utilities:jar:1.0.0 is invalid, transitive dependencies will not be resolved

Your POM file is missing required elements or has invalid XML. Run mvn validate locally before deploying. Common issues include missing <groupId>, <artifactId>, or <version> elements, or encoding issues in the XML header.

6. Gradle Publish Succeeds But Package Not Visible in Feed

Error: BUILD SUCCESSFUL but the package does not appear in the Azure Artifacts UI.

Check that you are publishing to the correct feed URL. Enable Gradle debug logging to see the exact HTTP requests:

gradle publish --debug 2>&1 | grep "PUT"

Also verify the publications block in your build.gradle includes from components.java. Without it, Gradle publishes an empty POM without the actual JAR.

Best Practices

  1. Use settings.xml for Maven credentials, never pom.xml. The pom.xml gets committed to source control and shared with everyone. Credentials belong in settings.xml which stays on the developer machine or is injected by the CI system via MavenAuthenticate@0.

  2. Use $(System.AccessToken) in pipelines instead of PATs. The system access token is automatically generated per pipeline run, has the minimum necessary permissions, and never expires. PATs are a liability -- they expire, they get shared, they get committed to source control.

  3. Enable Maven Central as an upstream source. This gives developers a single feed URL for both internal and public packages. It also creates a cached copy of every public dependency your organization uses, protecting against upstream outages and package deletions.

  4. Use the Gradle wrapper (gradlew) in pipelines. The wrapper ensures every developer and CI agent uses the same Gradle version. Check gradle/wrapper/gradle-wrapper.properties and the wrapper JAR into source control.

  5. Publish snapshots from feature branches, releases from main only. Feature branch builds should produce SNAPSHOT versions that are safe to iterate on. Only main branch builds should publish release versions to prevent accidental releases.

  6. Cache Maven and Gradle dependencies in pipelines. The Cache@2 task saves and restores the local repository between builds. This reduces build times significantly -- a cold Maven build that takes 5 minutes to download dependencies can drop to 30 seconds with caching.

  7. Set the <clear /> equivalent for Maven repositories. In your settings.xml, avoid inheriting default repositories by explicitly listing only your Azure Artifacts feed. This prevents builds from accidentally pulling packages from unexpected sources.

  8. Pin dependency versions explicitly. Do not use version ranges like [1.0,2.0) in production code. Lock to specific versions. Use a parent POM or Gradle platform to centralize version management across modules.

  9. Include sources and Javadoc JARs when publishing. This makes your internal packages as usable as public ones. In Gradle, add withSourcesJar() and withJavadocJar(). In Maven, configure the maven-source-plugin and maven-javadoc-plugin.

  10. Test your authentication locally before debugging in CI. Most feed issues are authentication-related. If mvn deploy or gradle publish works locally with your PAT, the issue in CI is almost certainly a missing or misconfigured authentication task.

References

Powered by Contentful