Skip to content

fix(java): match Go on both paths instead of accepting a trade-off #539

fix(java): match Go on both paths instead of accepting a trade-off

fix(java): match Go on both paths instead of accepting a trade-off #539

Workflow file for this run

name: CI
on:
push:
branches: [master]
paths-ignore:
- ".claude/**"
- ".githooks/**"
- ".github/workflows/agentic-*.yml"
- "apple_generated/**"
- "blog/**"
- "doc/**"
- "**/.gitignore"
- "AGENTS.md"
- "CLAUDE.md"
- "LICENSE"
- "README.md"
pull_request:
paths-ignore:
- ".claude/**"
- ".githooks/**"
- ".github/workflows/agentic-*.yml"
- "apple_generated/**"
- "blog/**"
- "doc/**"
- "**/.gitignore"
- "AGENTS.md"
- "CLAUDE.md"
- "LICENSE"
- "README.md"
# release.yml invokes this workflow on tag pushes as its CI gate. Direct
# tag push triggers were removed so the same SHA does not run CI twice
# (once on push:tags and once via workflow_call from release.yml).
workflow_call:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }}
cancel-in-progress: ${{ github.ref_name != 'master' }}
jobs:
go-lint:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# 双 tag 矩阵守门翻默认闸门 (c)(perf-evolution-roadmap §"决策门槛"):
# default = wangshu(默认 Lua 后端);lua_gopher = gopher-lua opt-in。
# 任一后端 lint 红即拦截,避免 gopher-lua 在默认 build 不被链入而静默漂移。
lua_backend: [default, gopher]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: pine-go/go.mod
cache-dependency-path: pine-go/go.sum
- name: gofmt drift check
if: matrix.lua_backend == 'default'
working-directory: pine-go
run: |
out=$(gofmt -l $(git ls-files '*.go'))
if [ -n "$out" ]; then
echo "::error::gofmt diff in:"
echo "$out"
exit 1
fi
- uses: golangci/golangci-lint-action@v9
with:
working-directory: pine-go
args: ${{ matrix.lua_backend == 'gopher' && '--build-tags=lua_gopher' || '' }}
apple-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.13"
- run: pip install ruff
- run: ruff check apple/
java-lint:
runs-on: ubuntu-latest
defaults:
run:
working-directory: pine-java
steps:
- uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "25"
cache: maven
- run: mvn checkstyle:check -B
go-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: pine-go/go.mod
cache-dependency-path: pine-go/go.sum
- run: make go-cover
- name: Upload Go coverage
uses: actions/upload-artifact@v7
if: always()
with:
name: go-coverage
path: pine-go/coverage.out
retention-days: 30
# 翻默认闸门 (c)(perf-evolution-roadmap §"决策门槛"):双 tag 全绿。
# 默认走 wangshu(go-test);此 job 守门 gopher-lua opt-in 后端,只跑直接消费
# Lua 后端的包(operators/lua + integration),避免重复全量回归。
go-test-gopher:
runs-on: ubuntu-latest
defaults:
run:
working-directory: pine-go
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: pine-go/go.mod
cache-dependency-path: pine-go/go.sum
- name: Test under -tags=lua_gopher
run: go test -race -tags=lua_gopher ./operators/lua/... ./integration/...
apple-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: pine-go/go.mod
cache-dependency-path: pine-go/go.sum
- uses: actions/setup-python@v6
with:
python-version: "3.13"
- run: pip install pytest pytest-cov
- run: python -m pytest apple/tests/ -v --cov=apple --cov-report=term --cov-report=xml:coverage.xml
- name: Upload Python coverage
uses: actions/upload-artifact@v7
if: always()
with:
name: apple-coverage
path: coverage.xml
retention-days: 30
java-test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: pine-java
steps:
- uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "25"
cache: maven
- run: mvn test -B -q
- name: Upload Java coverage
uses: actions/upload-artifact@v7
if: always()
with:
name: java-coverage
path: pine-java/target/site/jacoco/
retention-days: 30
cpp-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install LuaJIT and build deps
# ci-apt-install.sh retries with a per-attempt timeout so one slow
# mirror rotation doesn't fail the job (#125, #164). cmake / gcc /
# make (build-essential) are preinstalled on the runner image —
# asserted below instead of apt-installing shadowed duplicates.
run: |
bash scripts/ci-apt-install.sh libluajit-5.1-dev libcurl4-openssl-dev
cmake --version | head -1
- name: Configure pine-cpp
working-directory: pine-cpp
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
- name: Build pine-cpp
working-directory: pine-cpp
run: cmake --build build -j"$(nproc)"
- name: Smoke-test CLIs
working-directory: pine-cpp
run: |
SCHEMA_JSON=$(mktemp --suffix=.json)
./build/pineapple-codegen -schema-json "$SCHEMA_JSON"
head -c 200 "$SCHEMA_JSON" && echo
MINI_CFG=$(mktemp --suffix=.json)
python3 -c "import json; json.dump(json.load(open('../fixtures/pipelines/transform_then_filter.json')).get('config', {}), open('$MINI_CFG','w'))"
./build/pineapple-server -addr ":18999" -config "$MINI_CFG" &
SERVER_PID=$!
for i in 1 2 3 4 5 6 7 8 9 10; do
if curl -fsS http://localhost:18999/health >/dev/null 2>&1; then break; fi
sleep 0.3
done
curl -fsS http://localhost:18999/health || (kill $SERVER_PID; exit 1)
# P2-04: graceful kill — SIGTERM first so graceful_drain runs to
# completion (in-flight requests finish, ~Server() runs the
# destructors in order); fall back to SIGKILL if drain takes
# too long. Wait is bounded so the step does not hang on a
# stuck handler.
kill -TERM $SERVER_PID 2>/dev/null || true
for i in 1 2 3 4 5; do
kill -0 $SERVER_PID 2>/dev/null || break
sleep 1
done
kill -KILL $SERVER_PID 2>/dev/null || true
wait $SERVER_PID 2>/dev/null || true
cpp-sanitizer:
runs-on: ubuntu-latest
needs: cpp-build
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Install LuaJIT and build deps
# Resilient apt install — see cpp-build job comment (#125, #164).
run: |
bash scripts/ci-apt-install.sh libluajit-5.1-dev libcurl4-openssl-dev
cmake --version | head -1
- name: Run ASan/UBSan smoke
run: bash scripts/cpp-sanitizer-smoke.sh
cpp-tsan:
runs-on: ubuntu-latest
needs: cpp-build
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Install LuaJIT and build deps
# Resilient apt install — see cpp-build job comment (#125, #164).
run: |
bash scripts/ci-apt-install.sh libluajit-5.1-dev libcurl4-openssl-dev util-linux
cmake --version | head -1
- name: Run TSan smoke (high-fanout DAG stress)
run: bash scripts/cpp-tsan-smoke.sh
cpp-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install build deps
# Resilient apt install — see cpp-build job comment (#125, #164).
run: |
bash scripts/ci-apt-install.sh libluajit-5.1-dev libcurl4-openssl-dev
cmake --version | head -1
- name: Strict build (-Werror)
working-directory: pine-cpp
run: |
cmake -S . -B build-strict -DCMAKE_BUILD_TYPE=Release -DPINE_CPP_WERROR=ON
cmake --build build-strict -j"$(nproc)"
- name: Whitespace + tab hygiene
run: |
set -e
BAD=0
while IFS= read -r f; do
if grep -nP "[ \t]+$" "$f" >/dev/null 2>&1; then
echo "trailing whitespace in $f:"
grep -nP "[ \t]+$" "$f" | head -5
BAD=1
fi
if grep -nP "\t" "$f" >/dev/null 2>&1; then
echo "tab character in $f:"
grep -nP "\t" "$f" | head -5
BAD=1
fi
# P2-16: trailing newline guard. Files lacking a final \n
# produced silent diffs when subsequent commits appended
# to them; cpp-lint now rejects the shape outright.
if [[ $(tail -c1 "$f" | wc -l) -eq 0 ]]; then
echo "missing trailing newline in $f"
BAD=1
fi
done < <(find pine-cpp -type f \( -name '*.cpp' -o -name '*.hpp' \) -not -path '*/build*')
if [[ $BAD -ne 0 ]]; then
echo "cpp-lint: hygiene failures detected (see above)"
exit 1
fi
- name: Adjacent string-literal concat with bare identifier (typo guard)
run: |
# Catches `"... "" + var + ""` patterns where the author meant
# to embed an identifier inside double quotes but the C++ compiler
# silently concatenates adjacent string literals, dropping the
# intended quotes. Tracked under .code-review/from-v0.8.0 as
# P0-1 — five rounds of code review caught a single instance
# (config.cpp:191) that hid via this exact shape.
if grep -rEn '""[[:space:]]*\+[[:space:]]*[A-Za-z_][A-Za-z0-9_]*[[:space:]]*\+[[:space:]]*""' \
pine-cpp/src pine-cpp/include pine-cpp/cmd pine-cpp/operators; then
echo "cpp-lint: suspicious adjacent-literal concat with bare identifier" >&2
echo " (likely missing \\\" around an embedded variable; see P0-1)" >&2
exit 1
fi
cpp-test:
runs-on: ubuntu-latest
needs: cpp-build
steps:
- uses: actions/checkout@v6
- name: Install build deps
# Resilient apt install — see cpp-build job comment (#125, #164).
run: |
bash scripts/ci-apt-install.sh libluajit-5.1-dev libcurl4-openssl-dev
cmake --version | head -1
- name: Build + run tests
run: make cpp-test PARALLEL=$(nproc)
java-benchmark:
runs-on: ubuntu-latest
needs: java-test
defaults:
run:
working-directory: pine-java
steps:
- uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "25"
- name: Run benchmarks
run: mvn test -B -Dtest=BenchmarkTest -pl . 2>&1 | tee benchmark-java.txt
- name: Write benchmark summary
if: always()
shell: bash
run: |
{
echo "### Pine-Java Benchmark results"
echo
echo '```text'
grep -A 100 "=== Pine-Java Pipeline Benchmark ===" benchmark-java.txt || echo "No benchmark output"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
java-fuzz:
runs-on: ubuntu-latest
needs: java-test
defaults:
run:
working-directory: pine-java
steps:
- uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "25"
cache: maven
- name: Fuzz Config.load
run: mvn test -B -Dtest="JazzerFuzzTest#fuzzConfigLoad" -Djazzer.instrument="page.liam.pine.**"
env:
JAZZER_FUZZ: "1"
- name: Fuzz DAG.build
run: mvn test -B -Dtest="JazzerFuzzTest#fuzzDAGBuild" -Djazzer.instrument="page.liam.pine.**"
env:
JAZZER_FUZZ: "1"
- name: Fuzz Engine.create
run: mvn test -B -Dtest="JazzerFuzzTest#fuzzEngineCreate" -Djazzer.instrument="page.liam.pine.**"
env:
JAZZER_FUZZ: "1"
cross-validation:
runs-on: ubuntu-latest
needs: [go-test, java-test, cpp-build]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: pine-go/go.mod
cache-dependency-path: pine-go/go.sum
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "25"
cache: maven
- uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Install redis-server and LuaJIT
# Resilient apt install — see cpp-build job comment (#125, #164).
run: |
bash scripts/ci-apt-install.sh "redis-server=5:7.*" libluajit-5.1-dev libcurl4-openssl-dev
redis-server --version
cmake --version | head -1
- name: Cross-validate Go vs Java vs C++
run: make cross-validate | tee cross-validate-output.txt
- name: Fail on any divergence
run: |
if grep -q "^FAIL:" cross-validate-output.txt; then
echo "Cross-validation failures detected:"
grep "^FAIL:" cross-validate-output.txt
exit 1
fi
- name: Write summary
if: always()
shell: bash
run: |
{
echo "### Cross-Validation Summary"
echo ""
if grep -q "^FAIL:" cross-validate-output.txt 2>/dev/null; then
echo "| Check | Status |"
echo "|-------|--------|"
grep "^PASS:\|^FAIL:" cross-validate-output.txt | while read -r line; do
if [[ "$line" == PASS:* ]]; then
echo "| ${line#PASS: } | ✅ |"
else
echo "| ${line#FAIL: } | ❌ |"
fi
done
elif grep -q "^PASS:" cross-validate-output.txt 2>/dev/null; then
echo "| Check | Status |"
echo "|-------|--------|"
grep "^PASS:" cross-validate-output.txt | while read -r line; do
echo "| ${line#PASS: } | ✅ |"
done
else
echo "Cross-validation did not produce results."
fi
} >> "$GITHUB_STEP_SUMMARY"
codegen-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: pine-go/go.mod
cache-dependency-path: pine-go/go.sum
- run: make codegen-check
fuzz:
runs-on: ubuntu-latest
needs: go-test
defaults:
run:
working-directory: pine-go
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: pine-go/go.mod
cache-dependency-path: pine-go/go.sum
- name: Fuzz config.Load
run: go test -run=^$ -fuzz=FuzzLoad -fuzztime=30s -timeout=120s -parallel=4 ./internal/config/
- name: Fuzz dag.Build
run: go test -run=^$ -fuzz=FuzzBuild -fuzztime=30s -timeout=120s -parallel=4 ./internal/dag/
- name: Fuzz DataFrame ApplyOutput
run: go test -run=^$ -fuzz=FuzzApplyOutputStorageEquivalence -fuzztime=30s -timeout=120s -parallel=4 ./internal/dataframe/
- name: Fuzz data_parallel equivalence
run: go test -run=^$ -fuzz=FuzzDataParallelEquivalence -fuzztime=30s -timeout=120s -parallel=4 ./internal/runtime/
benchmark:
runs-on: ubuntu-latest
needs: go-test
defaults:
run:
working-directory: pine-go
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: pine-go/go.mod
cache-dependency-path: pine-go/go.sum
- name: Run benchmarks
shell: bash
run: |
set -o pipefail
# benchmarks 是独立子 module (pine-go/benchmarks/go.mod),通过 replace
# 指令引用主 module。`make bench` 默认带 -tags=pine_bench 让 stub
# 算子 + BenchmarkCalibrated 都被编译运行(否则 //go:build pine_bench
# 下的源文件在 CI 既不编译也不跑,silent-skip)。
make bench | tee benchmark.txt
# integration/ 仍在主 module,单独跑(不依赖 pine_bench)。
go test -bench=. -benchmem -count=1 -run=^$ ./integration/ | tee -a benchmark.txt
- name: Write benchmark summary
if: always()
shell: bash
run: |
{
echo "### Benchmark results"
echo
echo '```text'
if [ -s benchmark.txt ]; then
cat benchmark.txt
else
echo "Benchmark output was not generated."
fi
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload benchmark results
uses: actions/upload-artifact@v7
if: always()
with:
name: benchmark-results
path: pine-go/benchmark.txt
retention-days: 30
differential-fuzz:
runs-on: ubuntu-latest
needs: [go-test, java-test]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: pine-go/go.mod
cache-dependency-path: pine-go/go.sum
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "25"
cache: maven
- uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Build Go binary
run: go build -o pineapple-run ./cmd/pineapple-run/
working-directory: pine-go
- name: Build Java
run: mvn package -B -q -DskipTests
working-directory: pine-java
- name: Install LuaJIT and build deps
# Resilient apt install — see cpp-build job comment (#125, #164).
# libhiredis-dev was dropped: pine-cpp's redis client is raw-socket
# (no hiredis reference anywhere in the tree), and every other cpp
# job already builds without it.
run: |
bash scripts/ci-apt-install.sh libluajit-5.1-dev libcurl4-openssl-dev
cmake --version | head -1
- name: Build C++
run: |
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j"$(nproc)"
working-directory: pine-cpp
- name: Run differential fuzz
shell: bash
run: |
SEED=$(date +%s)
echo "Seed: $SEED (use this to reproduce failures)"
python3 scripts/differential-fuzz.py \
--rounds 100 \
--stability-runs 2 \
--seed "$SEED" \
--engines go,java,cpp \
--cpp-bin pine-cpp/build/pineapple-run \
--save-dir /tmp/diff-fuzz-ci | tee diff-fuzz-output.txt
- name: Upload divergences
uses: actions/upload-artifact@v7
if: failure()
with:
name: differential-fuzz-divergences
path: /tmp/diff-fuzz-ci/
retention-days: 30
- name: Write summary
if: always()
shell: bash
run: |
{
echo "### Differential Fuzz"
echo ""
echo '```text'
tail -10 diff-fuzz-output.txt
echo '```'
} >> "$GITHUB_STEP_SUMMARY"