mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-10-12 02:16:39 +00:00
1879 lines
108 KiB
YAML
1879 lines
108 KiB
YAML
name: sep-tests
|
|
permissions:
|
|
contents: read
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- 'main'
|
|
- '*.*.*'
|
|
paths-ignore:
|
|
- 'docs/**'
|
|
- '*.md'
|
|
- '*.mdx'
|
|
# The only difference between pull_request and pull_request_target is the context in which the workflow runs:
|
|
# — pull_request_target workflows use the workflow files from the default branch, and secrets are available.
|
|
# — pull_request workflows use the workflow files from the pull request branch, and secrets are unavailable.
|
|
# We use pull_request_target so that GITEE_TOKEN (and other repo secrets) are available when building
|
|
# ragflow:nightly from a fork PR, while still running the full test suite on PRs labeled with `ci`.
|
|
pull_request:
|
|
types: [synchronize, labeled]
|
|
paths-ignore:
|
|
- 'docs/**'
|
|
- '*.md'
|
|
- '*.mdx'
|
|
schedule:
|
|
- cron: '0 16 * * *' # This schedule runs every 16:00:00Z(00:00:00+08:00)
|
|
|
|
# https://docs.github.com/en/actions/using-jobs/using-concurrency
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}${{ github.event.action == 'labeled' && github.event.label.name != 'ci' && format('-ignore-{0}', github.run_id) || '' }}
|
|
cancel-in-progress: true
|
|
|
|
# infiniflow/onnxruntime_go is a fresh org mirror that is not yet present in the
|
|
# public Go checksum database / proxy. Mark it private so Go skips the proxy and
|
|
# sumdb for this path and resolves it directly from GitHub (Go's default
|
|
# GOPROXY keeps the ",direct" fallback). Required for the in-process DeepDoc
|
|
# backend to build against github.com/infiniflow/onnxruntime_go.
|
|
env:
|
|
GOPRIVATE: github.com/infiniflow/onnxruntime_go
|
|
|
|
jobs:
|
|
ragflow_preflight:
|
|
name: ragflow_preflight
|
|
# https://docs.github.com/en/actions/using-jobs/using-conditions-to-control-job-execution
|
|
# https://github.com/orgs/community/discussions/26261
|
|
if: ${{ github.event_name != 'pull_request' && github.event_name != 'pull_request_target' || (github.event.pull_request.draft == false && contains(github.event.pull_request.labels.*.name, 'ci') && (github.event.action != 'labeled' || github.event.label.name == 'ci')) }}
|
|
permissions:
|
|
actions: write
|
|
contents: read
|
|
runs-on: [ "self-hosted", "ragflow-test" ]
|
|
outputs:
|
|
http_api_test_level: ${{ steps.test_level.outputs.http_api_test_level }}
|
|
has_go_changes: ${{ steps.detect_changes.outputs.has_go_changes }}
|
|
has_web_changes: ${{ steps.detect_changes.outputs.has_web_changes }}
|
|
has_native_deepdoc_changes: ${{ steps.detect_changes.outputs.has_native_deepdoc_changes }}
|
|
api_proxy_schemes: ${{ steps.detect_changes.outputs.api_proxy_schemes }}
|
|
steps:
|
|
- name: Ensure workspace ownership
|
|
run: |
|
|
echo "Workflow triggered by ${{ github.event_name }}"
|
|
echo "chown -R ${USER} ${GITHUB_WORKSPACE}" && sudo chown -R ${USER} ${GITHUB_WORKSPACE}
|
|
|
|
# https://github.com/actions/checkout/issues/1781
|
|
- name: Check out code
|
|
uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && format('refs/pull/{0}/merge', github.event.pull_request.number) || github.sha }}
|
|
fetch-depth: 0
|
|
fetch-tags: true
|
|
allow-unsafe-pr-checkout: true
|
|
|
|
- name: Check workflow duplication
|
|
if: ${{ !cancelled() && !failure() }}
|
|
run: |
|
|
if [[ ${GITHUB_EVENT_NAME} != "pull_request" && ${GITHUB_EVENT_NAME} != "pull_request_target" && ${GITHUB_EVENT_NAME} != "schedule" ]]; then
|
|
HEAD=$(git rev-parse HEAD)
|
|
# Find a PR that introduced a given commit
|
|
gh auth login --with-token <<< "${{ secrets.GITHUB_TOKEN }}"
|
|
PR_NUMBER=$(gh pr list --search ${HEAD} --state merged --json number --jq .[0].number)
|
|
echo "HEAD=${HEAD}"
|
|
echo "PR_NUMBER=${PR_NUMBER}"
|
|
if [[ -n "${PR_NUMBER}" ]]; then
|
|
PR_SHA_FP=${RUNNER_WORKSPACE_PREFIX}/artifacts/${GITHUB_REPOSITORY}/PR_${PR_NUMBER}
|
|
if [[ -f "${PR_SHA_FP}" ]]; then
|
|
read -r PR_SHA PR_RUN_ID < "${PR_SHA_FP}"
|
|
# Calculate the hash of the current workspace content
|
|
HEAD_SHA=$(git rev-parse HEAD^{tree})
|
|
if [[ "${HEAD_SHA}" == "${PR_SHA}" ]]; then
|
|
echo "Cancel myself since the workspace content hash is the same with PR #${PR_NUMBER} merged. See ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${PR_RUN_ID} for details."
|
|
gh run cancel ${GITHUB_RUN_ID}
|
|
while true; do
|
|
status=$(gh run view ${GITHUB_RUN_ID} --json status -q .status)
|
|
[ "${status}" = "completed" ] && break
|
|
sleep 5
|
|
done
|
|
exit 1
|
|
fi
|
|
fi
|
|
fi
|
|
elif [[ ${GITHUB_EVENT_NAME} == "pull_request" || ${GITHUB_EVENT_NAME} == "pull_request_target" ]]; then
|
|
PR_NUMBER=${{ github.event.pull_request.number }}
|
|
PR_SHA_FP=${RUNNER_WORKSPACE_PREFIX}/artifacts/${GITHUB_REPOSITORY}/PR_${PR_NUMBER}
|
|
# Calculate the hash of the current workspace content
|
|
PR_SHA=$(git rev-parse HEAD^{tree})
|
|
echo "PR #${PR_NUMBER} workspace content hash: ${PR_SHA}"
|
|
mkdir -p ${RUNNER_WORKSPACE_PREFIX}/artifacts/${GITHUB_REPOSITORY}
|
|
echo "${PR_SHA} ${GITHUB_RUN_ID}" > ${PR_SHA_FP}
|
|
fi
|
|
ARTIFACTS_DIR=${RUNNER_WORKSPACE_PREFIX}/artifacts/${GITHUB_REPOSITORY}/${GITHUB_RUN_ID}
|
|
echo "ARTIFACTS_DIR=${ARTIFACTS_DIR}" >> ${GITHUB_ENV}
|
|
rm -rf ${ARTIFACTS_DIR} && mkdir -p ${ARTIFACTS_DIR}
|
|
|
|
# lefthook's web-checks hook resolves oxlint/oxfmt through
|
|
# tools/hooks/web_tools.sh, which needs node and npm on PATH.
|
|
- name: Set up Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: "22"
|
|
|
|
- name: Run Lefthook on changed files
|
|
run: |
|
|
set -euo pipefail
|
|
if [[ "${GITHUB_EVENT_NAME}" == "pull_request" || "${GITHUB_EVENT_NAME}" == "pull_request_target" ]]; then
|
|
changed_files=$(mktemp)
|
|
trap 'rm -f "$changed_files"' EXIT
|
|
git diff --name-only -z ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} \
|
|
| while IFS= read -r -d '' file; do
|
|
if [[ -f "$file" ]]; then
|
|
printf '%s\0' "$file"
|
|
fi
|
|
done > "$changed_files"
|
|
echo "Changed files to run lefthook on:"
|
|
if [[ -s "$changed_files" ]]; then
|
|
while IFS= read -r -d '' file; do
|
|
printf ' %q\n' "$file"
|
|
done < "$changed_files"
|
|
else
|
|
echo " (none — lefthook will be a no-op)"
|
|
fi
|
|
# LEFTHOOK_CHECK_ONLY=1 makes the pre-commit jobs verify without
|
|
# applying --fix or `git add`, so CI only checks and reports
|
|
# failures instead of rewriting the working tree.
|
|
LEFTHOOK_CHECK_ONLY=1 lefthook run pre-commit --files-from-stdin --no-auto-install < "$changed_files"
|
|
# web-checks is a separate hook because it is not wired into local
|
|
# commits: it installs a Node toolchain that only CI has a reason to
|
|
# pay for. It is always check-only, so it needs no CHECK_ONLY flag.
|
|
lefthook run web-checks --files-from-stdin --no-auto-install < "$changed_files"
|
|
fi
|
|
|
|
- name: Set test level
|
|
id: test_level
|
|
run: |
|
|
set -euo pipefail
|
|
if [[ ${GITHUB_EVENT_NAME} == "schedule" ]]; then
|
|
export HTTP_API_TEST_LEVEL=p3
|
|
else
|
|
export HTTP_API_TEST_LEVEL=p2
|
|
fi
|
|
echo "HTTP_API_TEST_LEVEL=${HTTP_API_TEST_LEVEL}" >> ${GITHUB_ENV}
|
|
echo "http_api_test_level=${HTTP_API_TEST_LEVEL}" >> ${GITHUB_OUTPUT}
|
|
|
|
- name: Detect changed file types
|
|
id: detect_changes
|
|
run: |
|
|
set -euo pipefail
|
|
has_go=false
|
|
has_web=false
|
|
has_native_deepdoc=false
|
|
|
|
if [[ "${GITHUB_EVENT_NAME}" == "schedule" ]]; then
|
|
has_go=true
|
|
has_web=true
|
|
has_native_deepdoc=true
|
|
else
|
|
if [[ "${GITHUB_EVENT_NAME}" == "pull_request" || "${GITHUB_EVENT_NAME}" == "pull_request_target" ]]; then
|
|
CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }})
|
|
else
|
|
BASE_SHA="${{ github.event.before }}"
|
|
if [[ -n "${BASE_SHA}" && ! "${BASE_SHA}" =~ ^0+$ ]]; then
|
|
CHANGED_FILES=$(git diff --name-only "${BASE_SHA}" "${GITHUB_SHA}")
|
|
else
|
|
CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD)
|
|
fi
|
|
fi
|
|
|
|
while IFS= read -r file; do
|
|
case "$file" in
|
|
# Docker, test, SDK, and workflow changes can alter either server path.
|
|
# build.sh also controls the C++/native build, so it must trigger native backend.
|
|
Dockerfile|Dockerfile_ci|docker/**|.github/workflows/**|sdk/**|build.sh)
|
|
has_go=true
|
|
has_native_deepdoc=true
|
|
;;
|
|
test/**)
|
|
has_go=true
|
|
;;
|
|
# DeepDoc native backend (Go inference layer, C++ static lib, pinned testdata).
|
|
# Must come BEFORE the generic "*.go"/"*.py" rules, otherwise deepdoc/*.go
|
|
# would be caught by "*.go" first and never set has_native_deepdoc.
|
|
internal/deepdoc/**|internal/binding/cpp/**|internal/deepdoc/native/testdata.ref)
|
|
has_go=true
|
|
has_native_deepdoc=true
|
|
;;
|
|
*.go|go.mod|go.sum) has_go=true ;;
|
|
web/*) has_web=true ;;
|
|
esac
|
|
done <<< "$CHANGED_FILES"
|
|
|
|
if [[ "$has_go" == "false" && "$has_web" == "false" ]]; then
|
|
has_go=true
|
|
has_web=true
|
|
has_native_deepdoc=true
|
|
fi
|
|
fi
|
|
|
|
api_proxy_schemes='["go"]'
|
|
|
|
echo "has_go_changes=${has_go}" >> $GITHUB_OUTPUT
|
|
echo "has_web_changes=${has_web}" >> $GITHUB_OUTPUT
|
|
echo "has_native_deepdoc_changes=${has_native_deepdoc}" >> $GITHUB_OUTPUT
|
|
echo "api_proxy_schemes=${api_proxy_schemes}" >> $GITHUB_OUTPUT
|
|
echo "Go: ${has_go}, Web: ${has_web}, Native DeepDoc: ${has_native_deepdoc}, proxy schemes: ${api_proxy_schemes}"
|
|
|
|
ragflow_tests_infinity:
|
|
name: ragflow_tests_infinity (${{ matrix.api_proxy_scheme }})
|
|
needs: ragflow_preflight
|
|
if: ${{ github.event_name != 'pull_request' && github.event_name != 'pull_request_target' || (github.event.pull_request.draft == false && contains(github.event.pull_request.labels.*.name, 'ci') && (github.event.action != 'labeled' || github.event.label.name == 'ci')) }}
|
|
runs-on: [ "self-hosted", "ragflow-test" ]
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
api_proxy_scheme: ${{ fromJSON(needs.ragflow_preflight.outputs.api_proxy_schemes) }}
|
|
env:
|
|
DOC_ENGINE: infinity
|
|
API_PROXY_SCHEME: ${{ matrix.api_proxy_scheme }}
|
|
RAGFLOW_IMAGE: infiniflow/ragflow:${{ github.run_id }}-infinity-${{ matrix.api_proxy_scheme }}
|
|
HTTP_API_TEST_LEVEL: ${{ needs.ragflow_preflight.outputs.http_api_test_level }}
|
|
HAS_GO: ${{ needs.ragflow_preflight.outputs.has_go_changes }}
|
|
HAS_WEB: ${{ needs.ragflow_preflight.outputs.has_web_changes }}
|
|
steps:
|
|
- name: Ensure workspace ownership
|
|
if: always()
|
|
run: |
|
|
echo "Workflow triggered by ${{ github.event_name }}"
|
|
echo "chown -R ${USER} ${GITHUB_WORKSPACE}" && sudo chown -R ${USER} ${GITHUB_WORKSPACE}
|
|
|
|
- name: Check out code
|
|
if: always()
|
|
uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && format('refs/pull/{0}/merge', github.event.pull_request.number) || github.sha }}
|
|
fetch-depth: 0
|
|
fetch-tags: true
|
|
allow-unsafe-pr-checkout: true
|
|
|
|
- name: Build ragflow go server
|
|
if: env.API_PROXY_SCHEME == 'go'
|
|
run: |
|
|
set -euo pipefail
|
|
BUILDER_CONTAINER=ragflow_build_${GITHUB_RUN_ID}_${DOC_ENGINE}_$(od -An -N4 -tx4 /dev/urandom | tr -d ' ')
|
|
cleanup_builder() {
|
|
if [[ -n "${BUILDER_CONTAINER:-}" ]]; then
|
|
sudo docker rm -f -v "${BUILDER_CONTAINER}" >/dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
trap cleanup_builder EXIT
|
|
|
|
TZ=${TZ:-$(readlink -f /etc/localtime | awk -F '/zoneinfo/' '{print $2}')}
|
|
sudo docker run --privileged -d --name "${BUILDER_CONTAINER}" \
|
|
-e TZ="${TZ}" \
|
|
-e UV_INDEX=https://mirrors.aliyun.com/pypi/simple \
|
|
-v "${PWD}:/ragflow" \
|
|
-v "${PWD}/internal/binding/cpp/resource:/usr/share/infinity/resource" \
|
|
infiniflow/infinity_builder:ubuntu22_clang20
|
|
sudo docker exec "${BUILDER_CONTAINER}" bash -c 'git config --global safe.directory "*" && cd /ragflow && ./build.sh --cpp'
|
|
./build.sh --go
|
|
|
|
- name: Run Go unit tests
|
|
if: env.API_PROXY_SCHEME == 'go'
|
|
# Runs after `./build.sh --go`, which guarantees the C++ static
|
|
# library (librag_tokenizer_c_api.a) is present on disk. The Go
|
|
# test binaries link against it transitively through
|
|
# `internal/binding`, so running `go test` before the C++ build
|
|
# fails the link step.
|
|
#
|
|
# Excludes packages whose tests fail for environmental reasons
|
|
# unrelated to the diff:
|
|
# - internal/tokenizer is split: the pure-Go BPE loader tests
|
|
# (bpe_loader_test.go) run in the default tier; the C++ binding /
|
|
# dict-dependent tests (tokenizer_test.go,
|
|
# tokenizer_concurrent_test.go) and the on-disk anchor test
|
|
# (bpe_loader_anchors_test.go) are tagged `manual` and need the
|
|
# docker builder's /usr/share/infinity/resource, so they stay out
|
|
# of the default run.
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
# Provide the cl100k BPE table for the offline tokenizer loader.
|
|
# The loader reads <repo>/ragflow_deps/cl100k_base.tiktoken (or
|
|
# TIKTOKEN_CACHE_DIR). Try to fetch it directly from the upstream
|
|
# openai blob first (small ~1.6MB, reachable from the CI runner — the
|
|
# pre-PR code downloaded it on the fly, so the network path is known
|
|
# good). If the direct fetch fails (e.g. GFW), fall back to extracting
|
|
# it from the local infiniflow/ragflow_deps image *if already present*
|
|
# on the runner (no `docker pull`, since that huge image is first
|
|
# fetched at the later "Build ragflow:nightly" step). Best-effort: if
|
|
# neither source works, the loader still fails loudly (no silent
|
|
# 0-token degradation).
|
|
# tiktoken is already installed in runner
|
|
mkdir -p ragflow_deps
|
|
cp /opt/ragflow_deps/cl100k_base.tiktoken ragflow_deps/cl100k_base.tiktoken || true
|
|
if [ ! -f ragflow_deps/cl100k_base.tiktoken ]; then
|
|
mkdir -p ragflow_deps
|
|
if command -v curl >/dev/null 2>&1; then
|
|
if curl -fsSL -o ragflow_deps/cl100k_base.tiktoken \
|
|
https://openaipublic.blob.core.windows.net/encodings/cl100k_base.tiktoken; then
|
|
echo "tiktoken: cl100k table fetched via curl from openai blob"
|
|
else
|
|
echo "tiktoken: curl fetch failed (network/GFW); will try cached image"
|
|
fi
|
|
fi
|
|
if [ ! -f ragflow_deps/cl100k_base.tiktoken ] && docker image inspect infiniflow/ragflow_deps:latest >/dev/null 2>&1; then
|
|
CID=$(docker create infiniflow/ragflow_deps:latest true) || true
|
|
if [ -n "${CID:-}" ]; then
|
|
if docker cp "$CID":/cl100k_base.tiktoken ragflow_deps/cl100k_base.tiktoken 2>/dev/null; then
|
|
echo "tiktoken: cl100k table copied from cached infiniflow/ragflow_deps image"
|
|
fi
|
|
docker rm -f "$CID" >/dev/null 2>&1 || true
|
|
fi
|
|
fi
|
|
fi
|
|
if [ -f ragflow_deps/cl100k_base.tiktoken ]; then
|
|
echo "tiktoken: cl100k table provisioned ($(wc -c < ragflow_deps/cl100k_base.tiktoken) bytes)"
|
|
else
|
|
echo "tiktoken: cl100k table NOT provisioned — offline loader will fail loudly"
|
|
fi
|
|
|
|
# Provide the tokenizer assets the four non-cl100k counters load
|
|
# (ragflow_deps/huggingface.co/<repo>/<file>). Their only no-network source
|
|
# is the infiniflow/ragflow_deps image — the runtime images read them from
|
|
# exactly the same path — so this is the same "if the image is already
|
|
# cached" fallback the table above uses (again, no `docker pull`). Without
|
|
# them counter_properties_test / asset_dir_test silently drop their
|
|
# non-cl100k coverage, which is how a counter regression goes unnoticed.
|
|
TOKENIZER_ASSET_PATHS=(
|
|
BAAI/bge-m3/sentencepiece.bpe.model
|
|
BAAI/bge-large-en-v1.5/vocab.txt
|
|
Qwen/Qwen3-Embedding-0.6B/tokenizer.json
|
|
intfloat/e5-mistral-7b-instruct/tokenizer.json
|
|
)
|
|
if docker image inspect infiniflow/ragflow_deps:latest >/dev/null 2>&1; then
|
|
CID=$(docker create infiniflow/ragflow_deps:latest true) || true
|
|
if [ -n "${CID:-}" ]; then
|
|
for asset in "${TOKENIZER_ASSET_PATHS[@]}"; do
|
|
target="ragflow_deps/huggingface.co/$asset"
|
|
if [ -f "$target" ]; then
|
|
continue
|
|
fi
|
|
mkdir -p "$(dirname "$target")"
|
|
if docker cp "$CID:/huggingface.co/$asset" "$target" 2>/dev/null; then
|
|
echo "tokenizer: $asset copied from cached infiniflow/ragflow_deps image"
|
|
else
|
|
echo "tokenizer: $asset absent from the cached image — its counter will skip"
|
|
fi
|
|
done
|
|
docker rm -f "$CID" >/dev/null 2>&1 || true
|
|
fi
|
|
else
|
|
echo "tokenizer: infiniflow/ragflow_deps not cached on this runner — non-cl100k counters will skip"
|
|
fi
|
|
|
|
PKGS=$(go list ./... 2>/dev/null \
|
|
| grep -v '/internal/storage$' \
|
|
| grep -v '/internal/handler$' || true)
|
|
if [ -z "$PKGS" ]; then
|
|
./build.sh --test
|
|
else
|
|
./build.sh --test -- $PKGS
|
|
fi
|
|
|
|
- name: Build ragflow:nightly
|
|
run: |
|
|
set -euo pipefail
|
|
RUNNER_WORKSPACE_PREFIX=${RUNNER_WORKSPACE_PREFIX:-${HOME}}
|
|
BUILD_LOCK_FILE=${RUNNER_WORKSPACE_PREFIX}/artifacts/${GITHUB_REPOSITORY}/docker-build.lock
|
|
mkdir -p "$(dirname "${BUILD_LOCK_FILE}")"
|
|
# Pass the Gitee personal access token to the Docker build via --build-arg GITEE_TOKEN.
|
|
# Dockerfile / Dockerfile_ci read it from ARG GITEE_TOKEN (NOT a BuildKit secret),
|
|
# so the runner must expose GITEE_TOKEN as an environment variable (e.g. via the
|
|
# runner's docker-compose `environment:` mapping sourced from its .env file).
|
|
# Pick the Dockerfile matching the API proxy scheme:
|
|
# - go -> Dockerfile (Go-native image)
|
|
# - * -> Dockerfile_ci (Python/legacy image)
|
|
DOCKERFILE=Dockerfile
|
|
(
|
|
flock -w 10800 9 || { echo "Timed out waiting for the shared Docker build slot" >&2; exit 1; }
|
|
echo "Acquired Docker build slot for ${DOC_ENGINE}/${API_PROXY_SCHEME} (dockerfile=${DOCKERFILE})"
|
|
##sudo docker pull ubuntu:24.04
|
|
sudo DOCKER_BUILDKIT=1 docker build --build-arg NEED_MIRROR=1 --build-arg HTTPS_PROXY=${HTTPS_PROXY} --build-arg HTTP_PROXY=${HTTP_PROXY} --build-arg GITEE_TOKEN=${GITEE_TOKEN} -f "${DOCKERFILE}" -t ${RAGFLOW_IMAGE} .
|
|
) 9>"${BUILD_LOCK_FILE}"
|
|
|
|
- name: Prepare Python test environment
|
|
if: env.API_PROXY_SCHEME != 'web'
|
|
run: |
|
|
git config --global url."https://oauth2:${GITEE_TOKEN}@gitee.com/".insteadOf "https://gitee.com/"
|
|
uv sync --python 3.13 --group test --frozen
|
|
uv pip install -e sdk/python
|
|
|
|
- name: Prepare function test environment
|
|
working-directory: docker
|
|
run: |
|
|
set -euo pipefail
|
|
sudo chmod 1777 /tmp
|
|
## ss already installed
|
|
##sudo apt update && sudo apt install -y iproute2
|
|
RUNNER_WORKSPACE_PREFIX=${RUNNER_WORKSPACE_PREFIX:-${HOME}}
|
|
COMPOSE_PROJECT_NAME="${GITHUB_RUN_ID}-${DOC_ENGINE}-${API_PROXY_SCHEME}"
|
|
echo "COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME}" >> ${GITHUB_ENV}
|
|
echo "RAGFLOW_CONTAINER=${COMPOSE_PROJECT_NAME}-ragflow-cpu-1" >> ${GITHUB_ENV}
|
|
ARTIFACTS_DIR=${RUNNER_WORKSPACE_PREFIX}/artifacts/${GITHUB_REPOSITORY}/${GITHUB_RUN_ID}/${DOC_ENGINE}/${API_PROXY_SCHEME}
|
|
echo "ARTIFACTS_DIR=${ARTIFACTS_DIR}" >> ${GITHUB_ENV}
|
|
rm -rf "${ARTIFACTS_DIR}" && mkdir -p "${ARTIFACTS_DIR}"
|
|
|
|
# Determine runner number (default to 1 if not found)
|
|
RUNNER_NUM=$(sudo docker inspect $(hostname) --format '{{index .Config.Labels "com.docker.compose.container-number"}}' 2>/dev/null || true)
|
|
if [[ -z "${RUNNER_NUM:-}" ]]; then
|
|
# GitHub self-hosted runners always expose RUNNER_NAME; hash it to spread candidates.
|
|
if [[ -n "${RUNNER_NAME:-}" ]]; then
|
|
RUNNER_NUM=$(( $(printf '%d' 0x$(echo -n "${RUNNER_NAME}" | md5sum | cut -c1-4)) % 1000 ))
|
|
else
|
|
RUNNER_NUM=$(( GITHUB_RUN_ID % 1000 ))
|
|
fi
|
|
fi
|
|
|
|
# Engine-specific offset partitions keep concurrent engine jobs from
|
|
# choosing the same host ports when they land on the same self-hosted runner.
|
|
# A lock plus reservation file closes the check/start race between parallel jobs.
|
|
PORT_BASES=(1200 1201 23817 23820 5432 5455 9000 9001 6379 6380 6381 6601 9380 9381 9382 9385 80 443 4222 8222 9900 8123)
|
|
PARTITION_SIZE=3000
|
|
# Keep every (base + offset) host port BELOW the Linux ephemeral
|
|
# range (net.ipv4.ip_local_port_range, default 32768-60999). An
|
|
# outbound connection from the host can transiently squat on a port
|
|
# inside that range, and `docker compose up` then fails with
|
|
# "address already in use" even though the reservation succeeded —
|
|
# the reservation cannot see it because `docker ps` only lists
|
|
# container-published ports. Largest base is 23820, so offsets must
|
|
# stay under 8948 for the ceiling to clear 32768.
|
|
case "${DOC_ENGINE}" in
|
|
elasticsearch) PARTITION_BASE=1000 ;; # offsets 1000-3999 -> ports <= 27819
|
|
infinity) PARTITION_BASE=4000 ;; # offsets 4000-6999 -> ports <= 30819
|
|
*) echo "Unsupported DOC_ENGINE=${DOC_ENGINE}" >&2; exit 1 ;;
|
|
esac
|
|
# Runners run inside docker containers that share the host docker daemon
|
|
# (/var/run/docker.sock), so neither /tmp nor `ss` can coordinate across runners:
|
|
# each container has its own /tmp and its own network namespace, and `ss` cannot
|
|
# see host ports bound by other runners' compose stacks. Use the shared daemon for
|
|
# both primitives:
|
|
# - per-port mutex = for each host port (base+offset) we intend to bind, create
|
|
# a docker network named ragflow-ci-port-<port> on the shared daemon. docker
|
|
# network create is NOT idempotent (fails on name conflict), so only one runner
|
|
# can claim a given port. This is per-PORT, not per-offset, because different
|
|
# (base, offset) pairs can produce the same host port (e.g. 6380+5999 ==
|
|
# 9380+2999 == 12379); a per-offset guard cannot prevent that collision.
|
|
# - port availability = docker ps published ports (sees every container on the
|
|
# shared daemon regardless of which runner started it).
|
|
# No file-system sharing required, no host netns required.
|
|
|
|
cleanup_stale_port_locks() {
|
|
local now stale_after net ts before after
|
|
now=$(date -u +%s)
|
|
stale_after=$((60 * 60))
|
|
before=$(sudo docker network ls --filter "label=ragflow-ci-created" --format '{{.Name}}' | wc -l)
|
|
for net in $(sudo docker network ls --filter "label=ragflow-ci-created" --format '{{.Name}}'); do
|
|
ts=$(sudo docker network inspect --format '{{ index .Labels "ragflow-ci-created" }}' "${net}" 2>/dev/null || true)
|
|
# Remove if timestamp is non-numeric (legacy network from a previous code
|
|
# version) or if it is numeric and older than stale_after.
|
|
if [[ ! "${ts}" =~ ^[0-9]+$ ]] || (( now - ts > stale_after )); then
|
|
sudo docker network rm "${net}" >/dev/null 2>&1 || true
|
|
fi
|
|
done
|
|
after=$(sudo docker network ls --filter "label=ragflow-ci-created" --format '{{.Name}}' | wc -l)
|
|
echo " cleanup_stale_port_locks: ${before} -> ${after} guards (TTL=${stale_after}s)" >&2
|
|
}
|
|
|
|
reserve_port_offset() {
|
|
local attempt candidate base port guard g ok reject_reason create_err guard_subnet
|
|
local -a guards
|
|
local used guarded need_refresh used_n guarded_n debug_count create_fail_streak
|
|
cleanup_stale_port_locks
|
|
echo "Reserving host ports for ${DOC_ENGINE} compose stack..." >&2
|
|
# Fail fast if the shared docker daemon is not reachable — avoids a
|
|
# long spin where every docker network create silently fails.
|
|
if ! sudo docker info >/dev/null 2>&1; then
|
|
echo " ERROR: 'sudo docker info' failed — docker daemon is not reachable from this runner" >&2
|
|
return 1
|
|
fi
|
|
echo " Running containers on shared daemon: $(sudo docker ps -q 2>/dev/null | wc -l)" >&2
|
|
echo " Docker networks on shared daemon: $(sudo docker network ls -q 2>/dev/null | wc -l)" >&2
|
|
need_refresh=1
|
|
debug_count=0
|
|
create_fail_streak=0
|
|
for attempt in $(seq 0 499); do
|
|
candidate=$(( PARTITION_BASE + ((GITHUB_RUN_ID + RUNNER_NUM * 1000 + attempt * 97) % PARTITION_SIZE) ))
|
|
# Refresh snapshots on first attempt and after a failed reservation.
|
|
if [[ ${need_refresh} -eq 1 ]]; then
|
|
used=$(sudo docker ps --format '{{.Ports}}' 2>/dev/null | grep -oE '[0-9]+->' | tr -d '>-' | sort -u || true)
|
|
# Strip both ragflow-ci-port- (current) and ragflow-ci-portguard- (legacy) prefixes.
|
|
guarded=$(sudo docker network ls --filter "label=ragflow-ci-created" --format '{{.Name}}' 2>/dev/null | sed 's/^ragflow-ci-port\(guard\)\?-//' | sort -u || true)
|
|
used_n=$(echo "${used}" | grep -c . 2>/dev/null || true)
|
|
guarded_n=$(echo "${guarded}" | grep -c . 2>/dev/null || true)
|
|
echo " Snapshot: ${used_n} bound ports, ${guarded_n} guarded ports" >&2
|
|
[[ ${used_n} -gt 0 ]] && echo " Bound sample: $(echo "${used}" | tr '\n' ' ' | cut -d' ' -f1-10)" >&2
|
|
[[ ${guarded_n} -gt 0 ]] && echo " Guarded sample: $(echo "${guarded}" | tr '\n' ' ' | cut -d' ' -f1-10)" >&2
|
|
need_refresh=0
|
|
fi
|
|
# Fast path: skip if any (base+candidate) port is already bound or guarded.
|
|
ok=1
|
|
reject_reason=""
|
|
for base in "${PORT_BASES[@]}"; do
|
|
port=$((base + candidate))
|
|
if grep -qx "${port}" <<< "${used}" 2>/dev/null; then
|
|
ok=0; reject_reason="port ${port} (base ${base}) already bound"; break
|
|
fi
|
|
if grep -qx "${port}" <<< "${guarded}" 2>/dev/null; then
|
|
ok=0; reject_reason="port ${port} (base ${base}) already guarded"; break
|
|
fi
|
|
done
|
|
if [[ ${ok} -eq 0 ]]; then
|
|
if [[ ${debug_count} -lt 5 ]]; then
|
|
echo " Candidate ${candidate}: rejected — ${reject_reason}" >&2
|
|
debug_count=$((debug_count + 1))
|
|
fi
|
|
continue
|
|
fi
|
|
# Atomically claim every (base+candidate) host port via a per-port docker
|
|
# network. docker network create fails on name conflict, so only one runner
|
|
# can claim a given port. If any port is already claimed, undo partial
|
|
# reservations and try the next offset.
|
|
guards=()
|
|
ok=1
|
|
for base in "${PORT_BASES[@]}"; do
|
|
port=$((base + candidate))
|
|
guard="ragflow-ci-port-${port}"
|
|
# Use an explicit subnet from 10.0.0.0/8 (outside Docker's
|
|
# default 172.17-31.x.x pools) to bypass "all predefined
|
|
# address pools have been fully subnetted" errors on shared
|
|
# daemons where auto-allocation is exhausted.
|
|
guard_subnet="10.$((port / 256)).$((port % 256)).0/24"
|
|
if ! create_err=$(sudo docker network create --subnet "${guard_subnet}" --label ragflow-ci-created="$(date -u +%s)" "${guard}" 2>&1 >/dev/null); then
|
|
echo " docker network create ${guard} failed: ${create_err}" >&2
|
|
ok=0; break
|
|
fi
|
|
guards+=("${guard}")
|
|
done
|
|
if [[ ${ok} -eq 1 ]]; then
|
|
PORT_OFFSET=${candidate}
|
|
PORT_RESERVATION="${guards[*]}"
|
|
echo "Reserved offset ${PORT_OFFSET} (attempt ${attempt})" >&2
|
|
return 0
|
|
fi
|
|
for g in "${guards[@]}"; do
|
|
sudo docker network rm "${g}" >/dev/null 2>&1 || true
|
|
done
|
|
# If docker network create keeps failing (daemon gone, permission, etc.),
|
|
# bail out early instead of looping 6000 times. A name-conflict failure
|
|
# should be rare because the fast path already filters guarded ports.
|
|
create_fail_streak=$((create_fail_streak + 1))
|
|
if [[ ${create_fail_streak} -ge 10 ]]; then
|
|
echo " docker network create failed ${create_fail_streak} consecutive candidates; aborting" >&2
|
|
return 1
|
|
fi
|
|
# Another runner just claimed a port; refresh snapshots before retrying.
|
|
need_refresh=1
|
|
done
|
|
echo " Exhausted ${attempt} candidates. Last snapshot: ${used_n} bound, ${guarded_n} guarded" >&2
|
|
return 1
|
|
}
|
|
|
|
if ! reserve_port_offset; then
|
|
echo "Failed to reserve a free host port range for ${DOC_ENGINE} docker compose" >&2
|
|
exit 1
|
|
fi
|
|
echo "PORT_RESERVATION=${PORT_RESERVATION}" >> ${GITHUB_ENV}
|
|
echo "Using ${DOC_ENGINE} host port offset ${PORT_OFFSET}"
|
|
ES_PORT=$((1200 + PORT_OFFSET))
|
|
OS_PORT=$((1201 + PORT_OFFSET))
|
|
INFINITY_THRIFT_PORT=$((23817 + PORT_OFFSET))
|
|
INFINITY_HTTP_PORT=$((23820 + PORT_OFFSET))
|
|
INFINITY_PSQL_PORT=$((5432 + PORT_OFFSET))
|
|
EXPOSE_MYSQL_PORT=$((5455 + PORT_OFFSET))
|
|
MINIO_PORT=$((9000 + PORT_OFFSET))
|
|
MINIO_CONSOLE_PORT=$((9001 + PORT_OFFSET))
|
|
REDIS_PORT=$((6379 + PORT_OFFSET))
|
|
EXPOSE_NATS_PORT=$((4222 + PORT_OFFSET))
|
|
NATS_MONITORING_PORT=$((8222 + PORT_OFFSET))
|
|
TEI_PORT=$((6380 + PORT_OFFSET))
|
|
KVROCKS_PORT=$((6381 + PORT_OFFSET))
|
|
KIBANA_PORT=$((6601 + PORT_OFFSET))
|
|
SVR_HTTP_PORT=$((9380 + PORT_OFFSET))
|
|
ADMIN_SVR_HTTP_PORT=$((9381 + PORT_OFFSET))
|
|
SVR_MCP_PORT=$((9382 + PORT_OFFSET))
|
|
SANDBOX_EXECUTOR_MANAGER_PORT=$((9385 + PORT_OFFSET))
|
|
SVR_WEB_HTTP_PORT=$((80 + PORT_OFFSET))
|
|
SVR_WEB_HTTPS_PORT=$((443 + PORT_OFFSET))
|
|
EXPOSE_CLICKHOUSE_TCP_PORT=$((9900 + PORT_OFFSET))
|
|
CLICKHOUSE_HTTP_PORT=$((8123 + PORT_OFFSET))
|
|
|
|
# Persist computed ports into .env so docker-compose uses the correct host bindings.
|
|
# Remove previous CI overrides first; docker compose uses the last duplicate key.
|
|
sed -i '/^ES_PORT=/d;/^OS_PORT=/d;/^INFINITY_THRIFT_PORT=/d;/^INFINITY_HTTP_PORT=/d;/^INFINITY_PSQL_PORT=/d;/^EXPOSE_MYSQL_PORT=/d;/^MINIO_PORT=/d;/^MINIO_CONSOLE_PORT=/d;/^REDIS_PORT=/d;/^KVROCKS_PORT=/d;/^EXPOSE_NATS_PORT=/d;/^NATS_MONITORING_PORT=/d;/^TEI_PORT=/d;/^KIBANA_PORT=/d;/^SVR_HTTP_PORT=/d;/^ADMIN_SVR_HTTP_PORT=/d;/^SVR_MCP_PORT=/d;/^SANDBOX_EXECUTOR_MANAGER_PORT=/d;/^SVR_WEB_HTTP_PORT=/d;/^SVR_WEB_HTTPS_PORT=/d;/^EXPOSE_CLICKHOUSE_TCP_PORT=/d;/^CLICKHOUSE_HTTP_PORT=/d;/^COMPOSE_PROFILES=/d;/^TEI_MODEL=/d;/^RAGFLOW_IMAGE=/d;/^DOC_ENGINE=/d;/^API_PROXY_SCHEME=/d' .env
|
|
# docker-compose-go.yml uses env_file: .env-go for the ragflow-cpu
|
|
# container, so the Go server process reads COMPOSE_PROFILES and
|
|
# TEI_MODEL from .env (not .env). Without syncing these two,
|
|
# the Go server's checkModelAvailable TEI bypass never triggers
|
|
# because .env's COMPOSE_PROFILES lacks "tei-", causing
|
|
# PATCH /models/default to fail for the Builtin/Local embedding
|
|
# model and datasets to be created with an empty embedding_model.
|
|
sed -i '/^COMPOSE_PROFILES=/d;/^RAGFLOW_DEV_MODE=/d;/^TEI_MODEL=/d' .env
|
|
{
|
|
echo ""
|
|
echo "ES_PORT=${ES_PORT}"
|
|
echo "OS_PORT=${OS_PORT}"
|
|
echo "INFINITY_THRIFT_PORT=${INFINITY_THRIFT_PORT}"
|
|
echo "INFINITY_HTTP_PORT=${INFINITY_HTTP_PORT}"
|
|
echo "INFINITY_PSQL_PORT=${INFINITY_PSQL_PORT}"
|
|
echo "EXPOSE_MYSQL_PORT=${EXPOSE_MYSQL_PORT}"
|
|
echo "MINIO_PORT=${MINIO_PORT}"
|
|
echo "MINIO_CONSOLE_PORT=${MINIO_CONSOLE_PORT}"
|
|
echo "REDIS_PORT=${REDIS_PORT}"
|
|
echo "KVROCKS_PORT=${KVROCKS_PORT}"
|
|
echo "EXPOSE_NATS_PORT=${EXPOSE_NATS_PORT}"
|
|
echo "NATS_MONITORING_PORT=${NATS_MONITORING_PORT}"
|
|
echo "TEI_PORT=${TEI_PORT}"
|
|
echo "KIBANA_PORT=${KIBANA_PORT}"
|
|
echo "SVR_HTTP_PORT=${SVR_HTTP_PORT}"
|
|
echo "ADMIN_SVR_HTTP_PORT=${ADMIN_SVR_HTTP_PORT}"
|
|
echo "SVR_MCP_PORT=${SVR_MCP_PORT}"
|
|
echo "SANDBOX_EXECUTOR_MANAGER_PORT=${SANDBOX_EXECUTOR_MANAGER_PORT}"
|
|
echo "SVR_WEB_HTTP_PORT=${SVR_WEB_HTTP_PORT}"
|
|
echo "SVR_WEB_HTTPS_PORT=${SVR_WEB_HTTPS_PORT}"
|
|
echo "EXPOSE_CLICKHOUSE_TCP_PORT=${EXPOSE_CLICKHOUSE_TCP_PORT}"
|
|
echo "CLICKHOUSE_HTTP_PORT=${CLICKHOUSE_HTTP_PORT}"
|
|
echo "COMPOSE_PROFILES=${DOC_ENGINE},cpu,ragflow-go,tei-cpu,deepdoc,metadata-mysql,clickhouse"
|
|
echo "TEI_MODEL=BAAI/bge-small-en-v1.5"
|
|
echo "RAGFLOW_IMAGE=${RAGFLOW_IMAGE}"
|
|
echo "DOC_ENGINE=${DOC_ENGINE}"
|
|
echo "API_PROXY_SCHEME=${API_PROXY_SCHEME}"
|
|
} >> .env
|
|
# Sync COMPOSE_PROFILES and TEI_MODEL into .env so the Go
|
|
# server container (env_file: .env) sees the same TEI bypass
|
|
# config as the Python container. Only these two affect the
|
|
# in-process checkModelAvailable logic; other .env keys are
|
|
# either yml-interpolation-only or already correct in .env.
|
|
# RAGFLOW_DEV_MODE rides along for the same reason.
|
|
{
|
|
echo ""
|
|
echo "COMPOSE_PROFILES=${DOC_ENGINE},cpu,ragflow-go,tei-cpu,deepdoc,metadata-mysql,clickhouse"
|
|
echo "TEI_MODEL=BAAI/bge-small-en-v1.5"
|
|
# The image stamps its git-describe output into /ragflow/VERSION,
|
|
# so this checkout reports v0.27.x while --migrate records the
|
|
# v1.0.0-rc1.dev1 database marker. Without the override the Go
|
|
# server refuses to boot against the marker it just wrote.
|
|
echo "RAGFLOW_DEV_MODE=true"
|
|
} >> .env
|
|
RAGFLOW_HTTP_PORT=${SVR_WEB_HTTP_PORT}
|
|
echo "RAGFLOW_HTTP_PORT=${RAGFLOW_HTTP_PORT}" >> ${GITHUB_ENV}
|
|
echo "HOST_ADDRESS=http://host.docker.internal:${RAGFLOW_HTTP_PORT}" >> ${GITHUB_ENV}
|
|
|
|
sed -i \
|
|
-e 's#${NATS_PORT:-4222}:4222#${EXPOSE_NATS_PORT}:4222#' \
|
|
-e 's#"8222:8222"#"${NATS_MONITORING_PORT}:8222"#' \
|
|
docker-compose-base.yml
|
|
|
|
- name: Start ragflow:nightly for Infinity
|
|
run: |
|
|
set -euo pipefail
|
|
COMPOSE_FILE=docker/docker-compose.yml
|
|
sudo docker compose -f "${COMPOSE_FILE}" -p ${COMPOSE_PROJECT_NAME} down -v || true
|
|
sudo docker ps -a --filter "label=com.docker.compose.project=${COMPOSE_PROJECT_NAME}" -q | xargs -r sudo docker rm -f
|
|
sudo docker compose -f "${COMPOSE_FILE}" -p ${COMPOSE_PROJECT_NAME} up -d
|
|
|
|
- name: Run New RESTFUL api tests against Infinity
|
|
if: env.API_PROXY_SCHEME != 'web'
|
|
run: |
|
|
export http_proxy=""; export https_proxy=""; export no_proxy=""; export HTTP_PROXY=""; export HTTPS_PROXY=""; export NO_PROXY=""
|
|
svc_ready=0
|
|
for i in $(seq 1 60); do
|
|
if sudo docker exec ${RAGFLOW_CONTAINER} curl -sf --connect-timeout 5 "${HOST_ADDRESS}/api/v1/system/ping" > /dev/null 2>&1; then
|
|
svc_ready=1
|
|
break
|
|
fi
|
|
echo "Waiting for service to be available... ($i/60)"
|
|
sleep 5
|
|
done
|
|
if [ "$svc_ready" -ne 1 ]; then
|
|
echo "Service did not become ready after 5 minutes. Docker logs:"
|
|
sudo docker logs ${RAGFLOW_CONTAINER}
|
|
exit 1
|
|
fi
|
|
source .venv/bin/activate && set -o pipefail; DOC_ENGINE=infinity pytest -s --tb=short --level=${HTTP_API_TEST_LEVEL} --durations=100 test/testcases/restful_api 2>&1 | tee infinity_restful_api_test.log
|
|
|
|
- name: Upload coverage reports to Codecov
|
|
uses: codecov/codecov-action@v5
|
|
if: ${{ !cancelled() }}
|
|
with:
|
|
token: ${{ secrets.CODECOV_TOKEN }}
|
|
fail_ci_if_error: false
|
|
|
|
- name: Collect ragflow log Infinity
|
|
if: ${{ !cancelled() }}
|
|
# Log collection is diagnostics only: rebuild the artifacts path here
|
|
# (the directory created by "Prepare function test environment" may be
|
|
# gone by now, which made `cp` fail with ENOENT) and never fail the job
|
|
# over a log copy error.
|
|
run: |
|
|
set -uo pipefail
|
|
RUNNER_WORKSPACE_PREFIX="${RUNNER_WORKSPACE_PREFIX:-${HOME}}"
|
|
ARTIFACTS_DIR="${RUNNER_WORKSPACE_PREFIX}/artifacts/${GITHUB_REPOSITORY}/${GITHUB_RUN_ID}/${DOC_ENGINE}/${API_PROXY_SCHEME}"
|
|
LOG_DEST="${ARTIFACTS_DIR}/ragflow-logs-infinity"
|
|
mkdir -p "${ARTIFACTS_DIR}"
|
|
if [ -d docker/ragflow-logs ]; then
|
|
sudo rm -rf "${LOG_DEST}"
|
|
if sudo cp -a docker/ragflow-logs "${LOG_DEST}"; then
|
|
sudo chown -R "$(id -u):$(id -g)" "${LOG_DEST}" || true
|
|
echo "Copied ragflow logs to ${LOG_DEST}"
|
|
else
|
|
echo "::warning::Failed to copy ragflow logs to ${LOG_DEST}; continuing"
|
|
fi
|
|
echo "ragflow log" && sudo tail -n 200 docker/ragflow-logs/ragflow_server.log || true
|
|
else
|
|
echo "No docker/ragflow-logs directory found; skipping log collection"
|
|
fi
|
|
sudo rm -rf docker/ragflow-logs || true
|
|
|
|
- name: Stop ragflow:nightly for Infinity
|
|
if: ${{ always() }}
|
|
run: |
|
|
set -euo pipefail
|
|
COMPOSE_FILE=docker/docker-compose.yml
|
|
# Sometimes `docker compose down` fail due to hang container, heavy load etc. Need to remove such containers to release resources(for example, listen ports).
|
|
sudo docker compose -f "${COMPOSE_FILE}" -p ${COMPOSE_PROJECT_NAME} down -v || true
|
|
sudo docker ps -a --filter "label=com.docker.compose.project=${COMPOSE_PROJECT_NAME}" -q | xargs -r sudo docker rm -f
|
|
if [[ -n ${RAGFLOW_IMAGE} ]]; then
|
|
sudo docker rmi -f ${RAGFLOW_IMAGE}
|
|
fi
|
|
if [[ -n ${PORT_RESERVATION:-} ]]; then
|
|
# PORT_RESERVATION is a space-separated list of per-port docker network guards
|
|
# created by the prepare step. Remove each to free the ports for other runners.
|
|
for guard in ${PORT_RESERVATION}; do
|
|
sudo docker network rm "${guard}" >/dev/null 2>&1 || true
|
|
done
|
|
fi
|
|
|
|
|
|
|
|
ragflow_tests_elasticsearch:
|
|
name: ragflow_tests_elasticsearch (${{ matrix.api_proxy_scheme }})
|
|
needs: ragflow_preflight
|
|
if: ${{ github.event_name != 'pull_request' && github.event_name != 'pull_request_target' || (github.event.pull_request.draft == false && contains(github.event.pull_request.labels.*.name, 'ci') && (github.event.action != 'labeled' || github.event.label.name == 'ci')) }}
|
|
runs-on: [ "self-hosted", "ragflow-test" ]
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
api_proxy_scheme: ${{ fromJSON(needs.ragflow_preflight.outputs.api_proxy_schemes) }}
|
|
env:
|
|
DOC_ENGINE: elasticsearch
|
|
API_PROXY_SCHEME: ${{ matrix.api_proxy_scheme }}
|
|
RAGFLOW_IMAGE: infiniflow/ragflow:${{ github.run_id }}-elasticsearch-${{ matrix.api_proxy_scheme }}
|
|
HTTP_API_TEST_LEVEL: ${{ needs.ragflow_preflight.outputs.http_api_test_level }}
|
|
HAS_GO: ${{ needs.ragflow_preflight.outputs.has_go_changes }}
|
|
HAS_WEB: ${{ needs.ragflow_preflight.outputs.has_web_changes }}
|
|
steps:
|
|
- name: Ensure workspace ownership
|
|
if: always()
|
|
run: |
|
|
echo "Workflow triggered by ${{ github.event_name }}"
|
|
echo "chown -R ${USER} ${GITHUB_WORKSPACE}" && sudo chown -R ${USER} ${GITHUB_WORKSPACE}
|
|
|
|
- name: Check out code
|
|
if: always()
|
|
uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && format('refs/pull/{0}/merge', github.event.pull_request.number) || github.sha }}
|
|
fetch-depth: 0
|
|
fetch-tags: true
|
|
allow-unsafe-pr-checkout: true
|
|
|
|
- name: Build ragflow go server
|
|
if: env.API_PROXY_SCHEME == 'go'
|
|
run: |
|
|
set -euo pipefail
|
|
BUILDER_CONTAINER=ragflow_build_${GITHUB_RUN_ID}_${DOC_ENGINE}_$(od -An -N4 -tx4 /dev/urandom | tr -d ' ')
|
|
cleanup_builder() {
|
|
if [[ -n "${BUILDER_CONTAINER:-}" ]]; then
|
|
sudo docker rm -f -v "${BUILDER_CONTAINER}" >/dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
trap cleanup_builder EXIT
|
|
|
|
TZ=${TZ:-$(readlink -f /etc/localtime | awk -F '/zoneinfo/' '{print $2}')}
|
|
sudo docker run --privileged -d --name "${BUILDER_CONTAINER}" \
|
|
-e TZ="${TZ}" \
|
|
-e UV_INDEX=https://mirrors.aliyun.com/pypi/simple \
|
|
-v "${PWD}:/ragflow" \
|
|
-v "${PWD}/internal/binding/cpp/resource:/usr/share/infinity/resource" \
|
|
infiniflow/infinity_builder:ubuntu22_clang20
|
|
sudo docker exec "${BUILDER_CONTAINER}" bash -c 'git config --global safe.directory "*" && cd /ragflow && ./build.sh --cpp'
|
|
./build.sh --go
|
|
|
|
- name: Run Go unit tests
|
|
if: env.API_PROXY_SCHEME == 'go'
|
|
# Runs after `./build.sh --go`, which guarantees the C++ static
|
|
# library (librag_tokenizer_c_api.a) is present on disk. The Go
|
|
# test binaries link against it transitively through
|
|
# `internal/binding`, so running `go test` before the C++ build
|
|
# fails the link step.
|
|
#
|
|
# Excludes packages whose tests fail for environmental reasons
|
|
# unrelated to the diff:
|
|
# - internal/tokenizer is split: the pure-Go BPE loader tests
|
|
# (bpe_loader_test.go) run in the default tier; the C++ binding /
|
|
# dict-dependent tests (tokenizer_test.go,
|
|
# tokenizer_concurrent_test.go) and the on-disk anchor test
|
|
# (bpe_loader_anchors_test.go) are tagged `manual` and need the
|
|
# docker builder's /usr/share/infinity/resource, so they stay out
|
|
# of the default run.
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
# Provide the cl100k BPE table for the offline tokenizer loader.
|
|
# The loader reads <repo>/ragflow_deps/cl100k_base.tiktoken (or
|
|
# TIKTOKEN_CACHE_DIR). Try to fetch it directly from the upstream
|
|
# openai blob first (small ~1.6MB, reachable from the CI runner — the
|
|
# pre-PR code downloaded it on the fly, so the network path is known
|
|
# good). If the direct fetch fails (e.g. GFW), fall back to extracting
|
|
# it from the local infiniflow/ragflow_deps image *if already present*
|
|
# on the runner (no `docker pull`, since that huge image is first
|
|
# fetched at the later "Build ragflow:nightly" step). Best-effort: if
|
|
# neither source works, the loader still fails loudly (no silent
|
|
# 0-token degradation).
|
|
# tiktoken is already installed in runner
|
|
mkdir -p ragflow_deps
|
|
cp /opt/ragflow_deps/cl100k_base.tiktoken ragflow_deps/cl100k_base.tiktoken || true
|
|
if [ ! -f ragflow_deps/cl100k_base.tiktoken ]; then
|
|
mkdir -p ragflow_deps
|
|
if command -v curl >/dev/null 2>&1; then
|
|
if curl -fsSL -o ragflow_deps/cl100k_base.tiktoken \
|
|
https://openaipublic.blob.core.windows.net/encodings/cl100k_base.tiktoken; then
|
|
echo "tiktoken: cl100k table fetched via curl from openai blob"
|
|
else
|
|
echo "tiktoken: curl fetch failed (network/GFW); will try cached image"
|
|
fi
|
|
fi
|
|
if [ ! -f ragflow_deps/cl100k_base.tiktoken ] && docker image inspect infiniflow/ragflow_deps:latest >/dev/null 2>&1; then
|
|
CID=$(docker create infiniflow/ragflow_deps:latest true) || true
|
|
if [ -n "${CID:-}" ]; then
|
|
if docker cp "$CID":/cl100k_base.tiktoken ragflow_deps/cl100k_base.tiktoken 2>/dev/null; then
|
|
echo "tiktoken: cl100k table copied from cached infiniflow/ragflow_deps image"
|
|
fi
|
|
docker rm -f "$CID" >/dev/null 2>&1 || true
|
|
fi
|
|
fi
|
|
fi
|
|
if [ -f ragflow_deps/cl100k_base.tiktoken ]; then
|
|
echo "tiktoken: cl100k table provisioned ($(wc -c < ragflow_deps/cl100k_base.tiktoken) bytes)"
|
|
else
|
|
echo "tiktoken: cl100k table NOT provisioned — offline loader will fail loudly"
|
|
fi
|
|
|
|
# Provide the tokenizer assets the four non-cl100k counters load
|
|
# (ragflow_deps/huggingface.co/<repo>/<file>). Their only no-network source
|
|
# is the infiniflow/ragflow_deps image — the runtime images read them from
|
|
# exactly the same path — so this is the same "if the image is already
|
|
# cached" fallback the table above uses (again, no `docker pull`). Without
|
|
# them counter_properties_test / asset_dir_test silently drop their
|
|
# non-cl100k coverage, which is how a counter regression goes unnoticed.
|
|
TOKENIZER_ASSET_PATHS=(
|
|
BAAI/bge-m3/sentencepiece.bpe.model
|
|
BAAI/bge-large-en-v1.5/vocab.txt
|
|
Qwen/Qwen3-Embedding-0.6B/tokenizer.json
|
|
intfloat/e5-mistral-7b-instruct/tokenizer.json
|
|
)
|
|
if docker image inspect infiniflow/ragflow_deps:latest >/dev/null 2>&1; then
|
|
CID=$(docker create infiniflow/ragflow_deps:latest true) || true
|
|
if [ -n "${CID:-}" ]; then
|
|
for asset in "${TOKENIZER_ASSET_PATHS[@]}"; do
|
|
target="ragflow_deps/huggingface.co/$asset"
|
|
if [ -f "$target" ]; then
|
|
continue
|
|
fi
|
|
mkdir -p "$(dirname "$target")"
|
|
if docker cp "$CID:/huggingface.co/$asset" "$target" 2>/dev/null; then
|
|
echo "tokenizer: $asset copied from cached infiniflow/ragflow_deps image"
|
|
else
|
|
echo "tokenizer: $asset absent from the cached image — its counter will skip"
|
|
fi
|
|
done
|
|
docker rm -f "$CID" >/dev/null 2>&1 || true
|
|
fi
|
|
else
|
|
echo "tokenizer: infiniflow/ragflow_deps not cached on this runner — non-cl100k counters will skip"
|
|
fi
|
|
|
|
PKGS=$(go list ./... 2>/dev/null \
|
|
| grep -v '/internal/storage$' \
|
|
| grep -v '/internal/handler$' || true)
|
|
if [ -z "$PKGS" ]; then
|
|
./build.sh --test
|
|
else
|
|
./build.sh --test -- $PKGS
|
|
fi
|
|
|
|
- name: Run ragflow_deps regression guards
|
|
if: env.API_PROXY_SCHEME == 'go'
|
|
# Pure-Python, local, needs no API keys or model downloads; guards (a) the
|
|
# ONNX Runtime extraction logic the in-process (Go) DeepDoc backend build
|
|
# depends on (notably the version-bump path C2 hardened) and (b) the
|
|
# tokenizer-asset chain: the downloader's asset list, the three runtime
|
|
# Dockerfiles' copy loops and the loaders' SHA-1 pins have to agree, or the
|
|
# Go counters silently degrade to the calibrated cl100k estimate. Run them
|
|
# in isolation so the test/testcases conftest (which requires
|
|
# SILICONFLOW_API_KEY) cannot fail the collection.
|
|
run: |
|
|
set -euo pipefail
|
|
uv sync --python 3.13 --group test --frozen
|
|
uv run pytest ragflow_deps/test_download_deps.py ragflow_deps/test_tokenizer_assets.py -q --durations=100
|
|
|
|
- name: Build ragflow:nightly
|
|
run: |
|
|
set -euo pipefail
|
|
RUNNER_WORKSPACE_PREFIX=${RUNNER_WORKSPACE_PREFIX:-${HOME}}
|
|
BUILD_LOCK_FILE=${RUNNER_WORKSPACE_PREFIX}/artifacts/${GITHUB_REPOSITORY}/docker-build.lock
|
|
mkdir -p "$(dirname "${BUILD_LOCK_FILE}")"
|
|
# Pass the Gitee personal access token to the Docker build via --build-arg GITEE_TOKEN.
|
|
# Dockerfile / Dockerfile_ci read it from ARG GITEE_TOKEN (NOT a BuildKit secret),
|
|
# so the runner must expose GITEE_TOKEN as an environment variable (e.g. via the
|
|
# runner's docker-compose `environment:` mapping sourced from its .env file).
|
|
# Pick the Dockerfile matching the API proxy scheme:
|
|
# - go -> Dockerfile (Go-native image)
|
|
# - * -> Dockerfile_ci (Python/legacy image)
|
|
DOCKERFILE=Dockerfile
|
|
(
|
|
flock -w 10800 9 || { echo "Timed out waiting for the shared Docker build slot" >&2; exit 1; }
|
|
echo "Acquired Docker build slot for ${DOC_ENGINE}/${API_PROXY_SCHEME} (dockerfile=${DOCKERFILE})"
|
|
##sudo docker pull ubuntu:24.04
|
|
sudo DOCKER_BUILDKIT=1 docker build --build-arg NEED_MIRROR=1 --build-arg HTTPS_PROXY=${HTTPS_PROXY} --build-arg HTTP_PROXY=${HTTP_PROXY} --build-arg GITEE_TOKEN=${GITEE_TOKEN} -f "${DOCKERFILE}" -t ${RAGFLOW_IMAGE} .
|
|
) 9>"${BUILD_LOCK_FILE}"
|
|
|
|
- name: Prepare Python test environment
|
|
if: env.API_PROXY_SCHEME != 'web'
|
|
run: |
|
|
git config --global url."https://oauth2:${GITEE_TOKEN}@gitee.com/".insteadOf "https://gitee.com/"
|
|
uv sync --python 3.13 --group test --frozen
|
|
uv pip install -e sdk/python
|
|
|
|
- name: Prepare function test environment
|
|
working-directory: docker
|
|
run: |
|
|
set -euo pipefail
|
|
sudo chmod 1777 /tmp
|
|
## ss already installed
|
|
##sudo apt update && sudo apt install -y iproute2
|
|
RUNNER_WORKSPACE_PREFIX=${RUNNER_WORKSPACE_PREFIX:-${HOME}}
|
|
COMPOSE_PROJECT_NAME="${GITHUB_RUN_ID}-${DOC_ENGINE}-${API_PROXY_SCHEME}"
|
|
echo "COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME}" >> ${GITHUB_ENV}
|
|
echo "RAGFLOW_CONTAINER=${COMPOSE_PROJECT_NAME}-ragflow-cpu-1" >> ${GITHUB_ENV}
|
|
ARTIFACTS_DIR=${RUNNER_WORKSPACE_PREFIX}/artifacts/${GITHUB_REPOSITORY}/${GITHUB_RUN_ID}/${DOC_ENGINE}/${API_PROXY_SCHEME}
|
|
echo "ARTIFACTS_DIR=${ARTIFACTS_DIR}" >> ${GITHUB_ENV}
|
|
rm -rf "${ARTIFACTS_DIR}" && mkdir -p "${ARTIFACTS_DIR}"
|
|
|
|
# Determine runner number (default to 1 if not found)
|
|
RUNNER_NUM=$(sudo docker inspect $(hostname) --format '{{index .Config.Labels "com.docker.compose.container-number"}}' 2>/dev/null || true)
|
|
if [[ -z "${RUNNER_NUM:-}" ]]; then
|
|
# GitHub self-hosted runners always expose RUNNER_NAME; hash it to spread candidates.
|
|
if [[ -n "${RUNNER_NAME:-}" ]]; then
|
|
RUNNER_NUM=$(( $(printf '%d' 0x$(echo -n "${RUNNER_NAME}" | md5sum | cut -c1-4)) % 1000 ))
|
|
else
|
|
RUNNER_NUM=$(( GITHUB_RUN_ID % 1000 ))
|
|
fi
|
|
fi
|
|
|
|
# Engine-specific offset partitions keep concurrent engine jobs from
|
|
# choosing the same host ports when they land on the same self-hosted runner.
|
|
# A lock plus reservation file closes the check/start race between parallel jobs.
|
|
PORT_BASES=(1200 1201 23817 23820 5432 5455 9000 9001 6379 6380 6381 6601 9380 9381 9382 9385 80 443 4222 8222 9900 8123)
|
|
PARTITION_SIZE=3000
|
|
# Keep every (base + offset) host port BELOW the Linux ephemeral
|
|
# range (net.ipv4.ip_local_port_range, default 32768-60999). An
|
|
# outbound connection from the host can transiently squat on a port
|
|
# inside that range, and `docker compose up` then fails with
|
|
# "address already in use" even though the reservation succeeded —
|
|
# the reservation cannot see it because `docker ps` only lists
|
|
# container-published ports. Largest base is 23820, so offsets must
|
|
# stay under 8948 for the ceiling to clear 32768.
|
|
case "${DOC_ENGINE}" in
|
|
elasticsearch) PARTITION_BASE=1000 ;; # offsets 1000-3999 -> ports <= 27819
|
|
infinity) PARTITION_BASE=4000 ;; # offsets 4000-6999 -> ports <= 30819
|
|
*) echo "Unsupported DOC_ENGINE=${DOC_ENGINE}" >&2; exit 1 ;;
|
|
esac
|
|
# Runners run inside docker containers that share the host docker daemon
|
|
# (/var/run/docker.sock), so neither /tmp nor `ss` can coordinate across runners:
|
|
# each container has its own /tmp and its own network namespace, and `ss` cannot
|
|
# see host ports bound by other runners' compose stacks. Use the shared daemon for
|
|
# both primitives:
|
|
# - per-port mutex = for each host port (base+offset) we intend to bind, create
|
|
# a docker network named ragflow-ci-port-<port> on the shared daemon. docker
|
|
# network create is NOT idempotent (fails on name conflict), so only one runner
|
|
# can claim a given port. This is per-PORT, not per-offset, because different
|
|
# (base, offset) pairs can produce the same host port (e.g. 6380+5999 ==
|
|
# 9380+2999 == 12379); a per-offset guard cannot prevent that collision.
|
|
# - port availability = docker ps published ports (sees every container on the
|
|
# shared daemon regardless of which runner started it).
|
|
# No file-system sharing required, no host netns required.
|
|
|
|
cleanup_stale_port_locks() {
|
|
local now stale_after net ts before after
|
|
now=$(date -u +%s)
|
|
stale_after=$((60 * 60))
|
|
before=$(sudo docker network ls --filter "label=ragflow-ci-created" --format '{{.Name}}' | wc -l)
|
|
for net in $(sudo docker network ls --filter "label=ragflow-ci-created" --format '{{.Name}}'); do
|
|
ts=$(sudo docker network inspect --format '{{ index .Labels "ragflow-ci-created" }}' "${net}" 2>/dev/null || true)
|
|
# Remove if timestamp is non-numeric (legacy network from a previous code
|
|
# version) or if it is numeric and older than stale_after.
|
|
if [[ ! "${ts}" =~ ^[0-9]+$ ]] || (( now - ts > stale_after )); then
|
|
sudo docker network rm "${net}" >/dev/null 2>&1 || true
|
|
fi
|
|
done
|
|
after=$(sudo docker network ls --filter "label=ragflow-ci-created" --format '{{.Name}}' | wc -l)
|
|
echo " cleanup_stale_port_locks: ${before} -> ${after} guards (TTL=${stale_after}s)" >&2
|
|
}
|
|
|
|
reserve_port_offset() {
|
|
local attempt candidate base port guard g ok reject_reason create_err guard_subnet
|
|
local -a guards
|
|
local used guarded need_refresh used_n guarded_n debug_count create_fail_streak
|
|
cleanup_stale_port_locks
|
|
echo "Reserving host ports for ${DOC_ENGINE} compose stack..." >&2
|
|
# Fail fast if the shared docker daemon is not reachable — avoids a
|
|
# long spin where every docker network create silently fails.
|
|
if ! sudo docker info >/dev/null 2>&1; then
|
|
echo " ERROR: 'sudo docker info' failed — docker daemon is not reachable from this runner" >&2
|
|
return 1
|
|
fi
|
|
echo " Running containers on shared daemon: $(sudo docker ps -q 2>/dev/null | wc -l)" >&2
|
|
echo " Docker networks on shared daemon: $(sudo docker network ls -q 2>/dev/null | wc -l)" >&2
|
|
need_refresh=1
|
|
debug_count=0
|
|
create_fail_streak=0
|
|
for attempt in $(seq 0 499); do
|
|
candidate=$(( PARTITION_BASE + ((GITHUB_RUN_ID + RUNNER_NUM * 1000 + attempt * 97) % PARTITION_SIZE) ))
|
|
# Refresh snapshots on first attempt and after a failed reservation.
|
|
if [[ ${need_refresh} -eq 1 ]]; then
|
|
used=$(sudo docker ps --format '{{.Ports}}' 2>/dev/null | grep -oE '[0-9]+->' | tr -d '>-' | sort -u || true)
|
|
# Strip both ragflow-ci-port- (current) and ragflow-ci-portguard- (legacy) prefixes.
|
|
guarded=$(sudo docker network ls --filter "label=ragflow-ci-created" --format '{{.Name}}' 2>/dev/null | sed 's/^ragflow-ci-port\(guard\)\?-//' | sort -u || true)
|
|
used_n=$(echo "${used}" | grep -c . 2>/dev/null || true)
|
|
guarded_n=$(echo "${guarded}" | grep -c . 2>/dev/null || true)
|
|
echo " Snapshot: ${used_n} bound ports, ${guarded_n} guarded ports" >&2
|
|
[[ ${used_n} -gt 0 ]] && echo " Bound sample: $(echo "${used}" | tr '\n' ' ' | cut -d' ' -f1-10)" >&2
|
|
[[ ${guarded_n} -gt 0 ]] && echo " Guarded sample: $(echo "${guarded}" | tr '\n' ' ' | cut -d' ' -f1-10)" >&2
|
|
need_refresh=0
|
|
fi
|
|
# Fast path: skip if any (base+candidate) port is already bound or guarded.
|
|
ok=1
|
|
reject_reason=""
|
|
for base in "${PORT_BASES[@]}"; do
|
|
port=$((base + candidate))
|
|
if grep -qx "${port}" <<< "${used}" 2>/dev/null; then
|
|
ok=0; reject_reason="port ${port} (base ${base}) already bound"; break
|
|
fi
|
|
if grep -qx "${port}" <<< "${guarded}" 2>/dev/null; then
|
|
ok=0; reject_reason="port ${port} (base ${base}) already guarded"; break
|
|
fi
|
|
done
|
|
if [[ ${ok} -eq 0 ]]; then
|
|
if [[ ${debug_count} -lt 5 ]]; then
|
|
echo " Candidate ${candidate}: rejected — ${reject_reason}" >&2
|
|
debug_count=$((debug_count + 1))
|
|
fi
|
|
continue
|
|
fi
|
|
# Atomically claim every (base+candidate) host port via a per-port docker
|
|
# network. docker network create fails on name conflict, so only one runner
|
|
# can claim a given port. If any port is already claimed, undo partial
|
|
# reservations and try the next offset.
|
|
guards=()
|
|
ok=1
|
|
for base in "${PORT_BASES[@]}"; do
|
|
port=$((base + candidate))
|
|
guard="ragflow-ci-port-${port}"
|
|
# Use an explicit subnet from 10.0.0.0/8 (outside Docker's
|
|
# default 172.17-31.x.x pools) to bypass "all predefined
|
|
# address pools have been fully subnetted" errors on shared
|
|
# daemons where auto-allocation is exhausted.
|
|
guard_subnet="10.$((port / 256)).$((port % 256)).0/24"
|
|
if ! create_err=$(sudo docker network create --subnet "${guard_subnet}" --label ragflow-ci-created="$(date -u +%s)" "${guard}" 2>&1 >/dev/null); then
|
|
echo " docker network create ${guard} failed: ${create_err}" >&2
|
|
ok=0; break
|
|
fi
|
|
guards+=("${guard}")
|
|
done
|
|
if [[ ${ok} -eq 1 ]]; then
|
|
PORT_OFFSET=${candidate}
|
|
PORT_RESERVATION="${guards[*]}"
|
|
echo "Reserved offset ${PORT_OFFSET} (attempt ${attempt})" >&2
|
|
return 0
|
|
fi
|
|
for g in "${guards[@]}"; do
|
|
sudo docker network rm "${g}" >/dev/null 2>&1 || true
|
|
done
|
|
# If docker network create keeps failing (daemon gone, permission, etc.),
|
|
# bail out early instead of looping 6000 times. A name-conflict failure
|
|
# should be rare because the fast path already filters guarded ports.
|
|
create_fail_streak=$((create_fail_streak + 1))
|
|
if [[ ${create_fail_streak} -ge 10 ]]; then
|
|
echo " docker network create failed ${create_fail_streak} consecutive candidates; aborting" >&2
|
|
return 1
|
|
fi
|
|
# Another runner just claimed a port; refresh snapshots before retrying.
|
|
need_refresh=1
|
|
done
|
|
echo " Exhausted ${attempt} candidates. Last snapshot: ${used_n} bound, ${guarded_n} guarded" >&2
|
|
return 1
|
|
}
|
|
|
|
if ! reserve_port_offset; then
|
|
echo "Failed to reserve a free host port range for ${DOC_ENGINE} docker compose" >&2
|
|
exit 1
|
|
fi
|
|
echo "PORT_RESERVATION=${PORT_RESERVATION}" >> ${GITHUB_ENV}
|
|
echo "Using ${DOC_ENGINE} host port offset ${PORT_OFFSET}"
|
|
ES_PORT=$((1200 + PORT_OFFSET))
|
|
OS_PORT=$((1201 + PORT_OFFSET))
|
|
INFINITY_THRIFT_PORT=$((23817 + PORT_OFFSET))
|
|
INFINITY_HTTP_PORT=$((23820 + PORT_OFFSET))
|
|
INFINITY_PSQL_PORT=$((5432 + PORT_OFFSET))
|
|
EXPOSE_MYSQL_PORT=$((5455 + PORT_OFFSET))
|
|
MINIO_PORT=$((9000 + PORT_OFFSET))
|
|
MINIO_CONSOLE_PORT=$((9001 + PORT_OFFSET))
|
|
REDIS_PORT=$((6379 + PORT_OFFSET))
|
|
EXPOSE_NATS_PORT=$((4222 + PORT_OFFSET))
|
|
NATS_MONITORING_PORT=$((8222 + PORT_OFFSET))
|
|
TEI_PORT=$((6380 + PORT_OFFSET))
|
|
KVROCKS_PORT=$((6381 + PORT_OFFSET))
|
|
KIBANA_PORT=$((6601 + PORT_OFFSET))
|
|
SVR_HTTP_PORT=$((9380 + PORT_OFFSET))
|
|
ADMIN_SVR_HTTP_PORT=$((9381 + PORT_OFFSET))
|
|
SVR_MCP_PORT=$((9382 + PORT_OFFSET))
|
|
SANDBOX_EXECUTOR_MANAGER_PORT=$((9385 + PORT_OFFSET))
|
|
SVR_WEB_HTTP_PORT=$((80 + PORT_OFFSET))
|
|
SVR_WEB_HTTPS_PORT=$((443 + PORT_OFFSET))
|
|
EXPOSE_CLICKHOUSE_TCP_PORT=$((9900 + PORT_OFFSET))
|
|
CLICKHOUSE_HTTP_PORT=$((8123 + PORT_OFFSET))
|
|
|
|
# Persist computed ports into .env so docker-compose uses the correct host bindings.
|
|
# Remove previous CI overrides first; docker compose uses the last duplicate key.
|
|
sed -i '/^ES_PORT=/d;/^OS_PORT=/d;/^INFINITY_THRIFT_PORT=/d;/^INFINITY_HTTP_PORT=/d;/^INFINITY_PSQL_PORT=/d;/^EXPOSE_MYSQL_PORT=/d;/^MINIO_PORT=/d;/^MINIO_CONSOLE_PORT=/d;/^REDIS_PORT=/d;/^KVROCKS_PORT=/d;/^EXPOSE_NATS_PORT=/d;/^NATS_MONITORING_PORT=/d;/^TEI_PORT=/d;/^KIBANA_PORT=/d;/^SVR_HTTP_PORT=/d;/^ADMIN_SVR_HTTP_PORT=/d;/^SVR_MCP_PORT=/d;/^SANDBOX_EXECUTOR_MANAGER_PORT=/d;/^SVR_WEB_HTTP_PORT=/d;/^SVR_WEB_HTTPS_PORT=/d;/^EXPOSE_CLICKHOUSE_TCP_PORT=/d;/^CLICKHOUSE_HTTP_PORT=/d;/^COMPOSE_PROFILES=/d;/^TEI_MODEL=/d;/^RAGFLOW_IMAGE=/d;/^DOC_ENGINE=/d;/^API_PROXY_SCHEME=/d' .env
|
|
# docker-compose-go.yml uses env_file: .env-go for the ragflow-cpu
|
|
# container, so the Go server process reads COMPOSE_PROFILES and
|
|
# TEI_MODEL from .env (not .env). Without syncing these two,
|
|
# the Go server's checkModelAvailable TEI bypass never triggers
|
|
# because .env's COMPOSE_PROFILES lacks "tei-", causing
|
|
# PATCH /models/default to fail for the Builtin/Local embedding
|
|
# model and datasets to be created with an empty embedding_model.
|
|
sed -i '/^COMPOSE_PROFILES=/d;/^RAGFLOW_DEV_MODE=/d;/^TEI_MODEL=/d' .env
|
|
{
|
|
echo ""
|
|
echo "ES_PORT=${ES_PORT}"
|
|
echo "OS_PORT=${OS_PORT}"
|
|
echo "INFINITY_THRIFT_PORT=${INFINITY_THRIFT_PORT}"
|
|
echo "INFINITY_HTTP_PORT=${INFINITY_HTTP_PORT}"
|
|
echo "INFINITY_PSQL_PORT=${INFINITY_PSQL_PORT}"
|
|
echo "EXPOSE_MYSQL_PORT=${EXPOSE_MYSQL_PORT}"
|
|
echo "MINIO_PORT=${MINIO_PORT}"
|
|
echo "MINIO_CONSOLE_PORT=${MINIO_CONSOLE_PORT}"
|
|
echo "REDIS_PORT=${REDIS_PORT}"
|
|
echo "KVROCKS_PORT=${KVROCKS_PORT}"
|
|
echo "EXPOSE_NATS_PORT=${EXPOSE_NATS_PORT}"
|
|
echo "NATS_MONITORING_PORT=${NATS_MONITORING_PORT}"
|
|
echo "TEI_PORT=${TEI_PORT}"
|
|
echo "KIBANA_PORT=${KIBANA_PORT}"
|
|
echo "SVR_HTTP_PORT=${SVR_HTTP_PORT}"
|
|
echo "ADMIN_SVR_HTTP_PORT=${ADMIN_SVR_HTTP_PORT}"
|
|
echo "SVR_MCP_PORT=${SVR_MCP_PORT}"
|
|
echo "SANDBOX_EXECUTOR_MANAGER_PORT=${SANDBOX_EXECUTOR_MANAGER_PORT}"
|
|
echo "SVR_WEB_HTTP_PORT=${SVR_WEB_HTTP_PORT}"
|
|
echo "SVR_WEB_HTTPS_PORT=${SVR_WEB_HTTPS_PORT}"
|
|
echo "EXPOSE_CLICKHOUSE_TCP_PORT=${EXPOSE_CLICKHOUSE_TCP_PORT}"
|
|
echo "CLICKHOUSE_HTTP_PORT=${CLICKHOUSE_HTTP_PORT}"
|
|
echo "COMPOSE_PROFILES=${DOC_ENGINE},cpu,ragflow-go,tei-cpu,deepdoc,metadata-mysql,clickhouse"
|
|
echo "TEI_MODEL=BAAI/bge-small-en-v1.5"
|
|
echo "RAGFLOW_IMAGE=${RAGFLOW_IMAGE}"
|
|
echo "DOC_ENGINE=${DOC_ENGINE}"
|
|
echo "API_PROXY_SCHEME=${API_PROXY_SCHEME}"
|
|
} >> .env
|
|
# Sync COMPOSE_PROFILES and TEI_MODEL into .env so the Go
|
|
# server container (env_file: .env) sees the same TEI bypass
|
|
# config as the Python container. Only these two affect the
|
|
# in-process checkModelAvailable logic; other .env keys are
|
|
# either yml-interpolation-only or already correct in .env.
|
|
# RAGFLOW_DEV_MODE rides along for the same reason.
|
|
{
|
|
echo ""
|
|
echo "COMPOSE_PROFILES=${DOC_ENGINE},cpu,ragflow-go,tei-cpu,deepdoc,metadata-mysql,clickhouse"
|
|
echo "TEI_MODEL=BAAI/bge-small-en-v1.5"
|
|
# The image stamps its git-describe output into /ragflow/VERSION,
|
|
# so this checkout reports v0.27.x while --migrate records the
|
|
# v1.0.0-rc1.dev1 database marker. Without the override the Go
|
|
# server refuses to boot against the marker it just wrote.
|
|
echo "RAGFLOW_DEV_MODE=true"
|
|
} >> .env
|
|
RAGFLOW_HTTP_PORT=${SVR_WEB_HTTP_PORT}
|
|
echo "RAGFLOW_HTTP_PORT=${RAGFLOW_HTTP_PORT}" >> ${GITHUB_ENV}
|
|
echo "HOST_ADDRESS=http://host.docker.internal:${RAGFLOW_HTTP_PORT}" >> ${GITHUB_ENV}
|
|
|
|
sed -i \
|
|
-e 's#${NATS_PORT:-4222}:4222#${EXPOSE_NATS_PORT}:4222#' \
|
|
-e 's#"8222:8222"#"${NATS_MONITORING_PORT}:8222"#' \
|
|
docker-compose-base.yml
|
|
|
|
- name: Start ragflow:nightly for Elasticsearch
|
|
run: |
|
|
COMPOSE_FILE=docker/docker-compose.yml
|
|
sudo docker compose -f "${COMPOSE_FILE}" -p ${COMPOSE_PROJECT_NAME} down -v || true
|
|
sudo docker ps -a --filter "label=com.docker.compose.project=${COMPOSE_PROJECT_NAME}" -q | xargs -r sudo docker rm -f
|
|
sudo docker compose -f "${COMPOSE_FILE}" -p ${COMPOSE_PROJECT_NAME} up -d
|
|
|
|
- name: Run New RESTFUL api tests against Elasticsearch
|
|
if: env.API_PROXY_SCHEME != 'web'
|
|
run: |
|
|
export http_proxy=""; export https_proxy=""; export no_proxy=""; export HTTP_PROXY=""; export HTTPS_PROXY=""; export NO_PROXY=""
|
|
svc_ready=0
|
|
for i in $(seq 1 60); do
|
|
if sudo docker exec ${RAGFLOW_CONTAINER} curl -sf --connect-timeout 5 "${HOST_ADDRESS}/api/v1/system/ping" > /dev/null 2>&1; then
|
|
svc_ready=1
|
|
break
|
|
fi
|
|
echo "Waiting for service to be available... ($i/60)"
|
|
sleep 5
|
|
done
|
|
if [ "$svc_ready" -ne 1 ]; then
|
|
echo "Service did not become ready after 5 minutes. Docker logs:"
|
|
sudo docker logs ${RAGFLOW_CONTAINER}
|
|
exit 1
|
|
fi
|
|
source .venv/bin/activate && set -o pipefail; pytest -s --tb=short --level=${HTTP_API_TEST_LEVEL} --durations=100 test/testcases/restful_api 2>&1 | tee es_restful_api_test.log
|
|
|
|
- name: Collect ragflow log Elasticsearch
|
|
if: ${{ !cancelled() }}
|
|
# Keep this step self-sufficient: rebuild the artifacts path instead of
|
|
# relying on the directory created earlier, and downgrade copy failures
|
|
# to warnings so a diagnostics step can never fail the job.
|
|
run: |
|
|
set -uo pipefail
|
|
RUNNER_WORKSPACE_PREFIX="${RUNNER_WORKSPACE_PREFIX:-${HOME}}"
|
|
ARTIFACTS_DIR="${RUNNER_WORKSPACE_PREFIX}/artifacts/${GITHUB_REPOSITORY}/${GITHUB_RUN_ID}/${DOC_ENGINE}/${API_PROXY_SCHEME}"
|
|
LOG_DEST="${ARTIFACTS_DIR}/ragflow-logs-es"
|
|
mkdir -p "${ARTIFACTS_DIR}"
|
|
if [ -d docker/ragflow-logs ]; then
|
|
sudo rm -rf "${LOG_DEST}"
|
|
if sudo cp -a docker/ragflow-logs "${LOG_DEST}"; then
|
|
sudo chown -R "$(id -u):$(id -g)" "${LOG_DEST}" || true
|
|
echo "Copied ragflow logs to ${LOG_DEST}"
|
|
else
|
|
echo "::warning::Failed to copy ragflow logs to ${LOG_DEST}; continuing"
|
|
fi
|
|
echo "ragflow log" && sudo tail -n 200 docker/ragflow-logs/ragflow_server.log || true
|
|
else
|
|
echo "No docker/ragflow-logs directory found; skipping log collection"
|
|
fi
|
|
sudo rm -rf docker/ragflow-logs || true
|
|
|
|
- name: Stop ragflow:nightly for Elasticsearch
|
|
if: ${{ always() }}
|
|
run: |
|
|
set -euo pipefail
|
|
COMPOSE_FILE=docker/docker-compose.yml
|
|
# Sometimes `docker compose down` fail due to hang container, heavy load etc. Need to remove such containers to release resources(for example, listen ports).
|
|
sudo docker compose -f "${COMPOSE_FILE}" -p ${COMPOSE_PROJECT_NAME} down -v || true
|
|
sudo docker ps -a --filter "label=com.docker.compose.project=${COMPOSE_PROJECT_NAME}" -q | xargs -r sudo docker rm -f
|
|
if [[ -n ${RAGFLOW_IMAGE} ]]; then
|
|
sudo docker rmi -f ${RAGFLOW_IMAGE}
|
|
fi
|
|
if [[ -n ${PORT_RESERVATION:-} ]]; then
|
|
# PORT_RESERVATION is a space-separated list of per-port docker network guards
|
|
# created by the prepare step. Remove each to free the ports for other runners.
|
|
for guard in ${PORT_RESERVATION}; do
|
|
sudo docker network rm "${guard}" >/dev/null 2>&1 || true
|
|
done
|
|
fi
|
|
|
|
ragflow_native_backend:
|
|
name: ragflow_native_backend
|
|
# Runs AFTER both per-engine go jobs (which contain the doc-engine-specific
|
|
# server build + Python RESTFUL correctness runs). The DeepDoc native
|
|
# backend is doc-engine-independent, so running it ONCE here — instead of
|
|
# duplicated (and memory-competing) inside each go job — removes the
|
|
# redundant second -race pass and the concurrent -race OOM risk. Running it
|
|
# last also keeps a flaky -race OOM from ever obscuring the functional
|
|
# verdict produced by the go jobs.
|
|
needs: [ragflow_tests_infinity, ragflow_tests_elasticsearch]
|
|
# Skip unless DeepDoc native backend code/build/testdata actually changed.
|
|
if: ${{ (github.event_name != 'pull_request' && github.event_name != 'pull_request_target' || (github.event.pull_request.draft == false && contains(github.event.pull_request.labels.*.name, 'ci') && (github.event.action != 'labeled' || github.event.label.name == 'ci'))) && needs.ragflow_preflight.outputs.has_native_deepdoc_changes == 'true' }}
|
|
runs-on: [ "self-hosted", "ragflow-test" ]
|
|
# Shared by the assert step and the test step below so the mount path is
|
|
# declared once.
|
|
env:
|
|
RAGFLOW_TESTDATA_DIR: /opt/ragflow-testdata
|
|
steps:
|
|
- name: Ensure workspace ownership
|
|
run: |
|
|
echo "Workflow triggered by ${{ github.event_name }}"
|
|
echo "chown -R ${USER} ${GITHUB_WORKSPACE}" && sudo chown -R ${USER} ${GITHUB_WORKSPACE}
|
|
|
|
- name: Check out code
|
|
uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && format('refs/pull/{0}/merge', github.event.pull_request.number) || github.sha }}
|
|
fetch-depth: 0
|
|
fetch-tags: true
|
|
allow-unsafe-pr-checkout: true
|
|
|
|
- name: Build C++ static library (ragflow native backend)
|
|
# Produces librag_tokenizer_c_api.a with the re2 symbols renamed into
|
|
# the ragtokre2_ namespace (so it no longer collides with onnxruntime's
|
|
# bundled re2 copy at link time). Only the .a is needed for the native
|
|
# integration tests; the per-engine go jobs build the full server.
|
|
run: |
|
|
set -euo pipefail
|
|
BUILDER_CONTAINER=ragflow_build_${GITHUB_RUN_ID}_native_$(od -An -N4 -tx4 /dev/urandom | tr -d ' ')
|
|
cleanup_builder() {
|
|
if [[ -n "${BUILDER_CONTAINER:-}" ]]; then
|
|
sudo docker rm -f -v "${BUILDER_CONTAINER}" >/dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
trap cleanup_builder EXIT
|
|
|
|
TZ=${TZ:-$(readlink -f /etc/localtime | awk -F '/zoneinfo/' '{print $2}')}
|
|
sudo docker run --privileged -d --name "${BUILDER_CONTAINER}" \
|
|
-e TZ="${TZ}" \
|
|
-e UV_INDEX=https://mirrors.aliyun.com/pypi/simple \
|
|
-v "${PWD}:/ragflow" \
|
|
-v "${PWD}/internal/binding/cpp/resource:/usr/share/infinity/resource" \
|
|
infiniflow/infinity_builder:ubuntu22_clang20
|
|
sudo docker exec "${BUILDER_CONTAINER}" bash -c 'git config --global safe.directory "*" && cd /ragflow && ./build.sh --cpp'
|
|
|
|
- name: Assert pre-seeded DeepDoc testdata
|
|
# Only the self-hosted ragflow-test runners bake infiniflow/ragflow-testdata
|
|
# into /opt/ragflow-testdata — the same image-level contract as
|
|
# MODEL_DIR=/opt/ragflow-deepdoc-models, which has no fallback either.
|
|
# Fail here rather than letting the fetch script silently fall back to
|
|
# cloning: a missing mount is an environment defect, and a silent
|
|
# fallback only makes the job slower while hiding it.
|
|
#
|
|
# Assert the `deepdoc` level, not a per-package subtree, so adding a
|
|
# package later does not invalidate the check.
|
|
run: |
|
|
set -euo pipefail
|
|
if [ ! -d "${RAGFLOW_TESTDATA_DIR}/deepdoc" ]; then
|
|
echo "ERROR: ${RAGFLOW_TESTDATA_DIR}/deepdoc missing on this runner." >&2
|
|
echo " Native backend tests expect the asset repo pre-seeded there." >&2
|
|
exit 1
|
|
fi
|
|
echo "✓ pre-seeded testdata found at ${RAGFLOW_TESTDATA_DIR}/deepdoc:"
|
|
ls -l "${RAGFLOW_TESTDATA_DIR}/deepdoc"
|
|
|
|
- name: Run in-process DeepDoc backend integration tests (correctness + race)
|
|
# Doc-engine-independent: the same native backend (det/layout/tsr/rec
|
|
# ONNX models + onnxruntime) is exercised regardless of DOC_ENGINE, so
|
|
# it is run ONCE here instead of duplicated inside each go job.
|
|
#
|
|
# Inside build.sh, the golden/comparison tests run WITHOUT -race (no
|
|
# race-detection value, ~10x memory tax) and only the concurrency
|
|
# correctness tests run WITH -race (see run_native_integration_tests).
|
|
#
|
|
# Testdata is pre-seeded on the runner at /opt/ragflow-testdata
|
|
# (asserted by the previous step). RAGFLOW_TESTDATA_DIR points the fetch
|
|
# script at it so the network clone is skipped (see
|
|
# scripts/fetch_deepdoc_testdata.sh) — which is why no actions/cache step
|
|
# is needed here. XDG_CACHE_HOME remains the location the script falls
|
|
# back to when it runs outside this job (local dev, no mount).
|
|
env:
|
|
MODEL_DIR: /opt/ragflow-deepdoc-models
|
|
CGO_ENABLED: '1'
|
|
DEEPDOC_NATIVE_REQUIRED: '1'
|
|
XDG_CACHE_HOME: ${{ github.workspace }}/.cache
|
|
run: |
|
|
set -euo pipefail
|
|
./build.sh --test-native
|
|
|
|
|
|
ragflow_go_integration:
|
|
name: ragflow_go_integration
|
|
# Runs the generic Go `integration` test tier against a real service stack
|
|
# (MySQL/MinIO/Kvrocks/NATS/Infinity/TEI) brought up via docker compose.
|
|
# The model-backed native DeepDoc tests are covered separately by
|
|
# `ragflow_native_backend`, so this job stays free of that heavy dependency.
|
|
needs: ragflow_preflight
|
|
if: ${{ (github.event_name != 'pull_request' && github.event_name != 'pull_request_target' || (github.event.pull_request.draft == false && contains(github.event.pull_request.labels.*.name, 'ci') && (github.event.action != 'labeled' || github.event.label.name == 'ci'))) && needs.ragflow_preflight.outputs.has_go_changes == 'true' }}
|
|
runs-on: [ "self-hosted", "ragflow-test" ]
|
|
env:
|
|
DOC_ENGINE: infinity
|
|
API_PROXY_SCHEME: go
|
|
steps:
|
|
- name: Ensure workspace ownership
|
|
run: |
|
|
echo "Workflow triggered by ${{ github.event_name }}"
|
|
echo "chown -R ${USER} ${GITHUB_WORKSPACE}" && sudo chown -R ${USER} ${GITHUB_WORKSPACE}
|
|
|
|
- name: Check out code
|
|
uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && format('refs/pull/{0}/merge', github.event.pull_request.number) || github.sha }}
|
|
fetch-depth: 0
|
|
fetch-tags: true
|
|
allow-unsafe-pr-checkout: true
|
|
|
|
- name: Build C++ static library
|
|
# Produces librag_tokenizer_c_api.a (with re2 symbols renamed into the
|
|
# ragtokre2_ namespace). Only the .a is needed for the integration test
|
|
# link; we do NOT build the full ragflow:nightly image because this job
|
|
# starts only the dependency services, not the ragflow-cpu container.
|
|
run: |
|
|
set -euo pipefail
|
|
BUILDER_CONTAINER=ragflow_build_${GITHUB_RUN_ID}_go_int_$(od -An -N4 -tx4 /dev/urandom | tr -d ' ')
|
|
cleanup_builder() {
|
|
if [[ -n "${BUILDER_CONTAINER:-}" ]]; then
|
|
sudo docker rm -f -v "${BUILDER_CONTAINER}" >/dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
trap cleanup_builder EXIT
|
|
TZ=${TZ:-$(readlink -f /etc/localtime | awk -F '/zoneinfo/' '{print $2}')}
|
|
sudo docker run --privileged -d --name "${BUILDER_CONTAINER}" \
|
|
-e TZ="${TZ}" \
|
|
-e UV_INDEX=https://mirrors.aliyun.com/pypi/simple \
|
|
-v "${PWD}:/ragflow" \
|
|
-v "${PWD}/internal/binding/cpp/resource:/usr/share/infinity/resource" \
|
|
infiniflow/infinity_builder:ubuntu22_clang20
|
|
sudo docker exec "${BUILDER_CONTAINER}" bash -c 'git config --global safe.directory "*" && cd /ragflow && ./build.sh --cpp'
|
|
|
|
- name: Prepare function test environment
|
|
working-directory: docker
|
|
run: |
|
|
set -euo pipefail
|
|
sudo chmod 1777 /tmp
|
|
## ss already installed
|
|
##sudo apt update && sudo apt install -y iproute2
|
|
RUNNER_WORKSPACE_PREFIX=${RUNNER_WORKSPACE_PREFIX:-${HOME}}
|
|
COMPOSE_PROJECT_NAME="${GITHUB_RUN_ID}-go-int-${DOC_ENGINE}-${API_PROXY_SCHEME}"
|
|
echo "COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME}" >> ${GITHUB_ENV}
|
|
echo "RAGFLOW_CONTAINER=${COMPOSE_PROJECT_NAME}-ragflow-cpu-1" >> ${GITHUB_ENV}
|
|
ARTIFACTS_DIR=${RUNNER_WORKSPACE_PREFIX}/artifacts/${GITHUB_REPOSITORY}/${GITHUB_RUN_ID}/${DOC_ENGINE}/${API_PROXY_SCHEME}
|
|
echo "ARTIFACTS_DIR=${ARTIFACTS_DIR}" >> ${GITHUB_ENV}
|
|
rm -rf "${ARTIFACTS_DIR}" && mkdir -p "${ARTIFACTS_DIR}"
|
|
|
|
# Determine runner number (default to 1 if not found)
|
|
RUNNER_NUM=$(sudo docker inspect $(hostname) --format '{{index .Config.Labels "com.docker.compose.container-number"}}' 2>/dev/null || true)
|
|
if [[ -z "${RUNNER_NUM:-}" ]]; then
|
|
# GitHub self-hosted runners always expose RUNNER_NAME; hash it to spread candidates.
|
|
if [[ -n "${RUNNER_NAME:-}" ]]; then
|
|
RUNNER_NUM=$(( $(printf '%d' 0x$(echo -n "${RUNNER_NAME}" | md5sum | cut -c1-4)) % 1000 ))
|
|
else
|
|
RUNNER_NUM=$(( GITHUB_RUN_ID % 1000 ))
|
|
fi
|
|
fi
|
|
|
|
# Engine-specific offset partitions keep concurrent engine jobs from
|
|
# choosing the same host ports when they land on the same self-hosted runner.
|
|
# A lock plus reservation file closes the check/start race between parallel jobs.
|
|
PORT_BASES=(1200 1201 23817 23820 5432 5455 9000 9001 6379 6380 6381 6601 9380 9381 9382 9385 80 443 4222 8222 9900 8123)
|
|
PARTITION_SIZE=3000
|
|
# Keep every (base + offset) host port BELOW the Linux ephemeral
|
|
# range (net.ipv4.ip_local_port_range, default 32768-60999). An
|
|
# outbound connection from the host can transiently squat on a port
|
|
# inside that range, and `docker compose up` then fails with
|
|
# "address already in use" even though the reservation succeeded —
|
|
# the reservation cannot see it because `docker ps` only lists
|
|
# container-published ports. Largest base is 23820, so offsets must
|
|
# stay under 8948 for the ceiling to clear 32768.
|
|
case "${DOC_ENGINE}" in
|
|
elasticsearch) PARTITION_BASE=1000 ;; # offsets 1000-3999 -> ports <= 27819
|
|
infinity) PARTITION_BASE=4000 ;; # offsets 4000-6999 -> ports <= 30819
|
|
*) echo "Unsupported DOC_ENGINE=${DOC_ENGINE}" >&2; exit 1 ;;
|
|
esac
|
|
# Runners run inside docker containers that share the host docker daemon
|
|
# (/var/run/docker.sock), so neither /tmp nor `ss` can coordinate across runners:
|
|
# each container has its own /tmp and its own network namespace, and `ss` cannot
|
|
# see host ports bound by other runners' compose stacks. Use the shared daemon for
|
|
# both primitives:
|
|
# - per-port mutex = for each host port (base+offset) we intend to bind, create
|
|
# a docker network named ragflow-ci-port-<port> on the shared daemon. docker
|
|
# network create is NOT idempotent (fails on name conflict), so only one runner
|
|
# can claim a given port. This is per-PORT, not per-offset, because different
|
|
# (base, offset) pairs can produce the same host port (e.g. 6380+5999 ==
|
|
# 9380+2999 == 12379); a per-offset guard cannot prevent that collision.
|
|
# - port availability = docker ps published ports (sees every container on the
|
|
# shared daemon regardless of which runner started it).
|
|
# No file-system sharing required, no host netns required.
|
|
|
|
cleanup_stale_port_locks() {
|
|
local now stale_after net ts before after
|
|
now=$(date -u +%s)
|
|
stale_after=$((60 * 60))
|
|
before=$(sudo docker network ls --filter "label=ragflow-ci-created" --format '{{.Name}}' | wc -l)
|
|
for net in $(sudo docker network ls --filter "label=ragflow-ci-created" --format '{{.Name}}'); do
|
|
ts=$(sudo docker network inspect --format '{{ index .Labels "ragflow-ci-created" }}' "${net}" 2>/dev/null || true)
|
|
# Remove if timestamp is non-numeric (legacy network from a previous code
|
|
# version) or if it is numeric and older than stale_after.
|
|
if [[ ! "${ts}" =~ ^[0-9]+$ ]] || (( now - ts > stale_after )); then
|
|
sudo docker network rm "${net}" >/dev/null 2>&1 || true
|
|
fi
|
|
done
|
|
after=$(sudo docker network ls --filter "label=ragflow-ci-created" --format '{{.Name}}' | wc -l)
|
|
echo " cleanup_stale_port_locks: ${before} -> ${after} guards (TTL=${stale_after}s)" >&2
|
|
}
|
|
|
|
reserve_port_offset() {
|
|
local attempt candidate base port guard g ok reject_reason create_err guard_subnet
|
|
local -a guards
|
|
local used guarded need_refresh used_n guarded_n debug_count create_fail_streak
|
|
cleanup_stale_port_locks
|
|
echo "Reserving host ports for ${DOC_ENGINE} compose stack..." >&2
|
|
# Fail fast if the shared docker daemon is not reachable — avoids a
|
|
# long spin where every docker network create silently fails.
|
|
if ! sudo docker info >/dev/null 2>&1; then
|
|
echo " ERROR: 'sudo docker info' failed — docker daemon is not reachable from this runner" >&2
|
|
return 1
|
|
fi
|
|
echo " Running containers on shared daemon: $(sudo docker ps -q 2>/dev/null | wc -l)" >&2
|
|
echo " Docker networks on shared daemon: $(sudo docker network ls -q 2>/dev/null | wc -l)" >&2
|
|
need_refresh=1
|
|
debug_count=0
|
|
create_fail_streak=0
|
|
for attempt in $(seq 0 499); do
|
|
candidate=$(( PARTITION_BASE + ((GITHUB_RUN_ID + RUNNER_NUM * 1000 + attempt * 97) % PARTITION_SIZE) ))
|
|
# Refresh snapshots on first attempt and after a failed reservation.
|
|
if [[ ${need_refresh} -eq 1 ]]; then
|
|
used=$(sudo docker ps --format '{{.Ports}}' 2>/dev/null | grep -oE '[0-9]+->' | tr -d '>-' | sort -u || true)
|
|
# Strip both ragflow-ci-port- (current) and ragflow-ci-portguard- (legacy) prefixes.
|
|
guarded=$(sudo docker network ls --filter "label=ragflow-ci-created" --format '{{.Name}}' 2>/dev/null | sed 's/^ragflow-ci-port\(guard\)\?-//' | sort -u || true)
|
|
used_n=$(echo "${used}" | grep -c . 2>/dev/null || true)
|
|
guarded_n=$(echo "${guarded}" | grep -c . 2>/dev/null || true)
|
|
echo " Snapshot: ${used_n} bound ports, ${guarded_n} guarded ports" >&2
|
|
[[ ${used_n} -gt 0 ]] && echo " Bound sample: $(echo "${used}" | tr '\n' ' ' | cut -d' ' -f1-10)" >&2
|
|
[[ ${guarded_n} -gt 0 ]] && echo " Guarded sample: $(echo "${guarded}" | tr '\n' ' ' | cut -d' ' -f1-10)" >&2
|
|
need_refresh=0
|
|
fi
|
|
# Fast path: skip if any (base+candidate) port is already bound or guarded.
|
|
ok=1
|
|
reject_reason=""
|
|
for base in "${PORT_BASES[@]}"; do
|
|
port=$((base + candidate))
|
|
if grep -qx "${port}" <<< "${used}" 2>/dev/null; then
|
|
ok=0; reject_reason="port ${port} (base ${base}) already bound"; break
|
|
fi
|
|
if grep -qx "${port}" <<< "${guarded}" 2>/dev/null; then
|
|
ok=0; reject_reason="port ${port} (base ${base}) already guarded"; break
|
|
fi
|
|
done
|
|
if [[ ${ok} -eq 0 ]]; then
|
|
if [[ ${debug_count} -lt 5 ]]; then
|
|
echo " Candidate ${candidate}: rejected — ${reject_reason}" >&2
|
|
debug_count=$((debug_count + 1))
|
|
fi
|
|
continue
|
|
fi
|
|
# Atomically claim every (base+candidate) host port via a per-port docker
|
|
# network. docker network create fails on name conflict, so only one runner
|
|
# can claim a given port. If any port is already claimed, undo partial
|
|
# reservations and try the next offset.
|
|
guards=()
|
|
ok=1
|
|
for base in "${PORT_BASES[@]}"; do
|
|
port=$((base + candidate))
|
|
guard="ragflow-ci-port-${port}"
|
|
# Use an explicit subnet from 10.0.0.0/8 (outside Docker's
|
|
# default 172.17-31.x.x pools) to bypass "all predefined
|
|
# address pools have been fully subnetted" errors on shared
|
|
# daemons where auto-allocation is exhausted.
|
|
guard_subnet="10.$((port / 256)).$((port % 256)).0/24"
|
|
if ! create_err=$(sudo docker network create --subnet "${guard_subnet}" --label ragflow-ci-created="$(date -u +%s)" "${guard}" 2>&1 >/dev/null); then
|
|
echo " docker network create ${guard} failed: ${create_err}" >&2
|
|
ok=0; break
|
|
fi
|
|
guards+=("${guard}")
|
|
done
|
|
if [[ ${ok} -eq 1 ]]; then
|
|
PORT_OFFSET=${candidate}
|
|
PORT_RESERVATION="${guards[*]}"
|
|
echo "Reserved offset ${PORT_OFFSET} (attempt ${attempt})" >&2
|
|
return 0
|
|
fi
|
|
for g in "${guards[@]}"; do
|
|
sudo docker network rm "${g}" >/dev/null 2>&1 || true
|
|
done
|
|
# If docker network create keeps failing (daemon gone, permission, etc.),
|
|
# bail out early instead of looping 6000 times. A name-conflict failure
|
|
# should be rare because the fast path already filters guarded ports.
|
|
create_fail_streak=$((create_fail_streak + 1))
|
|
if [[ ${create_fail_streak} -ge 10 ]]; then
|
|
echo " docker network create failed ${create_fail_streak} consecutive candidates; aborting" >&2
|
|
return 1
|
|
fi
|
|
# Another runner just claimed a port; refresh snapshots before retrying.
|
|
need_refresh=1
|
|
done
|
|
echo " Exhausted ${attempt} candidates. Last snapshot: ${used_n} bound, ${guarded_n} guarded" >&2
|
|
return 1
|
|
}
|
|
|
|
if ! reserve_port_offset; then
|
|
echo "Failed to reserve a free host port range for ${DOC_ENGINE} docker compose" >&2
|
|
exit 1
|
|
fi
|
|
echo "PORT_RESERVATION=${PORT_RESERVATION}" >> ${GITHUB_ENV}
|
|
echo "Using ${DOC_ENGINE} host port offset ${PORT_OFFSET}"
|
|
ES_PORT=$((1200 + PORT_OFFSET))
|
|
OS_PORT=$((1201 + PORT_OFFSET))
|
|
INFINITY_THRIFT_PORT=$((23817 + PORT_OFFSET))
|
|
INFINITY_HTTP_PORT=$((23820 + PORT_OFFSET))
|
|
INFINITY_PSQL_PORT=$((5432 + PORT_OFFSET))
|
|
EXPOSE_MYSQL_PORT=$((5455 + PORT_OFFSET))
|
|
MINIO_PORT=$((9000 + PORT_OFFSET))
|
|
MINIO_CONSOLE_PORT=$((9001 + PORT_OFFSET))
|
|
REDIS_PORT=$((6379 + PORT_OFFSET))
|
|
EXPOSE_NATS_PORT=$((4222 + PORT_OFFSET))
|
|
NATS_MONITORING_PORT=$((8222 + PORT_OFFSET))
|
|
TEI_PORT=$((6380 + PORT_OFFSET))
|
|
KVROCKS_PORT=$((6381 + PORT_OFFSET))
|
|
KIBANA_PORT=$((6601 + PORT_OFFSET))
|
|
SVR_HTTP_PORT=$((9380 + PORT_OFFSET))
|
|
ADMIN_SVR_HTTP_PORT=$((9381 + PORT_OFFSET))
|
|
SVR_MCP_PORT=$((9382 + PORT_OFFSET))
|
|
SANDBOX_EXECUTOR_MANAGER_PORT=$((9385 + PORT_OFFSET))
|
|
SVR_WEB_HTTP_PORT=$((80 + PORT_OFFSET))
|
|
SVR_WEB_HTTPS_PORT=$((443 + PORT_OFFSET))
|
|
EXPOSE_CLICKHOUSE_TCP_PORT=$((9900 + PORT_OFFSET))
|
|
CLICKHOUSE_HTTP_PORT=$((8123 + PORT_OFFSET))
|
|
|
|
# Persist computed ports into .env so docker-compose uses the correct host bindings.
|
|
# Remove previous CI overrides first; docker compose uses the last duplicate key.
|
|
sed -i '/^ES_PORT=/d;/^OS_PORT=/d;/^INFINITY_THRIFT_PORT=/d;/^INFINITY_HTTP_PORT=/d;/^INFINITY_PSQL_PORT=/d;/^EXPOSE_MYSQL_PORT=/d;/^MINIO_PORT=/d;/^MINIO_CONSOLE_PORT=/d;/^REDIS_PORT=/d;/^KVROCKS_PORT=/d;/^EXPOSE_NATS_PORT=/d;/^NATS_MONITORING_PORT=/d;/^TEI_PORT=/d;/^KIBANA_PORT=/d;/^SVR_HTTP_PORT=/d;/^ADMIN_SVR_HTTP_PORT=/d;/^SVR_MCP_PORT=/d;/^SANDBOX_EXECUTOR_MANAGER_PORT=/d;/^SVR_WEB_HTTP_PORT=/d;/^SVR_WEB_HTTPS_PORT=/d;/^EXPOSE_CLICKHOUSE_TCP_PORT=/d;/^CLICKHOUSE_HTTP_PORT=/d;/^COMPOSE_PROFILES=/d;/^TEI_MODEL=/d;/^RAGFLOW_IMAGE=/d;/^DOC_ENGINE=/d;/^API_PROXY_SCHEME=/d' .env
|
|
# docker-compose-go.yml uses env_file: .env-go for the ragflow-cpu
|
|
# container, so the Go server process reads COMPOSE_PROFILES and
|
|
# TEI_MODEL from .env (not .env). Without syncing these two,
|
|
# the Go server's checkModelAvailable TEI bypass never triggers
|
|
# because .env's COMPOSE_PROFILES lacks "tei-", causing
|
|
# PATCH /models/default to fail for the Builtin/Local embedding
|
|
# model and datasets to be created with an empty embedding_model.
|
|
sed -i '/^COMPOSE_PROFILES=/d;/^TEI_MODEL=/d' .env
|
|
{
|
|
echo ""
|
|
echo "ES_PORT=${ES_PORT}"
|
|
echo "OS_PORT=${OS_PORT}"
|
|
echo "INFINITY_THRIFT_PORT=${INFINITY_THRIFT_PORT}"
|
|
echo "INFINITY_HTTP_PORT=${INFINITY_HTTP_PORT}"
|
|
echo "INFINITY_PSQL_PORT=${INFINITY_PSQL_PORT}"
|
|
echo "EXPOSE_MYSQL_PORT=${EXPOSE_MYSQL_PORT}"
|
|
echo "MINIO_PORT=${MINIO_PORT}"
|
|
echo "MINIO_CONSOLE_PORT=${MINIO_CONSOLE_PORT}"
|
|
echo "REDIS_PORT=${REDIS_PORT}"
|
|
echo "KVROCKS_PORT=${KVROCKS_PORT}"
|
|
echo "EXPOSE_NATS_PORT=${EXPOSE_NATS_PORT}"
|
|
echo "NATS_MONITORING_PORT=${NATS_MONITORING_PORT}"
|
|
echo "TEI_PORT=${TEI_PORT}"
|
|
echo "KIBANA_PORT=${KIBANA_PORT}"
|
|
echo "SVR_HTTP_PORT=${SVR_HTTP_PORT}"
|
|
echo "ADMIN_SVR_HTTP_PORT=${ADMIN_SVR_HTTP_PORT}"
|
|
echo "SVR_MCP_PORT=${SVR_MCP_PORT}"
|
|
echo "SANDBOX_EXECUTOR_MANAGER_PORT=${SANDBOX_EXECUTOR_MANAGER_PORT}"
|
|
echo "SVR_WEB_HTTP_PORT=${SVR_WEB_HTTP_PORT}"
|
|
echo "SVR_WEB_HTTPS_PORT=${SVR_WEB_HTTPS_PORT}"
|
|
echo "EXPOSE_CLICKHOUSE_TCP_PORT=${EXPOSE_CLICKHOUSE_TCP_PORT}"
|
|
echo "CLICKHOUSE_HTTP_PORT=${CLICKHOUSE_HTTP_PORT}"
|
|
# NOTE: the engine profile (${DOC_ENGINE} -> "infinity") MUST be
|
|
# first, otherwise the engine service never starts. We drop
|
|
# `cpu` (the heavy ragflow-cpu server container) and `deepdoc`
|
|
# because this job runs the Go tests on the runner host against
|
|
# the dependency services directly — no server container needed.
|
|
# `ragflow-go` brings up kvrocks + nats; `tei-cpu` the embedding
|
|
# service; `metadata-mysql` the mysql metadata store.
|
|
echo "COMPOSE_PROFILES=infinity,ragflow-go,tei-cpu,metadata-mysql,clickhouse"
|
|
echo "TEI_MODEL=BAAI/bge-small-en-v1.5"
|
|
echo "DOC_ENGINE=${DOC_ENGINE}"
|
|
echo "API_PROXY_SCHEME=${API_PROXY_SCHEME}"
|
|
} >> .env
|
|
# Sync COMPOSE_PROFILES and TEI_MODEL into .env so the Go
|
|
# server container (env_file: .env) sees the same TEI bypass
|
|
# config as the Python container. Only these two affect the
|
|
# in-process checkModelAvailable logic; other .env keys are
|
|
# either yml-interpolation-only or already correct in .env.
|
|
{
|
|
echo ""
|
|
echo "COMPOSE_PROFILES=infinity,ragflow-go,tei-cpu,metadata-mysql,clickhouse"
|
|
echo "TEI_MODEL=BAAI/bge-small-en-v1.5"
|
|
} >> .env
|
|
RAGFLOW_HTTP_PORT=${SVR_WEB_HTTP_PORT}
|
|
echo "RAGFLOW_HTTP_PORT=${RAGFLOW_HTTP_PORT}" >> ${GITHUB_ENV}
|
|
echo "HOST_ADDRESS=http://host.docker.internal:${RAGFLOW_HTTP_PORT}" >> ${GITHUB_ENV}
|
|
|
|
sed -i \
|
|
-e 's#${NATS_PORT:-4222}:4222#${EXPOSE_NATS_PORT}:4222#' \
|
|
-e 's#"8222:8222"#"${NATS_MONITORING_PORT}:8222"#' \
|
|
docker-compose-base.yml
|
|
|
|
- name: Start ragflow CI service stack (infinity)
|
|
# COMPOSE_PROFILES (set in .env by the prepare step) drops `cpu`, so the
|
|
# heavy ragflow-cpu image is NOT started — only the dependency services
|
|
# (mysql/minio/kvrocks/redis/nats/infinity/tei/clickhouse) come up.
|
|
run: |
|
|
set -euo pipefail
|
|
# docker-compose-base.yml publishes kvrocks as "${KVROCKS_PORT:-6379}:6379",
|
|
# so a missing KVROCKS_PORT silently binds the UN-offset host port 6379 and
|
|
# collides with whatever already holds it ("port is already allocated").
|
|
# The offset reservation only guards 6379+offset, never bare 6379, so the
|
|
# guard cannot catch this. Fail loudly here instead of at compose up.
|
|
grep -q '^KVROCKS_PORT=' docker/.env || {
|
|
echo "KVROCKS_PORT missing from docker/.env — compose would fall back to host 6379" >&2
|
|
exit 1
|
|
}
|
|
COMPOSE_FILE=docker/docker-compose.yml
|
|
sudo docker compose -f "${COMPOSE_FILE}" -p ${COMPOSE_PROJECT_NAME} down -v || true
|
|
sudo docker ps -a --filter "label=com.docker.compose.project=${COMPOSE_PROJECT_NAME}" -q | xargs -r sudo docker rm -f
|
|
sudo docker compose -f "${COMPOSE_FILE}" -p ${COMPOSE_PROJECT_NAME} up -d
|
|
|
|
- name: Wait for service stack to be ready
|
|
run: |
|
|
set -euo pipefail
|
|
_port() { grep -E "^$1=" docker/.env | head -1 | cut -d= -f2-; }
|
|
INFINITY_HTTP_PORT=$(_port INFINITY_HTTP_PORT)
|
|
# Infinity must be reachable from the runner the SAME way the Go tests
|
|
# reach it: via host.docker.internal (the runner's bridge to the host).
|
|
# This also proves host.docker.internal resolves and the host-published
|
|
# port is reachable — the exact path the tests use. Use the same
|
|
# endpoint the infinity healthcheck uses (/admin/node/current) and DO
|
|
# NOT pass curl -f: infinity's root path returns a non-2xx, which -f
|
|
# would treat as failure even when the server is up. Any HTTP response
|
|
# (or a refused connection) decides readiness, matching the compose
|
|
# healthcheck exactly.
|
|
INFINITY_READY=0
|
|
for i in $(seq 1 60); do
|
|
if curl -s --connect-timeout 5 "http://host.docker.internal:${INFINITY_HTTP_PORT}/admin/node/current" >/dev/null 2>&1; then
|
|
echo "Infinity ready after ${i} tries"
|
|
INFINITY_READY=1
|
|
break
|
|
fi
|
|
echo "Waiting for Infinity... ($i/60)"
|
|
sleep 5
|
|
done
|
|
if [ "${INFINITY_READY}" != "1" ]; then
|
|
echo "::error::Infinity did not become ready on host.docker.internal:${INFINITY_HTTP_PORT}"
|
|
echo "Last curl attempt (verbose, for diagnosis):"
|
|
curl -sv --connect-timeout 5 "http://host.docker.internal:${INFINITY_HTTP_PORT}/admin/node/current" || true
|
|
exit 1
|
|
fi
|
|
# Every other service brought up by the compose profiles (mysql/minio/
|
|
# redis/kvrocks/nats/tei/clickhouse) must be healthy before the tests
|
|
# run. Wait on each container's own healthcheck (defined in
|
|
# docker-compose-base.yml) instead of re-implementing per-service
|
|
# probes — this covers all of them without drift and prevents the same
|
|
# class of "connection refused because the dependency wasn't up yet"
|
|
# failure we just hit with infinity. Containers without a healthcheck
|
|
# are treated as immediately healthy.
|
|
for c in $(sudo docker ps -q --filter "label=com.docker.compose.project=${COMPOSE_PROJECT_NAME}"); do
|
|
name=$(sudo docker inspect -f '{{.Name}}' "$c" | sed 's@^/@@')
|
|
st="starting"
|
|
for i in $(seq 1 60); do
|
|
st=$(sudo docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}healthy{{end}}' "$c")
|
|
if [ "$st" = "healthy" ]; then
|
|
break
|
|
fi
|
|
sleep 5
|
|
done
|
|
if [ "$st" != "healthy" ]; then
|
|
echo "::error::${name} did not become healthy (last status: ${st})"
|
|
sudo docker logs --tail 50 "$c" || true
|
|
exit 1
|
|
fi
|
|
echo "${name} healthy"
|
|
done
|
|
|
|
- name: Provide cl100k_base tiktoken vocabulary
|
|
# The Go integration tests (tokenizer / service / graph) panic without the
|
|
# cl100k_base BPE table. The runner already bakes it at
|
|
# /opt/ragflow_deps/cl100k_base.tiktoken, so copy from there first; fall
|
|
# back to the upstream openai blob only if the local copy is missing
|
|
# (e.g. a non-standard runner). Mirrors the ragflow_native_backend job.
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p ragflow_deps
|
|
cp /opt/ragflow_deps/cl100k_base.tiktoken ragflow_deps/cl100k_base.tiktoken || true
|
|
if [ ! -f ragflow_deps/cl100k_base.tiktoken ] && command -v curl >/dev/null 2>&1; then
|
|
curl -fsSL -o ragflow_deps/cl100k_base.tiktoken \
|
|
https://openaipublic.blob.core.windows.net/encodings/cl100k_base.tiktoken || \
|
|
echo "tiktoken: curl fetch failed; offline loader will fail loudly"
|
|
fi
|
|
if [ -f ragflow_deps/cl100k_base.tiktoken ]; then
|
|
echo "tiktoken: cl100k table provisioned ($(wc -c < ragflow_deps/cl100k_base.tiktoken) bytes)"
|
|
else
|
|
echo "tiktoken: cl100k table NOT provisioned — offline loader will fail loudly"
|
|
fi
|
|
|
|
- name: Run Go integration tests
|
|
# The Go tests run on the runner host (a container on a bridge network).
|
|
# The dependency stack is brought up via docker compose on the shared host
|
|
# daemon, so its ports are published on the *host*, not on the runner's
|
|
# loopback. We therefore point the tests at host.docker.internal:<port>
|
|
# (the runner's bridge to the host) — 127.0.0.1 would hit the runner's own
|
|
# loopback and get "connection refused". This mirrors the HOST_ADDRESS
|
|
# mechanic already used by the Python/SDK jobs in this file.
|
|
# Engine-specific suites (ES/OceanBase/SereneDB) self-skip unless their
|
|
# opt-in env vars are set, so only the infinity + mysql/minio/redis/nats
|
|
# exercised paths run here.
|
|
# The ./test/api HTTP e2e suite requires a live ragflow server (ragflow-cpu),
|
|
# which this job deliberately does NOT start (we drop the `cpu` profile to
|
|
# avoid spinning up the heavy server image), so it is excluded here. Service-
|
|
# level integration (storage/engine/ingestion/etc.) runs against the
|
|
# dependency stack brought up above.
|
|
env:
|
|
DOC_ENGINE: infinity
|
|
CGO_ENABLED: '1'
|
|
# In-process DeepDoc backend (now the sole inference path) needs the ONNX
|
|
# models; the self-hosted ragflow-test runner bakes them here.
|
|
MODEL_DIR: /opt/ragflow-deepdoc-models
|
|
run: |
|
|
set -euo pipefail
|
|
_port() { grep -E "^$1=" docker/.env | head -1 | cut -d= -f2-; }
|
|
INFINITY_THRIFT_PORT=$(_port INFINITY_THRIFT_PORT)
|
|
INFINITY_HTTP_PORT=$(_port INFINITY_HTTP_PORT)
|
|
EXPOSE_MYSQL_PORT=$(_port EXPOSE_MYSQL_PORT)
|
|
MINIO_PORT=$(_port MINIO_PORT)
|
|
REDIS_PORT=$(_port REDIS_PORT)
|
|
EXPOSE_NATS_PORT=$(_port EXPOSE_NATS_PORT)
|
|
EXPOSE_CLICKHOUSE_TCP_PORT=$(_port EXPOSE_CLICKHOUSE_TCP_PORT)
|
|
export RAGFLOW_INFINITY_URI="host.docker.internal:${INFINITY_THRIFT_PORT}"
|
|
export RAGFLOW_MYSQL_HOST=host.docker.internal
|
|
export RAGFLOW_MYSQL_PORT="${EXPOSE_MYSQL_PORT}"
|
|
export RAGFLOW_MINIO_HOST="host.docker.internal:${MINIO_PORT}"
|
|
export RAGFLOW_REDIS_HOST="host.docker.internal:${REDIS_PORT}"
|
|
export RAGFLOW_NATS_HOST=host.docker.internal
|
|
export RAGFLOW_NATS_PORT="${EXPOSE_NATS_PORT}"
|
|
export RAGFLOW_CLICKHOUSE_HOST=host.docker.internal
|
|
export RAGFLOW_CLICKHOUSE_PORT="${EXPOSE_CLICKHOUSE_TCP_PORT}"
|
|
# The model-backed native DeepDoc tests under internal/deepdoc/**
|
|
# register the in-process backend from within their own packages and are
|
|
# covered separately by the ragflow_native_backend job (--test-native),
|
|
# so we exclude them here. The chunker cgo PDF test is different: it
|
|
# registers the in-process backend itself (native_analyzer.Register)
|
|
# when MODEL_DIR is present, so it runs for real in this job instead of
|
|
# skipping — the runner bakes MODEL_DIR=/opt/ragflow-deepdoc-models.
|
|
# The ./test/api HTTP e2e suite needs a live ragflow-cpu server, which
|
|
# this job does not start.
|
|
# Like the per-engine go jobs (ragflow_tests_{infinity,elasticsearch}),
|
|
# we also drop internal/storage: its MinIO-backed integration tests need
|
|
# a fully-ready MinIO that this CI stack does not reliably provide
|
|
# (the compose /live probe passes while the S3 API still 502s), so they
|
|
# are exercised elsewhere, not here.
|
|
# internal/service/nlp is also excluded: its DatasetNav integration test
|
|
# compiles only against source fixes (Infinity 3013/3052, nav
|
|
# available_int, doc_ids_kwd) that are intentionally out of scope for this
|
|
# CI-enabling PR, so the package is left out of this job's package list
|
|
# rather than patched. Those fixes land in their own PR.
|
|
PKGS=$(go list ./... \
|
|
| grep -v '/test/api' \
|
|
| grep -v '^ragflow/internal/deepdoc' \
|
|
| grep -v '/internal/storage$' \
|
|
| grep -v '^ragflow/internal/service/nlp$')
|
|
./build.sh --test-integration-go $PKGS
|
|
|
|
- name: Tear down
|
|
if: always()
|
|
run: |
|
|
set -euo pipefail
|
|
COMPOSE_FILE=docker/docker-compose.yml
|
|
sudo docker compose -f "${COMPOSE_FILE}" -p ${COMPOSE_PROJECT_NAME} down -v || true
|
|
sudo docker ps -a --filter "label=com.docker.compose.project=${COMPOSE_PROJECT_NAME}" -q | xargs -r sudo docker rm -f
|
|
# Release the per-port docker-network guards reserved by the prepare step.
|
|
for g in ${PORT_RESERVATION}; do sudo docker network rm "$g" >/dev/null 2>&1 || true; done
|