diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8deaf620..db71e165 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,64 +3,198 @@ 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: 30 + 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 \ + -p 9980:9980 \ + -v "$PWD/artifacts:/output" \ + -v "$(realpath $IMG):/input/image.img:ro" \ + -e ARTIFACTS_DIR=/output \ + -e DISTRO_NAME="OctoPi" \ + -e KEEP_ALIVE=true \ + octopi-e2e-test + + - name: Wait for tests to complete + run: | + for i in $(seq 1 180); do + [ -f artifacts/exit-code ] && break + sleep 5 + done + if [ ! -f artifacts/exit-code ]; then + echo "ERROR: Tests did not complete within 15 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: Wait for OctoPrint to fully start + run: | + echo "Waiting for OctoPrint to finish startup..." + for i in $(seq 1 90); do + BODY=$(curl -4 -s --connect-timeout 5 http://127.0.0.1:9980 2>/dev/null || true) + if echo "$BODY" | grep -q "CONFIG_WIZARD"; then + echo "OctoPrint fully started (Setup Wizard ready)" + exit 0 + elif echo "$BODY" | grep -q "starting up"; then + printf "S" + else + printf "." + fi + sleep 5 + done + echo "" + echo "WARNING: OctoPrint may not be fully started yet, proceeding anyway" + + - name: Take OctoPrint screenshot + run: | + npm install puppeteer + node -e " + const puppeteer = require('puppeteer'); + (async () => { + const browser = await puppeteer.launch({ + headless: 'new', args: ['--no-sandbox'] + }); + const page = await browser.newPage(); + await page.setViewport({width: 1280, height: 900}); + + // Retry loading until OctoPrint finishes its startup phase + for (let attempt = 0; attempt < 30; attempt++) { + await page.goto('http://127.0.0.1:9980', { + waitUntil: 'networkidle2', timeout: 60000 + }); + const html = await page.content(); + if (html.includes('CONFIG_WIZARD') || html.includes('id=\"login\"')) break; + console.log('OctoPrint still starting up, retrying in 10s... (attempt ' + (attempt+1) + ')'); + await new Promise(r => setTimeout(r, 10000)); + } + + // Wait for the Setup Wizard dialog to appear + try { + await page.waitForSelector('#wizard_dialog', { visible: true, timeout: 120000 }); + } catch (e) { + console.log('Wizard did not appear, taking screenshot of current state'); + } + + // Dismiss notification popovers by clicking the wizard body + try { await page.click('#wizard_dialog .modal-body'); } catch(e) {} + await new Promise(r => setTimeout(r, 2000)); + + await page.screenshot({path: 'artifacts/octoprint-screenshot.png'}); + console.log('Screenshot captured'); + await browser.close(); + })(); + " + + - 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..ab56a399 --- /dev/null +++ b/testing/Dockerfile @@ -0,0 +1,20 @@ +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 \ + && 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 [ -n "$BODY" ]; then + echo "$BODY" > "$ARTIFACTS_DIR/octoprint-ui.html" + echo " Saved OctoPrint HTML to artifacts" +fi + +# Attempt headless screenshot from inside the container if a browser is available +HTTP_PORT="${QEMU_HTTP_PORT:-8080}" +for BROWSER in chromium chromium-browser google-chrome; do + if command -v "$BROWSER" &>/dev/null; then + echo " Using $BROWSER for headless screenshot..." + "$BROWSER" --headless --disable-gpu --no-sandbox \ + --virtual-time-budget=10000 \ + --screenshot="$ARTIFACTS_DIR/screenshot.png" \ + --window-size=1280,720 \ + "http://localhost:${HTTP_PORT}" 2>/dev/null || true + if [ -f "$ARTIFACTS_DIR/screenshot.png" ]; then + echo " Browser screenshot saved" + exit 0 + fi + fi +done + +echo " No headless browser available in container (HTML artifact saved instead)" diff --git a/testing/tests/test_octoprint_web.sh b/testing/tests/test_octoprint_web.sh new file mode 100755 index 00000000..adcfa3d7 --- /dev/null +++ b/testing/tests/test_octoprint_web.sh @@ -0,0 +1,55 @@ +#!/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 and fully started" + +OCTOPRINT_READY=0 +for i in $(seq 1 60); 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 "starting up"; then + printf "S" + elif echo "$BODY" | grep -q "CONFIG_WIZARD\|OctoPrint"; then + OCTOPRINT_READY=1 + break + else + printf "W" + fi + else + printf "." + fi + sleep 5 +done +echo "" + +if [ "$OCTOPRINT_READY" -eq 0 ]; then + echo " FAIL: OctoPrint did not fully start within 300s" + exit 1 +fi + +echo " OctoPrint web server is fully started (HTTP 200, UI loaded)" + +FULL_HTML=$($SSH_CMD "curl -s http://localhost" 2>/dev/null) + +if [ -n "$ARTIFACTS_DIR" ]; then + echo "$FULL_HTML" > "$ARTIFACTS_DIR/octoprint.html" + echo " Saved HTML to $ARTIFACTS_DIR/octoprint.html" +fi + +if echo "$FULL_HTML" | grep -q "OctoPrint"; then + echo " PASS: OctoPrint web UI returned expected content" + exit 0 +else + echo " FAIL: Response did not contain 'OctoPrint'" + exit 1 +fi