blob: 853ebf8ec395ab49fc333f522972d24a3899d5be [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# Client integration tests against a SAME-REVISION server (FIP-40 §3.2).
# Builds the Fluss server image from this source tree ONCE, caches + saves it,
# then fans out the Rust / Python / C++ / Elixir integration suites against that
# fluss:dev image (build-once-fan-out, à la Temporal/PyFlink). The image build is
# cached on server/proto hashes, so client-only PRs reuse it instead of rebuilding.
name: Client Integration
on:
push:
branches:
- main
paths:
- 'fluss-rpc/src/main/proto/**'
- 'fluss-server/**'
- 'fluss-common/**'
- 'fluss-dist/**'
- 'docker/fluss/**'
- 'fluss-rust/crates/**'
- 'fluss-rust/bindings/**'
- 'fluss-rust/Cargo.toml'
- 'fluss-rust/Cargo.lock'
- '.github/workflows/client-integration.yml'
pull_request:
branches:
- main
paths:
- 'fluss-rpc/src/main/proto/**'
- 'fluss-server/**'
- 'fluss-common/**'
- 'fluss-dist/**'
- 'docker/fluss/**'
- 'fluss-rust/crates/**'
- 'fluss-rust/bindings/**'
- 'fluss-rust/Cargo.toml'
- 'fluss-rust/Cargo.lock'
- '.github/workflows/client-integration.yml'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
cancel-in-progress: true
jobs:
# Decide which client suites to run, mirroring the per-binding scoping the
# standalone fluss-rust repo had: a binding suite runs only when its own
# binding, the core fluss-rs crate, or the server/proto changed. On non-PR
# events (push to main, manual) everything runs.
detect-changes:
runs-on: ubuntu-latest
outputs:
rust: ${{ steps.filter.outputs.rust }}
python: ${{ steps.filter.outputs.python }}
cpp: ${{ steps.filter.outputs.cpp }}
elixir: ${{ steps.filter.outputs.elixir }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- id: filter
run: |
if [ "${{ github.event_name }}" != "pull_request" ]; then
all=true; changed=""
else
all=false
changed=$(git diff --name-only "${{ github.event.pull_request.base.sha }}...HEAD")
fi
echo "Changed files:"; echo "$changed"
has() { echo "$changed" | grep -qE "$1"; }
# a change to this workflow itself should exercise every suite
has '^\.github/workflows/client-integration\.yml' && all=true || true
protocol=false; core=false; py=false; cpp=false; ex=false
has '^(fluss-rpc/src/main/proto/|fluss-server/|fluss-common/|fluss-dist/|docker/fluss/)' && protocol=true || true
has '^(fluss-rust/crates/|fluss-rust/Cargo\.)' && core=true || true
has '^fluss-rust/bindings/python/' && py=true || true
has '^fluss-rust/bindings/cpp/' && cpp=true || true
has '^fluss-rust/bindings/elixir/' && ex=true || true
# a suite runs if: non-PR (all) OR core crate OR server/proto OR its own binding changed
gate() { if [ "$all" = true ] || [ "$core" = true ] || [ "$protocol" = true ] || [ "$1" = true ]; then echo true; else echo false; fi; }
{
echo "rust=$(gate false)"
echo "python=$(gate $py)"
echo "cpp=$(gate $cpp)"
echo "elixir=$(gate $ex)"
} >> "$GITHUB_OUTPUT"
# Build the server image from THIS source tree once; cache it on server/proto
# hashes so client-only PRs restore it instead of rebuilding. The saved image
# is uploaded as an artifact and loaded by every client integration job.
build-server-image:
needs: detect-changes
if: needs.detect-changes.outputs.rust == 'true' || needs.detect-changes.outputs.python == 'true' || needs.detect-changes.outputs.cpp == 'true' || needs.detect-changes.outputs.elixir == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Cache server image
id: image-cache
uses: actions/cache@v4
with:
path: /tmp/fluss-dev.tar
key: fluss-dev-image-${{ hashFiles('fluss-server/**', 'fluss-common/**', 'fluss-rpc/**', 'fluss-dist/**', 'docker/fluss/**', 'pom.xml') }}
- name: Set up JDK 17
if: steps.image-cache.outputs.cache-hit != 'true'
uses: actions/setup-java@v5
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Build server image (fluss:dev) from source
if: steps.image-cache.outputs.cache-hit != 'true'
run: |
./mvnw -B --no-transfer-progress clean package -pl fluss-dist -am -DskipTests
rm -rf docker/fluss/build-target
mkdir -p docker/fluss/build-target
cp -r build-target/* docker/fluss/build-target/
docker build -t fluss:dev docker/fluss
docker save fluss:dev -o /tmp/fluss-dev.tar
- name: Upload server image
uses: actions/upload-artifact@v4
with:
name: fluss-dev-image
path: /tmp/fluss-dev.tar
retention-days: 1
rust-integration:
needs: [detect-changes, build-server-image]
if: needs.detect-changes.outputs.rust == 'true'
timeout-minutes: 60
runs-on: ubuntu-latest
defaults:
run:
working-directory: fluss-rust
env:
FLUSS_IMAGE: fluss
FLUSS_VERSION: dev
steps:
- uses: actions/checkout@v6
- uses: actions/download-artifact@v4
with:
name: fluss-dev-image
path: /tmp
- name: Load server image
run: docker load -i /tmp/fluss-dev.tar
- name: Install protoc
uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Rust Cache
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
workspaces: fluss-rust
- name: Integration tests
run: cargo test --features integration_tests --test test_fluss -p fluss-rs
env:
RUST_LOG: DEBUG
RUST_BACKTRACE: full
python-integration:
needs: [detect-changes, build-server-image]
if: needs.detect-changes.outputs.python == 'true'
timeout-minutes: 60
runs-on: ubuntu-latest
strategy:
matrix:
python: ["3.9", "3.10", "3.11", "3.12"]
defaults:
run:
working-directory: fluss-rust
env:
FLUSS_TEST_CLUSTER_BIN: ${{ github.workspace }}/fluss-rust/target/debug/fluss-test-cluster
FLUSS_IMAGE: fluss
FLUSS_VERSION: dev
steps:
- uses: actions/checkout@v6
- uses: actions/download-artifact@v4
with:
name: fluss-dev-image
path: /tmp
- name: Load server image
run: docker load -i /tmp/fluss-dev.tar
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python }}
- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39
- name: Install protoc
uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Rust Cache
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
workspaces: fluss-rust
- name: Build fluss-test-cluster binary
run: cargo build -p fluss-test-cluster
- name: Build Python bindings
working-directory: fluss-rust/bindings/python
run: |
uv sync --extra dev --no-install-project
uv run --no-sync maturin develop --uv
- name: Run tests (parallel)
working-directory: fluss-rust/bindings/python
run: uv run --no-sync pytest test/ -v -n 2 --dist=loadfile
env:
RUST_LOG: DEBUG
RUST_BACKTRACE: full
FLUSS_SKIP_CLUSTER_TEARDOWN: "1"
- name: Dump fluss cluster container logs
if: always()
run: |
mkdir -p cluster-logs
for c in $(docker ps -a --filter "name=shared-test" --format '{{.Names}}'); do
docker logs "$c" > "cluster-logs/$c.log" 2>&1 || true
done
- uses: actions/upload-artifact@v4
if: always()
with:
name: cluster-logs-${{ matrix.python }}
path: fluss-rust/cluster-logs/
if-no-files-found: ignore
retention-days: 3
python-stubtest:
# Stub-drift check only needs the module importable, so it runs without the
# test cluster or server image.
needs: [detect-changes]
if: needs.detect-changes.outputs.python == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
defaults:
run:
working-directory: fluss-rust
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39
- name: Install protoc
uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Rust Cache
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
workspaces: fluss-rust
- name: Build Python bindings
working-directory: fluss-rust/bindings/python
run: |
uv sync --extra dev --no-install-project
uv run --no-sync maturin develop --uv
- name: Check stub drift (stubtest)
working-directory: fluss-rust/bindings/python
run: |
uv run --no-sync python -m mypy.stubtest fluss \
--mypy-config-file pyproject.toml \
--allowlist stubtest-allowlist.txt
cpp-integration:
needs: [detect-changes, build-server-image]
if: needs.detect-changes.outputs.cpp == 'true'
timeout-minutes: 60
runs-on: ubuntu-latest
defaults:
run:
working-directory: fluss-rust
env:
FLUSS_TEST_CLUSTER_BIN: ${{ github.workspace }}/fluss-rust/target/debug/fluss-test-cluster
FLUSS_IMAGE: fluss
FLUSS_VERSION: dev
steps:
- uses: actions/checkout@v6
- uses: actions/download-artifact@v4
with:
name: fluss-dev-image
path: /tmp
- name: Load server image
run: docker load -i /tmp/fluss-dev.tar
- name: Install protoc
uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install Apache Arrow C++
run: |
sudo apt-get install -y -V ca-certificates lsb-release wget
wget https://apache.jfrog.io/artifactory/arrow/$(lsb_release --id --short | tr 'A-Z' 'a-z')/apache-arrow-apt-source-latest-$(lsb_release --codename --short).deb
sudo apt-get install -y -V ./apache-arrow-apt-source-latest-$(lsb_release --codename --short).deb
sudo apt-get update
sudo apt-get install -y -V libarrow-dev
- name: Rust Cache
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
workspaces: fluss-rust
- name: Setup sccache
uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10
- name: Build fluss-test-cluster binary
run: cargo build -p fluss-test-cluster
- name: Build C++ bindings and tests
working-directory: fluss-rust/bindings/cpp
env:
SCCACHE_GHA_ENABLED: "true"
run: |
cmake -B build \
-DFLUSS_ENABLE_TESTING=ON \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_C_COMPILER_LAUNCHER=sccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=sccache
cmake --build build --parallel
sccache --show-stats
- name: Run C++ integration tests (parallel)
working-directory: fluss-rust/bindings/cpp
run: cd build && ctest -j$(nproc) --output-on-failure --timeout 300
env:
RUST_LOG: DEBUG
RUST_BACKTRACE: full
elixir-integration:
needs: [detect-changes, build-server-image]
if: needs.detect-changes.outputs.elixir == 'true'
timeout-minutes: 60
runs-on: ubuntu-latest
defaults:
run:
working-directory: fluss-rust
env:
OTP_VERSION: "28.5.0.2"
ELIXIR_VERSION: "1.19.5"
FLUSS_TEST_CLUSTER_BIN: ${{ github.workspace }}/fluss-rust/target/debug/fluss-test-cluster
MIX_ENV: test
FLUSS_IMAGE: fluss
FLUSS_VERSION: dev
steps:
- uses: actions/checkout@v6
- uses: actions/download-artifact@v4
with:
name: fluss-dev-image
path: /tmp
- name: Load server image
run: docker load -i /tmp/fluss-dev.tar
- name: Set up BEAM
uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
with:
otp-version: ${{ env.OTP_VERSION }}
elixir-version: ${{ env.ELIXIR_VERSION }}
- name: Install protoc
uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Rust Cache
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
workspaces: fluss-rust
- name: Cache Mix deps
uses: actions/cache@v4
with:
path: fluss-rust/bindings/elixir/deps
key: ${{ runner.os }}-mixdeps-otp${{ env.OTP_VERSION }}-elixir${{ env.ELIXIR_VERSION }}-${{ hashFiles('fluss-rust/bindings/elixir/mix.lock') }}
restore-keys: |
${{ runner.os }}-mixdeps-otp${{ env.OTP_VERSION }}-elixir${{ env.ELIXIR_VERSION }}-
- name: Build fluss-test-cluster binary
run: cargo build -p fluss-test-cluster
- name: Fetch Elixir deps
working-directory: fluss-rust/bindings/elixir
run: mix deps.get
- name: Check formatting
working-directory: fluss-rust/bindings/elixir
run: mix format --check-formatted
- name: Compile (warnings as errors)
working-directory: fluss-rust/bindings/elixir
run: mix compile --warnings-as-errors
- name: Credo
working-directory: fluss-rust/bindings/elixir
run: mix credo
- name: Cache PLT
uses: actions/cache@v4
with:
path: fluss-rust/bindings/elixir/priv/plts/
key: ${{ runner.os }}-mix-otp${{ env.OTP_VERSION }}-elixir${{ env.ELIXIR_VERSION }}-${{ hashFiles('fluss-rust/bindings/elixir/mix.lock') }}-plt
restore-keys: |
${{ runner.os }}-mix-otp${{ env.OTP_VERSION }}-elixir${{ env.ELIXIR_VERSION }}-
- name: Dialyzer
working-directory: fluss-rust/bindings/elixir
run: mix dialyzer
- name: Run unit tests
working-directory: fluss-rust/bindings/elixir
run: mix test
- name: Run integration tests
working-directory: fluss-rust/bindings/elixir
run: mix test --include integration --only integration
env:
RUST_LOG: DEBUG
RUST_BACKTRACE: full