Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 129 additions & 55 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/
1 change: 1 addition & 0 deletions testing/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
images/
3 changes: 3 additions & 0 deletions testing/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
images/
custompios/
*.png
23 changes: 23 additions & 0 deletions testing/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
30 changes: 30 additions & 0 deletions testing/hooks/prepare-image.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/bin/bash
set -e
IMAGE_FILE="${1:?Usage: $0 <image.qcow2>}"

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" <<GFEOF
run
mount /dev/sda2 /
download /etc/haproxy/haproxy.cfg /tmp/haproxy.cfg
umount /
GFEOF

echo 'Fixing haproxy for IPv4-only (QEMU has no IPv6)...'
sed -i 's/bind :::80 v4v6/bind *:80/' /tmp/haproxy.cfg
sed -i 's/bind :::443 v4v6/bind *:443/' /tmp/haproxy.cfg

guestfish -a "$IMAGE_FILE" <<GFEOF2
run
mount /dev/sda2 /
upload /tmp/haproxy.cfg /etc/haproxy/haproxy.cfg
umount /
GFEOF2

echo 'OctoPi patches applied'
63 changes: 63 additions & 0 deletions testing/hooks/screenshot.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/bin/bash
set -e
SSH_HOST="${1:-localhost}"
SSH_PORT="${2:-2222}"
ARTIFACTS_DIR="${3:-/output}"

SSH_CMD="sshpass -p raspberry ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o PreferredAuthentications=password -o PubkeyAuthentication=no -o LogLevel=ERROR -p $SSH_PORT ${SSH_HOST}"

echo "Waiting for OctoPrint wizard page before capturing..."

WIZARD_READY=0
for i in $(seq 1 24); do
BODY=$(${SSH_CMD} -l pi "curl -s http://localhost" 2>/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
76 changes: 76 additions & 0 deletions testing/tests/test_octoprint_web.sh
Original file line number Diff line number Diff line change
@@ -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
Loading