Skip to content

Publish JAR

Publish JAR #7

Workflow file for this run

name: Publish JAR
# Builds (and uploads) a TRUE multi-platform zvec-java fat JAR.
#
# Strategy (mirrors, and improves on, the seqeralabs/zvec-java packaging model):
# 1. build-natives — a matrix job builds the zvec C-API library and the
# JavaCPP JNI glue on every supported platform, then stages the per-platform
# native bundle (target/classes/<javacpp-platform>/) as an artifact. The
# zvec C build is cached with a key anchored to the zvec submodule commit,
# so a submodule bump automatically invalidates the cache.
# 2. build-jar — a single job builds the linux zvec C library (version-anchored
# cache), regenerates the git-ignored ZvecNative.java via the JavaCPP parser
# and compiles the linux JNI glue, then drops the macOS/Windows native
# bundles from step 1 into src/main/resources/<platform>/ and packages ONE
# fat JAR containing the libraries for all platforms.
#
# The workflow is manual-only (workflow_dispatch) by design: the Release
# workflow publishes the pre-built C libraries first, a human verifies the
# release, and only then is this run by hand with the same tag to build the
# JAR family and (opt-in via deploy_central) deploy to Maven Central.
# Its final artifact is "zvec-java-jars".
on:
workflow_dispatch:
inputs:
tag:
description: 'Release tag / version (e.g., v0.7.0)'
required: true
type: string
deploy_central:
description: 'Also deploy the full artifact matrix (all-platform + per-platform + nolib JARs) to Maven Central'
required: false
type: boolean
default: false
permissions:
contents: read
jobs:
# ===========================================================================
# Build the native bundle (zvec_c_api + jnizvec glue) for each platform
# ===========================================================================
build-natives:
name: Natives (${{ matrix.name }})
runs-on: ${{ matrix.runner }}
# The Linux leg runs inside the same manylinux_2_28 image upstream zvec uses
# for its prebuilt SDK (see zvec/.github/workflows/_build_prebuilt.yml), so
# the .so we ship still loads on glibc 2.28 distros - CentOS 8, RHEL 8,
# Ubuntu 20.04. Building on the bare ubuntu-24.04 runner links against glibc
# 2.39 and yields a library that demands GLIBC_2.38, which only Ubuntu
# 23.10+, Debian 13 and Fedora 38+ can satisfy. macOS and Windows need no
# container: macos-15 and windows-2025 are exactly what upstream builds on.
container: ${{ matrix.container || null }}
strategy:
fail-fast: false
matrix:
include:
- name: darwin-arm64
runner: macos-15
container: ''
cache_abi: host
- name: linux-x64
runner: ubuntu-24.04
# Pinned dated tag, the same batch upstream uses.
container: 'quay.io/pypa/manylinux_2_28_x86_64:2026.03.01-1'
cache_abi: manylinux_2_28
- name: linux-arm64
runner: ubuntu-24.04-arm
# Same image family upstream builds its aarch64 SDK in.
container: 'quay.io/pypa/manylinux_2_28_aarch64:2026.03.01-1'
cache_abi: manylinux_2_28
- name: windows-x64
runner: windows-2025
container: ''
cache_abi: host
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
submodules: recursive
- name: Set up JDK 11
uses: actions/setup-java@v6
with:
distribution: temurin
java-version: '11'
cache: maven
# The manylinux image ships no Maven, and the runner's preinstalled
# /usr/share/maven is not visible inside a container job.
- name: Install Maven (Linux container)
if: runner.os == 'Linux'
shell: bash
run: |
MVN_VERSION=3.9.16
curl -fsSL "https://archive.apache.org/dist/maven/maven-3/${MVN_VERSION}/binaries/apache-maven-${MVN_VERSION}-bin.tar.gz" \
| tar -xz -C /opt
echo "/opt/apache-maven-${MVN_VERSION}/bin" >> "$GITHUB_PATH"
# --- Version-anchored cache: keyed by the zvec submodule commit and the
# release tag, since the tag is compiled into the library ---
- name: Resolve zvec submodule commit and version
id: zvec
shell: bash
run: |
echo "sha=$(git -C zvec rev-parse HEAD)" >> "$GITHUB_OUTPUT"
# zvec/cmake/version.cmake derives the version from `git describe
# --tags` and otherwise falls back to a deliberately obvious dummy
# v0.0.0. In CI that fallback always wins, because the submodule is
# checked out detached and without tags - so the published library
# used to report v0.0.0-g<sha> through Zvec.getVersion(), and 0.0.0
# through getVersionMajor/Minor/Patch. Feed the release tag in
# explicitly, the way upstream zvec does in _build_prebuilt.yml.
# Anything that is not a vX.Y.Z string is dropped rather than passed
# on: version.cmake raises FATAL_ERROR on a malformed override.
TAG="${{ inputs.tag || github.ref_name }}"
case "$TAG" in v*) ;; *) TAG="v$TAG" ;; esac
if printf '%s' "$TAG" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+'; then
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
else
echo "::warning::'$TAG' is not a vX.Y.Z version; the native build will report zvec's fallback version"
echo "tag=" >> "$GITHUB_OUTPUT"
fi
# Restore-only; see the gated save step after the build steps below.
- name: Restore zvec build cache
id: cache-zvec
uses: actions/cache/restore@v6
with:
path: zvec/build
# cache_abi is part of the key so switching the Linux build into the
# manylinux container cannot reuse a host-glibc build tree, and the
# tag is part of it because OVERRIDE_GIT_DESCRIBE is baked into the
# compiled library - without it a cache hit would keep serving a
# tree built against the old version string.
key: zvec-build-${{ matrix.name }}-${{ matrix.cache_abi }}-${{ steps.zvec.outputs.sha }}-${{ steps.zvec.outputs.tag }}
# --- Unix build toolchain ---
- name: Set up Python (for CMake / Ninja)
if: runner.os == 'macOS' && steps.cache-zvec.outputs.cache-hit != 'true'
uses: actions/setup-python@v7
with:
python-version: '3.10'
- name: Install build tools (Unix)
if: runner.os != 'Windows' && steps.cache-zvec.outputs.cache-hit != 'true'
shell: bash
run: |
if [ "$(uname -s)" = "Linux" ]; then
# setup-python is skipped inside the container: the CPython it
# downloads is built for the host glibc and will not run in a
# glibc 2.28 image. Use the bundled CPython instead, the same way
# upstream zvec's _build_prebuilt.yml does.
PYBIN=$(ls -d /opt/python/cp3*/bin | head -1)
echo "$PYBIN" >> "$GITHUB_PATH"
export PATH="$PYBIN:$PATH"
fi
python3 -m pip install --upgrade pip cmake==3.30.0 ninja==1.11.1
echo "$(python3 -c 'import site; print(site.USER_BASE)')/bin" >> "$GITHUB_PATH"
# --- Windows build toolchain ---
- name: Set up MSVC (Windows)
if: runner.os == 'Windows'
uses: ilammy/msvc-dev-cmd@v1
- name: Install CMake and Ninja (Windows)
if: runner.os == 'Windows' && steps.cache-zvec.outputs.cache-hit != 'true'
shell: pwsh
run: pip install cmake==3.30.0 ninja==1.11.1
# --- Build the zvec C-API library (skipped on cache hit) ---
- name: Build C-API library (Unix)
if: runner.os != 'Windows' && steps.cache-zvec.outputs.cache-hit != 'true'
shell: bash
env:
ZVEC_TAG: ${{ steps.zvec.outputs.tag }}
run: |
NPROC=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 2)
cd zvec
mkdir -p build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release -DBUILD_C_BINDINGS=ON \
-DOVERRIDE_GIT_DESCRIBE="$ZVEC_TAG" -G Ninja
cmake --build . -j$NPROC --target zvec_c_api
touch .zvec-build-ok
- name: Build C-API library (Windows)
if: runner.os == 'Windows' && steps.cache-zvec.outputs.cache-hit != 'true'
shell: pwsh
env:
ZVEC_TAG: ${{ steps.zvec.outputs.tag }}
run: |
cd zvec
New-Item -ItemType Directory -Force -Path build | Out-Null
cd build
cmake .. -DCMAKE_BUILD_TYPE=Release -DBUILD_C_BINDINGS=ON `
"-DOVERRIDE_GIT_DESCRIBE=$env:ZVEC_TAG" -G Ninja
cmake --build . --target zvec_c_api -j $env:NUMBER_OF_PROCESSORS
New-Item -ItemType File -Force -Path .zvec-build-ok | Out-Null
# Save as soon as the build is known good: the marker file is written only
# by a successful build, so a later failure (JNI glue, staging, upload)
# still leaves a reusable cache and an incomplete tree is never stored.
# continue-on-error absorbs the 409 raised when a concurrent run already
# stored this key.
- name: Save zvec build cache
if: always() && steps.cache-zvec.outputs.cache-hit != 'true' && hashFiles('zvec/build/.zvec-build-ok') != ''
uses: actions/cache/save@v6
continue-on-error: true
with:
path: zvec/build
key: zvec-build-${{ matrix.name }}-${{ matrix.cache_abi }}-${{ steps.zvec.outputs.sha }}-${{ steps.zvec.outputs.tag }}
# --- Locate the directory that holds the built library / import lib ---
# JavaCPP links against <linkPaths> and copies the runtime library out of
# that very same directory (copyLibs=true in pom.xml), so the link library
# and the runtime library must sit side by side. Windows splits the CMake
# output: the import library zvec_c_api.lib lands in build/lib while the
# runtime zvec_c_api.dll lands in build/bin. Resolving build/lib alone
# therefore produces a Windows bundle holding only the JNI glue, which
# links fine but fails to load at runtime, so pull the DLL across.
- name: Resolve zvec lib directory
shell: bash
run: |
find_runtime_lib() {
find "$1" -maxdepth 1 -type f \
\( -name '*zvec_c_api*.dll' -o -name '*zvec_c_api*.so*' -o -name '*zvec_c_api*.dylib' \) \
2>/dev/null | head -n1
}
for d in \
"$GITHUB_WORKSPACE/zvec/build/lib/Release" \
"$GITHUB_WORKSPACE/zvec/build/lib" \
"$GITHUB_WORKSPACE/zvec/build/bin/Release" \
"$GITHUB_WORKSPACE/zvec/build/bin"; do
if [ -d "$d" ] && ls "$d"/*zvec_c_api* >/dev/null 2>&1; then
ZVEC_LIB_DIR="$d"
echo "Using zvec lib dir: $d"
break
fi
done
if [ -z "$ZVEC_LIB_DIR" ]; then
echo "::error::No zvec_c_api build output found under zvec/build"
exit 1
fi
if [ -z "$(find_runtime_lib "$ZVEC_LIB_DIR")" ]; then
for b in \
"$GITHUB_WORKSPACE/zvec/build/bin/Release" \
"$GITHUB_WORKSPACE/zvec/build/bin"; do
if [ -d "$b" ] && [ -n "$(find_runtime_lib "$b")" ]; then
echo "Copying runtime library from $b into $ZVEC_LIB_DIR"
find "$b" -maxdepth 1 -type f -name '*zvec_c_api*' \
\( -name '*.dll' -o -name '*.so*' -o -name '*.dylib' \) \
-exec cp {} "$ZVEC_LIB_DIR"/ \;
break
fi
done
fi
if [ -z "$(find_runtime_lib "$ZVEC_LIB_DIR")" ]; then
echo "::error::zvec_c_api runtime library (.dll/.so/.dylib) is not present in $ZVEC_LIB_DIR"
find "$GITHUB_WORKSPACE/zvec/build" -maxdepth 2 -name '*zvec_c_api*' -print
exit 1
fi
echo "ZVEC_LIB_DIR=$ZVEC_LIB_DIR" >> "$GITHUB_ENV"
echo "=== zvec_c_api files in $ZVEC_LIB_DIR ==="
find "$ZVEC_LIB_DIR" -maxdepth 1 -name '*zvec_c_api*' -exec ls -la {} \;
# --- Compile the JavaCPP JNI glue and copy libs into target/classes ---
- name: Build JNI glue (mvn package)
shell: bash
run: mvn package -DskipTests -Dzvec.lib.path="$ZVEC_LIB_DIR" -B
# --- Stage the JavaCPP platform bundle for aggregation ---
- name: Stage native bundle
id: stage
shell: bash
run: |
# The javacpp compiler mojo emits the JNI glue + copied libs under
# the bound class' package path (org/zvec/binding/<platform>/).
PLATDIR=$(find target/classes/org/zvec/binding -mindepth 1 -maxdepth 1 -type d -name '*-*' | head -n1)
if [ -z "$PLATDIR" ]; then
# Fallback: root-level platform directory.
PLATDIR=$(find target/classes -mindepth 1 -maxdepth 1 -type d -name '*-*' | head -n1)
fi
if [ -z "$PLATDIR" ]; then
echo "::error::No JavaCPP platform directory found under target/classes"
ls -la target/classes || true
exit 1
fi
PLATNAME=$(basename "$PLATDIR")
echo "JavaCPP platform: $PLATNAME"
mkdir -p "staging/$PLATNAME"
# Only runtime shared libraries belong in the JAR; the MSVC import
# library and export file the compiler drops next to the glue would
# just add dead weight to every artifact.
find "$PLATDIR" -maxdepth 1 -type f \
\( -name '*.dll' -o -name '*.so*' -o -name '*.dylib' \) \
-exec cp {} "staging/$PLATNAME/" \;
if [ -z "$(ls -A "staging/$PLATNAME")" ]; then
echo "::error::No runtime native library staged out of $PLATDIR"
ls -la "$PLATDIR" || true
exit 1
fi
echo "=== Staged native bundle ($PLATNAME) ==="
ls -la "staging/$PLATNAME/"
- name: Upload native bundle
uses: actions/upload-artifact@v7
with:
name: natives-${{ matrix.name }}
path: staging/
# ===========================================================================
# Aggregate every platform bundle into a single multi-platform fat JAR
# ===========================================================================
build-jar:
name: Build JAR
needs: build-natives
runs-on: ubuntu-24.04
# This job compiles the linux zvec_c_api and the linux JNI glue that end up
# in every JAR variant, so it has to run in the same manylinux_2_28 image as
# the build-natives linux leg - otherwise the shipped .so files demand
# GLIBC_2.38 and refuse to load on CentOS 8 / RHEL 8 / Ubuntu 20.04.
container: 'quay.io/pypa/manylinux_2_28_x86_64:2026.03.01-1'
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
# Submodules are needed: the JavaCPP parser reads the zvec headers to
# regenerate the git-ignored ZvecNative.java, and the compiler links
# the linux JNI glue against the built zvec_c_api.
submodules: recursive
- name: Set up JDK 11
uses: actions/setup-java@v6
with:
distribution: temurin
java-version: '11'
cache: maven
# The manylinux image ships no Maven, and the runner's preinstalled
# /usr/share/maven is not visible inside a container job.
- name: Install Maven (Linux container)
shell: bash
run: |
MVN_VERSION=3.9.16
curl -fsSL "https://archive.apache.org/dist/maven/maven-3/${MVN_VERSION}/binaries/apache-maven-${MVN_VERSION}-bin.tar.gz" \
| tar -xz -C /opt
echo "/opt/apache-maven-${MVN_VERSION}/bin" >> "$GITHUB_PATH"
# --- Version-anchored cache for the linux zvec build (JNI glue compile) ---
- name: Resolve zvec submodule commit and version
id: zvec
shell: bash
run: |
echo "sha=$(git -C zvec rev-parse HEAD)" >> "$GITHUB_OUTPUT"
# zvec/cmake/version.cmake derives the version from `git describe
# --tags` and otherwise falls back to a deliberately obvious dummy
# v0.0.0. In CI that fallback always wins, because the submodule is
# checked out detached and without tags - so the published library
# used to report v0.0.0-g<sha> through Zvec.getVersion(), and 0.0.0
# through getVersionMajor/Minor/Patch. Feed the release tag in
# explicitly, the way upstream zvec does in _build_prebuilt.yml.
# Anything that is not a vX.Y.Z string is dropped rather than passed
# on: version.cmake raises FATAL_ERROR on a malformed override.
TAG="${{ inputs.tag || github.ref_name }}"
case "$TAG" in v*) ;; *) TAG="v$TAG" ;; esac
if printf '%s' "$TAG" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+'; then
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
else
echo "::warning::'$TAG' is not a vX.Y.Z version; the native build will report zvec's fallback version"
echo "tag=" >> "$GITHUB_OUTPUT"
fi
# Restore-only; see the gated save step after the build step below.
- name: Restore zvec build cache
id: cache-zvec
uses: actions/cache/restore@v6
with:
path: zvec/build
key: zvec-build-linux-x64-manylinux_2_28-${{ steps.zvec.outputs.sha }}-${{ steps.zvec.outputs.tag }}
- name: Install build tools
if: steps.cache-zvec.outputs.cache-hit != 'true'
shell: bash
run: |
# setup-python cannot be used here: the CPython it downloads is built
# for the host glibc and will not run inside this glibc 2.28 image.
# Use the image's bundled CPython, as upstream zvec does.
PYBIN=$(ls -d /opt/python/cp3*/bin | head -1)
echo "$PYBIN" >> "$GITHUB_PATH"
export PATH="$PYBIN:$PATH"
python3 -m pip install --upgrade pip cmake==3.30.0 ninja==1.11.1
echo "$(python3 -c 'import site; print(site.USER_BASE)')/bin" >> "$GITHUB_PATH"
- name: Build C-API library (linux)
if: steps.cache-zvec.outputs.cache-hit != 'true'
shell: bash
env:
ZVEC_TAG: ${{ steps.zvec.outputs.tag }}
run: |
NPROC=$(nproc 2>/dev/null || echo 2)
cd zvec
mkdir -p build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release -DBUILD_C_BINDINGS=ON \
-DOVERRIDE_GIT_DESCRIBE="$ZVEC_TAG" -G Ninja
cmake --build . -j$NPROC --target zvec_c_api
touch .zvec-build-ok
# Save as soon as the build is known good, so a later failure (fat JAR
# assembly, deploy) still leaves a reusable cache for the next run.
# continue-on-error absorbs the 409 raised when a concurrent run already
# stored this key.
- name: Save zvec build cache
if: always() && steps.cache-zvec.outputs.cache-hit != 'true' && hashFiles('zvec/build/.zvec-build-ok') != ''
uses: actions/cache/save@v6
continue-on-error: true
with:
path: zvec/build
key: zvec-build-linux-x64-manylinux_2_28-${{ steps.zvec.outputs.sha }}-${{ steps.zvec.outputs.tag }}
- name: Download prebuilt native bundles
uses: actions/download-artifact@v8
with:
pattern: natives-*
path: native-artifacts
- name: Stage foreign-platform native libraries into resources
shell: bash
run: |
mkdir -p src/main/resources/org/zvec/binding
# Stage every platform bundle EXCEPT linux-x86_64, whose glue the mvn
# build below compiles locally. linux-arm64 cannot be built here - the
# job runs on an x86_64 runner - so it arrives prebuilt from the
# build-natives leg like the macOS and Windows bundles do. Each
# artifact holds a single <javacpp-platform>/ directory, and natives
# land under the JavaCPP package path so the JAR layout is uniform
# across platforms (org/zvec/binding/<platform>/).
find native-artifacts -mindepth 2 -maxdepth 2 -type d \
! -name 'linux-x86_64' -exec cp -r {} src/main/resources/org/zvec/binding/ \;
echo "=== Foreign platforms staged into resources ==="
ls -la src/main/resources/org/zvec/binding/ || true
- name: Set project version from tag
shell: bash
run: |
TAG="${{ inputs.tag }}"
VERSION="${TAG#v}"
mvn versions:set -DnewVersion="$VERSION" -DgenerateBackupPoms=false -B
echo "Set Maven version to $VERSION"
# Full build: the JavaCPP parser regenerates ZvecNative.java (git-ignored)
# from the zvec headers, and the compiler builds the linux JNI glue into
# target/classes/linux-x86_64/. The macOS/Windows libraries staged into
# resources above are bundled alongside, yielding a multi-platform fat JAR.
- name: Build multi-platform fat JAR
shell: bash
run: mvn package -DskipTests -Dzvec.lib.path="$GITHUB_WORKSPACE/zvec/build/lib" -B
- name: Verify JAR bundles all platforms
shell: bash
run: |
JAR=$(ls target/zvec-java-*-with-dependencies.jar | head -n1)
ENTRIES=$(jar tf "$JAR")
echo "=== Native libraries inside $JAR ==="
echo "$ENTRIES" | grep -E '(macosx|linux|windows).*\.(dylib|so|dll)$' | sort
# Both halves of the native stack have to be there for every platform:
# the JavaCPP JNI glue and the zvec C-API runtime library. Asserting
# only "some native file exists" lets a Windows bundle that ships
# jniZvecNative.dll without zvec_c_api.dll pass and then blow up with
# an UnsatisfiedLinkError on the consumer's machine.
# Exact JavaCPP platform directory names: a bare "linux" prefix would
# let a JAR carrying only linux-x86_64 satisfy the linux-arm64 check.
for entry in macosx-arm64:dylib linux-x86_64:so linux-arm64:so windows-x86_64:dll; do
p="${entry%%:*}"
ext="${entry##*:}"
for lib in jniZvecNative zvec_c_api; do
if ! echo "$ENTRIES" | grep -qE "${p}/(lib)?${lib}\.${ext}(\.[0-9.]+)?$"; then
echo "::error::${lib} runtime library for platform '$p' is missing from the fat JAR"
exit 1
fi
done
done
echo "=== Bundled jieba FTS dict inside $JAR ==="
echo "$ENTRIES" | grep 'zvec/jieba_dict/' || true
for f in jieba.dict.utf8 hmm_model.utf8; do
if ! echo "$ENTRIES" | grep -q "zvec/jieba_dict/$f"; then
echo "::error::jieba dict file zvec/jieba_dict/$f missing from the fat JAR"
exit 1
fi
done
- name: Verify the linux natives stay glibc 2.28 compatible
shell: bash
run: |
# The linux libraries are built in manylinux_2_28 so they keep loading
# on CentOS 8 / RHEL 8 / Ubuntu 20.04. Building them on the bare
# runner instead links glibc 2.39 and produces a library that demands
# GLIBC_2.38 - loadable only on Ubuntu 23.10+, Debian 13 and Fedora
# 38+. Check the exact files every JAR variant packs: the main JAR,
# the per-platform classifier JARs and the shaded one.
LIMIT=2.28
if ! command -v readelf >/dev/null 2>&1; then
echo "::warning::readelf is unavailable, skipping the glibc floor check"
exit 0
fi
# linux-x86_64 is compiled by this job; linux-arm64 arrives prebuilt
# from its build-natives leg. readelf rather than objdump because
# objdump in this x86_64 image has no aarch64 backend.
for LIBDIR in \
target/classes/org/zvec/binding/linux-x86_64 \
src/main/resources/org/zvec/binding/linux-arm64; do
if [ -z "$(find "$LIBDIR" -maxdepth 1 -name '*.so' -print -quit 2>/dev/null)" ]; then
echo "::error::no linux native library found under $LIBDIR"
exit 1
fi
for so in "$LIBDIR"/*.so; do
MAX=$(readelf -V "$so" 2>/dev/null | grep -oE 'GLIBC_[0-9]+\.[0-9]+' | sed 's/GLIBC_//' | sort -uV | tail -1)
echo "$LIBDIR/$(basename "$so"): highest versioned GLIBC symbol = ${MAX:-none}"
if [ -n "$MAX" ] && [ "$(printf '%s\n%s\n' "$MAX" "$LIMIT" | sort -V | tail -1)" != "$LIMIT" ]; then
echo "::error::$so requires GLIBC_$MAX but the floor is GLIBC_$LIMIT - it was not built inside a manylinux_2_28 container"
exit 1
fi
done
done
- name: Upload JAR artifacts
uses: actions/upload-artifact@v7
with:
name: zvec-java-jars
path: |
target/zvec-java-*.jar
# ------------------------------------------------------------------
# Maven Central deployment: all-platform main JAR + per-platform
# classifier JARs + nolib JAR (layout follows org.duckdb:duckdb_jdbc),
# opt-in via the deploy_central input. Requires repository secrets:
# CENTRAL_TOKEN_USER / CENTRAL_TOKEN_PASSWORD (portal user token)
# GPG_SIGNING_KEY (base64 armored private key) / GPG_SIGNING_PASSPHRASE
# The deploy re-runs the build with the release profiles, attaching
# sources + javadoc JARs, per-platform classifier JARs, the nolib JAR
# and GPG signatures, then uploads one deployment bundle to the
# Sonatype Central Portal.
# ------------------------------------------------------------------
- name: Import GPG signing key
if: inputs.deploy_central
env:
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}
run: |
# This job now runs inside the manylinux image, which is not
# guaranteed to ship gpg; maven-gpg-plugin needs it to sign.
command -v gpg >/dev/null 2>&1 || dnf install -y gnupg2 >/dev/null \
|| yum install -y gnupg2 >/dev/null
echo "$GPG_SIGNING_KEY" | base64 -d | gpg --batch --import
- name: Write Central Portal settings.xml
if: inputs.deploy_central
run: |
cat > central-settings.xml <<'XML'
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0">
<servers>
<server>
<id>central</id>
<username>${env.CENTRAL_TOKEN_USER}</username>
<password>${env.CENTRAL_TOKEN_PASSWORD}</password>
</server>
</servers>
</settings>
XML
- name: Deploy to Maven Central
if: inputs.deploy_central
env:
CENTRAL_TOKEN_USER: ${{ secrets.CENTRAL_TOKEN_USER }}
CENTRAL_TOKEN_PASSWORD: ${{ secrets.CENTRAL_TOKEN_PASSWORD }}
GPG_SIGNING_PASSPHRASE: ${{ secrets.GPG_SIGNING_PASSPHRASE }}
run: |
mvn -s central-settings.xml -B deploy -Prelease,release-natives \
-DskipTests \
-Dzvec.lib.path="$GITHUB_WORKSPACE/zvec/build/lib" \
-Dgpg.passphrase="$GPG_SIGNING_PASSPHRASE"
# ===========================================================================
# Load the built JAR on the glibc floor we advertise, before anyone publishes
# ===========================================================================
# build-jar uploads the deployment bundle to the Central Portal, but the
# release profile sets autoPublish=false with waitUntil=validated, so the
# bundle stops at VALIDATED and nothing reaches Maven Central until a human
# clicks "Publish". This job runs inside that window: it is the last signal
# before that button gets pressed.
#
# It runs inside manylinux_2_28 on both linux architectures rather than on a
# bare runner, deliberately. The runners' own glibc 2.39 is newer than
# anything the README claims to support, so a pass there would prove nothing;
# glibc 2.28 is the documented floor, and a .so linked against something
# newer dies here with exactly the UnsatisfiedLinkError a consumer would hit.
#
# Linux only: macOS and Windows have no equivalent ABI floor to defend, and
# their runners already match upstream zvec's.
smoke-test:
name: Smoke test (${{ matrix.name }})
needs: build-jar
runs-on: ${{ matrix.runner }}
container: ${{ matrix.container }}
strategy:
fail-fast: false
matrix:
include:
- name: linux-x86_64
runner: ubuntu-24.04
container: 'quay.io/pypa/manylinux_2_28_x86_64:2026.03.01-1'
- name: linux-arm64
runner: ubuntu-24.04-arm
container: 'quay.io/pypa/manylinux_2_28_aarch64:2026.03.01-1'
steps:
# No submodules here: the smoke test runs against the built JAR, not
# against the sources, so the zvec checkout is not needed.
- name: Checkout code
uses: actions/checkout@v7
- name: Set up JDK 11
uses: actions/setup-java@v6
with:
distribution: temurin
java-version: '11'
- name: Download the built JARs
uses: actions/download-artifact@v8
with:
name: zvec-java-jars
path: jars
- name: Report the runtime under test
shell: bash
run: |
echo "arch : $(uname -m)"
echo "glibc : $(getconf GNU_LIBC_VERSION 2>/dev/null || ldd --version | head -1)"
java -version
- name: Load the JAR and exercise the database
shell: bash
run: |
MAIN_JAR=$(find jars -maxdepth 1 -type f -name 'zvec-java-*.jar' \
! -name '*-with-dependencies.jar' | head -1)
if [ -z "$MAIN_JAR" ]; then
echo "::error::no main zvec-java JAR in the downloaded artifact"
ls -la jars
exit 1
fi
# Test the multi-platform JAR rather than a classifier: that is what a
# consumer gets with no classifier, and JavaCPP has to pick this
# runner's own platform directory out of it, so the resolution path
# under test is the real one. The script resolves javacpp itself and
# falls through to a direct repo1 download, since the image has no
# ~/.m2 and ships no Maven.
# The tag is the version the native libraries were compiled with, so
# this also guards the OVERRIDE_GIT_DESCRIBE plumbing above: a build
# that silently fell back to zvec's v0.0.0 dummy fails right here.
bash scripts/smoke-test.sh --jar "$MAIN_JAR" \
--expect-version "${{ inputs.tag }}"