Skip to content
Draft
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
17 changes: 17 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
version: 2
updates:
- package-ecosystem: gomod
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 10

- package-ecosystem: docker
directory: /
schedule:
interval: weekly

- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
285 changes: 285 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
name: CI

on:
push:
branches: [master]
pull_request:
branches: [master]

concurrency:
group: ci-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true

permissions:
contents: read

jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version: "1.26"

- name: golangci-lint
uses: golangci/golangci-lint-action@v7
with:
version: v2.10.1

test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version: "1.26"

- name: Run tests
run: go test -v -race -coverprofile=coverage.out -count=1 ./...

- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage.out

e2e:
name: E2E Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version: "1.26"

- name: Run E2E tests
run: go test -tags e2e -v -race -count=1 ./test/e2e/...

build:
name: Build
runs-on: ubuntu-latest
needs: [lint, test, e2e]
strategy:
matrix:
goarch: [amd64, arm64]
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version: "1.26"

- name: Install cross-compilation tools
if: matrix.goarch == 'arm64'
run: sudo apt-get update && sudo apt-get install -y gcc-aarch64-linux-gnu

- name: Build
env:
GOOS: linux
GOARCH: ${{ matrix.goarch }}
CGO_ENABLED: 1
CC: ${{ matrix.goarch == 'arm64' && 'aarch64-linux-gnu-gcc' || 'gcc' }}
run: go build -ldflags "-s -w -linkmode external -extldflags '-static'" -o booty-${{ matrix.goarch }}

- name: Upload binary
uses: actions/upload-artifact@v4
with:
name: booty-${{ matrix.goarch }}
path: booty-${{ matrix.goarch }}

kvm-boot:
name: KVM Boot Validation
runs-on: ubuntu-latest
needs: [build]
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version: "1.26"

- name: Install QEMU and dependencies
run: |
sudo apt-get update
sudo apt-get install -y qemu-system-x86 cpio busybox-static zstd

- name: Build initramfs image
run: |
# Build BOOTy as a static binary for linux/amd64
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 \
go build -ldflags "-linkmode external -extldflags '-static' -s -w" -o booty

# Create initramfs directory structure
mkdir -p initramfs/{bin,sbin,dev,proc,sys,etc,tmp,usr/bin,lib/modules,mnt,home}

# Create device nodes so the kernel can give init a working console
sudo mknod initramfs/dev/console c 5 1
sudo mknod initramfs/dev/ttyS0 c 4 64
sudo mknod initramfs/dev/null c 1 3

# Install busybox with shell and utility symlinks
cp /bin/busybox initramfs/bin/busybox
cd initramfs/bin
for cmd in sh mount umount insmod setsid cttyhack; do
ln -sf busybox "$cmd"
done
cd ../..
cd initramfs/usr/bin && ln -sf ../../bin/busybox setsid && cd ../../..

# Copy BOOTy binary
cp booty initramfs/booty

# Copy e1000 kernel module (QEMU default virtual NIC)
MOD_PATH=$(find /lib/modules/$(uname -r) -name "e1000.ko*" | head -1)
if [ -n "$MOD_PATH" ]; then
case "$MOD_PATH" in
*.zst) zstd -d "$MOD_PATH" -o initramfs/lib/modules/e1000.ko ;;
*.xz) xz -dc "$MOD_PATH" > initramfs/lib/modules/e1000.ko ;;
*.gz) gzip -dc "$MOD_PATH" > initramfs/lib/modules/e1000.ko ;;
*) cp "$MOD_PATH" initramfs/lib/modules/e1000.ko ;;
esac
echo "Copied e1000 module from $MOD_PATH"
else
echo "WARNING: e1000 module not found"
fi

# Create init wrapper: loads NIC driver and sets BOOTYURL before starting BOOTy
cat > initramfs/init << 'INITEOF'
#!/bin/sh
/bin/insmod /lib/modules/e1000.ko 2>/dev/null || true
export BOOTYURL="http://10.0.2.2:3000/booty"
exec /booty
INITEOF
chmod +x initramfs/init

# Package initramfs
cd initramfs
sudo find . -print0 | sudo cpio --null -ov --format=newc 2>/dev/null | gzip > ../test-initramfs.cpio.gz
cd ..

- name: Prepare kernel
run: |
sudo cp /boot/vmlinuz-$(uname -r) vmlinuz
sudo chmod 644 vmlinuz

- name: Start test provisioning server
run: |
mkdir -p images
dd if=/dev/urandom bs=1024 count=1 of=images/test.img 2>/dev/null

# Config for QEMU default MAC 52:54:00:12:34:56 (dash-format: 52-54-00-12-34-56)
cat > test-config.json << 'EOF'
{
"action": "writeImage",
"sourceImage": "http://10.0.2.2:3000/images/test.img",
"destinationDevice": "/dev/sda",
"compressed": false,
"dryRun": false,
"dropToShell": false,
"wipeDevice": false,
"growPartition": 1,
"lvmRootName": "/dev/ubuntu-vg/root"
}
EOF

python3 << 'PYEOF' &
import http.server

class Handler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
if self.path == '/booty/52-54-00-12-34-56.bty':
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
with open('test-config.json', 'rb') as f:
self.wfile.write(f.read())
elif self.path.startswith('/images/'):
super().do_GET()
else:
self.send_response(404)
self.end_headers()
def log_message(self, format, *args):
print(f"[server] {format % args}")

server = http.server.HTTPServer(('0.0.0.0', 3000), Handler)
print("[server] Listening on :3000")
server.serve_forever()
PYEOF
echo $! > server.pid
sleep 2
curl -sf http://localhost:3000/booty/52-54-00-12-34-56.bty | python3 -m json.tool
echo "Provisioning server ready"

- name: Boot with QEMU and validate
timeout-minutes: 5
run: |
timeout 120 qemu-system-x86_64 \
-kernel vmlinuz \
-initrd test-initramfs.cpio.gz \
-append "console=ttyS0 panic=1" \
-m 512 \
-nographic \
-no-reboot \
-net nic,macaddr=52:54:00:12:34:56,model=e1000 \
-net user \
-serial file:serial.log \
2>&1 || true

echo ""
echo "======================================="
echo " Serial Console Output "
echo "======================================="
cat serial.log || true
echo ""
echo "======================================="

PASS=true

if grep -q "Starting DHCP client" serial.log 2>/dev/null; then
echo "PASS: BOOTy init started — mount and device setup successful"
else
echo "FAIL: BOOTy init did not reach DHCP client"
PASS=false
fi

if grep -q "Starting BOOTy" serial.log 2>/dev/null; then
echo "PASS: BOOTy main flow reached"
else
echo "FAIL: BOOTy did not reach main flow"
PASS=false
fi

if grep -q "Connecting to provisioning server" serial.log 2>/dev/null; then
echo "PASS: Network operational — provisioning server contacted"
else
echo "FAIL: Provisioning server not contacted"
PASS=false
fi

if [ "$PASS" = "false" ]; then
echo ""
echo "E2E BOOT VALIDATION FAILED"
exit 1
fi

echo ""
echo "ALL E2E CHECKS PASSED"

- name: Upload serial log
if: always()
uses: actions/upload-artifact@v4
with:
name: kvm-boot-serial-log
path: serial.log
if-no-files-found: ignore

- name: Cleanup
if: always()
run: |
[ -f server.pid ] && kill $(cat server.pid) 2>/dev/null || true
Loading