Skip to content

/clock Topic Not Published and Unreal Engine 5.7 Build Fix #40

/clock Topic Not Published and Unreal Engine 5.7 Build Fix

/clock Topic Not Published and Unreal Engine 5.7 Build Fix #40

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,
});
}