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
244 changes: 189 additions & 55 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/
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
20 changes: 20 additions & 0 deletions testing/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
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'
36 changes: 36 additions & 0 deletions testing/hooks/screenshot.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/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 "Capturing OctoPrint web UI artifacts..."

# Save rendered HTML via curl through SSH (inside the guest, hitting localhost:80)
BODY=$(${SSH_CMD} -l pi "curl -s http://localhost" 2>/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)"
Loading
Loading