diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8deaf620..51d9ea68 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,64 +3,138 @@ name: Build Image on: repository_dispatch: push: - schedule: + schedule: - cron: '0 0 * * *' jobs: build: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - board: raspberrypiarmhf + arch: armhf + - board: raspberrypiarm64 + arch: arm64 steps: - - name: Install Dependencies - run: | - sudo apt update - sudo apt install coreutils p7zip-full qemu-user-static python3-git - - - name: Checkout CustomPiOS - uses: actions/checkout@v2 - with: - repository: 'guysoft/CustomPiOS' - path: CustomPiOS - - - name: Checkout Project Repository - uses: actions/checkout@v2 - with: - path: repository - submodules: true - - - name: Download Raspbian Image - run: | - cd repository/src/image - wget -c --trust-server-names 'https://downloads.raspberrypi.org/raspios_lite_armhf_latest' - - - name: Update CustomPiOS Paths - run: | - cd repository/src - ../../CustomPiOS/src/update-custompios-paths - - # - name: Force apt mirror to work around intermittent mirror hiccups - # run: | - # echo "OCTOPI_APTMIRROR=http://mirror.us.leaseweb.net/raspbian/raspbian" > repository/src/config.local - - - name: Build Image - run: | - sudo modprobe loop - cd repository/src - sudo bash -x ./build_dist - - - name: Copy output - id: copy - run: | - source repository/src/config - NOW=$(date +"%Y-%m-%d-%H%M") - IMAGE=$NOW-octopi-$DIST_VERSION - - cp repository/src/workspace/*.img $IMAGE.img - - echo "::set-output name=image::$IMAGE" - - # artifact upload will take care of zipping for us - - uses: actions/upload-artifact@v4 - if: github.event_name == 'schedule' - with: - name: ${{ steps.copy.outputs.image }} - path: ${{ steps.copy.outputs.image }}.img + - name: Install Dependencies + run: | + sudo apt-get update + sudo apt-get install -y coreutils p7zip-full qemu-user-static \ + python3-git python3-yaml + + - name: Checkout CustomPiOS + uses: actions/checkout@v4 + with: + repository: 'guysoft/CustomPiOS' + ref: feature/e2e + path: CustomPiOS + + - name: Checkout Project Repository + uses: actions/checkout@v4 + with: + path: repository + submodules: true + + - name: Update CustomPiOS Paths + run: | + cd repository/src + ../../CustomPiOS/src/update-custompios-paths + + - name: Download Base Image + run: | + cd repository/src + export DIST_PATH=$(pwd) + export CUSTOM_PI_OS_PATH=$(cat custompios_path) + export BASE_BOARD=${{ matrix.board }} + $CUSTOM_PI_OS_PATH/base_image_downloader_wrapper.sh + + - name: Build Image + run: | + sudo modprobe loop + cd repository/src + sudo BASE_BOARD=${{ matrix.board }} bash -x ./build_dist + + - name: Copy output + id: copy + run: | + source repository/src/config + NOW=$(date +"%Y-%m-%d-%H%M") + IMAGE="${NOW}-octopi-${DIST_VERSION}-${{ matrix.arch }}" + cp repository/src/workspace/*.img ${IMAGE}.img + echo "image=${IMAGE}" >> $GITHUB_OUTPUT + + - uses: actions/upload-artifact@v4 + with: + name: octopi-${{ matrix.arch }} + path: ${{ steps.copy.outputs.image }}.img + + e2e-test: + needs: build + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + + - name: Checkout CustomPiOS + uses: actions/checkout@v4 + with: + repository: 'guysoft/CustomPiOS' + ref: feature/e2e + path: CustomPiOS + + - name: Download arm64 image from build + uses: actions/download-artifact@v4 + with: + name: octopi-arm64 + path: image/ + + - name: Prepare testing context + run: | + mkdir -p testing/custompios + cp -r CustomPiOS/src/distro_testing/scripts testing/custompios/scripts + cp -r CustomPiOS/src/distro_testing/tests testing/custompios/tests + + - name: Build test Docker image + run: DOCKER_BUILDKIT=0 docker build -t octopi-e2e-test ./testing/ + + - name: Start E2E test container + run: | + mkdir -p artifacts + IMG=$(find image/ -name '*.img' | head -1) + docker run -d --name octopi-test \ + -v "$PWD/artifacts:/output" \ + -v "$(realpath $IMG):/input/image.img:ro" \ + -e ARTIFACTS_DIR=/output \ + -e DISTRO_NAME="OctoPi" \ + octopi-e2e-test + + - name: Wait for tests to complete + run: | + for i in $(seq 1 420); do + [ -f artifacts/exit-code ] && break + sleep 5 + done + if [ ! -f artifacts/exit-code ]; then + echo "ERROR: Tests did not complete within 35 minutes" + docker logs octopi-test 2>&1 | tail -80 + exit 1 + fi + echo "Tests finished with exit code: $(cat artifacts/exit-code)" + cat artifacts/test-results.txt 2>/dev/null || true + + - name: Collect logs and stop container + if: always() + run: | + docker logs octopi-test > artifacts/container.log 2>&1 || true + docker stop octopi-test 2>/dev/null || true + + - name: Check test result + run: exit "$(cat artifacts/exit-code 2>/dev/null || echo 1)" + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: e2e-test-results + path: artifacts/ diff --git a/testing/.dockerignore b/testing/.dockerignore new file mode 100644 index 00000000..47241b6e --- /dev/null +++ b/testing/.dockerignore @@ -0,0 +1 @@ +images/ diff --git a/testing/.gitignore b/testing/.gitignore new file mode 100644 index 00000000..8a86c2b0 --- /dev/null +++ b/testing/.gitignore @@ -0,0 +1,3 @@ +images/ +custompios/ +*.png diff --git a/testing/Dockerfile b/testing/Dockerfile new file mode 100644 index 00000000..aaaf9baf --- /dev/null +++ b/testing/Dockerfile @@ -0,0 +1,23 @@ +FROM ptrsr/pi-ci:latest + +ENV LIBGUESTFS_BACKEND=direct + +RUN apt-get update && apt-get install -y --no-install-recommends \ + sshpass openssh-client curl socat imagemagick wget gnupg chromium \ + && wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/google-chrome.gpg \ + && echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome.gpg] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list \ + && apt-get update && apt-get install -y --no-install-recommends google-chrome-stable \ + && rm -rf /var/lib/apt/lists/* + +# Shared framework from CustomPiOS (copied into build context by CI) +COPY custompios/scripts/ /test/scripts/ +COPY custompios/tests/ /test/tests/ + +# OctoPi-specific tests and hooks +COPY tests/ /test/tests/ +COPY hooks/ /test/hooks/ + +RUN chmod +x /test/scripts/*.sh /test/tests/*.sh; \ + chmod +x /test/hooks/*.sh 2>/dev/null || true + +ENTRYPOINT ["/test/scripts/entrypoint.sh"] diff --git a/testing/hooks/prepare-image.sh b/testing/hooks/prepare-image.sh new file mode 100755 index 00000000..66ca08a9 --- /dev/null +++ b/testing/hooks/prepare-image.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e +IMAGE_FILE="${1:?Usage: $0 }" + +export LIBGUESTFS_BACKEND=direct +export LIBGUESTFS_DEBUG=0 +export LIBGUESTFS_TRACE=0 + +echo '=== OctoPi-specific image patches ===' + +echo 'Downloading haproxy config for IPv4 patching...' +guestfish -a "$IMAGE_FILE" </dev/null || echo "") + if echo "$BODY" | grep -q "CONFIG_WIZARD"; then + WIZARD_READY=1 + echo "$BODY" > "$ARTIFACTS_DIR/octoprint-ui.html" + echo " Saved OctoPrint wizard HTML to artifacts (after ${i}x5s)" + break + fi + printf "." + sleep 5 +done +echo "" + +if [ "$WIZARD_READY" -eq 0 ]; then + echo " WARNING: CONFIG_WIZARD not found after 120s, saving current page" + if [ -n "$BODY" ]; then + echo "$BODY" > "$ARTIFACTS_DIR/octoprint-ui.html" + fi +fi + +HTTP_PORT="${QEMU_HTTP_PORT:-8080}" +SCREENSHOT_TAKEN=0 + +for BROWSER in google-chrome-stable chromium chromium-browser; do + BROWSER_PATH=$(command -v "$BROWSER" 2>/dev/null || true) + [ -n "$BROWSER_PATH" ] || continue + + echo " Taking headless screenshot with $BROWSER_PATH ..." + timeout 30 "$BROWSER_PATH" --headless --no-sandbox --disable-gpu \ + --disable-dev-shm-usage --virtual-time-budget=15000 \ + --screenshot="$ARTIFACTS_DIR/screenshot.png" \ + --window-size=1280,720 \ + "http://localhost:${HTTP_PORT}" 2>/dev/null || true + + if [ -f "$ARTIFACTS_DIR/screenshot.png" ]; then + SIZE=$(stat -c%s "$ARTIFACTS_DIR/screenshot.png" 2>/dev/null || echo "0") + if [ "$SIZE" -gt 10000 ]; then + echo " Screenshot saved (${SIZE} bytes) -- wizard page captured" + SCREENSHOT_TAKEN=1 + break + else + echo " WARNING: Screenshot is only ${SIZE} bytes (may be blank), trying next browser" + rm -f "$ARTIFACTS_DIR/screenshot.png" + fi + else + echo " Screenshot file was NOT created by $BROWSER, trying next browser" + fi +done + +if [ "$SCREENSHOT_TAKEN" -eq 0 ]; then + echo " WARNING: No browser produced a valid screenshot" +fi diff --git a/testing/tests/test_octoprint_web.sh b/testing/tests/test_octoprint_web.sh new file mode 100755 index 00000000..7fec1a26 --- /dev/null +++ b/testing/tests/test_octoprint_web.sh @@ -0,0 +1,76 @@ +#!/bin/bash +set -e + +HOST="${1:-localhost}" +PORT="${2:-2222}" +ARTIFACTS_DIR="${3:-}" +USER="pi" +PASS="raspberry" + +SSH_CMD="sshpass -p $PASS ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o PreferredAuthentications=password -o PubkeyAuthentication=no -o LogLevel=ERROR -p $PORT ${USER}@${HOST}" + +echo "Test: OctoPrint web server is accessible with CONFIG_WIZARD" + +OCTOPRINT_READY=0 +for i in $(seq 1 120); do + BODY=$($SSH_CMD "curl -s http://localhost" 2>/dev/null || echo "") + HTTP_CODE=$($SSH_CMD "curl -s -o /dev/null -w '%{http_code}' http://localhost" 2>/dev/null || echo "000") + + if [ "$HTTP_CODE" = "200" ]; then + if echo "$BODY" | grep -q "CONFIG_WIZARD"; then + OCTOPRINT_READY=1 + break + elif echo "$BODY" | grep -q "starting up\|still starting"; then + printf "S" + else + printf "?" + fi + else + printf "." + fi + sleep 5 +done +echo "" + +if [ "$OCTOPRINT_READY" -eq 0 ]; then + echo " FAIL: OctoPrint CONFIG_WIZARD did not appear within 600s" + echo " Last HTTP code: $HTTP_CODE" + echo " Last body (first 200 chars): $(echo "$BODY" | head -c 200)" + exit 1 +fi + +echo " OctoPrint CONFIG_WIZARD is loaded (HTTP 200)" + +if [ -n "$ARTIFACTS_DIR" ]; then + echo "$BODY" > "$ARTIFACTS_DIR/octoprint.html" + echo " Saved wizard HTML to $ARTIFACTS_DIR/octoprint.html" +fi + +if ! echo "$BODY" | grep -q "OctoPrint"; then + echo " FAIL: Wizard page did not contain 'OctoPrint'" + exit 1 +fi + +HTTP_PORT="${QEMU_HTTP_PORT:-8080}" +BROWSER=$(command -v google-chrome-stable 2>/dev/null || true) +if [ -n "$BROWSER" ] && [ -n "$ARTIFACTS_DIR" ]; then + echo " Taking wizard screenshot via headless Chrome..." + timeout 30 "$BROWSER" --headless=new --no-sandbox --disable-gpu \ + --disable-dev-shm-usage --virtual-time-budget=15000 \ + --screenshot="$ARTIFACTS_DIR/screenshot.png" \ + --window-size=1280,720 \ + "http://localhost:${HTTP_PORT}" 2>/dev/null || true + if [ -f "$ARTIFACTS_DIR/screenshot.png" ]; then + SIZE=$(stat -c%s "$ARTIFACTS_DIR/screenshot.png" 2>/dev/null || echo "0") + if [ "$SIZE" -gt 10000 ]; then + echo " Screenshot saved (${SIZE} bytes) -- wizard page captured" + else + echo " WARNING: Screenshot is only ${SIZE} bytes (may be blank)" + fi + else + echo " WARNING: Screenshot was not created" + fi +fi + +echo " PASS: OctoPrint wizard page verified" +exit 0