/clock Topic Not Published and Unreal Engine 5.7 Build Fix
#40
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: ProjectAirSim CI (Self-Hosted) | |
| on: | |
| pull_request: | |
| branches: | |
| - main | |
| - main_public | |
| paths: | |
| - ".github/workflows/test_on_self_hosted.yml" | |
| - "CMakeLists.txt" | |
| - "build.sh" | |
| - "build_linux.mk" | |
| - "setup_linux_dev_tools.sh" | |
| - "unreal-linux-toolchain.cmake" | |
| - "client/python/**" | |
| - "core_sim/**" | |
| - "mavlinkcom/**" | |
| - "physics/**" | |
| - "rendering/**" | |
| - "samples/**" | |
| - "simserver/**" | |
| - "tools/**" | |
| - "unity/**" | |
| - "unreal/**" | |
| - "vehicle_apis/**" | |
| # Run when the self-hosted label is added or the labeled PR is updated; the job still | |
| # rejects draft PRs and PRs without run-self-hosted. | |
| types: [labeled, synchronize] | |
| # Restrict GITHUB_TOKEN permissions (least privilege). | |
| # This workflow needs read access to checkout code and write access to | |
| # pull request comments to post CI results back to the PR. | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| jobs: | |
| build-and-test: | |
| if: | | |
| (github.event.pull_request.base.ref == 'main' || github.event.pull_request.base.ref == 'main_public') && | |
| !github.event.pull_request.draft && | |
| contains(github.event.pull_request.labels.*.name, 'run-self-hosted') | |
| runs-on: [self-hosted] | |
| timeout-minutes: 120 | |
| env: | |
| AIRSIM_PY_ENV: ${{ github.workspace }}/airsimenv | |
| DEBIAN_FRONTEND: noninteractive | |
| defaults: | |
| run: | |
| shell: bash | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: recursive | |
| - name: Sync submodules | |
| run: | | |
| git submodule sync | |
| git submodule update --init --recursive | |
| - name: Load Unreal Engine environment | |
| run: | | |
| set -euo pipefail | |
| # GitHub Actions invokes an explicit `bash` shell as | |
| # `bash --noprofile --norc`, so UE_ROOT exported from ~/.bashrc is | |
| # not visible to workflow steps. Keep an inherited value when the | |
| # runner service already provides one, and otherwise load the | |
| # runner user's login environment. | |
| ue_root="${UE_ROOT:-}" | |
| if [ -z "$ue_root" ]; then | |
| ue_root="$(bash -ilc 'printf "%s" "${UE_ROOT:-}"' 2>/dev/null || true)" | |
| fi | |
| if [ -z "$ue_root" ]; then | |
| echo "::error::UE_ROOT is not available to the GitHub Actions runner. Configure it for the runner service account (or in ~/.bashrc) and restart the runner service." | |
| exit 1 | |
| fi | |
| ue_root="$(realpath -e -- "$ue_root")" | |
| test -d "$ue_root/Engine" | |
| test -x "$ue_root/Engine/Build/BatchFiles/Linux/Build.sh" | |
| test -x "$ue_root/Engine/Binaries/Linux/UnrealEditor" | |
| # GITHUB_ENV makes the value available to every subsequent step, | |
| # including CMake's simlibs configuration. | |
| printf 'UE_ROOT=%s\n' "$ue_root" >> "$GITHUB_ENV" | |
| echo "Using UE_ROOT=$ue_root" | |
| - name: Setup system dependencies | |
| run: | | |
| set -euo pipefail | |
| sudo apt-get -o DPkg::Lock::Timeout=300 update | |
| sudo apt-get -o DPkg::Lock::Timeout=300 install -y \ | |
| python3 \ | |
| python3-venv \ | |
| python3-pip \ | |
| build-essential \ | |
| lsof \ | |
| ninja-build \ | |
| vulkan-tools \ | |
| libvulkan1 \ | |
| clang-15 \ | |
| libc++-15-dev \ | |
| libc++abi-15-dev \ | |
| libunwind-15-dev | |
| echo "System dependencies installed" | |
| - name: Setup AirSim development tools | |
| run: | | |
| chmod +x setup_linux_dev_tools.sh | |
| ./setup_linux_dev_tools.sh | |
| - name: Clean previous build | |
| run: rm -rf build | |
| - name: Build simlibs Release | |
| run: ./build.sh simlibs_release | |
| - name: Verify JSBSim SimLibs artifacts | |
| run: | | |
| test -d unreal/Blocks/Plugins/ProjectAirSim/SimLibs/core_sim/jsbsim/include | |
| test -f unreal/Blocks/Plugins/ProjectAirSim/SimLibs/core_sim/jsbsim/lib/Release/libJSBSim.so | |
| - name: Display UE_ROOT | |
| run: | | |
| echo "UE_ROOT=$UE_ROOT" | |
| - name: Build Unreal Editor (Blocks) | |
| run: | | |
| "$UE_ROOT/Engine/Build/BatchFiles/Linux/Build.sh" \ | |
| ShaderCompileWorker Linux Development \ | |
| -waitmutex | |
| test -x "$UE_ROOT/Engine/Binaries/Linux/ShaderCompileWorker" | |
| "$UE_ROOT/Engine/Build/BatchFiles/Linux/Build.sh" \ | |
| BlocksEditor Linux Development \ | |
| "$(pwd)/unreal/Blocks/Blocks.uproject" \ | |
| -waitmutex | |
| - name: Setup Python virtual environment and install dependencies | |
| run: | | |
| rm -rf "$AIRSIM_PY_ENV" | |
| python3 -m venv "$AIRSIM_PY_ENV" | |
| source "$AIRSIM_PY_ENV/bin/activate" | |
| python -m pip install --upgrade pip | |
| python -m pip install "setuptools<81" wheel | |
| python -m pip install -e "client/python/projectairsim[datacollection]" | |
| python -m pip install pytest pytest-cov pexpect | |
| - name: Clean stale simulator processes and ports | |
| run: | | |
| pkill -f UnrealEditor || true | |
| pkill -f Blocks || true | |
| sleep 2 | |
| for p in 8989 8990 8991 41451; do | |
| lsof -ti :"$p" | xargs -r kill -9 || true | |
| done | |
| rm -f unreal.pid unreal.log | |
| - name: Diagnose GPU and Vulkan | |
| run: | | |
| echo "USER=$(whoami)" | |
| echo "DISPLAY=$DISPLAY" | |
| echo "XDG_SESSION_TYPE=$XDG_SESSION_TYPE" | |
| echo "VK_ICD_FILENAMES=${VK_ICD_FILENAMES:-not set}" | |
| echo "==== nvidia-smi ====" | |
| nvidia-smi || true | |
| echo "==== vulkaninfo summary ====" | |
| vulkaninfo --summary || true | |
| echo "==== Vulkan ICD files ====" | |
| ls -lah /usr/share/vulkan/icd.d/ || true | |
| echo "==== NVIDIA devices ====" | |
| ls -lah /dev/nvidia* || true | |
| - name: Launch Unreal headless with Play Mode | |
| env: | |
| PROJECTAIRSIM_CI: "1" | |
| run: | | |
| export DISPLAY=:0 | |
| export SDL_VIDEODRIVER=x11 | |
| "$UE_ROOT/Engine/Binaries/Linux/UnrealEditor" \ | |
| "$(pwd)/unreal/Blocks/Blocks.uproject" \ | |
| -game \ | |
| -enginedir="$UE_ROOT" \ | |
| -RenderOffScreen \ | |
| -vulkan \ | |
| -nosound -unattended -nopause -NoSplash -log \ | |
| -ResX=640 -ResY=480 \ | |
| -qualitylevel=0 \ | |
| > unreal.log 2>&1 & | |
| echo $! > unreal.pid | |
| - name: Wait for simulator readiness | |
| run: | | |
| source "$AIRSIM_PY_ENV/bin/activate" | |
| echo "Waiting for ProjectAirSim sockets on ports 8989/8990..." | |
| readiness_timeout_seconds=600 | |
| for ((i = 1; i <= readiness_timeout_seconds; i++)); do | |
| if ps -p "$(cat unreal.pid)" > /dev/null 2>&1; then | |
| if lsof -i :8989 >/dev/null 2>&1 && lsof -i :8990 >/dev/null 2>&1; then | |
| echo "Ports 8989/8990 are listening, validating client handshake..." | |
| if timeout 30 python -c 'from projectairsim import ProjectAirSimClient; c=ProjectAirSimClient(); c.connect(); c.get_topic_info(); c.disconnect(); print("Simulator ready")'; then | |
| echo "Simulator is ready" | |
| exit 0 | |
| fi | |
| fi | |
| else | |
| echo "Unreal process has exited unexpectedly" | |
| tail -n 300 unreal.log | |
| exit 1 | |
| fi | |
| sleep 1 | |
| done | |
| echo "Simulator failed to start within timeout (${readiness_timeout_seconds} sec)" | |
| tail -n 300 unreal.log | |
| exit 1 | |
| - name: Run Python tests | |
| run: | | |
| source "$AIRSIM_PY_ENV/bin/activate" | |
| cd client/python/projectairsim/tests | |
| python -m pytest -v --junitxml=pytest-results.xml | |
| - name: Stop Unreal | |
| if: always() | |
| run: | | |
| if [ -f unreal.pid ]; then | |
| kill "$(cat unreal.pid)" || true | |
| fi | |
| pkill -f UnrealEditor || true | |
| pkill -f Blocks || true | |
| - name: Dump Unreal logs on failure | |
| if: failure() | |
| run: | | |
| echo "==== Unreal Log Output ====" | |
| cat unreal.log || true | |
| - name: Upload test results | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: test-results | |
| path: "**/*.xml" | |
| - name: Build PR comment body | |
| if: always() | |
| id: ci_summary | |
| env: | |
| JOB_STATUS: ${{ job.status }} | |
| RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| run: | | |
| set -euo pipefail | |
| ICON="❓" | |
| case "$JOB_STATUS" in | |
| success) ICON="✅" ;; | |
| failure) ICON="❌" ;; | |
| cancelled) ICON="⚠️" ;; | |
| esac | |
| XML_PATH="$(find . -name pytest-results.xml | head -n 1 || true)" | |
| TESTS="N/A" | |
| FAILURES="N/A" | |
| ERRORS="N/A" | |
| SKIPPED="N/A" | |
| if [ -n "$XML_PATH" ] && [ -f "$XML_PATH" ]; then | |
| SUITE_LINE="$(grep -m1 -E '<testsuite ' "$XML_PATH" || true)" | |
| TESTS="$(echo "$SUITE_LINE" | sed -n 's/.*tests="\([0-9]\+\)".*/\1/p')" | |
| FAILURES="$(echo "$SUITE_LINE" | sed -n 's/.*failures="\([0-9]\+\)".*/\1/p')" | |
| ERRORS="$(echo "$SUITE_LINE" | sed -n 's/.*errors="\([0-9]\+\)".*/\1/p')" | |
| SKIPPED="$(echo "$SUITE_LINE" | sed -n 's/.*skipped="\([0-9]\+\)".*/\1/p')" | |
| TESTS="${TESTS:-N/A}" | |
| FAILURES="${FAILURES:-N/A}" | |
| ERRORS="${ERRORS:-N/A}" | |
| SKIPPED="${SKIPPED:-N/A}" | |
| fi | |
| { | |
| echo "body<<EOF" | |
| echo "### ${ICON} ProjectAirSim Self-Hosted CI" | |
| echo | |
| echo "- Status: **${JOB_STATUS}**" | |
| echo "- Pytest results: tests=${TESTS}, failures=${FAILURES}, errors=${ERRORS}, skipped=${SKIPPED}" | |
| echo "- Artifacts: [test-results](${RUN_URL})" | |
| echo | |
| echo "[View full workflow run](${RUN_URL})" | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Comment results on PR | |
| if: always() | |
| continue-on-error: true | |
| uses: actions/github-script@v7 | |
| env: | |
| COMMENT_BODY: ${{ steps.ci_summary.outputs.body }} | |
| with: | |
| script: | | |
| const marker = '<!-- projectairsim-self-hosted-ci -->'; | |
| const issue_number = context.payload.pull_request?.number; | |
| if (!issue_number) { | |
| core.info('No pull_request context found; skipping comment.'); | |
| return; | |
| } | |
| const body = `${marker}\n${process.env.COMMENT_BODY}`; | |
| const { owner, repo } = context.repo; | |
| const comments = await github.paginate(github.rest.issues.listComments, { | |
| owner, | |
| repo, | |
| issue_number, | |
| per_page: 100, | |
| }); | |
| const existing = comments.find( | |
| (c) => c.user?.type === 'Bot' && typeof c.body === 'string' && c.body.includes(marker) | |
| ); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner, | |
| repo, | |
| comment_id: existing.id, | |
| body, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number, | |
| body, | |
| }); | |
| } |