From cc785ac03d517efea1e9176e18cf7d9831f5672a Mon Sep 17 00:00:00 2001 From: Michael Sarahan Date: Wed, 14 Oct 2026 16:28:21 -0500 Subject: [PATCH 1/4] Enable bugbear and Python security checks for cuVS tooling Extend the existing Ruff policy with bugbear coverage and add Bandit without introducing duplicate flake8 checks. Keep this lint policy change separate from Maven release implementation. --- .pre-commit-config.yaml | 5 +++++ pyproject.toml | 1 + 2 files changed, 6 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 02bf88d226..f4e4bb0ded 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,6 +30,11 @@ repos: - id: ruff-check args: [--fix] - id: ruff-format + - repo: https://github.com/PyCQA/bandit + rev: 1.8.6 + hooks: + - id: bandit + exclude: (^|/)test_[^/]*[.]py$ - repo: https://github.com/pre-commit/mirrors-mypy rev: 'v1.19.1' hooks: diff --git a/pyproject.toml b/pyproject.toml index ac3fc1e4fa..d3b2743d40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ exclude = [ ] [tool.ruff.lint] +extend-select = ["B"] ignore = [ # whitespace before : "E203", From 0cfb690f45911aaafd623cd9decc34e8482cc008 Mon Sep 17 00:00:00 2001 From: Michael Sarahan Date: Wed, 14 Oct 2026 16:28:21 -0500 Subject: [PATCH 2/4] Publish cuVS Java and Lucene releases from release tags Remove the manual publication handoff for both Maven coordinates by gathering the existing CUDA build artifacts and calling the shared publisher. Select common artifacts explicitly, preserve native classifiers, reject version and classifier conflicts, and require Lucene to depend on the matching Java release. Publish Java before Lucene and dispatch each module's docs only after publication. Make the verified staging namespace configurable and prevent release cancellation. Offline assembly tests and workflow checks pass; live build, namespace authorization, publication, and docs integration remain rollout checks. --- .github/workflows/build.yaml | 81 ++++++++++++++- java/ci/RELEASE.md | 46 +++++++++ java/ci/assemble_maven_repo.py | 148 ++++++++++++++++++++++++++++ java/ci/test_assemble_maven_repo.py | 142 ++++++++++++++++++++++++++ 4 files changed, 412 insertions(+), 5 deletions(-) create mode 100644 java/ci/RELEASE.md create mode 100644 java/ci/assemble_maven_repo.py create mode 100644 java/ci/test_assemble_maven_repo.py diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index e1bfb24038..e8fee23c61 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -30,7 +30,7 @@ on: concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}-${{ inputs.build_type || 'branch' }} - cancel-in-progress: true + cancel-in-progress: ${{ github.ref_type != 'tag' }} permissions: {} @@ -78,8 +78,7 @@ jobs: packages: read pull-requests: read uses: ./.github/workflows/matrix-job.yaml - # Artifacts are not published from these jobs, so it's safe to run for multiple CUDA versions. - # If these jobs start producing artifacts, the names will have to differentiate between CUDA versions. + # Keep CUDA builds separate; the release gather job retains their native classifiers. with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -96,8 +95,7 @@ jobs: packages: read pull-requests: read uses: ./.github/workflows/matrix-job.yaml - # Artifacts are not published from these jobs, so it's safe to run for multiple CUDA versions. - # If these jobs start producing artifacts, the names will have to differentiate between CUDA versions. + # Build against the matching cuvs-java artifact from this workflow run. with: build_type: ${{ inputs.build_type || 'branch' }} branch: ${{ inputs.branch }} @@ -148,6 +146,79 @@ jobs: script: "ci/build_lucene.sh" artifact-name-prefix: "cuvs-lucene" file_to_upload: "java/cuvs-lucene/target/" + java-maven-gather: + if: ${{ github.ref_type == 'tag' && startsWith(github.ref_name, 'v') }} + needs: [java-build, lucene-build] + runs-on: linux-amd64-cpu4 + permissions: + contents: read + actions: read + strategy: + fail-fast: false + matrix: + module: [cuvs-java, cuvs-lucene] + steps: + - name: Checkout release source + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + ref: ${{ inputs.sha || github.sha }} + persist-credentials: false + - name: Download module builds from this run + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + pattern: ${{ matrix.module }}-cuda* + path: ${{ runner.temp }}/java-builds + - name: Assemble Maven repository + env: + MODULE: ${{ matrix.module }} + run: | + python3 java/ci/assemble_maven_repo.py \ + --module "${MODULE}" --tag "${GITHUB_REF_NAME}" \ + --artifacts-dir "${RUNNER_TEMP}/java-builds" \ + --output-dir "${RUNNER_TEMP}/maven-repo" + - name: Upload assembled Maven repository + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: ${{ matrix.module }}_maven_repo + path: ${{ runner.temp }}/maven-repo + if-no-files-found: error + java-publish: + needs: java-maven-gather + permissions: + actions: read + contents: read + uses: rapidsai/shared-workflows/.github/workflows/maven-publish.yaml@main + secrets: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + MAVEN_DEPLOY_TOKEN: ${{ secrets.MAVEN_DEPLOY_TOKEN }} + DOCS_DISPATCH_APP_PRIVATE_KEY: ${{ secrets.DOCS_DISPATCH_APP_PRIVATE_KEY }} + with: + artifact-name: cuvs-java_maven_repo + source-git-sha: ${{ inputs.sha || github.sha }} + profile-id: ${{ vars.MAVEN_STAGING_PROFILE_ID || 'com.nvidia.cuvs' }} + publish-to-maven-central: true + docs-project: cuvs-java + lucene-publish: + # The Lucene release depends on the matching published Java coordinate. + # A failed Java docs dispatch must not prevent publishing Lucene itself. + if: ${{ !cancelled() && needs.java-publish.outputs.deployment-state == 'PUBLISHED' }} + needs: [java-maven-gather, java-publish] + permissions: + actions: read + contents: read + uses: rapidsai/shared-workflows/.github/workflows/maven-publish.yaml@main + secrets: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + MAVEN_DEPLOY_TOKEN: ${{ secrets.MAVEN_DEPLOY_TOKEN }} + DOCS_DISPATCH_APP_PRIVATE_KEY: ${{ secrets.DOCS_DISPATCH_APP_PRIVATE_KEY }} + with: + artifact-name: cuvs-lucene_maven_repo + source-git-sha: ${{ inputs.sha || github.sha }} + profile-id: ${{ vars.MAVEN_STAGING_PROFILE_ID || 'com.nvidia.cuvs' }} + publish-to-maven-central: true + docs-project: cuvs-lucene python-build: needs: [build-details, cpp-build] permissions: diff --git a/java/ci/RELEASE.md b/java/ci/RELEASE.md new file mode 100644 index 0000000000..e5f27f6757 --- /dev/null +++ b/java/ci/RELEASE.md @@ -0,0 +1,46 @@ +# Java and Lucene Maven releases + +Release tags build both modules using the existing CUDA matrix. The release +gather job downloads each module's artifacts from the same workflow run, keeps +the CUDA builds in separate directories, and creates one Maven repository tree +per coordinate: + +- `com.nvidia.cuvs:cuvs-java` +- `com.nvidia.cuvs.lucene:cuvs-lucene` + +`assemble_maven_repo.py` takes the unclassified jar, sources, javadoc, and POM +from the highest numeric CUDA version in the matrix. This preserves the existing +higher-CUDA selection for common release artifacts and avoids merging +same-named jars from separate builds. Java retains each native architecture/CUDA +classifier. Lucene retains its attached `jar-with-dependencies` assembly when +present. Test jars and other Maven target directory contents are excluded. + +Every input POM must match the release tag after normalizing a zero-padded patch +number. Lucene's POM must depend on the same Java version. Conflicting native +classifiers, missing required artifacts, invalid JARs, and unexpected build +directories fail gathering before publication. + +Both modules use the shared Maven publisher with automatic publication enabled. +Lucene waits for the Java publisher's `PUBLISHED` output, so its dependency is +released first. Java docs dispatch failure does not prevent Lucene publication. +Each published module dispatches its registered docs project to `rapidsai/docs`. +Branch and nightly builds keep their existing build behavior; these newly added +publication jobs run on release tags. + +## Configuration and rollout + +Use the existing `GPG_PRIVATE_KEY`, `GPG_PASSPHRASE`, `MAVEN_DEPLOY_USERNAME`, and +`MAVEN_DEPLOY_TOKEN` conventions. The Sonatype token must authorize both Maven +coordinates. The common staging profile defaults to `com.nvidia.cuvs`; set +`MAVEN_STAGING_PROFILE_ID` to the actual verified parent namespace if different, +for example `com.nvidia`. Confirm the namespace and account access during rollout. + +Configure `DOCS_DISPATCH_APP_ID` and `DOCS_DISPATCH_APP_PRIVATE_KEY` as described +in the shared publisher's documentation. Deploy the `cuvs-java` and `cuvs-lucene` +docs registrations and the shared workflow output contract before enabling this +caller on the 26.12 release branch. Only one system should publish these +coordinates for a release; retire the legacy manual publishing path at cutover. + +The local checks exercise Maven assembly and docs configuration without compiling +Java or CUDA. The complete matrix build, Sonatype namespace authorization, App +dispatch, and S3 upload still require validation in the release environment. diff --git a/java/ci/assemble_maven_repo.py b/java/ci/assemble_maven_repo.py new file mode 100644 index 0000000000..08dc50906c --- /dev/null +++ b/java/ci/assemble_maven_repo.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Gather cuVS Java build outputs into one Maven coordinate per module.""" + +import argparse +import filecmp +import re +import shutil +import xml.etree.ElementTree as ET # nosec B405: DTDs are rejected below. +import zipfile +from pathlib import Path + +GROUPS = { + "cuvs-java": "com.nvidia.cuvs", + "cuvs-lucene": "com.nvidia.cuvs.lucene", +} +NS = {"m": "http://maven.apache.org/POM/4.0.0"} + + +def read_pom(pom): + """Read direct POM coordinates without accepting entity declarations.""" + text = pom.read_text() + if "" + "com.nvidia.cuvs" + "cuvs-java" + f"{dependency}" + "" + ) + (build / "pom.xml").write_text( + '' + f"{GROUPS[module]}" + f"{module}{version}" + f"{dependencies}" + ) + for suffix in (".jar", "-sources.jar", "-javadoc.jar", "-tests.jar"): + make_jar(build / f"{module}-{version}{suffix}", cuda) + if module == "cuvs-java": + make_jar( + build / f"{module}-{version}-x86_64-cuda{cuda.split('.')[0]}.jar", + cuda, + ) + else: + make_jar(build / f"{module}-{version}-jar-with-dependencies.jar", cuda) + (build / "unrelated.txt").write_text("not a Maven artifact") + return build + + +def test_native_classifiers_and_explicit_primary_selection(tmp_path): + """Preserve both CUDA classifiers while selecting one common artifact.""" + builds = tmp_path / "builds" + make_build(builds, "cuvs-java", "12.9.2") + primary = make_build(builds, "cuvs-java", "13.3.0") + destination = assemble( + "cuvs-java", "v26.12.00", builds, tmp_path / "maven" + ) + assert destination.relative_to(tmp_path / "maven").as_posix() == ( + "com/nvidia/cuvs/cuvs-java/26.12.0" + ) + assert (destination / "cuvs-java-26.12.0.jar").read_bytes() == ( + primary / "cuvs-java-26.12.0.jar" + ).read_bytes() + assert {path.name for path in destination.iterdir()} == { + "cuvs-java-26.12.0.pom", + "cuvs-java-26.12.0.jar", + "cuvs-java-26.12.0-sources.jar", + "cuvs-java-26.12.0-javadoc.jar", + "cuvs-java-26.12.0-x86_64-cuda12.jar", + "cuvs-java-26.12.0-x86_64-cuda13.jar", + } + + +def test_lucene_layout_and_dependency_assembly(tmp_path): + """Keep Lucene in its own coordinate with its attached assembly jar.""" + builds = tmp_path / "builds" + make_build(builds, "cuvs-lucene", "12.9.2") + make_build(builds, "cuvs-lucene", "13.3.0") + destination = assemble( + "cuvs-lucene", "v26.12.0", builds, tmp_path / "maven" + ) + assert destination.relative_to(tmp_path / "maven").as_posix() == ( + "com/nvidia/cuvs/lucene/cuvs-lucene/26.12.0" + ) + assert (destination / "cuvs-lucene-26.12.0.pom").is_file() + assert ( + destination / "cuvs-lucene-26.12.0-jar-with-dependencies.jar" + ).is_file() + assert not (destination / "cuvs-lucene-26.12.0-tests.jar").exists() + + +@pytest.mark.parametrize("module", GROUPS) +def test_pom_tag_mismatch_is_rejected(tmp_path, module): + """Fail before creating output when a build belongs to another version.""" + builds = tmp_path / "builds" + make_build(builds, module, "13.3.0", version="26.10.0") + with pytest.raises(ValueError, match="POM does not match"): + assemble(module, "v26.12.0", builds, tmp_path / "maven") + assert not (tmp_path / "maven").exists() + + +def test_native_classifier_collision_is_rejected(tmp_path): + """Reject byte-different jars targeting the same native coordinate.""" + builds = tmp_path / "builds" + make_build(builds, "cuvs-java", "13.2.0") + make_build(builds, "cuvs-java", "13.3.0") + with pytest.raises(ValueError, match="Conflicting native classifier"): + assemble("cuvs-java", "v26.12.0", builds, tmp_path / "maven") + + +@pytest.mark.parametrize( + "suffix", ["-sources.jar", "-javadoc.jar", "-x86_64-cuda13.jar"] +) +def test_missing_release_artifact_is_rejected(tmp_path, suffix): + """Require the complete publication set before any upload.""" + builds = tmp_path / "builds" + primary = make_build(builds, "cuvs-java", "13.3.0") + (primary / f"cuvs-java-26.12.0{suffix}").unlink() + with pytest.raises(ValueError): + assemble("cuvs-java", "v26.12.0", builds, tmp_path / "maven") + assert not (tmp_path / "maven").exists() + + +def test_lucene_dependency_version_must_match(tmp_path): + """Reject a Lucene release that points at a different Java version.""" + builds = tmp_path / "builds" + make_build(builds, "cuvs-lucene", "13.3.0", dependency="26.10.0") + with pytest.raises(ValueError, match="matching Java release"): + assemble("cuvs-lucene", "v26.12.0", builds, tmp_path / "maven") + + +def test_pom_entity_declaration_is_rejected(tmp_path): + """Reject XML entity declarations before the XML parser is called.""" + builds = tmp_path / "builds" + primary = make_build(builds, "cuvs-java", "13.3.0") + pom = primary / "pom.xml" + pom.write_text( + ']>' + pom.read_text() + ) + with pytest.raises(ValueError, match="entity declarations"): + assemble("cuvs-java", "v26.12.0", builds, tmp_path / "maven") From 4e218f9835144c05992407e52d2866bfc0c29861 Mon Sep 17 00:00:00 2001 From: Michael Sarahan Date: Wed, 14 Oct 2026 18:03:24 -0500 Subject: [PATCH 3/4] Use publication receipts for cuVS Java and Lucene docs Keep Java-before-Lucene publication ordering and remove direct docs App credentials from both callers. Each completed publication is consumed by the standalone release-tracking dispatcher, so docs failures and release-controller readiness do not block the Maven release path. --- .github/workflows/build.yaml | 4 +--- java/ci/RELEASE.md | 13 +++++++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index e8fee23c61..54f31f8815 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -192,7 +192,6 @@ jobs: GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} MAVEN_DEPLOY_TOKEN: ${{ secrets.MAVEN_DEPLOY_TOKEN }} - DOCS_DISPATCH_APP_PRIVATE_KEY: ${{ secrets.DOCS_DISPATCH_APP_PRIVATE_KEY }} with: artifact-name: cuvs-java_maven_repo source-git-sha: ${{ inputs.sha || github.sha }} @@ -201,7 +200,7 @@ jobs: docs-project: cuvs-java lucene-publish: # The Lucene release depends on the matching published Java coordinate. - # A failed Java docs dispatch must not prevent publishing Lucene itself. + # Documentation is dispatched separately from release-tracking. if: ${{ !cancelled() && needs.java-publish.outputs.deployment-state == 'PUBLISHED' }} needs: [java-maven-gather, java-publish] permissions: @@ -212,7 +211,6 @@ jobs: GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} MAVEN_DEPLOY_TOKEN: ${{ secrets.MAVEN_DEPLOY_TOKEN }} - DOCS_DISPATCH_APP_PRIVATE_KEY: ${{ secrets.DOCS_DISPATCH_APP_PRIVATE_KEY }} with: artifact-name: cuvs-lucene_maven_repo source-git-sha: ${{ inputs.sha || github.sha }} diff --git a/java/ci/RELEASE.md b/java/ci/RELEASE.md index e5f27f6757..ecfcfa6ccf 100644 --- a/java/ci/RELEASE.md +++ b/java/ci/RELEASE.md @@ -22,8 +22,9 @@ directories fail gathering before publication. Both modules use the shared Maven publisher with automatic publication enabled. Lucene waits for the Java publisher's `PUBLISHED` output, so its dependency is -released first. Java docs dispatch failure does not prevent Lucene publication. -Each published module dispatches its registered docs project to `rapidsai/docs`. +released first. Each published module emits a completed-publication receipt. +The standalone dispatcher in release-tracking starts the registered docs project +in `rapidsai/docs`; docs failures do not prevent Lucene publication. Branch and nightly builds keep their existing build behavior; these newly added publication jobs run on release tags. @@ -35,10 +36,10 @@ coordinates. The common staging profile defaults to `com.nvidia.cuvs`; set `MAVEN_STAGING_PROFILE_ID` to the actual verified parent namespace if different, for example `com.nvidia`. Confirm the namespace and account access during rollout. -Configure `DOCS_DISPATCH_APP_ID` and `DOCS_DISPATCH_APP_PRIVATE_KEY` as described -in the shared publisher's documentation. Deploy the `cuvs-java` and `cuvs-lucene` -docs registrations and the shared workflow output contract before enabling this -caller on the 26.12 release branch. Only one system should publish these +App credentials remain in release-tracking's existing `release-dispatch` +environment. Deploy the standalone docs dispatcher, `cuvs-java` and `cuvs-lucene` +docs registrations, and the shared workflow receipt/output contract before +enabling this caller on the release branch. Only one system should publish these coordinates for a release; retire the legacy manual publishing path at cutover. The local checks exercise Maven assembly and docs configuration without compiling From 6da0d0bc622001b69cd7a1403c069ff00c9d4622 Mon Sep 17 00:00:00 2001 From: Michael Sarahan Date: Wed, 14 Oct 2026 19:06:25 -0500 Subject: [PATCH 4/4] Use standard Maven release publication behavior Release tags now always publish through the shared workflow. Remove the obsolete opt-in input so the caller matches the simplified interface. --- .github/workflows/build.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 54f31f8815..ec0419dbfe 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -196,7 +196,6 @@ jobs: artifact-name: cuvs-java_maven_repo source-git-sha: ${{ inputs.sha || github.sha }} profile-id: ${{ vars.MAVEN_STAGING_PROFILE_ID || 'com.nvidia.cuvs' }} - publish-to-maven-central: true docs-project: cuvs-java lucene-publish: # The Lucene release depends on the matching published Java coordinate. @@ -215,7 +214,6 @@ jobs: artifact-name: cuvs-lucene_maven_repo source-git-sha: ${{ inputs.sha || github.sha }} profile-id: ${{ vars.MAVEN_STAGING_PROFILE_ID || 'com.nvidia.cuvs' }} - publish-to-maven-central: true docs-project: cuvs-lucene python-build: needs: [build-details, cpp-build]