From 2723453053baecf70b3b4b85a819dec477243c27 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 27 Nov 2025 13:17:15 +0800 Subject: [PATCH] Add GitHub Actions workflow for KVM-based testing of debi.sh with semantic matrix and caching (#3) * Initial plan * Add GitHub Actions workflow for KVM-based testing Co-authored-by: cha0sCat <47235156+cha0sCat@users.noreply.github.com> * Improve error handling and add security note to workflow Co-authored-by: cha0sCat <47235156+cha0sCat@users.noreply.github.com> * Add explicit permissions to workflow for security Co-authored-by: cha0sCat <47235156+cha0sCat@users.noreply.github.com> * Fix QEMU daemonize incompatibility with nographic option Co-authored-by: cha0sCat <47235156+cha0sCat@users.noreply.github.com> * Fix serial.log permissions and artifact naming conflicts Co-authored-by: cha0sCat <47235156+cha0sCat@users.noreply.github.com> * Fix script execution with pseudo-TTY for stty commands Co-authored-by: cha0sCat <47235156+cha0sCat@users.noreply.github.com> * Add full installation verification with reboot and new password check Co-authored-by: cha0sCat <47235156+cha0sCat@users.noreply.github.com> * fix: only add backports if has backports * Refactor workflow with semantic matrix, caching, and Debian 11 base image Co-authored-by: cha0sCat <47235156+cha0sCat@users.noreply.github.com> * Fix APT cache warning by using user-owned cache directory Co-authored-by: cha0sCat <47235156+cha0sCat@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: cha0sCat <47235156+cha0sCat@users.noreply.github.com> Co-authored-by: Anonymous <> --- .github/workflows/README.md | 218 +++++++++++++++++++ .github/workflows/test.yml | 415 ++++++++++++++++++++++++++++++++++++ 2 files changed, 633 insertions(+) create mode 100644 .github/workflows/README.md create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000..f9d1fa5 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,218 @@ +# GitHub Actions Workflow for Testing debi.sh + +## Overview + +This workflow tests the Debian Network Reinstall Script (`debi.sh`) using KVM virtualization in GitHub Actions. It validates that the script correctly prepares a system for Debian installation, performs the actual installation, and verifies the newly installed system. + +## Test Matrix + +The workflow uses a semantic matrix strategy to test ~15 different configurations across: + +- **Debian Versions**: 10 (Buster), 11 (Bullseye), 12 (Bookworm) +- **Mirrors**: Default, USTC (China), Cloudflare +- **Network Interface Naming**: Standard (`ethx`) or predictable names +- **User Account**: `root` or `debian` +- **Network Console**: Always enabled for remote installation monitoring + +**Key combinations tested:** +- Debian 10 with default mirror (baseline) +- Debian 11 with all mirrors and both naming schemes +- Debian 12 with all mirrors and various user configurations + +**Base Image**: All tests use Debian 11 (Bullseye) cloud image as the starting point, regardless of target version. + +## How It Works + +### 1. Environment Setup +- Enables KVM support on GitHub Actions runner +- Caches APT packages for faster subsequent runs +- Installs QEMU and related tools (qemu-kvm, cloud-utils, sshpass, etc.) + +### 2. Base Image Caching +- Downloads Debian 11 cloud image (only once, then cached) +- Reuses cached image across all matrix jobs +- Significantly speeds up workflow execution + +### 3. VM Preparation +- Creates a copy-on-write disk image from the cached base image +- Generates cloud-init configuration for initial VM setup +- Configures root SSH access with password `rootpass123` + +### 4. VM Execution +- Starts QEMU VM with KVM acceleration +- Waits for SSH to become available +- Uploads `debi.sh` script to the VM + +### 5. Script Testing +- **Builds arguments dynamically** from matrix parameters: + - `--version` from `matrix.version` + - `--ustc` or `--cloudflare` from `matrix.mirror` + - `--ethx` from `matrix.ethx` + - `--network-console` (always enabled) + - `--user` and `--password` from `matrix.user` +- Executes `debi.sh` with built arguments +- Captures output for debugging + +### 6. Installation Validation +The workflow validates that the script: +- Successfully downloads Debian installer components to `/boot/debian-*/` +- Creates the required `linux` and `initrd.gz` files +- Updates GRUB configuration with Debian Installer entry +- Sets GRUB default to boot into the installer + +### 7. Full Installation Test +After validation, the workflow: +- Reboots the VM to start the Debian installer +- Waits for the installation to complete (up to 30 minutes) +- Polls for SSH availability with the **new password** (`newpass123`) +- Verifies the newly installed system + +### 7. Success Criteria + +A test passes if: +- ✓ Script executes without critical errors +- ✓ Installer files exist in `/boot/debian-*/` +- ✓ GRUB configuration contains "Debian Installer" entry +- ✓ VM successfully reboots into installer +- ✓ Installation completes automatically +- ✓ New system boots and is accessible via SSH with new credentials + +## Running the Tests + +### Automatic Execution +Tests run automatically on: +- Push to `master` or `main` branch +- Pull requests targeting `master` or `main` + +### Manual Execution +You can manually trigger the workflow: +1. Go to Actions tab in GitHub +2. Select "Test Debian Installation Script" +3. Click "Run workflow" + +## Test Artifacts + +Each test run uploads: +- `debi-output.log` - Complete output from script execution +- `serial.log` - VM serial console log + +Access artifacts from the workflow run summary page. + +## Interpreting Results + +### ✅ Success +- All validation checks pass +- New system is accessible with new credentials +- Green checkmark in GitHub Actions UI + +### ❌ Failure +Common failure scenarios and debugging: + +1. **SSH connection timeout (old credentials)** + - Check serial console log + - Verify cloud-init configuration + +2. **Installer files not downloaded** + - Review debi-output.log + - Check network connectivity + - Verify mirror availability + +3. **GRUB update failed** + - Check for GRUB installation issues + - Verify disk partitioning + +4. **Installation timeout** + - Installation may take longer than expected + - Check serial console for installer progress + - Verify network connectivity to mirrors + +5. **Cannot connect with new credentials** + - Installation may have failed + - Check serial console for installation errors + - Verify preseed configuration + +## Password Strategy + +The workflow uses two different passwords to verify installation success: + +| Phase | User | Password | Purpose | +|-------|------|----------|---------| +| Initial VM (cloud-init) | root | `rootpass123` | Access the base system to run debi.sh | +| New System (installed) | root/debian | `newpass123` | Verify the new system was installed | + +If SSH connects with `newpass123`, it proves the new Debian system was installed successfully. + +## Limitations + +This workflow tests the complete installation process: +- ✓ Script argument parsing +- ✓ Installer file downloads +- ✓ GRUB configuration updates +- ✓ Reboot into installer +- ✓ Unattended installation +- ✓ New system verification + +Note: Full installation takes 10-30 minutes per test case. + +## Performance Optimization + +The workflow includes several optimizations: + +1. **APT Package Caching**: Dependencies are cached across workflow runs +2. **Base Image Caching**: Debian 11 cloud image is downloaded once and reused +3. **Single Base Image**: All tests use Debian 11 as starting point (smaller cache footprint) +4. **Matrix Exclusions**: Intelligent filtering reduces redundant test combinations + +## Adding New Test Cases + +The workflow uses a semantic matrix. To modify test coverage: + +**Add a new Debian version:** +```yaml +matrix: + version: [10, 11, 12, 13] # Add 13 +``` + +**Test with a new mirror:** +```yaml +matrix: + mirror: ['default', 'ustc', 'cloudflare', 'tuna'] # Add tuna +``` + +**Modify exclusions:** +```yaml +exclude: + # Add exclusions to prevent unwanted combinations + - version: 13 + mirror: 'ustc' # Skip USTC for Debian 13 +``` + +The workflow automatically builds command-line arguments from matrix parameters. + +## Troubleshooting + +### KVM not available +If KVM is not available, the workflow will fail. GitHub Actions runners support KVM, but some self-hosted runners may not. + +### Network timeouts +If downloads fail, consider: +- Adding retry logic +- Using alternative mirrors +- Checking GitHub Actions network restrictions + +### VM boot failures +Check the serial console log artifact for kernel messages and boot errors. + +### Installation hangs +If the installation takes too long: +- Check the serial console for progress +- Verify mirror connectivity +- Consider using a faster mirror (USTC, Cloudflare) + +### Cache issues +To clear caches and force fresh downloads: +1. Go to Actions tab → Caches +2. Delete relevant cache entries +3. Re-run the workflow + +Cache keys are based on workflow file hash, so modifying the workflow automatically invalidates caches. diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..a486854 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,415 @@ +name: Test Debian Installation Script + +on: + push: + branches: [ master, main ] + pull_request: + branches: [ master, main ] + workflow_dispatch: + +jobs: + test-debi: + name: "Debian ${{ matrix.version }} | ${{ matrix.mirror }} | ethx:${{ matrix.ethx }} | user:${{ matrix.user }} | nc:${{ matrix.network_console }}" + runs-on: ubuntu-22.04 + timeout-minutes: 45 + + permissions: + contents: read + + strategy: + fail-fast: false + matrix: + # Target Debian version to install + version: [10, 11, 12] + # Mirror configuration + mirror: ['default', 'ustc', 'cloudflare'] + # Network interface naming + ethx: [true, false] + # User to create + user: ['root', 'debian'] + # Enable network console for remote installation + network_console: [true] + + # Exclude combinations to keep matrix manageable (~12 tests) + exclude: + # Debian 10 - only test with default mirror and root user + - version: 10 + mirror: 'ustc' + - version: 10 + mirror: 'cloudflare' + - version: 10 + user: 'debian' + - version: 10 + ethx: false + + # For Debian 11 & 12, test key combinations + # Skip debian user with ustc (redundant) + - version: 11 + mirror: 'ustc' + user: 'debian' + - version: 12 + mirror: 'ustc' + user: 'debian' + + # Skip cloudflare without ethx (less common) + - mirror: 'cloudflare' + ethx: false + + # Skip default mirror without ethx for Debian 12 + - version: 12 + mirror: 'default' + ethx: false + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Cache APT packages + uses: actions/cache@v4 + id: cache-apt + with: + path: ~/apt-cache + key: ${{ runner.os }}-apt-${{ hashFiles('.github/workflows/test.yml') }} + restore-keys: | + ${{ runner.os }}-apt- + + - name: Restore APT cache + if: steps.cache-apt.outputs.cache-hit == 'true' + run: | + if [ -d ~/apt-cache ] && [ "$(ls -A ~/apt-cache 2>/dev/null)" ]; then + sudo cp ~/apt-cache/*.deb /var/cache/apt/archives/ 2>/dev/null || true + fi + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y qemu-kvm qemu-utils cloud-image-utils genisoimage expect sshpass + + - name: Save APT cache + if: steps.cache-apt.outputs.cache-hit != 'true' + run: | + mkdir -p ~/apt-cache + sudo cp /var/cache/apt/archives/*.deb ~/apt-cache/ 2>/dev/null || true + sudo chown -R $USER:$USER ~/apt-cache + + - name: Check KVM availability + run: | + ls -la /dev/kvm + kvm-ok || true + + - name: Cache Debian cloud image + id: cache-debian-image + uses: actions/cache@v4 + with: + path: /tmp/debian-11-base.qcow2 + key: debian-11-cloud-image-${{ hashFiles('.github/workflows/test.yml') }} + restore-keys: | + debian-11-cloud-image- + + - name: Download Debian 11 cloud image + if: steps.cache-debian-image.outputs.cache-hit != 'true' + run: | + wget -O /tmp/debian-11-base.qcow2 "https://cloud.debian.org/images/cloud/bullseye/latest/debian-11-generic-amd64.qcow2" + + - name: Create working disk image + run: | + qemu-img create -f qcow2 -F qcow2 -b /tmp/debian-11-base.qcow2 /tmp/test-disk.qcow2 20G + qemu-img resize /tmp/test-disk.qcow2 20G + + - name: Create cloud-init configuration + run: | + mkdir -p /tmp/cloud-init + + # meta-data + cat > /tmp/cloud-init/meta-data << 'EOF' + instance-id: test-debi-vm + local-hostname: test-debi + EOF + + # user-data with root access + cat > /tmp/cloud-init/user-data << 'EOF' + #cloud-config + users: + - name: root + lock_passwd: false + plain_text_passwd: 'rootpass123' + + ssh_pwauth: true + disable_root: false + + packages: + - wget + - curl + + runcmd: + - sed -i 's/^#PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config + - systemctl restart sshd + EOF + + # Create ISO for cloud-init + genisoimage -output /tmp/cloud-init.iso \ + -volid cidata -rational-rock -joliet \ + /tmp/cloud-init/user-data /tmp/cloud-init/meta-data + + - name: Start VM and wait for boot + run: | + # Start QEMU in background (daemonize mode doesn't support -nographic) + sudo qemu-system-x86_64 \ + -machine type=pc,accel=kvm \ + -cpu host \ + -m 2048 \ + -display none \ + -drive file=/tmp/test-disk.qcow2,format=qcow2,if=virtio \ + -cdrom /tmp/cloud-init.iso \ + -net nic,model=virtio \ + -net user,hostfwd=tcp::2222-:22 \ + -serial file:/tmp/serial.log \ + -monitor unix:/tmp/qemu-monitor.sock,server,nowait \ + -daemonize + + # Fix permissions on serial.log so it can be uploaded as artifact + sudo chmod 644 /tmp/serial.log + + echo "Waiting for VM to boot and SSH to become available..." + for i in {1..60}; do + if sshpass -p 'rootpass123' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 root@localhost 'echo SSH ready' 2>/dev/null; then + echo "SSH is ready!" + break + fi + echo "Attempt $i/60: Waiting for SSH..." + sleep 5 + done + + # Verify SSH is working + sshpass -p 'rootpass123' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 root@localhost 'uname -a' + + - name: Upload debi.sh to VM + run: | + sshpass -p 'rootpass123' scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -P 2222 \ + ./debi.sh root@localhost:/root/debi.sh + + - name: Make script executable + run: | + sshpass -p 'rootpass123' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 root@localhost \ + 'chmod +x /root/debi.sh' + + - name: Build debi.sh arguments from matrix parameters + id: build-args + run: | + # Start with version + ARGS="--version ${{ matrix.version }}" + + # Add mirror flag + if [ "${{ matrix.mirror }}" = "ustc" ]; then + ARGS="$ARGS --ustc" + elif [ "${{ matrix.mirror }}" = "cloudflare" ]; then + ARGS="$ARGS --cloudflare" + fi + # default mirror doesn't need a flag + + # Add ethx flag + if [ "${{ matrix.ethx }}" = "true" ]; then + ARGS="$ARGS --ethx" + fi + + # Add network-console flag + if [ "${{ matrix.network_console }}" = "true" ]; then + ARGS="$ARGS --network-console" + fi + + # Add user and password + ARGS="$ARGS --user ${{ matrix.user }} --password newpass123" + + echo "args=$ARGS" >> $GITHUB_OUTPUT + echo "Generated arguments: $ARGS" + + - name: Run debi.sh with test arguments + run: | + echo "Running debi.sh with arguments: ${{ steps.build-args.outputs.args }}" + + # Run the script with a pseudo-TTY to handle stty commands + # Use 'script' command to provide a PTY for non-interactive SSH + sshpass -p 'rootpass123' ssh -t -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 root@localhost \ + "script -q -c 'cd /root && ./debi.sh ${{ steps.build-args.outputs.args }}' /tmp/debi-output.log; echo \"Script exit code: \$?\" >> /tmp/debi-output.log" || echo "Script execution finished with non-zero exit code, will validate via file checks" + + - name: Download and check debi.sh output + run: | + sshpass -p 'rootpass123' scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -P 2222 \ + root@localhost:/tmp/debi-output.log /tmp/debi-output.log || echo "Could not download output log" + + if [ -f /tmp/debi-output.log ]; then + echo "=== debi.sh output ===" + cat /tmp/debi-output.log + echo "======================" + fi + + - name: Verify installation preparation + run: | + echo "Checking if Debian installer files were downloaded..." + + # Check if installer directory was created + sshpass -p 'rootpass123' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 root@localhost \ + 'ls -la /boot/debian-* || echo "No installer directory found"' + + # Check for installer components + sshpass -p 'rootpass123' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 root@localhost \ + 'ls -lh /boot/debian-*/linux /boot/debian-*/initrd.gz 2>/dev/null || echo "Installer files not found"' + + # Check GRUB configuration + sshpass -p 'rootpass123' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 root@localhost \ + 'cat /etc/default/grub.d/zz-debi.cfg 2>/dev/null || echo "GRUB config not found"' + + # Check if GRUB was updated with installer entry + sshpass -p 'rootpass123' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 root@localhost \ + 'grep -A 5 "Debian Installer" /boot/grub/grub.cfg || echo "Debian Installer entry not found in GRUB"' + + - name: Validate installation files exist + run: | + # Verify key files exist + sshpass -p 'rootpass123' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 root@localhost \ + 'test -f /boot/debian-*/linux && test -f /boot/debian-*/initrd.gz && echo "Installation files verified!" || exit 1' + + # Verify GRUB config was updated + sshpass -p 'rootpass123' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 root@localhost \ + 'grep -q "Debian Installer" /boot/grub/grub.cfg && echo "GRUB config verified!" || exit 1' + + echo "✓ Script execution completed successfully" + echo "✓ Installer files downloaded" + echo "✓ GRUB configuration updated" + echo "✓ System ready for reboot into Debian installer" + + - name: Reboot into Debian installer + run: | + echo "Rebooting VM to start Debian installation..." + + # Trigger reboot - the system should boot into Debian installer + sshpass -p 'rootpass123' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 root@localhost \ + 'reboot' 2>/dev/null || true + + echo "Reboot command sent. VM will now boot into Debian installer." + echo "The installation process will take several minutes..." + + - name: Wait for Debian installation to complete + run: | + echo "Waiting for Debian installation to complete..." + echo "This typically takes 10-20 minutes depending on network speed and mirror." + echo "The installer will automatically reboot after completion." + + # Wait for the old system to go down + sleep 30 + + # The installation process: + # 1. VM boots into Debian installer (netboot) + # 2. Installer runs unattended using preseed configuration + # 3. System installs packages from mirror + # 4. System reboots into newly installed Debian + + # We'll poll for SSH availability with the NEW password + # This indicates the new system has been installed and booted + + MAX_WAIT=1800 # 30 minutes maximum wait + POLL_INTERVAL=30 + ELAPSED=0 + + echo "Polling for new system availability (max wait: ${MAX_WAIT}s)..." + + while [ $ELAPSED -lt $MAX_WAIT ]; do + echo "Attempt at ${ELAPSED}s: Trying to connect with new credentials..." + + # Try to connect with the NEW password (set by debi.sh installation) + if sshpass -p 'newpass123' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 -p 2222 ${{ matrix.user }}@localhost 'echo "NEW SYSTEM CONNECTED!"' 2>/dev/null; then + echo "✓ Successfully connected to newly installed system!" + echo "✓ Installation completed successfully!" + exit 0 + fi + + # Also check if old system is still responding (installation not started yet) + if sshpass -p 'rootpass123' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 -p 2222 root@localhost 'echo "old system"' 2>/dev/null; then + echo " Old system still responding - installation may not have started" + fi + + sleep $POLL_INTERVAL + ELAPSED=$((ELAPSED + POLL_INTERVAL)) + done + + echo "✗ Timeout: Could not connect to new system after ${MAX_WAIT}s" + exit 1 + + - name: Verify new system installation + run: | + echo "Verifying the newly installed Debian system..." + + # Connect with new credentials and verify system + sshpass -p 'newpass123' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 ${{ matrix.user }}@localhost << 'VERIFY_EOF' + echo "=== System Information ===" + uname -a + + echo "" + echo "=== OS Release ===" + cat /etc/os-release + + echo "" + echo "=== Disk Usage ===" + df -h + + echo "" + echo "=== Memory Info ===" + free -h + + echo "" + echo "=== Network Configuration ===" + ip addr show + + echo "" + echo "=== Hostname ===" + hostname + + echo "" + echo "✓ New Debian system is running successfully!" + VERIFY_EOF + + - name: Collect debug information on failure + if: failure() + run: | + echo "=== Serial console log ===" + cat /tmp/serial.log || echo "No serial log available" + + echo "=== Trying to connect with old credentials ===" + sshpass -p 'rootpass123' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 -p 2222 root@localhost \ + 'journalctl -n 100' 2>/dev/null || echo "Could not connect with old credentials" + + echo "=== Trying to connect with new credentials ===" + sshpass -p 'newpass123' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 -p 2222 ${{ matrix.user }}@localhost \ + 'journalctl -n 100' 2>/dev/null || echo "Could not connect with new credentials" + + - name: Shutdown VM + if: always() + run: | + # Try graceful shutdown with new credentials first (if new system is installed) + sshpass -p 'newpass123' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 -p 2222 ${{ matrix.user }}@localhost \ + 'sudo poweroff' 2>/dev/null || true + + # Also try with old credentials (if still on old system) + sshpass -p 'rootpass123' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 -p 2222 root@localhost \ + 'poweroff' 2>/dev/null || true + + sleep 5 + + # Force kill QEMU if still running + sudo pkill qemu-system-x86_64 || true + + - name: Upload logs as artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-logs-v${{ matrix.version }}-${{ matrix.mirror }}-ethx${{ matrix.ethx }}-${{ matrix.user }}-nc${{ matrix.network_console }} + path: | + /tmp/debi-output.log + /tmp/serial.log + if-no-files-found: warn