Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions .github/scripts/prove-cli.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/usr/bin/env bash
# Runs every kdrant subcommand against a Qdrant that is already up on 127.0.0.1:6333.
#
# It lives in a script rather than inside a workflow because two workflows run it. 2.2.0 took three
# release attempts and all three failed here, on defects any push could have caught: migrate could not
# create the collection it migrated into, and the step that proved the binary called docker on a macOS
# runner. Nothing below the release workflow had ever started the binary, so the release was where the
# tool ran for the first time. Now CI runs this on every push and the release runs it again.
#
# Usage: prove-cli.sh <kotlin-native-target>
set -euo pipefail

TARGET="${1:?usage: prove-cli.sh <target>, e.g. linuxX64}"
OUT="${RUNNER_TEMP:-/tmp}"

BINARY="kdrant-cli/build/bin/$TARGET/kdrantReleaseExecutable/kdrant.kexe"
[ -f "$BINARY" ] || BINARY="kdrant-cli/build/bin/$TARGET/kdrantReleaseExecutable/kdrant.exe"
[ -f "$BINARY" ] || { echo "::error::no kdrant binary for $TARGET"; exit 1; }

say() { echo; echo "== $* =="; }

say "health"
# It exits 1 on a node that is not ready, which is the point of it, so a failure here is a real one.
"$BINARY" health

say "collection lifecycle"
"$BINARY" collection create cli-source --size 4 --distance dot
"$BINARY" collection describe cli-source
"$BINARY" collections

say "seed"
curl -fsS -X PUT "http://127.0.0.1:6333/collections/cli-source/points?wait=true" \
-H 'content-type: application/json' \
-d '{"points":[{"id":1,"vector":[1,0,0,0]},{"id":2,"vector":[0,1,0,0]}]}' > /dev/null

say "migrate"
"$BINARY" migrate cli-source cli-target --checkpoint "$OUT/cli.checkpoint"
"$BINARY" scroll cli-target --limit 5

say "collection snapshot round trip"
SNAPSHOT="$("$BINARY" snapshot create cli-target | cut -f1)"
"$BINARY" snapshot list cli-target
"$BINARY" snapshot download cli-target "$SNAPSHOT" --out "$OUT/cli.snapshot"
[ -s "$OUT/cli.snapshot" ] || { echo "::error::the snapshot came back empty"; exit 1; }
"$BINARY" snapshot delete cli-target "$SNAPSHOT"

say "shard snapshot round trip"
SHARD_SNAPSHOT="$("$BINARY" snapshot create cli-target --shard 0 | cut -f1)"
"$BINARY" snapshot list cli-target --shard 0
"$BINARY" snapshot download cli-target "$SHARD_SNAPSHOT" --shard 0 --out "$OUT/cli-shard.snapshot"
[ -s "$OUT/cli-shard.snapshot" ] || { echo "::error::the shard snapshot came back empty"; exit 1; }
"$BINARY" snapshot delete cli-target "$SHARD_SNAPSHOT" --shard 0

say "storage snapshot round trip"
STORAGE_SNAPSHOT="$("$BINARY" storage-snapshot create | cut -f1)"
"$BINARY" storage-snapshot list
"$BINARY" storage-snapshot download "$STORAGE_SNAPSHOT" --out "$OUT/cli-storage.snapshot"
[ -s "$OUT/cli-storage.snapshot" ] || { echo "::error::the storage snapshot came back empty"; exit 1; }
"$BINARY" storage-snapshot delete "$STORAGE_SNAPSHOT"

# `snapshot restore` is deliberately not here. It takes a location the *server* resolves, so a file://
# URL pointing at what this script just downloaded names a path inside the runner rather than inside the
# container, and an http:// one would need somewhere to serve it from. Restoring is covered against a
# real server by the shared client contract, which runs in the same process as the node it talks to.

say "delete refuses without --yes"
if "$BINARY" collection delete cli-source 2>/dev/null; then
echo "::error::collection delete dropped a collection without --yes"; exit 1
fi
"$BINARY" collection delete cli-source --yes
"$BINARY" collection delete cli-target --yes

say "every command ran"
4 changes: 2 additions & 2 deletions .github/workflows/benchmarks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
inputs:
qdrant:
description: Qdrant image to benchmark against
default: "qdrant/qdrant:v1.18.2"
default: "qdrant/qdrant:v1.19.1"
type: string

permissions:
Expand All @@ -28,7 +28,7 @@ jobs:
- uses: actions/checkout@v7

- name: Set up JDK 17
uses: actions/setup-java@v5
uses: actions/setup-java@v6
with:
java-version: "17"
distribution: temurin
Expand Down
95 changes: 84 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,27 @@ jobs:
- uses: actions/checkout@v7
- uses: gradle/actions/wrapper-validation@v6

vendored-qdrant:
name: Vendored Qdrant files
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- name: Set up JDK 21
uses: actions/setup-java@v6
with:
java-version: "21"
distribution: temurin

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v6

# The protobuf definitions and the OpenAPI document the contract test validates against are
# copies of upstream's, and a copy nobody diffs is a copy that drifts. This reaches the network,
# which is why it is a job of its own rather than part of `check`.
- name: Compare every vendored file with the pinned Qdrant tag
run: ./gradlew verifyVendoredQdrant --no-daemon --stacktrace

build:
name: Build (JDK ${{ matrix.java }})
runs-on: ubuntu-latest
Expand All @@ -42,7 +63,7 @@ jobs:
run: sudo apt-get update && sudo apt-get install -y libcurl4-openssl-dev

- name: Set up JDK ${{ matrix.java }}
uses: actions/setup-java@v5
uses: actions/setup-java@v6
with:
java-version: ${{ matrix.java }}
distribution: temurin
Expand All @@ -53,6 +74,14 @@ jobs:
- name: Build, test, lint (ktlint + detekt) and verify public API
run: ./gradlew build --no-daemon --stacktrace

# The JMH sources are not part of `build`, so nothing compiled them and they rotted: the official
# client moved PointId into another generated class at 1.19 and the comparison harness stopped
# compiling without anything saying so. A harness that does not build publishes no numbers, which
# is a slower way to have no benchmark than not writing one.
- name: Compile the benchmark harness
if: matrix.java == '17'
run: ./gradlew :benchmarks:compileJmhKotlin --no-daemon --stacktrace

- name: Coverage (Kover)
if: matrix.java == '17'
run: ./gradlew koverXmlReport koverHtmlReport koverVerify --no-daemon --stacktrace
Expand Down Expand Up @@ -85,7 +114,7 @@ jobs:
- uses: actions/checkout@v7

- name: Set up JDK 17
uses: actions/setup-java@v5
uses: actions/setup-java@v6
with:
java-version: "17"
distribution: temurin
Expand Down Expand Up @@ -121,7 +150,7 @@ jobs:
runs-on: ubuntu-latest
services:
qdrant:
image: qdrant/qdrant:v1.18.2
image: qdrant/qdrant:v1.19.1
ports:
- 6333:6333
steps:
Expand All @@ -133,7 +162,7 @@ jobs:
run: sudo apt-get update && sudo apt-get install -y libcurl4-openssl-dev

- name: Set up JDK 17
uses: actions/setup-java@v5
uses: actions/setup-java@v6
with:
java-version: "17"
distribution: temurin
Expand Down Expand Up @@ -184,10 +213,10 @@ jobs:
done
echo "::error::Qdrant did not become ready"; exit 1
env:
QDRANT_VERSION: "1.18.2"
QDRANT_VERSION: "1.19.1"

- name: Set up JDK 17
uses: actions/setup-java@v5
uses: actions/setup-java@v6
with:
java-version: "17"
distribution: temurin
Expand All @@ -211,7 +240,7 @@ jobs:
runs-on: ubuntu-latest
services:
qdrant:
image: qdrant/qdrant:v1.18.2
image: qdrant/qdrant:v1.19.1
ports:
- 6333:6333
steps:
Expand All @@ -220,7 +249,7 @@ jobs:
# Two JDKs on purpose: the library modules compile against a 17 toolchain, and native-image comes
# from the GraalVM distribution, which is a 21.
- name: Set up JDK 17
uses: actions/setup-java@v5
uses: actions/setup-java@v6
with:
java-version: "17"
distribution: temurin
Expand Down Expand Up @@ -264,20 +293,64 @@ jobs:
echo "| Reflection metadata | generated from the classpath, shipped in kdrant-transport-rest |"
} >> "$GITHUB_STEP_SUMMARY"

cli:
name: kdrant-cli against a real Qdrant
runs-on: ubuntu-latest
# Linux only, and on purpose. This job exists to catch a defect before a tag, and every defect that
# stopped 2.2.0 three times was in the tool rather than in a platform: the Linux runner has Docker,
# so it is the cheap one, and the release still builds and proves all three binaries. A macOS or
# Windows job here would triple the wall clock to re-prove what the release re-proves anyway.
services:
qdrant:
image: qdrant/qdrant:v1.19.1
ports:
- 6333:6333
steps:
- uses: actions/checkout@v7

- name: Install libcurl
run: sudo apt-get update && sudo apt-get install -y libcurl4-openssl-dev

- name: Set up JDK 17
uses: actions/setup-java@v6
with:
java-version: "17"
distribution: temurin

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v6

- name: Build the binary
run: >
./gradlew :kdrant-cli:linkKdrantReleaseExecutableLinuxX64
--no-daemon --no-configuration-cache --stacktrace

- name: Wait for Qdrant
run: |
for _ in $(seq 1 60); do
curl -fsS http://127.0.0.1:6333/readyz >/dev/null 2>&1 && exit 0
sleep 1
done
echo "::error::Qdrant did not become ready"; exit 1

# The same script the release runs, so the release is the second time these commands execute.
- name: Run every subcommand
run: bash .github/scripts/prove-cli.sh linuxX64

qdrant-compat:
name: Integration (${{ matrix.qdrant }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
qdrant:
- "qdrant/qdrant:v1.18.2"
- "qdrant/qdrant:v1.19.1"
- "qdrant/qdrant:latest"
steps:
- uses: actions/checkout@v7

- name: Set up JDK 17
uses: actions/setup-java@v5
uses: actions/setup-java@v6
with:
java-version: "17"
distribution: temurin
Expand Down Expand Up @@ -309,7 +382,7 @@ jobs:
- uses: actions/checkout@v7

- name: Set up JDK 17
uses: actions/setup-java@v5
uses: actions/setup-java@v6
with:
java-version: "17"
distribution: temurin
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
- uses: actions/checkout@v7

- name: Set up JDK 17
uses: actions/setup-java@v5
uses: actions/setup-java@v6
with:
java-version: "17"
distribution: temurin
Expand Down
112 changes: 112 additions & 0 deletions .github/workflows/qdrant-watch.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
name: Qdrant watch

# This client's entire claim is that it speaks current Qdrant, and nothing here noticed when Qdrant
# moved. 1.19.0 shipped on 2026-08-05 with seven additions to the API surface; five weeks later the
# vendored schema was still a pre-1.19 snapshot, the CI matrix still ran 1.18.2, and three of those
# seven features — the per-query IDF corpus, the min and max formula expressions, and the explicit
# stemmer switch — had no board item at all. They had none because planning them depended on somebody
# happening to read a changelog, and nobody is reliably somebody.
#
# So the changelog gets read on a schedule instead. When upstream's newest release is ahead of the pin
# in gradle.properties, this opens an issue carrying the features section of the release notes, and
# moving the pin becomes a decision that was taken rather than one that was missed.

on:
schedule:
# Monday morning, after Qdrant's usual mid-week release cadence has settled.
- cron: "0 7 * * 1"
workflow_dispatch:

permissions:
contents: read
issues: write

concurrency:
group: qdrant-watch
cancel-in-progress: false

jobs:
compare:
name: Compare upstream with the pin
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- name: Read the pinned Qdrant version
id: pin
run: |
PINNED=$(sed -n 's/^qdrantVersion=//p' gradle.properties | tr -d '[:space:]')
if [ -z "$PINNED" ]; then
echo "::error::gradle.properties has no qdrantVersion, so there is nothing to compare against"
exit 1
fi
echo "pinned=$PINNED" >> "$GITHUB_OUTPUT"

- name: Read Qdrant's newest release
id: upstream
env:
GH_TOKEN: ${{ github.token }}
run: |
gh api repos/qdrant/qdrant/releases/latest > release.json
LATEST=$(jq -r '.tag_name' release.json | sed 's/^v//')
echo "latest=$LATEST" >> "$GITHUB_OUTPUT"
echo "url=$(jq -r '.html_url' release.json)" >> "$GITHUB_OUTPUT"

- name: Decide whether upstream is ahead
id: decide
env:
PINNED: ${{ steps.pin.outputs.pinned }}
LATEST: ${{ steps.upstream.outputs.latest }}
run: |
NEWEST=$(printf '%s\n%s\n' "$PINNED" "$LATEST" | sort -V | tail -1)
if [ "$PINNED" = "$LATEST" ] || [ "$NEWEST" = "$PINNED" ]; then
echo "Kdrant is pinned to $PINNED and Qdrant's newest release is $LATEST. Nothing to do."
echo "ahead=false" >> "$GITHUB_OUTPUT"
else
echo "Qdrant $LATEST is ahead of the pinned $PINNED."
echo "ahead=true" >> "$GITHUB_OUTPUT"
fi

# An issue per release, not per run. Reopening the same one every Monday would train everybody
# to ignore it, which is the failure this workflow exists to prevent.
- name: Open an issue, unless one is already open for this release
if: steps.decide.outputs.ahead == 'true'
env:
GH_TOKEN: ${{ github.token }}
PINNED: ${{ steps.pin.outputs.pinned }}
LATEST: ${{ steps.upstream.outputs.latest }}
RELEASE_URL: ${{ steps.upstream.outputs.url }}
run: |
TITLE="Qdrant $LATEST is out and this client is pinned to $PINNED"
EXISTING=$(gh issue list --state open --search "in:title Qdrant $LATEST is out" --json number --jq 'length')
if [ "$EXISTING" != "0" ]; then
echo "An issue for Qdrant $LATEST is already open. Leaving it alone."
exit 0
fi

# The features section is the part that becomes work here. Improvements and bug fixes land in
# the server and reach a client's users without a client change; a new field, filter or query
# variant does not.
FEATURES=$(jq -r '.body' release.json | awk '
/^## *Features/ { capture = 1; next }
/^## / { capture = 0 }
capture { print }
')
[ -n "$(printf '%s' "$FEATURES" | tr -d '[:space:]')" ] || FEATURES="This release lists no features section; read the notes to see whether anything reaches the client surface."

{
echo "Qdrant released **$LATEST** on $RELEASE_URL. \`gradle.properties\` pins this client to \`$PINNED\`."
echo
echo "What the release adds to the API surface:"
echo
printf '%s\n' "$FEATURES"
echo
echo "Moving the pin is one command — raise \`qdrantVersion\` in \`gradle.properties\` and run"
echo "\`./gradlew refreshVendoredQdrant\`, which rewrites the vendored protobuf definitions and the"
echo "OpenAPI document together. \`verifyQdrantPin\` then names every other place that still"
echo "disagrees, and \`verifyVendoredQdrant\` proves the files came from the tag they claim."
echo
echo "Deciding not to move is a fine answer, and closing this is how it gets recorded."
} > body.md

gh issue create --title "$TITLE" --body-file body.md
Loading