Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7f52b5a
Add arm64 build and E2E test system
guysoft Feb 20, 2026
2a9064d
Move E2E test into build.yml, enable e2e-test on feature branch
guysoft Feb 21, 2026
daf0122
Remove standalone e2e-test.yml
guysoft Feb 21, 2026
896303c
Fix puppeteer screenshot: npm install instead of npx browsers install
guysoft Feb 22, 2026
caf03f9
Wait for OctoPrint Setup Wizard before taking screenshot
guysoft Feb 22, 2026
b547ccf
Refactor e2e tests to use shared CustomPiOS distro_testing framework
guysoft Mar 10, 2026
8ea7d14
Fix e2e CI: remove old puppeteer screenshot step and port forward
guysoft Mar 12, 2026
45480fa
Fix e2e: wait for CONFIG_WIZARD before capturing OctoPrint artifacts
guysoft Mar 25, 2026
68321d2
Fix e2e: show headless Chromium errors, add missing deps for screenshot
guysoft Mar 25, 2026
c2cc14f
Fix e2e screenshot: skip snap stub, find real chromium binary
guysoft Mar 25, 2026
0fb9200
Fix e2e screenshot: use google-chrome-stable, add wizard validation
guysoft Mar 26, 2026
91c0b83
Fix e2e timeout: add 30s kill timer to Chrome, increase wait to 35min
guysoft Mar 30, 2026
4ce3dfd
Add chromium fallback browser for e2e screenshot capture
guysoft Mar 31, 2026
3ca0db4
Fix e2e screenshot: capture browser stderr, add container-friendly flags
guysoft Apr 4, 2026
5549c1d
Fix e2e screenshot: add dbus, skip snap stubs, drop --single-process
guysoft Apr 5, 2026
d5c0cea
Fix e2e screenshot: use dbus-run-session, add fonts-liberation
guysoft Apr 5, 2026
63e82b2
Fix e2e screenshot: use legacy --headless mode, write to /tmp first
guysoft Apr 5, 2026
4703512
Add OCR-based retry loop to wait for wizard in screenshot
guysoft Apr 5, 2026
b462da3
Add --virtual-time-budget=30000 to give Chrome time for websocket
guysoft Apr 5, 2026
8837368
Refactor E2E to use CustomPiOS multi-stage Docker build and reusable …
guysoft Apr 14, 2026
6e476a2
Remove internal E2E_HANDOFF.md development file
guysoft Apr 16, 2026
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
107 changes: 60 additions & 47 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions testing/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
images/
2 changes: 2 additions & 0 deletions testing/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
images/
*.png
25 changes: 25 additions & 0 deletions testing/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
ARG CUSTOMPIOS_TAG=devel
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"]
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'
116 changes: 116 additions & 0 deletions testing/hooks/screenshot.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#!/bin/bash
set -e
export E2E_SSH_HOST="${1:-localhost}"
export E2E_SSH_PORT="${2:-2222}"
ARTIFACTS_DIR="${3:-/output}"
source /test/scripts/ssh-helpers.sh

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

WIZARD_READY=0
for i in $(seq 1 24); do
BODY=$(ssh_cmd "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}"

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:-<empty>}"
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
Loading
Loading