Skip to content
Merged
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
115 changes: 115 additions & 0 deletions .github/scripts/test-qemu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""Boot a Buildroot QEMU image and validate the ShellHub agent installation."""

import sys
import os
import pexpect

TIMEOUT_BOOT = 180
TIMEOUT_CMD = 30
LOG_FILE = "/tmp/qemu-test.log"


def main():
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} <images-dir>")
sys.exit(1)

images_dir = sys.argv[1]
bzimage = os.path.join(images_dir, "bzImage")
rootfs = os.path.join(images_dir, "rootfs.ext2")

for path in (bzimage, rootfs):
if not os.path.exists(path):
print(f"ERROR: {path} not found")
sys.exit(1)

qemu_cmd = (
f"qemu-system-x86_64"
f" -M pc"
f" -kernel {bzimage}"
f" -drive file={rootfs},if=virtio,format=raw"
f" -append \"rootwait root=/dev/vda console=ttyS0\""
f" -nographic"
f" -no-reboot"
)

print(f"Starting QEMU: {qemu_cmd}")
logfile = open(LOG_FILE, "wb")
child = pexpect.spawn(qemu_cmd, timeout=TIMEOUT_BOOT, logfile=logfile, encoding=None)

tests_passed = 0
tests_total = 3

try:
# Wait for login prompt
print("Waiting for login prompt...")
child.expect(b"login:", timeout=TIMEOUT_BOOT)
child.sendline(b"root")

# Wait for shell prompt
child.expect(b"#", timeout=TIMEOUT_CMD)
print("Logged in as root.")

# Test 1: Binary exists and is executable
print("Test 1: Checking binary exists...")
child.sendline(b"test -x /usr/bin/shellhub-agent && echo TEST1_PASS || echo TEST1_FAIL")
child.expect(b"TEST1_(PASS|FAIL)", timeout=TIMEOUT_CMD)
if child.match.group(1) == b"PASS":
print(" PASS: /usr/bin/shellhub-agent exists and is executable")
tests_passed += 1
else:
print(" FAIL: /usr/bin/shellhub-agent not found or not executable")

child.expect(b"#", timeout=TIMEOUT_CMD)

# Test 2: Binary runs (--version or --help)
print("Test 2: Checking binary runs...")
child.sendline(b"shellhub-agent --version && echo TEST2_PASS || echo TEST2_FAIL")
child.expect(b"TEST2_(PASS|FAIL)", timeout=TIMEOUT_CMD)
if child.match.group(1) == b"PASS":
print(" PASS: shellhub-agent --version works")
tests_passed += 1
else:
print(" FAIL: shellhub-agent --version failed")

child.expect(b"#", timeout=TIMEOUT_CMD)

# Test 3: Init script installed
print("Test 3: Checking init script...")
child.sendline(b"test -f /etc/init.d/S42shellhub && echo TEST3_PASS || echo TEST3_FAIL")
child.expect(b"TEST3_(PASS|FAIL)", timeout=TIMEOUT_CMD)
if child.match.group(1) == b"PASS":
print(" PASS: /etc/init.d/S42shellhub exists")
tests_passed += 1
else:
print(" FAIL: /etc/init.d/S42shellhub not found")

child.expect(b"#", timeout=TIMEOUT_CMD)

# Shutdown
print("Shutting down...")
child.sendline(b"poweroff")
child.expect(pexpect.EOF, timeout=60)

except pexpect.TIMEOUT:
print("ERROR: Timeout waiting for QEMU")
child.close(force=True)
logfile.close()
sys.exit(1)
except pexpect.EOF:
print("ERROR: QEMU exited unexpectedly")
logfile.close()
sys.exit(1)

logfile.close()

print(f"\nResults: {tests_passed}/{tests_total} tests passed")
if tests_passed < tests_total:
sys.exit(1)

print("All tests passed!")


if __name__ == "__main__":
main()
136 changes: 136 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
name: CI

on:
pull_request:
paths:
- 'package/shellhub/**'
- 'rootfs_overlay/**'
- 'Config.in'
- 'external.mk'
- '.github/**'

permissions:
contents: write
pull-requests: write

jobs:
update-hash:
runs-on: ubuntu-latest
if: github.actor == 'renovate[bot]'

steps:
- name: Checkout PR
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
token: ${{ secrets.GITHUB_TOKEN }}

- name: Update hash
run: |
VERSION=$(grep "^SHELLHUB_VERSION" package/shellhub/shellhub.mk | cut -d= -f2 | tr -d '[:space:]')
URL="https://github.com/shellhub-io/shellhub/releases/download/v${VERSION}/shellhub-agent.tar.gz"

echo "Downloading ${URL}..."
curl -L -o /tmp/shellhub-agent.tar.gz "${URL}"

NEW_MD5=$(md5sum /tmp/shellhub-agent.tar.gz | awk '{print $1}')
CURRENT_MD5=$(awk '{print $2}' package/shellhub/shellhub.hash 2>/dev/null || echo "")
rm /tmp/shellhub-agent.tar.gz

if [ "$NEW_MD5" = "$CURRENT_MD5" ]; then
echo "Hash already up to date, skipping commit."
exit 0
fi

echo "md5 ${NEW_MD5} shellhub-agent.tar.gz" > package/shellhub/shellhub.hash
echo "Hash file updated successfully!"

- name: Commit hash changes
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add package/shellhub/shellhub.hash
git diff --staged --quiet || git commit -m "Update hash for new version"
git push

build-and-test:
needs: [update-hash]
if: always() && (needs.update-hash.result == 'success' || needs.update-hash.result == 'skipped')
runs-on: ubuntu-latest
timeout-minutes: 120

steps:
- name: Checkout PR
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}

- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
build-essential libncurses-dev bc python3 rsync cpio unzip wget file \
qemu-system-x86 python3-pexpect

- name: Clone Buildroot
run: |
git clone --depth 1 --branch 2026.02 https://github.com/buildroot/buildroot.git /tmp/buildroot

- name: Cache Buildroot downloads
uses: actions/cache@v4
with:
path: /tmp/buildroot/dl
key: buildroot-dl-${{ hashFiles('package/shellhub/shellhub.mk') }}
restore-keys: buildroot-dl-

- name: Cache ccache
uses: actions/cache@v4
with:
path: ~/.buildroot-ccache
key: buildroot-ccache-${{ github.run_id }}
restore-keys: buildroot-ccache-

- name: Configure Buildroot
working-directory: /tmp/buildroot
run: |
make BR2_EXTERNAL=$GITHUB_WORKSPACE qemu_x86_64_defconfig
cat >> .config << 'EOF'
BR2_TOOLCHAIN_EXTERNAL=y
BR2_TOOLCHAIN_EXTERNAL_BOOTLIN=y
BR2_TOOLCHAIN_EXTERNAL_BOOTLIN_X86_64_CORE_I7_GLIBC_STABLE=y
BR2_PACKAGE_SHELLHUB=y
BR2_CCACHE=y
BR2_CCACHE_DIR="/home/runner/.buildroot-ccache"
EOF
make olddefconfig

- name: Build
working-directory: /tmp/buildroot
run: make -j$(nproc)

- name: Test with QEMU
run: python3 .github/scripts/test-qemu.py /tmp/buildroot/output/images

- name: Upload logs on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: qemu-test-logs
path: /tmp/qemu-test.log

auto-merge:
needs: [build-and-test]
runs-on: ubuntu-latest
if: github.actor == 'renovate[bot]'

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Auto-approve
uses: hmarr/auto-approve-action@v4

- name: Auto-merge
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh pr merge ${{ github.event.pull_request.number }} --squash --auto
49 changes: 0 additions & 49 deletions .github/workflows/update-hash.yml

This file was deleted.