From 2a87cfbd7466e9f47248ca28938197358b3a3428 Mon Sep 17 00:00:00 2001 From: Guy Sheffer Date: Wed, 22 Apr 2026 00:48:39 +0300 Subject: [PATCH 1/4] Fix E2E SSH: set pi user shell to /bin/bash in prepare-image Raspberry Pi OS Trixie ships the pi user with /usr/sbin/nologin as the default shell. Since we remove userconfig.service (which normally handles first-boot user setup), the shell stays nologin and SSH sessions fail with "This account is currently not available." Download /etc/passwd alongside /etc/shadow and replace nologin with /bin/bash for the pi user. --- src/distro_testing/scripts/prepare-image.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/distro_testing/scripts/prepare-image.sh b/src/distro_testing/scripts/prepare-image.sh index d78de60..0e715ce 100755 --- a/src/distro_testing/scripts/prepare-image.sh +++ b/src/distro_testing/scripts/prepare-image.sh @@ -71,17 +71,20 @@ chmod 0644 /etc/ssh/ssh_host_ecdsa_key.pub chmod 0644 /etc/ssh/ssh_host_ed25519_key.pub download /etc/shadow /tmp/shadow.bak +download /etc/passwd /tmp/passwd.bak umount / GFEOF -echo 'Setting pi user password...' +echo 'Setting pi user password and shell...' sed -i "s|^pi:[^:]*:|pi:${PIPASS}:|" /tmp/shadow.bak +sed -i 's|^pi:\(.*\):/usr/sbin/nologin$|pi:\1:/bin/bash|' /tmp/passwd.bak guestfish -a "$OUTPUT_IMAGE" < Date: Tue, 19 May 2026 15:12:58 +0300 Subject: [PATCH 2/4] ci: build docker image for bugfix/e2e branch --- .github/workflows/docker-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 91172d1..3000bf0 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -7,6 +7,7 @@ on: - "docker-github-actions" - "release/v1" - "beta" + - "bugfix/e2e" tags: - "*" jobs: From 3b7827d3685cf572f6a697166496e7f578323ade Mon Sep 17 00:00:00 2001 From: Guy Sheffer Date: Tue, 14 Apr 2026 21:24:16 +0300 Subject: [PATCH 3/4] Add reusable E2E workflow, SSH helpers, and CI trigger for feature/e2e - Add feature/e2e to docker-build.yml branch triggers so CI produces a custompios:feature-e2e container tag for development - Create shared ssh-helpers.sh with canonical ssh_cmd/scp_cmd functions to replace duplicated SSH boilerplate across distro test scripts - Create reusable e2e-test.yml workflow that distros can call with uses: guysoft/CustomPiOS/.github/workflows/e2e-test.yml@ref - Update test_boot.sh to source ssh-helpers.sh --- .github/workflows/e2e-test.yml | 83 +++++++++++++++++++++++ src/distro_testing/scripts/ssh-helpers.sh | 26 +++++++ src/distro_testing/tests/test_boot.sh | 11 ++- 3 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/e2e-test.yml create mode 100755 src/distro_testing/scripts/ssh-helpers.sh diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml new file mode 100644 index 0000000..30a7085 --- /dev/null +++ b/.github/workflows/e2e-test.yml @@ -0,0 +1,83 @@ +name: E2E Test (reusable) + +on: + workflow_call: + inputs: + image-artifact-name: + required: true + type: string + distro-name: + required: true + type: string + docker-context: + required: false + type: string + default: 'testing/' + timeout-minutes: + required: false + type: number + default: 45 + poll-interval: + required: false + type: number + default: 5 + max-poll-iterations: + required: false + type: number + default: 360 + +jobs: + e2e-test: + runs-on: ubuntu-latest + timeout-minutes: ${{ inputs.timeout-minutes }} + steps: + - uses: actions/checkout@v4 + + - name: Download image from build + uses: actions/download-artifact@v4 + with: + name: ${{ inputs.image-artifact-name }} + path: image/ + + - name: Build test Docker image + run: DOCKER_BUILDKIT=0 docker build -t e2e-test ${{ inputs.docker-context }} + + - name: Start E2E test container + run: | + mkdir -p artifacts + IMG=$(find image/ -name '*.img' | head -1) + docker run -d --name e2e-test \ + -v "$PWD/artifacts:/output" \ + -v "$(realpath $IMG):/input/image.img:ro" \ + -e ARTIFACTS_DIR=/output \ + -e DISTRO_NAME="${{ inputs.distro-name }}" \ + e2e-test + + - name: Wait for tests to complete + run: | + for i in $(seq 1 ${{ inputs.max-poll-iterations }}); do + [ -f artifacts/exit-code ] && break + sleep ${{ inputs.poll-interval }} + done + if [ ! -f artifacts/exit-code ]; then + echo "ERROR: Tests did not complete within timeout" + docker logs e2e-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 e2e-test > artifacts/container.log 2>&1 || true + docker stop e2e-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/src/distro_testing/scripts/ssh-helpers.sh b/src/distro_testing/scripts/ssh-helpers.sh new file mode 100755 index 0000000..125467a --- /dev/null +++ b/src/distro_testing/scripts/ssh-helpers.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# Shared SSH helpers for E2E test scripts. +# Source this file: source /test/scripts/ssh-helpers.sh +# +# Set E2E_SSH_HOST, E2E_SSH_PORT, E2E_SSH_USER, E2E_SSH_PASS before sourcing +# to override defaults. Test scripts typically do: +# export E2E_SSH_HOST="${1:-localhost}" +# export E2E_SSH_PORT="${2:-2222}" +# source /test/scripts/ssh-helpers.sh + +E2E_SSH_HOST="${E2E_SSH_HOST:-localhost}" +E2E_SSH_PORT="${E2E_SSH_PORT:-2222}" +E2E_SSH_USER="${E2E_SSH_USER:-pi}" +E2E_SSH_PASS="${E2E_SSH_PASS:-raspberry}" + +SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ + -o PreferredAuthentications=password -o PubkeyAuthentication=no \ + -o ConnectTimeout=10 -o LogLevel=ERROR" + +ssh_cmd() { + sshpass -p "$E2E_SSH_PASS" ssh $SSH_OPTS -p "$E2E_SSH_PORT" "${E2E_SSH_USER}@${E2E_SSH_HOST}" "$@" +} + +scp_cmd() { + sshpass -p "$E2E_SSH_PASS" scp $SSH_OPTS -P "$E2E_SSH_PORT" "$@" +} diff --git a/src/distro_testing/tests/test_boot.sh b/src/distro_testing/tests/test_boot.sh index ad4e624..e9e8be0 100755 --- a/src/distro_testing/tests/test_boot.sh +++ b/src/distro_testing/tests/test_boot.sh @@ -1,16 +1,15 @@ #!/bin/bash set -e -HOST="${1:-localhost}" -PORT="${2:-2222}" -USER="pi" -PASS="raspberry" +export E2E_SSH_HOST="${1:-localhost}" +export E2E_SSH_PORT="${2:-2222}" -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}" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "$(dirname "$SCRIPT_DIR")/scripts/ssh-helpers.sh" echo "Test: SSH login and run 'echo hello world'" -OUTPUT=$($SSH_CMD 'echo hello world' 2>/dev/null) +OUTPUT=$(ssh_cmd 'echo hello world' 2>/dev/null) if [ "$OUTPUT" = "hello world" ]; then echo " Output: '$OUTPUT'" From 4f055f3984adf8247e6f56f7d628fe3a8ea09bbe Mon Sep 17 00:00:00 2001 From: Guy Sheffer Date: Tue, 19 May 2026 16:59:08 +0300 Subject: [PATCH 4/4] Seed /etc/sudoers.d/010_pi-nopasswd in prepare-image userconfig.service is what normally creates this file on first boot; since we remove that service for headless QEMU testing, pi sudo requires a password and breaks post-boot hooks that try to install packages or start services over non-TTY SSH. Write the canonical 'pi ALL=(ALL) NOPASSWD: ALL' rule directly via guestfish so 'sudo apt-get install ...' and similar work from ssh_cmd "sudo ..." calls. --- src/distro_testing/scripts/prepare-image.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/distro_testing/scripts/prepare-image.sh b/src/distro_testing/scripts/prepare-image.sh index 0e715ce..7a7dcc6 100755 --- a/src/distro_testing/scripts/prepare-image.sh +++ b/src/distro_testing/scripts/prepare-image.sh @@ -23,6 +23,13 @@ write /etc/fstab "proc /proc proc defaults 0 0\n/dev/vda1 /boot/firmware vfat de -rm /etc/systemd/system/multi-user.target.wants/userconfig.service -rm /usr/lib/systemd/system/userconfig.service +# userconfig.service is what normally drops /etc/sudoers.d/010_pi-nopasswd. +# Since we remove it, seed the file ourselves so test scripts (post-boot hooks, +# apt-get installs, etc.) can use passwordless sudo over a non-TTY SSH session. +mkdir-p /etc/sudoers.d +write /etc/sudoers.d/010_pi-nopasswd "pi ALL=(ALL) NOPASSWD: ALL\n" +chmod 0440 /etc/sudoers.d/010_pi-nopasswd + mkdir-p /etc/ssh/sshd_config.d write /etc/ssh/sshd_config.d/99-qemu-test.conf "PasswordAuthentication yes\nPermitRootLogin yes\nKbdInteractiveAuthentication yes\n"