diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8deaf620..b34febb8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,64 +3,77 @@ 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: 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@v2 - with: - repository: 'guysoft/CustomPiOS' - path: CustomPiOS + - name: Checkout CustomPiOS + uses: actions/checkout@v4 + with: + repository: 'guysoft/CustomPiOS' + ref: devel + path: CustomPiOS - - name: Checkout Project Repository - uses: actions/checkout@v2 - with: - path: repository - submodules: true + - name: Checkout Project Repository + uses: actions/checkout@v4 + 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: 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: 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 bash -x ./build_dist + - 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 + - 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 - cp repository/src/workspace/*.img $IMAGE.img + - uses: actions/upload-artifact@v4 + with: + name: octopi-${{ matrix.arch }} + path: ${{ steps.copy.outputs.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 + e2e-test: + needs: build + uses: guysoft/CustomPiOS/.github/workflows/e2e-test.yml@feature/e2e + with: + image-artifact-name: octopi-arm64 + distro-name: OctoPi + timeout-minutes: 45 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..2d4732fd --- /dev/null +++ b/testing/.gitignore @@ -0,0 +1,2 @@ +images/ +*.png diff --git a/testing/Dockerfile b/testing/Dockerfile new file mode 100644 index 00000000..aa1f416b --- /dev/null +++ b/testing/Dockerfile @@ -0,0 +1,25 @@ +ARG CUSTOMPIOS_TAG=feature-e2e +FROM ghcr.io/guysoft/custompios:${CUSTOMPIOS_TAG} AS custompios + +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 \ + dbus dbus-x11 fonts-liberation tesseract-ocr \ + && 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/* + +COPY --from=custompios /CustomPiOS/distro_testing/scripts/ /test/scripts/ +COPY --from=custompios /CustomPiOS/distro_testing/tests/ /test/tests/ + +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}" + +echo " Verifying localhost:${HTTP_PORT} is reachable from container..." +HTTP_CHECK=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:${HTTP_PORT}" 2>/dev/null || echo "000") +echo " HTTP status from container: ${HTTP_CHECK}" + +DBUS_PREFIX="" +if command -v dbus-run-session &>/dev/null; then + DBUS_PREFIX="dbus-run-session --" +fi + +BROWSER_PATH="" +for BROWSER in google-chrome-stable chromium chromium-browser; do + B=$(command -v "$BROWSER" 2>/dev/null || true) + [ -n "$B" ] || continue + if "$B" --version 2>&1 | grep -qi "snap"; then + echo " Skipping $B (snap stub)" + continue + fi + BROWSER_PATH="$B" + break +done + +if [ -z "$BROWSER_PATH" ]; then + echo " WARNING: No usable browser found for screenshot" +else + echo " Using $BROWSER_PATH ($(${BROWSER_PATH} --version 2>/dev/null || echo 'unknown'))" + + WIZARD_VISIBLE=0 + for attempt in $(seq 1 12); do + rm -f /tmp/screenshot.png + timeout 60 $DBUS_PREFIX "$BROWSER_PATH" --headless --no-sandbox --disable-gpu \ + --disable-dev-shm-usage --disable-setuid-sandbox \ + --disable-software-rasterizer --hide-scrollbars \ + --virtual-time-budget=30000 \ + --screenshot="/tmp/screenshot.png" \ + --window-size=1280,720 \ + "http://localhost:${HTTP_PORT}" 2>"$ARTIFACTS_DIR/browser-screenshot.log" || true + + if [ ! -f /tmp/screenshot.png ]; then + echo " Attempt $attempt: screenshot not created" + cat "$ARTIFACTS_DIR/browser-screenshot.log" 2>/dev/null | head -10 || true + sleep 10 + continue + fi + + SIZE=$(stat -c%s /tmp/screenshot.png 2>/dev/null || echo "0") + if [ "$SIZE" -le 10000 ]; then + echo " Attempt $attempt: screenshot too small (${SIZE} bytes)" + rm -f /tmp/screenshot.png + sleep 10 + continue + fi + + cp /tmp/screenshot.png "$ARTIFACTS_DIR/screenshot.png" + + OCR_TEXT="" + if command -v tesseract &>/dev/null; then + OCR_TEXT=$(tesseract /tmp/screenshot.png stdout 2>/dev/null || echo "") + fi + + if echo "$OCR_TEXT" | grep -qi "wizard\|access.control\|setup"; then + echo " Attempt $attempt: wizard detected via OCR (${SIZE} bytes)" + WIZARD_VISIBLE=1 + rm -f /tmp/screenshot.png + break + fi + + OCR_FIRST_LINE=$(echo "$OCR_TEXT" | head -c 80) + echo " Attempt $attempt: wizard not visible yet, OCR: ${OCR_FIRST_LINE:-}" + rm -f /tmp/screenshot.png + sleep 10 + done + + if [ -n "$OCR_TEXT" ]; then + echo "$OCR_TEXT" > "$ARTIFACTS_DIR/screenshot-ocr.txt" + fi + + if [ "$WIZARD_VISIBLE" -eq 1 ]; then + echo " Screenshot captured with wizard visible" + elif [ -f "$ARTIFACTS_DIR/screenshot.png" ]; then + echo " WARNING: Wizard not detected via OCR after 12 attempts, keeping last screenshot" + else + echo " WARNING: No valid screenshot produced" + fi +fi diff --git a/testing/tests/test_octoprint_web.sh b/testing/tests/test_octoprint_web.sh new file mode 100755 index 00000000..84116ece --- /dev/null +++ b/testing/tests/test_octoprint_web.sh @@ -0,0 +1,117 @@ +#!/bin/bash +set -e + +export E2E_SSH_HOST="${1:-localhost}" +export E2E_SSH_PORT="${2:-2222}" +ARTIFACTS_DIR="${3:-}" +source /test/scripts/ssh-helpers.sh + +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 + DBUS_PREFIX="" + if command -v dbus-run-session &>/dev/null; then + DBUS_PREFIX="dbus-run-session --" + fi + + echo " Taking wizard screenshot via headless Chrome (with OCR retry)..." + WIZARD_VISIBLE=0 + for attempt in $(seq 1 12); do + rm -f /tmp/screenshot.png + timeout 60 $DBUS_PREFIX "$BROWSER" --headless --no-sandbox --disable-gpu \ + --disable-dev-shm-usage --disable-setuid-sandbox \ + --disable-software-rasterizer --hide-scrollbars \ + --virtual-time-budget=30000 \ + --screenshot="/tmp/screenshot.png" \ + --window-size=1280,720 \ + "http://localhost:${HTTP_PORT}" 2>"$ARTIFACTS_DIR/browser-screenshot.log" || true + + if [ ! -f /tmp/screenshot.png ]; then + echo " Attempt $attempt: screenshot not created" + sleep 10 + continue + fi + + SIZE=$(stat -c%s /tmp/screenshot.png 2>/dev/null || echo "0") + if [ "$SIZE" -le 10000 ]; then + echo " Attempt $attempt: screenshot too small (${SIZE} bytes)" + rm -f /tmp/screenshot.png + sleep 10 + continue + fi + + cp /tmp/screenshot.png "$ARTIFACTS_DIR/screenshot.png" + + OCR_TEXT="" + if command -v tesseract &>/dev/null; then + OCR_TEXT=$(tesseract /tmp/screenshot.png stdout 2>/dev/null || echo "") + fi + + if echo "$OCR_TEXT" | grep -qi "wizard\|access.control\|setup"; then + echo " Attempt $attempt: wizard detected via OCR (${SIZE} bytes)" + WIZARD_VISIBLE=1 + rm -f /tmp/screenshot.png + break + fi + + OCR_FIRST_LINE=$(echo "$OCR_TEXT" | head -c 80) + echo " Attempt $attempt: wizard not visible yet, OCR: ${OCR_FIRST_LINE:-}" + rm -f /tmp/screenshot.png + sleep 10 + done + + if [ -n "$OCR_TEXT" ]; then + echo "$OCR_TEXT" > "$ARTIFACTS_DIR/screenshot-ocr.txt" + fi + + if [ "$WIZARD_VISIBLE" -eq 1 ]; then + echo " Screenshot captured with wizard visible" + elif [ -f "$ARTIFACTS_DIR/screenshot.png" ]; then + echo " WARNING: Wizard not detected via OCR after 12 attempts, keeping last screenshot" + fi +fi + +echo " PASS: OctoPrint wizard page verified" +exit 0