diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8f120b12e8..ee043844b6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,12 +29,14 @@ concurrency: cancel-in-progress: true jobs: - ragflow_tests: - name: ragflow_tests + 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.pull_request.draft == false && contains(github.event.pull_request.labels.*.name, 'ci')) }} + if: ${{ github.event_name != 'pull_request' || (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" ] + outputs: + http_api_test_level: ${{ steps.test_level.outputs.http_api_test_level }} steps: - name: Ensure workspace ownership run: | @@ -157,61 +159,17 @@ jobs: echo "No Go files changed" fi - - name: Build ragflow go server + - name: Set test level + id: test_level run: | set -euo pipefail - BUILDER_CONTAINER=ragflow_build_$(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 - - echo "BUILDER_CONTAINER=${BUILDER_CONTAINER}" >> "${GITHUB_ENV}" - 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/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: Prepare test resources -# run: | -# RESOURCE_REPO=https://github.com/infiniflow/resource.git -# RESOURCE_REF=549feaaf998954d65b668667f009125bc84a9c5e -# rm -rf /tmp/resource -# git clone "${RESOURCE_REPO}" /tmp/resource -# git -C /tmp/resource checkout "${RESOURCE_REF}" -# sudo mkdir -p /usr/share/infinity -# sudo ln -sf /tmp/resource /usr/share/infinity/resource -# mkdir -p resource -# ln -sf /tmp/resource/wordnet resource/wordnet -# -# - name: Test Go packages -# run: | -# set -euo pipefail -# packages=$(go list ./internal/... | grep -vE '/storage(/|$)') -# CGO_ENABLED=1 GOPROXY=${GOPROXY:-https://goproxy.cn,https://proxy.golang.org,direct} \ -# go test -count=1 ${packages} - - - name: Build ragflow:nightly - run: | - RUNNER_WORKSPACE_PREFIX=${RUNNER_WORKSPACE_PREFIX:-${HOME}} - RAGFLOW_IMAGE=infiniflow/ragflow:${GITHUB_RUN_ID} - echo "RAGFLOW_IMAGE=${RAGFLOW_IMAGE}" >> ${GITHUB_ENV} - 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} -f Dockerfile -t ${RAGFLOW_IMAGE} . 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 "RAGFLOW_CONTAINER=${GITHUB_RUN_ID}-ragflow-cpu-1" >> ${GITHUB_ENV} + echo "http_api_test_level=${HTTP_API_TEST_LEVEL}" >> ${GITHUB_OUTPUT} - name: Prepare Python test environment run: | @@ -225,23 +183,92 @@ jobs: echo "Start to run unit test" python3 run_tests.py -i + + + ragflow_tests_infinity: + name: ragflow_tests_infinity + needs: ragflow_preflight + if: ${{ github.event_name != 'pull_request' || (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" ] + env: + DOC_ENGINE: infinity + RAGFLOW_IMAGE: infiniflow/ragflow:${{ github.run_id }}-infinity + HTTP_API_TEST_LEVEL: ${{ needs.ragflow_preflight.outputs.http_api_test_level }} + 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 + + - name: Build ragflow go server + 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/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: Build ragflow:nightly + run: | + set -euo pipefail + 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} -f Dockerfile -t ${RAGFLOW_IMAGE} . + + - name: Prepare Python test environment + run: | + 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 # install ss - sudo apt update && sudo apt install -y iproute2 + sudo apt update && sudo apt install -y iproute2 + RUNNER_WORKSPACE_PREFIX=${RUNNER_WORKSPACE_PREFIX:-${HOME}} + COMPOSE_PROJECT_NAME="${GITHUB_RUN_ID}-${DOC_ENGINE}" + 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} + 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) RUNNER_NUM=${RUNNER_NUM:-1} - # Per-runner seed plus per-workflow-run offset avoids most clashes when - # multiple CI jobs share the same self-hosted runner concurrently. Probe - # the final host ports too, because stale compose projects can still hold - # a deterministic port from a previous run. - PORT_BASES=(1200 1201 23817 23820 5432 5455 9000 9001 6379 6380 6601 9380 9381 9382 9384 9383 9385 80 443) - MAX_PORT_OFFSET=$((65000 - 23820)) - PORT_OFFSET=$(( (GITHUB_RUN_ID % 4000) + RUNNER_NUM * 1000 )) - OFFSET_FOUND=false + # 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 6601 9380 9381 9382 9384 9383 9385 80 443 4222) + PARTITION_SIZE=6000 + case "${DOC_ENGINE}" in + elasticsearch) PARTITION_BASE=1000 ;; + infinity) PARTITION_BASE=31000 ;; + *) echo "Unsupported DOC_ENGINE=${DOC_ENGINE}" >&2; exit 1 ;; + esac + PORT_LOCK_DIR=${RUNNER_WORKSPACE_PREFIX}/artifacts/${GITHUB_REPOSITORY}/port-locks + mkdir -p "${PORT_LOCK_DIR}" port_offset_available() { local offset=$1 @@ -255,25 +282,43 @@ jobs: return 0 } - for ATTEMPT in $(seq 0 9); do - CANDIDATE_OFFSET=$(( (PORT_OFFSET + ATTEMPT * 4000) % MAX_PORT_OFFSET )) - if [ "${CANDIDATE_OFFSET}" -lt 1000 ]; then - CANDIDATE_OFFSET=$((CANDIDATE_OFFSET + 1000)) - fi + cleanup_stale_port_locks() { + local now stale_after lock lock_ts + now=$(date -u +%s) + stale_after=$((6 * 60 * 60)) + for lock in "${PORT_LOCK_DIR}"/*.lock; do + [[ -e "${lock}" ]] || continue + lock_ts=$(awk '{print $3}' "${lock}" 2>/dev/null || true) + if [[ "${lock_ts}" =~ ^[0-9]+$ ]] && (( now - lock_ts > stale_after )); then + rm -f "${lock}" + fi + done + } - if port_offset_available "${CANDIDATE_OFFSET}"; then - PORT_OFFSET=${CANDIDATE_OFFSET} - OFFSET_FOUND=true - break - fi - done + reserve_port_offset() { + local attempt candidate reservation + cleanup_stale_port_locks + for attempt in $(seq 0 59); do + candidate=$(( PARTITION_BASE + ((GITHUB_RUN_ID + RUNNER_NUM * 1000 + attempt * 97) % PARTITION_SIZE) )) + reservation="${PORT_LOCK_DIR}/${candidate}.lock" + if ( set -o noclobber; echo "${GITHUB_RUN_ID} ${DOC_ENGINE} $(date -u +%s)" > "${reservation}" ) 2>/dev/null; then + if port_offset_available "${candidate}"; then + PORT_OFFSET=${candidate} + PORT_RESERVATION=${reservation} + return 0 + fi + rm -f "${reservation}" + fi + done + return 1 + } - if [ "${OFFSET_FOUND}" != "true" ]; then - echo "Failed to find a free host port range for docker compose" >&2 + if ! reserve_port_offset; then + echo "Failed to reserve a free host port range for ${DOC_ENGINE} docker compose" >&2 exit 1 fi - - echo "Using host port offset ${PORT_OFFSET}" + 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)) @@ -283,6 +328,7 @@ jobs: MINIO_PORT=$((9000 + PORT_OFFSET)) MINIO_CONSOLE_PORT=$((9001 + PORT_OFFSET)) REDIS_PORT=$((6379 + PORT_OFFSET)) + NATS_PORT=$((4222 + PORT_OFFSET)) TEI_PORT=$((6380 + PORT_OFFSET)) KIBANA_PORT=$((6601 + PORT_OFFSET)) SVR_HTTP_PORT=$((9380 + PORT_OFFSET)) @@ -294,31 +340,36 @@ jobs: SVR_WEB_HTTP_PORT=$((80 + PORT_OFFSET)) SVR_WEB_HTTPS_PORT=$((443 + PORT_OFFSET)) - # Persist computed ports into .env so docker-compose uses the correct host bindings - echo "" >> .env - echo -e "ES_PORT=${ES_PORT}" >> .env - echo -e "OS_PORT=${OS_PORT}" >> .env - echo -e "INFINITY_THRIFT_PORT=${INFINITY_THRIFT_PORT}" >> .env - echo -e "INFINITY_HTTP_PORT=${INFINITY_HTTP_PORT}" >> .env - echo -e "INFINITY_PSQL_PORT=${INFINITY_PSQL_PORT}" >> .env - echo -e "EXPOSE_MYSQL_PORT=${EXPOSE_MYSQL_PORT}" >> .env - echo -e "MINIO_PORT=${MINIO_PORT}" >> .env - echo -e "MINIO_CONSOLE_PORT=${MINIO_CONSOLE_PORT}" >> .env - echo -e "REDIS_PORT=${REDIS_PORT}" >> .env - echo -e "TEI_PORT=${TEI_PORT}" >> .env - echo -e "KIBANA_PORT=${KIBANA_PORT}" >> .env - echo -e "SVR_HTTP_PORT=${SVR_HTTP_PORT}" >> .env - echo -e "ADMIN_SVR_HTTP_PORT=${ADMIN_SVR_HTTP_PORT}" >> .env - echo -e "SVR_MCP_PORT=${SVR_MCP_PORT}" >> .env - echo -e "GO_HTTP_PORT=${GO_HTTP_PORT}" >> .env - echo -e "GO_ADMIN_PORT=${GO_ADMIN_PORT}" >> .env - echo -e "SANDBOX_EXECUTOR_MANAGER_PORT=${SANDBOX_EXECUTOR_MANAGER_PORT}" >> .env - echo -e "SVR_WEB_HTTP_PORT=${SVR_WEB_HTTP_PORT}" >> .env - echo -e "SVR_WEB_HTTPS_PORT=${SVR_WEB_HTTPS_PORT}" >> .env - - echo -e "COMPOSE_PROFILES=\${COMPOSE_PROFILES},tei-cpu" >> .env - echo -e "TEI_MODEL=BAAI/bge-small-en-v1.5" >> .env - echo -e "RAGFLOW_IMAGE=${RAGFLOW_IMAGE}" >> .env + # 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;/^TEI_PORT=/d;/^KIBANA_PORT=/d;/^SVR_HTTP_PORT=/d;/^ADMIN_SVR_HTTP_PORT=/d;/^SVR_MCP_PORT=/d;/^GO_HTTP_PORT=/d;/^GO_ADMIN_PORT=/d;/^SANDBOX_EXECUTOR_MANAGER_PORT=/d;/^SVR_WEB_HTTP_PORT=/d;/^SVR_WEB_HTTPS_PORT=/d;/^NATS_PORT=/d;/^COMPOSE_PROFILES=/d;/^TEI_MODEL=/d;/^RAGFLOW_IMAGE=/d;/^DOC_ENGINE=/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 "NATS_PORT=${NATS_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 "GO_HTTP_PORT=${GO_HTTP_PORT}" + echo "GO_ADMIN_PORT=${GO_ADMIN_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 "COMPOSE_PROFILES=${DOC_ENGINE},cpu,tei-cpu" + echo "TEI_MODEL=BAAI/bge-small-en-v1.5" + echo "RAGFLOW_IMAGE=${RAGFLOW_IMAGE}" + echo "DOC_ENGINE=${DOC_ENGINE}" + } >> .env echo "HOST_ADDRESS=http://host.docker.internal:${SVR_HTTP_PORT}" >> ${GITHUB_ENV} # Patch entrypoint.sh for coverage @@ -327,10 +378,9 @@ jobs: - name: Start ragflow:nightly for Infinity run: | - sed -i 's/^DOC_ENGINE=.*$/DOC_ENGINE=infinity/' docker/.env - sudo docker compose -f docker/docker-compose.yml -p ${GITHUB_RUN_ID} down -v || true - sudo docker ps -a --filter "label=com.docker.compose.project=${GITHUB_RUN_ID}" -q | xargs -r sudo docker rm -f - sudo docker compose -f docker/docker-compose.yml -p ${GITHUB_RUN_ID} up -d + sudo docker compose -f docker/docker-compose.yml -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 docker/docker-compose.yml -p ${COMPOSE_PROJECT_NAME} up -d - name: Run sdk tests against Infinity run: | @@ -495,7 +545,7 @@ jobs: else echo "ragflow_server.py not found!" fi - sudo docker compose -f docker/docker-compose.yml -p ${GITHUB_RUN_ID} stop + sudo docker compose -f docker/docker-compose.yml -p ${COMPOSE_PROJECT_NAME} stop - name: Generate server coverage report Infinity if: ${{ !cancelled() }} @@ -538,15 +588,213 @@ jobs: if: always() # always run this step even if previous steps failed run: | # 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 docker/docker-compose.yml -p ${GITHUB_RUN_ID} down -v || true - sudo docker ps -a --filter "label=com.docker.compose.project=${GITHUB_RUN_ID}" -q | xargs -r sudo docker rm -f + sudo docker compose -f docker/docker-compose.yml -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 + rm -f "${PORT_RESERVATION}" + fi + + + + ragflow_tests_elasticsearch: + name: ragflow_tests_elasticsearch + needs: ragflow_preflight + if: ${{ github.event_name != 'pull_request' || (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" ] + env: + DOC_ENGINE: elasticsearch + RAGFLOW_IMAGE: infiniflow/ragflow:${{ github.run_id }}-elasticsearch + HTTP_API_TEST_LEVEL: ${{ needs.ragflow_preflight.outputs.http_api_test_level }} + 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 + + - name: Build ragflow go server + 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/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: Build ragflow:nightly + run: | + set -euo pipefail + 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} -f Dockerfile -t ${RAGFLOW_IMAGE} . + + - name: Prepare Python test environment + run: | + 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 + # install ss + sudo apt update && sudo apt install -y iproute2 + RUNNER_WORKSPACE_PREFIX=${RUNNER_WORKSPACE_PREFIX:-${HOME}} + COMPOSE_PROJECT_NAME="${GITHUB_RUN_ID}-${DOC_ENGINE}" + 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} + 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) + RUNNER_NUM=${RUNNER_NUM:-1} + + # 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 6601 9380 9381 9382 9384 9383 9385 80 443 4222) + PARTITION_SIZE=6000 + case "${DOC_ENGINE}" in + elasticsearch) PARTITION_BASE=1000 ;; + infinity) PARTITION_BASE=31000 ;; + *) echo "Unsupported DOC_ENGINE=${DOC_ENGINE}" >&2; exit 1 ;; + esac + PORT_LOCK_DIR=${RUNNER_WORKSPACE_PREFIX}/artifacts/${GITHUB_REPOSITORY}/port-locks + mkdir -p "${PORT_LOCK_DIR}" + + port_offset_available() { + local offset=$1 + local base port + for base in "${PORT_BASES[@]}"; do + port=$((base + offset)) + if ss -ltnH "sport = :${port}" | grep -q .; then + return 1 + fi + done + return 0 + } + + cleanup_stale_port_locks() { + local now stale_after lock lock_ts + now=$(date -u +%s) + stale_after=$((6 * 60 * 60)) + for lock in "${PORT_LOCK_DIR}"/*.lock; do + [[ -e "${lock}" ]] || continue + lock_ts=$(awk '{print $3}' "${lock}" 2>/dev/null || true) + if [[ "${lock_ts}" =~ ^[0-9]+$ ]] && (( now - lock_ts > stale_after )); then + rm -f "${lock}" + fi + done + } + + reserve_port_offset() { + local attempt candidate reservation + cleanup_stale_port_locks + for attempt in $(seq 0 59); do + candidate=$(( PARTITION_BASE + ((GITHUB_RUN_ID + RUNNER_NUM * 1000 + attempt * 97) % PARTITION_SIZE) )) + reservation="${PORT_LOCK_DIR}/${candidate}.lock" + if ( set -o noclobber; echo "${GITHUB_RUN_ID} ${DOC_ENGINE} $(date -u +%s)" > "${reservation}" ) 2>/dev/null; then + if port_offset_available "${candidate}"; then + PORT_OFFSET=${candidate} + PORT_RESERVATION=${reservation} + return 0 + fi + rm -f "${reservation}" + fi + done + 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)) + NATS_PORT=$((4222 + PORT_OFFSET)) + TEI_PORT=$((6380 + 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)) + GO_HTTP_PORT=$((9384 + PORT_OFFSET)) + GO_ADMIN_PORT=$((9383 + PORT_OFFSET)) + SANDBOX_EXECUTOR_MANAGER_PORT=$((9385 + PORT_OFFSET)) + SVR_WEB_HTTP_PORT=$((80 + PORT_OFFSET)) + SVR_WEB_HTTPS_PORT=$((443 + 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;/^TEI_PORT=/d;/^KIBANA_PORT=/d;/^SVR_HTTP_PORT=/d;/^ADMIN_SVR_HTTP_PORT=/d;/^SVR_MCP_PORT=/d;/^GO_HTTP_PORT=/d;/^GO_ADMIN_PORT=/d;/^SANDBOX_EXECUTOR_MANAGER_PORT=/d;/^SVR_WEB_HTTP_PORT=/d;/^SVR_WEB_HTTPS_PORT=/d;/^NATS_PORT=/d;/^COMPOSE_PROFILES=/d;/^TEI_MODEL=/d;/^RAGFLOW_IMAGE=/d;/^DOC_ENGINE=/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 "NATS_PORT=${NATS_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 "GO_HTTP_PORT=${GO_HTTP_PORT}" + echo "GO_ADMIN_PORT=${GO_ADMIN_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 "COMPOSE_PROFILES=${DOC_ENGINE},cpu,tei-cpu" + echo "TEI_MODEL=BAAI/bge-small-en-v1.5" + echo "RAGFLOW_IMAGE=${RAGFLOW_IMAGE}" + echo "DOC_ENGINE=${DOC_ENGINE}" + } >> .env + echo "HOST_ADDRESS=http://host.docker.internal:${SVR_HTTP_PORT}" >> ${GITHUB_ENV} + + # Patch entrypoint.sh for coverage + sed -i '/"\$PY" api\/ragflow_server.py \${INIT_SUPERUSER_ARGS} &/c\ echo "Ensuring coverage is installed..."\n "$PY" -m pip install coverage -i https://mirrors.aliyun.com/pypi/simple\n export COVERAGE_FILE=/ragflow/logs/.coverage\n echo "Starting ragflow_server with coverage..."\n "$PY" -m coverage run --source=./api/apps --omit="*/tests/*,*/migrations/*" -a api/ragflow_server.py ${INIT_SUPERUSER_ARGS} &' ./entrypoint.sh + - name: Start ragflow:nightly for Elasticsearch run: | - sed -i 's/^DOC_ENGINE=.*$/DOC_ENGINE=elasticsearch/' docker/.env - sudo docker compose -f docker/docker-compose.yml -p ${GITHUB_RUN_ID} down -v || true - sudo docker ps -a --filter "label=com.docker.compose.project=${GITHUB_RUN_ID}" -q | xargs -r sudo docker rm -f - sudo docker compose -f docker/docker-compose.yml -p ${GITHUB_RUN_ID} up -d + sudo docker compose -f docker/docker-compose.yml -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 docker/docker-compose.yml -p ${COMPOSE_PROJECT_NAME} up -d - name: Run sdk tests against Elasticsearch run: | @@ -566,8 +814,8 @@ jobs: exit 1 fi echo "Start to run test sdk on Elasticsearch" - source .venv/bin/activate && set -o pipefail; pytest -s --tb=short --level=${HTTP_API_TEST_LEVEL} --junitxml=pytest-infinity-sdk.xml --cov=sdk/python/ragflow_sdk --cov-branch --cov-report=xml:coverage-es-sdk.xml test/testcases/test_sdk_api 2>&1 | tee es_sdk_test.log - + source .venv/bin/activate && set -o pipefail; pytest -s --tb=short --level=${HTTP_API_TEST_LEVEL} --junitxml=pytest-es-sdk.xml --cov=sdk/python/ragflow_sdk --cov-branch --cov-report=xml:coverage-es-sdk.xml test/testcases/test_sdk_api 2>&1 | tee es_sdk_test.log + - name: Run New RESTFUL api tests against Elasticsearch run: | export http_proxy=""; export https_proxy=""; export no_proxy=""; export HTTP_PROXY=""; export HTTPS_PROXY=""; export NO_PROXY="" @@ -711,7 +959,7 @@ jobs: else echo "ragflow_server.py not found!" fi - sudo docker compose -f docker/docker-compose.yml -p ${GITHUB_RUN_ID} stop + sudo docker compose -f docker/docker-compose.yml -p ${COMPOSE_PROJECT_NAME} stop - name: Generate server coverage report Elasticsearch if: ${{ !cancelled() }} @@ -733,7 +981,7 @@ jobs: else echo ".coverage file not found!" fi - + - name: Collect ragflow log Elasticsearch if: ${{ !cancelled() }} run: | @@ -749,8 +997,11 @@ jobs: if: always() # always run this step even if previous steps failed run: | # 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 docker/docker-compose.yml -p ${GITHUB_RUN_ID} down -v || true - sudo docker ps -a --filter "label=com.docker.compose.project=${GITHUB_RUN_ID}" -q | xargs -r sudo docker rm -f + sudo docker compose -f docker/docker-compose.yml -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 + rm -f "${PORT_RESERVATION}" + fi