Skip to content

Commit 7f52b5a

Browse files
committed
Add arm64 build and E2E test system
- Update build.yml to build both armhf and arm64 using CustomPiOS v2 board system (BASE_BOARD matrix) with base_image_downloader - Add testing/ directory with Docker+QEMU-based E2E test harness that boots an OctoPi arm64 image, verifies SSH access, and checks OctoPrint web server availability - Add e2e-test.yml workflow triggered by build completion that runs E2E tests on the arm64 artifact and uploads screenshot + logs
1 parent 1ad955f commit 7f52b5a

12 files changed

Lines changed: 607 additions & 48 deletions

File tree

.github/workflows/build.yml

Lines changed: 52 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,64 +3,68 @@ name: Build Image
33
on:
44
repository_dispatch:
55
push:
6-
schedule:
6+
schedule:
77
- cron: '0 0 * * *'
88

99
jobs:
1010
build:
1111
runs-on: ubuntu-latest
12+
strategy:
13+
fail-fast: false
14+
matrix:
15+
include:
16+
- board: raspberrypiarmhf
17+
arch: armhf
18+
- board: raspberrypiarm64
19+
arch: arm64
1220
steps:
13-
- name: Install Dependencies
14-
run: |
15-
sudo apt update
16-
sudo apt install coreutils p7zip-full qemu-user-static python3-git
21+
- name: Install Dependencies
22+
run: |
23+
sudo apt-get update
24+
sudo apt-get install -y coreutils p7zip-full qemu-user-static \
25+
python3-git python3-yaml
1726
18-
- name: Checkout CustomPiOS
19-
uses: actions/checkout@v2
20-
with:
21-
repository: 'guysoft/CustomPiOS'
22-
path: CustomPiOS
27+
- name: Checkout CustomPiOS
28+
uses: actions/checkout@v4
29+
with:
30+
repository: 'guysoft/CustomPiOS'
31+
path: CustomPiOS
2332

24-
- name: Checkout Project Repository
25-
uses: actions/checkout@v2
26-
with:
27-
path: repository
28-
submodules: true
33+
- name: Checkout Project Repository
34+
uses: actions/checkout@v4
35+
with:
36+
path: repository
37+
submodules: true
2938

30-
- name: Download Raspbian Image
31-
run: |
32-
cd repository/src/image
33-
wget -c --trust-server-names 'https://downloads.raspberrypi.org/raspios_lite_armhf_latest'
39+
- name: Update CustomPiOS Paths
40+
run: |
41+
cd repository/src
42+
../../CustomPiOS/src/update-custompios-paths
3443
35-
- name: Update CustomPiOS Paths
36-
run: |
37-
cd repository/src
38-
../../CustomPiOS/src/update-custompios-paths
39-
40-
# - name: Force apt mirror to work around intermittent mirror hiccups
41-
# run: |
42-
# echo "OCTOPI_APTMIRROR=http://mirror.us.leaseweb.net/raspbian/raspbian" > repository/src/config.local
44+
- name: Download Base Image
45+
run: |
46+
cd repository/src
47+
export DIST_PATH=$(pwd)
48+
export CUSTOM_PI_OS_PATH=$(cat custompios_path)
49+
export BASE_BOARD=${{ matrix.board }}
50+
$CUSTOM_PI_OS_PATH/base_image_downloader_wrapper.sh
4351
44-
- name: Build Image
45-
run: |
46-
sudo modprobe loop
47-
cd repository/src
48-
sudo bash -x ./build_dist
52+
- name: Build Image
53+
run: |
54+
sudo modprobe loop
55+
cd repository/src
56+
sudo BASE_BOARD=${{ matrix.board }} bash -x ./build_dist
4957
50-
- name: Copy output
51-
id: copy
52-
run: |
53-
source repository/src/config
54-
NOW=$(date +"%Y-%m-%d-%H%M")
55-
IMAGE=$NOW-octopi-$DIST_VERSION
58+
- name: Copy output
59+
id: copy
60+
run: |
61+
source repository/src/config
62+
NOW=$(date +"%Y-%m-%d-%H%M")
63+
IMAGE="${NOW}-octopi-${DIST_VERSION}-${{ matrix.arch }}"
64+
cp repository/src/workspace/*.img ${IMAGE}.img
65+
echo "image=${IMAGE}" >> $GITHUB_OUTPUT
5666
57-
cp repository/src/workspace/*.img $IMAGE.img
58-
59-
echo "::set-output name=image::$IMAGE"
60-
61-
# artifact upload will take care of zipping for us
62-
- uses: actions/upload-artifact@v4
63-
if: github.event_name == 'schedule'
64-
with:
65-
name: ${{ steps.copy.outputs.image }}
66-
path: ${{ steps.copy.outputs.image }}.img
67+
- uses: actions/upload-artifact@v4
68+
with:
69+
name: octopi-${{ matrix.arch }}
70+
path: ${{ steps.copy.outputs.image }}.img

.github/workflows/e2e-test.yml

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
name: E2E Test
2+
3+
on:
4+
workflow_run:
5+
workflows: ["Build Image"]
6+
types: [completed]
7+
workflow_dispatch:
8+
inputs:
9+
image_url:
10+
description: "OctoPi image zip URL (arm64). Leave empty to use stable 1.1.0."
11+
required: false
12+
13+
jobs:
14+
e2e-test:
15+
runs-on: ubuntu-latest
16+
timeout-minutes: 30
17+
if: >
18+
github.event_name == 'workflow_dispatch' ||
19+
github.event.workflow_run.conclusion == 'success'
20+
steps:
21+
- uses: actions/checkout@v4
22+
23+
- name: Download arm64 image from build
24+
if: github.event_name == 'workflow_run'
25+
uses: actions/download-artifact@v4
26+
with:
27+
name: octopi-arm64
28+
path: image/
29+
run-id: ${{ github.event.workflow_run.id }}
30+
github-token: ${{ secrets.GITHUB_TOKEN }}
31+
32+
- name: Download arm64 image from URL
33+
if: github.event_name == 'workflow_dispatch'
34+
run: |
35+
URL="${{ github.event.inputs.image_url }}"
36+
if [ -z "$URL" ]; then
37+
URL="https://unofficialpi.org/Distros/OctoPi/octopi-bookworm-arm64-lite-1.1.0.zip"
38+
fi
39+
wget -q --show-progress -O octopi.zip "$URL"
40+
mkdir -p image && unzip octopi.zip '*.img' -d image/
41+
42+
- name: Build test Docker image
43+
run: DOCKER_BUILDKIT=0 docker build -t octopi-e2e-test ./testing/
44+
45+
- name: Start E2E test container
46+
run: |
47+
mkdir -p artifacts
48+
IMG=$(find image/ -name '*.img' | head -1)
49+
docker run -d --name octopi-test \
50+
-p 9980:9980 \
51+
-v "$PWD/artifacts:/output" \
52+
-v "$(realpath $IMG):/input/image.img:ro" \
53+
-e ARTIFACTS_DIR=/output \
54+
-e KEEP_ALIVE=true \
55+
octopi-e2e-test
56+
57+
- name: Wait for tests to complete
58+
run: |
59+
for i in $(seq 1 180); do
60+
[ -f artifacts/exit-code ] && break
61+
sleep 5
62+
done
63+
if [ ! -f artifacts/exit-code ]; then
64+
echo "ERROR: Tests did not complete within 15 minutes"
65+
docker logs octopi-test 2>&1 | tail -80
66+
exit 1
67+
fi
68+
echo "Tests finished with exit code: $(cat artifacts/exit-code)"
69+
cat artifacts/test-results.txt 2>/dev/null || true
70+
71+
- name: Wait for OctoPrint web server
72+
run: |
73+
for i in $(seq 1 60); do
74+
HTTP=$(curl -4 -s -o /dev/null -w '%{http_code}' \
75+
--connect-timeout 5 http://127.0.0.1:9980 2>/dev/null || true)
76+
[ "$HTTP" = "200" ] && break
77+
sleep 5
78+
done
79+
80+
- name: Take OctoPrint screenshot
81+
run: |
82+
npx --yes puppeteer browsers install chrome
83+
node -e "
84+
const puppeteer = require('puppeteer');
85+
(async () => {
86+
const browser = await puppeteer.launch({
87+
headless: 'new', args: ['--no-sandbox']
88+
});
89+
const page = await browser.newPage();
90+
await page.setViewport({width: 1280, height: 900});
91+
await page.goto('http://127.0.0.1:9980', {
92+
waitUntil: 'networkidle2', timeout: 60000
93+
});
94+
await page.screenshot({path: 'artifacts/octoprint-screenshot.png'});
95+
await browser.close();
96+
})();
97+
"
98+
99+
- name: Collect logs and stop container
100+
if: always()
101+
run: |
102+
docker logs octopi-test > artifacts/container.log 2>&1 || true
103+
docker stop octopi-test 2>/dev/null || true
104+
105+
- name: Check test result
106+
run: exit "$(cat artifacts/exit-code 2>/dev/null || echo 1)"
107+
108+
- uses: actions/upload-artifact@v4
109+
if: always()
110+
with:
111+
name: e2e-test-results
112+
path: artifacts/

testing/.dockerignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
images/

testing/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
images/

testing/Dockerfile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
FROM ptrsr/pi-ci:latest
2+
3+
ENV LIBGUESTFS_BACKEND=direct
4+
5+
RUN apt-get update && apt-get install -y --no-install-recommends sshpass openssh-client curl && rm -rf /var/lib/apt/lists/*
6+
7+
COPY scripts/ /test/scripts/
8+
COPY tests/ /test/tests/
9+
RUN chmod +x /test/scripts/*.sh /test/tests/*.sh
10+
11+
ENTRYPOINT ["/test/scripts/entrypoint.sh"]

testing/run-test.sh

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
#!/bin/bash
2+
set -e
3+
4+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
5+
IMAGE_DIR="${SCRIPT_DIR}/images"
6+
7+
OCTOPI_URL="${IMAGE_URL:-https://unofficialpi.org/Distros/OctoPi/octopi-bookworm-arm64-lite-1.1.0.zip}"
8+
OCTOPI_ZIP="octopi-bookworm-arm64-lite-1.1.0.zip"
9+
OCTOPI_MD5="74cfd8e6c5b6ff9d8443aaa357201bcd"
10+
DOCKER_IMAGE="octopi-e2e-test"
11+
HTTP_PORT=9980
12+
13+
mkdir -p "$IMAGE_DIR"
14+
15+
if [ -n "$IMAGE_PATH" ]; then
16+
IMG_FILE="$(readlink -f "$IMAGE_PATH")"
17+
echo "Using provided image: $IMG_FILE"
18+
else
19+
ZIP_PATH="${IMAGE_DIR}/${OCTOPI_ZIP}"
20+
21+
if [ ! -f "$ZIP_PATH" ]; then
22+
echo "Downloading OctoPi arm64 image from $OCTOPI_URL..."
23+
wget -q --show-progress -O "$ZIP_PATH" "$OCTOPI_URL"
24+
else
25+
echo "Using cached download: $ZIP_PATH"
26+
fi
27+
28+
if [ "$OCTOPI_URL" = "https://unofficialpi.org/Distros/OctoPi/octopi-bookworm-arm64-lite-1.1.0.zip" ]; then
29+
echo "Verifying checksum..."
30+
ACTUAL_MD5=$(md5sum "$ZIP_PATH" | awk '{print $1}')
31+
if [ "$ACTUAL_MD5" != "$OCTOPI_MD5" ]; then
32+
echo "ERROR: MD5 mismatch! Expected: $OCTOPI_MD5 Got: $ACTUAL_MD5"
33+
rm -f "$ZIP_PATH"
34+
exit 1
35+
fi
36+
echo "Checksum OK."
37+
fi
38+
39+
IMG_NAME=$(unzip -Z1 "$ZIP_PATH" | grep '\.img$' | head -1)
40+
if [ -z "$IMG_NAME" ]; then
41+
echo "ERROR: No .img file found inside $ZIP_PATH"
42+
exit 1
43+
fi
44+
45+
IMG_FILE="${IMAGE_DIR}/${IMG_NAME}"
46+
47+
if [ ! -f "$IMG_FILE" ]; then
48+
echo "Extracting $IMG_NAME..."
49+
unzip -o "$ZIP_PATH" "$IMG_NAME" -d "$IMAGE_DIR"
50+
else
51+
echo "Using cached image: $IMG_FILE"
52+
fi
53+
fi
54+
55+
if [ ! -f "$IMG_FILE" ]; then
56+
echo "ERROR: Image file not found: $IMG_FILE"
57+
exit 1
58+
fi
59+
60+
echo ""
61+
echo "Image: $IMG_FILE"
62+
echo "Size: $(du -h "$IMG_FILE" | awk '{print $1}')"
63+
echo ""
64+
65+
echo "Building Docker image..."
66+
DOCKER_BUILDKIT=0 docker build -t "$DOCKER_IMAGE" "$SCRIPT_DIR"
67+
68+
DOCKER_RUN_ARGS="docker run --rm"
69+
if [ -n "$KEEP_ALIVE" ]; then
70+
DOCKER_RUN_ARGS+=" -p ${HTTP_PORT}:${HTTP_PORT}"
71+
DOCKER_RUN_ARGS+=" -e KEEP_ALIVE=true"
72+
fi
73+
if [ -n "$ARTIFACTS_DIR" ]; then
74+
DOCKER_RUN_ARGS+=" -v $(realpath "$ARTIFACTS_DIR"):/output"
75+
DOCKER_RUN_ARGS+=" -e ARTIFACTS_DIR=/output"
76+
fi
77+
78+
echo ""
79+
echo "Running E2E test..."
80+
$DOCKER_RUN_ARGS \
81+
-v "${IMG_FILE}:/input/image.img:ro" \
82+
"$DOCKER_IMAGE"

testing/scripts/boot-qemu.sh

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/bin/bash
2+
set -e
3+
4+
IMAGE_FILE="${1:?Usage: $0 <image.qcow2>}"
5+
KERNEL="${2:-/base/kernel.img}"
6+
SSH_PORT="${3:-2222}"
7+
LOG_FILE="${4:-/tmp/qemu-serial.log}"
8+
HTTP_PORT="${5:-8080}"
9+
10+
echo "=== Starting QEMU (aarch64, -M virt) ==="
11+
echo " Image: $IMAGE_FILE"
12+
echo " Kernel: $KERNEL"
13+
echo " SSH: port $SSH_PORT -> guest:22"
14+
echo " HTTP: port $HTTP_PORT -> guest:80"
15+
16+
qemu-system-aarch64 \
17+
-machine virt \
18+
-cpu cortex-a72 \
19+
-m 2G \
20+
-smp 4 \
21+
-kernel "$KERNEL" \
22+
-append "rw console=ttyAMA0 root=/dev/vda2 rootfstype=ext4 rootdelay=1 loglevel=2" \
23+
-drive "file=$IMAGE_FILE,format=qcow2,id=hd0,if=none,cache=writeback" \
24+
-device virtio-blk,drive=hd0,bootindex=0 \
25+
-netdev "user,id=mynet,hostfwd=tcp::${SSH_PORT}-:22,hostfwd=tcp::${HTTP_PORT}-:80" \
26+
-device virtio-net-pci,netdev=mynet \
27+
-nographic \
28+
-no-reboot \
29+
2>&1 | tee "$LOG_FILE"

0 commit comments

Comments
 (0)