Publish snapshot into maven central from github action

Note: post covers only Gradle, but it could be easilly adopted for Maven (github action would be nearly the same - all important moments described; some maven-related links included)
Suppose you developed a library and related examples in a single repository (e.g. examples stored as a separate project in the examples
directory). It would be great to not only run library tests on CI, but also check examples compatibility with the latest snapshot. In theory it’s simple: run library tests, publish snapshot and run examples with a just published snapshot.
Previously, I have tried to use github packages for snapshots publication, but this was far from perfect:
Github packages are not accessible without authorization - users have to create their own github tokens in order to access snapshots.
Each published snapshot is counted as a separate package and so, after some time, you have to clean up outdated versions manually (becuase storage space is limited)
Recently, sonatype sunset OSSRH publication so I have to update my maven central publications and reviewed my snapshots approach.
Maven central snapshots publication specifics:
Snapshot is removed after 90 days (more than enough)
No signing is required (and overall publication validation is disabled)
Published snapshots are available immediately (in contrast to releases, which are available after ~1h)
Users would have to use a custom repository to access snapshots (but without required authorization!)
Sonatype configuration
Important: It is assumed that your namespace was already migrated (migration ended on 30th of June 2025). If you did not perform migration manually, it would be performed automatically (in some time).
First of all, snapshots must be enabled for your namespace:
It is also important to generate a new token. Even if you already have token, generated before June 30th, it’s better to re-generate it (there are many messages on the sonatype forum about 401 problems with the old tokens).
Open https://central.sonatype.com/account and hit “generate token”. Copy generated tokens (user and password tokens) because window will close in 1 minute.
Project configuration
In spite of the fact that OSSRH is shut down, sonatype provides a OSSRH-compatible publication API. With it you can just change target maven repositories and don’t need to search for new plugins.
I use gradle maven-publish plugin. In my case, configuration looks like:
nexusPublishing {
repositories {
sonatype {
nexusUrl = uri("https://ossrh-staging-api.central.sonatype.com/service/local/")
snapshotRepositoryUrl = uri("https://central.sonatype.com/repository/maven-snapshots/")
username = findProperty('sonatypeUser')
password = findProperty('sonatypePassword')
}
}
}
Only urls are important here. The plugin configures sonatype
maven publication, which is only important for snapshot publication (you can configure maven publication manually - only release would require additinoal actions, handled by this plugin).
Locally, sonatypeUser
and sonatypePassword
properties could be specified in the global gradle properties file (~/.gradle/gradle.properties
). On CI, environment variables would be used (ORG_GRADLE_PROJECT_sonatypeUser
and ORG_GRADLE_PROJECT_sonatypePassword
).
For the examples project, it is required to configure a custom repository to access snapshots:
repositories {
mavenLocal()
mavenCentral()
maven {
name = 'Central Portal Snapshots'
url = 'https://central.sonatype.com/repository/maven-snapshots/'
mavenContent {
snapshotsOnly()
includeGroupAndSubgroups('ru.vyarus')
}
}
}
For security reasons, repository should be limited to snapshots and one group (in my case sub groups are aslo included because they are used by library modules).
Github actions
Credentials
Generated sonatype tokens must be declared as a github repositry secrets:
Actions
For simplicity, my script split into 3 files:
(ignore dependencies.yml
- it updates project dependencies for github dependency graph)
CI script runs build matrix for multiple java versions: check build, run tests (publish coverage data). Also, CI script runs snapshot publication and examples workflows, if required:
name: CI
on:
push:
pull_request:
jobs:
build:
runs-on: ubuntu-latest
name: Java ${{ matrix.java }}
strategy:
fail-fast: false
matrix:
java: [11, 17, 21]
outputs:
version: ${{ steps.project.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Set up JDK ${{ matrix.java }}
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: ${{ matrix.java }}
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
- name: Build
run: |
chmod +x gradlew
./gradlew assemble --no-daemon
- name: Test
run: ./gradlew check --no-daemon
- name: Extract Project version
id: 'project'
run: |
ver=$(./gradlew :properties --property version --no-daemon --console=plain -q | grep "^version:" | awk '{printf $2}')
echo "Project version: $ver"
echo "version=$ver" >> $GITHUB_OUTPUT
publish:
if: ${{ github.ref == 'refs/heads/dw-3' && github.event_name != 'pull_request' && endsWith(needs.build.outputs.version, '-SNAPSHOT') }}
needs: build
uses: ./.github/workflows/publish-snapshot.yml
# workflow can't see secrets directly
secrets:
sonatype_user: ${{ secrets.SONATYPE_USERNAME }}
sonatype_password: ${{ secrets.SONATYPE_PASSWORD }}
examples:
if: ${{ github.ref == 'refs/heads/dw-3' && github.event_name != 'pull_request' && endsWith(needs.build.outputs.version, '-SNAPSHOT') }}
needs: [build, publish]
uses: ./.github/workflows/examples-CI.yml
Snapshot publication must not be performed for pull requests, forks and on release (including release tag).
In order to prevent snapshot publication for release we need to know project version:
- name: Extract Project version
id: 'project'
run: |
ver=$(./gradlew :properties --property version --no-daemon --console=plain -q | grep "^version:" | awk '{printf $2}')
echo "Project version: $ver"
echo "version=$ver" >> $GITHUB_OUTPUT
Note: —property version
works starting from Gradle 7.5. For older gradle versions, this part could be removed and still the correct version value would be selected.
(for maven, help:evaluate could be used for version extraction: mvn help:evaluate -Dexpression=project.version -q -DforceStdout
)
echo "version=$ver" >> $GITHUB_OUTPUT
publish extracted version as a build step output. Note that version is published for each matrix step (it’s almost immediate, so not an issue; just in case, if you want to store separate data from matrix steps, see this post).
In order to use stored version in the separate job, we should declare it as a build
job’s output:
outputs:
version: ${{ steps.project.outputs.version }}
After successful build, snapshot publication run from the separate file:
publish:
if: ${{ github.ref == 'refs/heads/dw-3' && github.event_name != 'pull_request' && endsWith(needs.build.outputs.version, '-SNAPSHOT') }}
needs: build
uses: ./.github/workflows/publish-snapshot.yml
# workflow can't see secrets directly
secrets:
sonatype_user: ${{ secrets.SONATYPE_USERNAME }}
sonatype_password: ${{ secrets.SONATYPE_PASSWORD }}
needs: build
would hold this job until build
job completion
if
would allow snapshot publication only for direct branch commit and only for snapshot versions. Note that it reference build job outputs, referenced from needs (needs.build.outputs.version
)
It is important to bypass required secrets here because they would not be availble in a separate worklow (executed under workflow_call
)
Snapshot publication
publish-snapshot.yml
name: Publish snapshot
on:
workflow_call:
secrets:
sonatype_user:
required: true
sonatype_password:
required: true
jobs:
publish:
name: Publish snapshot
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: 17
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
- name: Build without tests
run: |
chmod +x gradlew
./gradlew build -x check --no-daemon
- name: Publish
env:
ORG_GRADLE_PROJECT_sonatypeUser: ${{ secrets.sonatype_user }}
ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.sonatype_password }}
run: ./gradlew publishToSonatype
This workflow is triggered by the main script and can’t access secrets (github actions restriction) and so required secrets must be declared:
on:
workflow_call:
secrets:
sonatype_user:
required: true
sonatype_password:
required: true
As it is a seprate workflow, we have to checkout and build project again (but without tests now):
- name: Build without tests
run: |
chmod +x gradlew
./gradlew build -x check --no-daemon
And, finally, we need to call publishToSonatype
task to publish snapshot:
- name: Publish
env:
ORG_GRADLE_PROJECT_sonatypeUser: ${{ secrets.sonatype_user }}
ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.sonatype_password }}
run: ./gradlew publishToSonatype
Note that required credentials passed as environment variables (ORG_GRADLE_PROJECT_sonatypeUser
), which gradle would apply as a project properties value.
After successful snapshot publication, examples could be run
Examples run
examples-CI.yml
name: Examples CI
on:
workflow_call:
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: examples
name: Java ${{ matrix.java }}
strategy:
matrix:
java: [11, 17]
steps:
- uses: actions/checkout@v4
- name: Set up JDK ${{ matrix.java }}
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: ${{ matrix.java }}
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
- name: Build and Check
run: |
chmod +x gradlew
./gradlew build --no-daemon
Again, as we have a separate workflow, project must be cheked out again.
Default directory changed to examples
where separate gradle project is stored:
defaults:
run:
working-directory: examples
Correct snapshot version and required maven repository is configured in the examples project.
Summary
Action execution would look like this:
Publication waits build copletion (in case of build error - no need to publish snapshot). And examples wait for snapshot publication (and so will not run if publication fails).
Full scripts could be found here
More details could be found in this blog post, which helped me a lot.
Subscribe to my newsletter
Read articles from Vyacheslav Rusakov directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
