A Gradle plugin that enables bidirectional Maven artifact management with OCI-compliant registries using the ORAS (OCI Registry as Storage) protocol.
- 📤 Publish Maven artifacts to OCI registries
- 📥 Consume Maven dependencies from OCI registries
- 🔄 Seamless integration with existing Gradle repositories
- 📂 Local caching for performance and offline access
- 🔐 Authentication support (username/password)
- 🗂️ Intelligent Maven ↔ OCI coordinate mapping
- Gradle 6.0+
- Java 17+ (required by ORAS Java SDK)
- Network access to OCI registries
plugins {
id 'java'
id 'io.seqera.maven-oci-registry'
}The plugin provides a clean, consistent syntax for both consuming and publishing:
Dependency Resolution:
repositories {
mavenOci {
url = 'https://registry.com/maven'
}
}Publishing:
publishing.repositories {
mavenOci {
name = 'myRegistry'
url = 'https://registry.com/maven'
credentials { ... }
}
}Add OCI repositories to your repositories block:
repositories {
mavenCentral() // Fallback for standard dependencies
mavenOci {
url = 'https://registry.example.com/maven'
}
}
dependencies {
implementation 'com.example:my-library:1.0.0'
}Configure publishing with the mavenOci extension. You can optionally specify a name for clean task names:
publishing {
publications {
maven(MavenPublication) {
from components.java
}
}
}
publishing {
publications {
maven(MavenPublication) {
from components.java
}
}
repositories {
// Traditional Maven repository
maven {
name = 'central'
url = 'https://repo1.maven.org/maven2/'
}
// Maven repository over OCI registry
mavenOci {
name = 'myRegistry'
url = 'https://registry.example.com'
credentials {
username = 'user'
password = 'pass'
}
}
}
}Then publish:
./gradlew publishToOciRegistriesThe plugin provides an overwritePolicy option to control behavior when publishing artifacts that already exist in the registry.
fail(default): Fails the build if the package already existsoverride: Replaces the existing package with the new versionskip: Skips publishing if the package already exists (build continues successfully)
publishing {
repositories {
mavenOci {
name = 'myRegistry'
url = 'https://registry.example.com'
overwritePolicy = 'skip' // fail | override | skip
credentials {
username = 'user'
password = 'pass'
}
}
}
}mavenOci {
name = 'registry'
overwritePolicy = 'fail' // or omit for default
}- Behavior: Build fails with clear error message if package exists
- Use case: Prevent accidental overwrites in production
- Output:
Package already exists in registry: registry.com/namespace/artifact:version. Use overwritePolicy = 'override' to replace it, or overwritePolicy = 'skip' to skip publishing.
mavenOci {
name = 'registry'
overwritePolicy = 'override'
}- Behavior: Replaces existing packages without checking
- Use case: Continuous deployment where artifacts should be updated
- Output: Package is replaced with new version
mavenOci {
name = 'registry'
overwritePolicy = 'skip'
}- Behavior: Skips publishing if package exists, build continues successfully
- Use case: Safe republishing scenarios where duplicates should be ignored
- Output:
Package already exists, skipping publication: registry.com/namespace/artifact:version
The Maven OCI Registry Plugin enables storing and retrieving Maven artifacts in OCI (Open Container Initiative) registries using the ORAS (OCI Registry as Storage) protocol. This requires a systematic mapping between Maven's coordinate system and OCI's naming conventions.
Maven uses a three-part coordinate system:
groupId:artifactId:version
For example:
com.example:my-library:1.0.0org.springframework:spring-core:5.3.21io.seqera.nextflow:nextflow-core:22.04.0
OCI registries use a hierarchical naming system:
registry.com/namespace/repository:tag
The plugin maps Maven coordinates to OCI references using the following algorithm:
- Registry Base: Use the configured registry URL as the base
- Group Sanitization: Transform the Maven
groupIdto be OCI-compliant - Repository Path: Combine sanitized group and
artifactId - Tag: Use the Maven
versionas the OCI tag
Maven: groupId:artifactId:version
OCI: registry.com/[namespace/]sanitized-groupId/artifactId:version
Maven group IDs often contain characters that are not valid in OCI repository names. The sanitization process applies these transformations:
-
Case Normalization: Convert to lowercase
Com.Example→com.example
-
Dot Replacement: Replace dots with hyphens for Docker compatibility
com.example→com-example
-
Invalid Character Removal: Keep only alphanumeric, dots, hyphens, underscores
com.example@version→com-exampleversiongroup/with/slashes→groupwithslashes
-
Separator Consolidation: Replace consecutive separators with single hyphens
com..example→com-examplemixed.-._.separators→mixed-separators
-
Trimming: Remove leading and trailing separators
.com.example.→com-example
-
Safety Prefix: Add "g" prefix if result starts with hyphen or underscore (handled by cleanup)
-example→example(separators are removed)
| Maven Coordinate | Sanitized Group | OCI Reference |
|---|---|---|
com.example:my-lib:1.0.0 |
com-example |
registry.com/com-example/my-lib:1.0.0 |
org.springframework:spring-core:5.3.21 |
org-springframework |
registry.com/org-springframework/spring-core:5.3.21 |
io.seqera.nextflow:nextflow-core:22.04.0 |
io-seqera-nextflow |
registry.com/io-seqera-nextflow/nextflow-core:22.04.0 |
Com.EXAMPLE.Test:artifact:1.0 |
com-example-test |
registry.com/com-example-test/artifact:1.0 |
com.fasterxml.jackson.core:jackson-core:2.13.0 |
com-fasterxml-jackson-core |
registry.com/com-fasterxml-jackson-core/jackson-core:2.13.0 |
OCI registries often use namespaces to organize repositories. The plugin supports multiple namespace levels embedded in the registry URL:
URL Path Namespace: Supports unlimited nested namespace levels
mavenOci {
url = 'https://registry.com/org/team/maven'
}Result: registry.com/org/team/maven/sanitized-group/artifact:version
Examples of supported namespace patterns:
https://registry.com→ No namespace (root level)https://registry.com/maven→ Single namespace levelhttps://registry.com/org/maven/snapshots→ Multiple namespace levelshttps://ghcr.io/myorg/maven→ GitHub Container Registry with namespace
For publishing, you can also configure namespace separately:
publishing {
repositories {
mavenOci {
name = 'docker'
url = 'https://registry.com'
namespace = 'org/team/maven'
}
}
}The Maven OCI Registry Plugin uses an HTTP proxy architecture that provides efficient dependency resolution while maintaining full compatibility with Gradle's dependency resolution system.
The plugin creates a local HTTP proxy server that mimics Maven repository structure and handles OCI resolution on-demand only when Gradle actually requests artifacts.
graph TD
A[User Configuration] --> B[Plugin Creates HTTP Proxy]
B --> C[Gradle Repository Points to Proxy]
C --> D[Gradle Dependency Resolution Starts]
D --> E[Try Maven Central First]
E -->|Found| F[Use Maven Central Artifact]
E -->|Not Found| G[Try OCI Proxy]
G --> H[Proxy Intercepts HTTP Request]
H --> I[Parse Maven Request Path]
I --> J[Convert to OCI Reference]
J --> K[Fetch from OCI Registry with ORAS]
K -->|Success| L[Stream Artifact to Gradle]
K -->|Not Found| M[Return HTTP 404]
L --> N[Gradle Uses Artifact]
M --> O[Gradle Tries Next Repository]
The HTTP proxy approach ensures proper repository ordering:
| Scenario | Resolution Flow |
|---|---|
org.slf4j:slf4j-api:2.0.7 |
1. Try Maven Central (success) STOP - no OCI call needed |
com.example:my-lib:1.0.0 |
1. Try Maven Central (fail) 2. Try OCI (success) |
When you configure mavenOci { url = 'https://registry.com' }:
- Plugin starts local HTTP server on
http://localhost:RANDOM_PORT/maven/ - Maven repository is configured to point to this proxy URL
- Works naturally with Gradle's resolution order
When Gradle makes HTTP requests like:
GET http://localhost:8543/maven/com/example/my-lib/1.0.0/my-lib-1.0.0.jar
The proxy:
- Parses the Maven coordinate:
com.example:my-lib:1.0.0 - Converts to OCI reference:
registry.com/com-example/my-lib:1.0.0 - Uses ORAS Java SDK to fetch from OCI registry on-demand
- Downloads artifacts to temporary directory
- Streams artifact bytes directly back via HTTP response
- Sets appropriate
Content-Typeheaders - Caches in-memory for build session (optional)
- Artifact not found in OCI: Returns HTTP 404, Gradle continues to next repository
- Network failure: Returns HTTP 500, Gradle continues to next repository
- Invalid request: Returns HTTP 400, request rejected
✅ True Lazy Resolution: Only resolves artifacts when Gradle actually requests them
✅ Repository Order Respect: No unnecessary OCI calls for Maven Central artifacts
✅ Network Efficiency: Dramatic reduction in network calls for mixed dependency trees
✅ Memory Efficiency: No persistent cache directories to manage
✅ Build Speed: Faster builds, especially for projects with many standard dependencies
repositories {
mavenCentral() # Checked FIRST
mavenOci { url = '...' } # Only if needed
}
dependencies {
implementation 'org.slf4j:slf4j-api:2.0.7'
}Resolution:
- Gradle tries Maven Central → FOUND ✅
- Proxy never called → Zero unnecessary OCI network calls
dependencies {
implementation 'com.example:my-lib:1.0.0'
}Resolution:
- Gradle tries Maven Central → 404 Not Found
- Gradle tries OCI proxy → GET http://localhost:8543/maven/...
- Proxy converts to OCI:
registry.com/com-example/my-lib:1.0.0 - ORAS SDK fetches artifact → HTTP 200 + JAR bytes
The proxy server is automatically managed:
- Startup: Dynamic port allocation avoids conflicts
- Runtime: Thread-safe concurrent request handling
- Session Caching: Optional in-memory cache for repeated requests
- Shutdown: Automatic cleanup when Gradle build finishes
The plugin creates the following tasks:
publishToOciRegistries- Publishes all publications to all OCI repositoriespublish<Publication>To<Repository>Repository- Publishes specific publication to specific repository- Example:
publishMavenPublicationToMyRegistryRepository
- Example:
The plugin integrates with standard Gradle publishing:
publish- Publishes to all configured repositories (Maven + OCI)publishToMavenLocal- Local Maven repository publishing- All standard
maven-publishplugin tasks
mavenOci {
name = 'registry'
url = 'https://registry.example.com'
credentials {
username = 'user'
password = 'password'
}
}mavenOci {
name = 'registry'
url = 'https://registry.example.com'
credentials {
username = System.getenv('REGISTRY_USERNAME')
password = System.getenv('REGISTRY_PASSWORD')
}
}mavenOci {
url = 'http://localhost:5000'
insecure = true
}repositories {
mavenOci {
url = 'https://internal.company.com/maven'
credentials { /* ... */ }
}
mavenOci {
url = 'https://public.registry.com/maven'
}
mavenCentral() // Fallback
}publishing {
repositories {
mavenOci {
name = 'custom'
url = 'https://registry.com'
namespace = 'my-org/maven-artifacts'
}
}
}repositories {
mavenCentral()
// Public OCI registry
mavenOci {
url = 'https://public.cr.seqera.io/maven'
}
// Private registry with authentication
mavenOci {
url = 'https://registry.company.com/maven'
credentials {
username = project.findProperty('registryUser')
password = project.findProperty('registryPass')
}
}
// Local development registry
mavenOci {
url = 'http://localhost:5000'
insecure = true
}
}
dependencies {
implementation 'com.company:internal-lib:1.0.0' // Tries OCI first
implementation 'org.springframework:spring-core:5.3.21' // Falls back to Maven Central
}publishing {
publications {
maven(MavenPublication) {
from components.java
}
}
repositories {
mavenOci {
name = 'dockerHub'
url = 'https://registry-1.docker.io/maven'
credentials {
username = System.getenv('DOCKER_USERNAME')
password = System.getenv('DOCKER_PASSWORD')
}
}
}
}- Group ID Design: Use consistent, hierarchical group IDs that sanitize well
- Registry Organization: Use namespaces to organize different types of artifacts
- Authentication: Use environment variables or Gradle properties for credentials
- Fallback Strategy: Always include Maven Central or other standard repositories
- Cache Management: Monitor cache sizes and clean up if necessary
- Network Configuration: Configure appropriate timeouts for your network environment
- Authentication: Currently supports username/password only (no OAuth, tokens, or Docker credential helpers)
- Repository Creation: Requires container registries that allow creating arbitrary nested repositories on-demand (not all registries support this)
- Metadata: POM files may be generated if not included in OCI artifacts
- Transitive Dependencies: Only direct artifacts are cached; transitive dependency metadata relies on generated POMs
- Parallel Downloads: OCI resolution is sequential (no parallel downloads)
- Error Recovery: Failed OCI resolutions require cache cleanup for retry
- Registry Features: No support for OCI registry-specific features (signatures, attestations, etc.)
Enable debug logging to see detailed resolution information:
./gradlew build --debugThis will show:
- OCI reference generation
- Cache hit/miss information
- Network requests and responses
- Error details and stack traces
rm -rf .gradle/oci-cache/- Sanitization Problems: Check how your group ID is being sanitized
- Authentication Failures: Verify credentials and registry permissions
- Network Issues: Check connectivity to the OCI registry
- Cache Corruption: Delete the cache directory and rebuild
- Network connectivity: Check firewall and proxy settings
- Dependency not found: Verify OCI reference mapping
Working examples in example/:
example/publisher/- Publishing artifactsexample/consumer/- Consuming dependencies
# Test publisher
cd example/publisher && ./gradlew publishToOciRegistries --dry-run
# Test consumer
cd example/consumer && ./gradlew runLicensed under the Apache License 2.0. See LICENSE for details.
- ORAS Java SDK - OCI Registry as Storage
- Inspired by oras-maven-plugin