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 publishToOciRegistries
The 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.0
org.springframework:spring-core:5.3.21
io.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
groupId
to be OCI-compliant - Repository Path: Combine sanitized group and
artifactId
- Tag: Use the Maven
version
as 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-exampleversion
group/with/slashes
→groupwithslashes
-
Separator Consolidation: Replace consecutive separators with single hyphens
com..example
→com-example
mixed.-._.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-Type
headers - 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-publish
plugin 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 --debug
This 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 run
Licensed under the Apache License 2.0. See LICENSE for details.
- ORAS Java SDK - OCI Registry as Storage
- Inspired by oras-maven-plugin