chore(release): 1.0.0 — general availability #149
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - 'v*.*.*' | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: 'Version to release (e.g., v1.0.0)' | |
| required: true | |
| type: string | |
| env: | |
| CARGO_TERM_COLOR: always | |
| RUST_BACKTRACE: 1 | |
| # Optimize build performance | |
| RUSTFLAGS: "-C link-arg=-fuse-ld=lld" | |
| jobs: | |
| # Gate: Ensure CI passes before building release | |
| ci-gate: | |
| name: CI Gate | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| components: rustfmt, clippy | |
| - name: Check formatting | |
| run: cargo fmt -- --check | |
| - name: Run clippy | |
| run: cargo clippy --all-targets --all-features -- -D warnings | |
| - name: Install cargo-audit | |
| run: cargo install cargo-audit | |
| - name: Run security audit | |
| run: cargo audit | |
| - name: Check shell scripts | |
| run: | | |
| sudo apt-get update && sudo apt-get install -y shellcheck | |
| find scripts/install -name "*.sh" -type f -exec bash -n {} \; | |
| find scripts/install -name "*.sh" -type f -exec shellcheck -x {} \; | |
| build-binaries: | |
| name: Build Release Binaries | |
| needs: ci-gate | |
| runs-on: self-hosted | |
| # Alternatives: | |
| # runs-on: ubuntu-24.04 | |
| # runs-on: buildjet-4vcpu-ubuntu-2204 | |
| strategy: | |
| matrix: | |
| target: | |
| - x86_64-unknown-linux-gnu | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: x86_64-unknown-linux-gnu,x86_64-unknown-linux-musl | |
| - name: Install system dependencies | |
| run: | | |
| # Skip the package-manager dance on a runner that's already | |
| # provisioned (typical for self-hosted Arch / RHEL boxes where | |
| # apt-get isn't available). Falls through to apt on a fresh | |
| # Debian/Ubuntu runner. | |
| MISSING="" | |
| for t in cargo pkg-config clang ld.lld musl-gcc psql; do | |
| command -v "$t" >/dev/null || MISSING="$MISSING $t" | |
| done | |
| if [ -z "$MISSING" ]; then | |
| echo "All build tools present; skipping system package install." | |
| exit 0 | |
| fi | |
| echo "Missing tools:$MISSING" | |
| if command -v apt-get >/dev/null; then | |
| sudo apt-get update | |
| sudo apt-get install -y \ | |
| build-essential pkg-config libssl-dev \ | |
| musl-tools lld clang \ | |
| postgresql postgresql-contrib | |
| else | |
| echo "::error::Missing tools and no apt-get available — provision the runner manually." | |
| exit 1 | |
| fi | |
| - name: Setup PostgreSQL | |
| run: | | |
| # If a local Postgres is already reachable as nexus/nexus on the | |
| # default or dockerized port, use it. Avoids needing sudo to | |
| # bootstrap a fresh Postgres on a self-hosted runner. | |
| for port in 5432 5435; do | |
| if PGPASSWORD=nexus psql -h 127.0.0.1 -p "$port" -U nexus -d nexus -c '\q' 2>/dev/null; then | |
| echo "Local postgres reachable on :$port — skipping bootstrap." | |
| echo "DATABASE_URL=postgres://nexus:nexus@127.0.0.1:$port/nexus" >> "$GITHUB_ENV" | |
| exit 0 | |
| fi | |
| done | |
| # No reachable Postgres — fall back to the apt-style bootstrap. | |
| sudo systemctl start postgresql || true | |
| sudo -u postgres psql -tc "SELECT 1 FROM pg_database WHERE datname = 'nexus'" | grep -q 1 || \ | |
| sudo -u postgres psql -c "CREATE DATABASE nexus;" | |
| sudo -u postgres psql -tc "SELECT 1 FROM pg_user WHERE usename = 'nexus'" | grep -q 1 || \ | |
| sudo -u postgres psql -c "CREATE USER nexus WITH PASSWORD 'nexus';" | |
| sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE nexus TO nexus;" || true | |
| sudo -u postgres psql -d nexus -c "GRANT ALL ON SCHEMA public TO nexus;" || true | |
| # CREATEROLE needed for migration 23 (audit schema creates audit_reader role) | |
| sudo -u postgres psql -c "ALTER USER nexus CREATEROLE;" || true | |
| - name: Setup sccache (local) | |
| run: | | |
| # Install sccache to ~/.cargo/bin (already on PATH) — no sudo needed, | |
| # works on self-hosted runners without passwordless-sudo configured. | |
| if ! command -v sccache &> /dev/null; then | |
| mkdir -p "$HOME/.cargo/bin" | |
| curl -L https://github.com/mozilla/sccache/releases/download/v0.7.7/sccache-v0.7.7-x86_64-unknown-linux-musl.tar.gz \ | |
| | tar xz -C /tmp | |
| mv /tmp/sccache-*/sccache "$HOME/.cargo/bin/" | |
| chmod +x "$HOME/.cargo/bin/sccache" | |
| fi | |
| # Use local disk cache | |
| echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV | |
| echo "SCCACHE_DIR=$HOME/.cache/sccache" >> $GITHUB_ENV | |
| - name: Cache Cargo registry | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cargo/registry/index/ | |
| ~/.cargo/registry/cache/ | |
| ~/.cargo/git/db/ | |
| key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-cargo-registry- | |
| - name: Build manager (static musl for portability) | |
| run: cargo build --release --target x86_64-unknown-linux-musl -p manager | |
| env: | |
| DATABASE_URL: postgresql://nexus:nexus@localhost/nexus | |
| # Use vendored OpenSSL for musl builds | |
| OPENSSL_STATIC: 1 | |
| OPENSSL_LIB_DIR: /usr/lib/x86_64-linux-gnu | |
| OPENSSL_INCLUDE_DIR: /usr/include | |
| - name: Build agent (static musl for portability) | |
| run: cargo build --release --target x86_64-unknown-linux-musl -p agent | |
| env: | |
| OPENSSL_STATIC: 1 | |
| OPENSSL_LIB_DIR: /usr/lib/x86_64-linux-gnu | |
| OPENSSL_INCLUDE_DIR: /usr/include | |
| - name: Build guest-agent (static musl) | |
| run: cargo build --release --target x86_64-unknown-linux-musl -p guest-agent | |
| - name: Build installer (static musl) | |
| run: cargo build --release --target x86_64-unknown-linux-musl -p nqr-installer | |
| env: | |
| LICENSE_API_KEY: ${{ secrets.LICENSE_API_KEY }} | |
| LICENSE_PUBLIC_KEY: ${{ secrets.LICENSE_PUBLIC_KEY }} | |
| - name: Verify binaries | |
| run: | | |
| # Verify all binaries are statically linked | |
| for bin in manager agent guest-agent nqr-installer; do | |
| test -x ./target/x86_64-unknown-linux-musl/release/$bin | |
| file ./target/x86_64-unknown-linux-musl/release/$bin | |
| # Check it's statically linked (handles both "statically linked" and "static-pie linked") | |
| file ./target/x86_64-unknown-linux-musl/release/$bin | grep -E "static.*linked" || { | |
| echo "WARNING: $bin may not be statically linked" | |
| ldd ./target/x86_64-unknown-linux-musl/release/$bin || true | |
| } | |
| done | |
| - name: Rename binaries with target suffix | |
| run: | | |
| cp target/x86_64-unknown-linux-musl/release/manager nqrust-manager-x86_64-linux-musl | |
| cp target/x86_64-unknown-linux-musl/release/agent nqrust-agent-x86_64-linux-musl | |
| cp target/x86_64-unknown-linux-musl/release/guest-agent nqrust-guest-agent-x86_64-linux-musl | |
| cp target/x86_64-unknown-linux-musl/release/nqr-installer nqr-installer-x86_64-linux-musl | |
| - name: Upload binaries as artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: binaries-${{ matrix.target }} | |
| path: | | |
| nqrust-manager-x86_64-linux-musl | |
| nqrust-agent-x86_64-linux-musl | |
| nqrust-guest-agent-x86_64-linux-musl | |
| nqr-installer-x86_64-linux-musl | |
| retention-days: 1 | |
| - name: Show sccache statistics | |
| run: sccache --show-stats | |
| build-images: | |
| name: Build Base Images | |
| needs: [ci-gate, build-binaries] | |
| # Build images on every stable tag (vX.Y.Z) so the GitHub release | |
| # carries the kernel + rootfs assets the general installer downloads | |
| # via scripts/install/install.sh → nqr-installer → download_base_images. | |
| # Skip on prereleases (alpha/beta/rc) since those are short-lived and | |
| # operators are expected to install from a prior stable release. The | |
| # `BUILD_IMAGES=true` repo variable forces a build for those tags | |
| # when the kernel / rootfs / runtime sources actually changed. | |
| # | |
| # The earlier "skip by default" rule (commit history) assumed the | |
| # installer would fall back to a previous release's assets — it | |
| # never did, so v0.4.0 / v0.4.1 shipped without base images and the | |
| # general installer silently produced an empty /srv/images. | |
| if: ${{ vars.BUILD_IMAGES == 'true' || !contains(github.ref_name, '-') }} | |
| runs-on: self-hosted | |
| # This job builds VM images (kernels, rootfs, runtimes) | |
| # Requires: KVM access, ~10GB disk space, internet for downloads | |
| # Now depends on build-binaries so guest-agent binary is available | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Clean up and check disk space | |
| run: | | |
| # Clean up from any previous runs - INCLUDING alpine cache to ensure fresh builds | |
| sudo rm -rf /tmp/ubuntu-rootfs /tmp/node-rootfs /tmp/python-rootfs /tmp/alpine-*.tar.gz 2>/dev/null || true | |
| sudo rm -rf /mnt/alpine /mnt/busybox /mnt/ubuntu /mnt/node /mnt/python /mnt/container 2>/dev/null || true | |
| sudo rm -rf /tmp/alpine-cache 2>/dev/null || true | |
| # Clean up any old build directories from runtime scripts | |
| sudo rm -rf /tmp/build-node-runtime /tmp/build-python-runtime /tmp/build-bun-runtime 2>/dev/null || true | |
| sudo rm -rf "$(pwd)/build-node-runtime" "$(pwd)/build-python-runtime" "$(pwd)/build-bun-runtime" 2>/dev/null || true | |
| # IMPORTANT: Remove any cached runtime images from previous builds on self-hosted runner | |
| sudo rm -f /srv/images/node-runtime.ext4 /srv/images/python-runtime.ext4 /srv/images/bun-runtime.ext4 2>/dev/null || true | |
| # Show available disk space | |
| echo "=== Disk space ===" | |
| df -h /tmp | |
| df -h . | |
| - name: Install dependencies | |
| run: | | |
| # Same self-hosted-tolerant logic as the binaries job: skip | |
| # apt-get if the tools we actually need are already on PATH. | |
| MISSING="" | |
| for t in qemu-img debootstrap mkfs.ext4 curl xz cpio bc flex bison; do | |
| command -v "$t" >/dev/null || MISSING="$MISSING $t" | |
| done | |
| if [ -z "$MISSING" ]; then | |
| echo "All image-build tools present; skipping system package install." | |
| exit 0 | |
| fi | |
| echo "Missing tools:$MISSING" | |
| if command -v apt-get >/dev/null; then | |
| sudo apt-get update | |
| sudo apt-get install -y \ | |
| qemu-utils debootstrap e2fsprogs curl xz-utils cpio bc \ | |
| flex bison libelf-dev libssl-dev | |
| else | |
| echo "::error::Missing tools and no apt-get available — provision the runner manually." | |
| exit 1 | |
| fi | |
| - name: Create images directory | |
| run: | | |
| # Remove any existing images to force fresh builds | |
| rm -rf images/ | |
| mkdir -p images/ | |
| - name: Download guest-agent from build-binaries | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: binaries-x86_64-unknown-linux-gnu | |
| path: /tmp/binaries/ | |
| - name: Setup guest-agent for image builds | |
| run: | | |
| # Copy guest-agent to expected location for build scripts | |
| mkdir -p target/x86_64-unknown-linux-musl/release/ | |
| cp /tmp/binaries/nqrust-guest-agent-x86_64-linux-musl target/x86_64-unknown-linux-musl/release/guest-agent | |
| chmod +x target/x86_64-unknown-linux-musl/release/guest-agent | |
| echo "Guest-agent ready for image builds:" | |
| ls -la target/x86_64-unknown-linux-musl/release/guest-agent | |
| - name: Download Firecracker kernel | |
| run: | | |
| # Download pre-built Firecracker kernel 5.10 | |
| curl -fsSL -o images/vmlinux-5.10.fc.bin \ | |
| https://s3.amazonaws.com/spec.ccfc.min/img/quickstart_guide/x86_64/kernels/vmlinux.bin | |
| ls -lh images/vmlinux-5.10.fc.bin | |
| - name: Build Alpine rootfs | |
| run: | | |
| # Build Alpine image with REAL OpenRC init system using apk.static | |
| # This produces images identical to the working dev server images | |
| ALPINE_VERSION=3.18 | |
| # Download apk-tools-static to bootstrap Alpine | |
| curl -fsSL -o /tmp/apk-static.apk \ | |
| https://dl-cdn.alpinelinux.org/alpine/v${ALPINE_VERSION}/main/x86_64/apk-tools-static-2.14.4-r0.apk | |
| # Extract apk.static binary | |
| mkdir -p /tmp/apk-extract | |
| tar -xzf /tmp/apk-static.apk -C /tmp/apk-extract 2>/dev/null || true | |
| chmod +x /tmp/apk-extract/sbin/apk.static | |
| # Create ext4 image (100MB for Alpine with OpenRC) | |
| dd if=/dev/zero of=images/alpine-3.18-minimal.ext4 bs=1M count=100 | |
| mkfs.ext4 -F images/alpine-3.18-minimal.ext4 | |
| # Mount and bootstrap with apk.static | |
| sudo mkdir -p /mnt/alpine | |
| sudo mount -o loop images/alpine-3.18-minimal.ext4 /mnt/alpine | |
| # Bootstrap full Alpine with OpenRC | |
| sudo /tmp/apk-extract/sbin/apk.static \ | |
| -X https://dl-cdn.alpinelinux.org/alpine/v${ALPINE_VERSION}/main \ | |
| -U --allow-untrusted --root /mnt/alpine --initdb \ | |
| add alpine-base openrc | |
| # Set hostname | |
| echo 'alpine-vm' | sudo tee /mnt/alpine/etc/hostname > /dev/null | |
| # Create resolv.conf | |
| echo 'nameserver 8.8.8.8' | sudo tee /mnt/alpine/etc/resolv.conf > /dev/null | |
| # Set root password (password: root) | |
| sudo chroot /mnt/alpine /bin/sh -c 'export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; echo "root:root" | chpasswd' | |
| # Create inittab for OpenRC (matches working dev image exactly) | |
| printf '%s\n' \ | |
| '# /etc/inittab' \ | |
| '' \ | |
| '::sysinit:/sbin/openrc sysinit' \ | |
| '::sysinit:/sbin/openrc boot' \ | |
| '::wait:/sbin/openrc default' \ | |
| '' \ | |
| '# Serial console' \ | |
| 'ttyS0::respawn:/sbin/getty -L ttyS0 115200 vt100' \ | |
| '' \ | |
| '::ctrlaltdel:/sbin/reboot' \ | |
| '::shutdown:/sbin/openrc shutdown' \ | |
| | sudo tee /mnt/alpine/etc/inittab > /dev/null | |
| # Configure networking | |
| printf '%s\n' \ | |
| 'auto lo' \ | |
| 'iface lo inet loopback' \ | |
| '' \ | |
| 'auto eth0' \ | |
| 'iface eth0 inet dhcp' \ | |
| | sudo tee /mnt/alpine/etc/network/interfaces > /dev/null | |
| # Enable networking service in default runlevel | |
| sudo chroot /mnt/alpine /sbin/rc-update add networking default 2>/dev/null || \ | |
| sudo ln -sf /etc/init.d/networking /mnt/alpine/etc/runlevels/default/networking | |
| # Create udhcpc default script | |
| sudo mkdir -p /mnt/alpine/etc/udhcpc | |
| printf '%s\n' \ | |
| '#!/bin/sh' \ | |
| '[ -z "$1" ] && echo "Error: should be called from udhcpc" && exit 1' \ | |
| '' \ | |
| 'case "$1" in' \ | |
| ' deconfig)' \ | |
| ' ip addr flush dev $interface 2>/dev/null || ifconfig $interface 0.0.0.0' \ | |
| ' ;;' \ | |
| ' renew|bound)' \ | |
| ' CIDR=24' \ | |
| ' case "$subnet" in' \ | |
| ' 255.255.255.0) CIDR=24 ;;' \ | |
| ' 255.255.255.128) CIDR=25 ;;' \ | |
| ' 255.255.255.192) CIDR=26 ;;' \ | |
| ' 255.255.0.0) CIDR=16 ;;' \ | |
| ' 255.0.0.0) CIDR=8 ;;' \ | |
| ' esac' \ | |
| ' ip addr add $ip/$CIDR dev $interface 2>/dev/null || ifconfig $interface $ip netmask ${subnet:-255.255.255.0}' \ | |
| ' [ -n "$router" ] && (ip route add default via $router 2>/dev/null || route add default gw $router)' \ | |
| ' if [ -n "$dns" ]; then' \ | |
| ' echo -n > /etc/resolv.conf' \ | |
| ' for i in $dns; do echo "nameserver $i" >> /etc/resolv.conf; done' \ | |
| ' fi' \ | |
| ' ;;' \ | |
| 'esac' \ | |
| 'exit 0' \ | |
| | sudo tee /mnt/alpine/etc/udhcpc/default.script > /dev/null | |
| sudo chmod +x /mnt/alpine/etc/udhcpc/default.script | |
| # Set TERM for serial console | |
| sudo mkdir -p /mnt/alpine/etc/profile.d | |
| echo 'export TERM=vt100' | sudo tee /mnt/alpine/etc/profile.d/term.sh | |
| sudo chmod +x /mnt/alpine/etc/profile.d/term.sh | |
| sudo umount /mnt/alpine | |
| rm -rf /tmp/apk-extract /tmp/apk-static.apk | |
| ls -lh images/alpine-3.18-minimal.ext4 | |
| - name: Build BusyBox rootfs | |
| run: | | |
| # Build "BusyBox" image using Alpine Linux base with REAL OpenRC | |
| # The dev server's busybox-1.36.ext4 is actually Alpine with OpenRC | |
| ALPINE_VERSION=3.18 | |
| # Download apk-tools-static to bootstrap Alpine | |
| curl -fsSL -o /tmp/apk-static.apk \ | |
| https://dl-cdn.alpinelinux.org/alpine/v${ALPINE_VERSION}/main/x86_64/apk-tools-static-2.14.4-r0.apk | |
| # Extract apk.static binary | |
| mkdir -p /tmp/apk-extract | |
| tar -xzf /tmp/apk-static.apk -C /tmp/apk-extract 2>/dev/null || true | |
| chmod +x /tmp/apk-extract/sbin/apk.static | |
| # Create ext4 image (100MB for Alpine-based BusyBox with OpenRC) | |
| dd if=/dev/zero of=images/busybox-1.35.ext4 bs=1M count=100 | |
| mkfs.ext4 -F images/busybox-1.35.ext4 | |
| # Mount and bootstrap with apk.static | |
| sudo mkdir -p /mnt/busybox | |
| sudo mount -o loop images/busybox-1.35.ext4 /mnt/busybox | |
| # Bootstrap full Alpine with OpenRC | |
| sudo /tmp/apk-extract/sbin/apk.static \ | |
| -X https://dl-cdn.alpinelinux.org/alpine/v${ALPINE_VERSION}/main \ | |
| -U --allow-untrusted --root /mnt/busybox --initdb \ | |
| add alpine-base openrc | |
| # Set hostname | |
| echo 'busybox-vm' | sudo tee /mnt/busybox/etc/hostname > /dev/null | |
| # Create resolv.conf | |
| echo 'nameserver 8.8.8.8' | sudo tee /mnt/busybox/etc/resolv.conf > /dev/null | |
| # Set root password (password: root) | |
| sudo chroot /mnt/busybox /bin/sh -c 'export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; echo "root:root" | chpasswd' | |
| # Create inittab for OpenRC (matches working dev image exactly) | |
| printf '%s\n' \ | |
| '# /etc/inittab' \ | |
| '' \ | |
| '::sysinit:/sbin/openrc sysinit' \ | |
| '::sysinit:/sbin/openrc boot' \ | |
| '::wait:/sbin/openrc default' \ | |
| '' \ | |
| '# Serial console' \ | |
| 'ttyS0::respawn:/sbin/getty -L ttyS0 115200 vt100' \ | |
| '' \ | |
| '::ctrlaltdel:/sbin/reboot' \ | |
| '::shutdown:/sbin/openrc shutdown' \ | |
| | sudo tee /mnt/busybox/etc/inittab > /dev/null | |
| # Configure networking | |
| printf '%s\n' \ | |
| 'auto lo' \ | |
| 'iface lo inet loopback' \ | |
| '' \ | |
| 'auto eth0' \ | |
| 'iface eth0 inet dhcp' \ | |
| | sudo tee /mnt/busybox/etc/network/interfaces > /dev/null | |
| # Enable networking service in default runlevel | |
| sudo chroot /mnt/busybox /sbin/rc-update add networking default 2>/dev/null || \ | |
| sudo ln -sf /etc/init.d/networking /mnt/busybox/etc/runlevels/default/networking | |
| # Create udhcpc default script (matches working image) | |
| sudo mkdir -p /mnt/busybox/etc/udhcpc | |
| printf '%s\n' \ | |
| '#!/bin/sh' \ | |
| '[ -z "$1" ] && echo "Error: should be called from udhcpc" && exit 1' \ | |
| '' \ | |
| 'case "$1" in' \ | |
| ' deconfig)' \ | |
| ' ip addr flush dev $interface' \ | |
| ' ;;' \ | |
| ' renew|bound)' \ | |
| ' CIDR=24' \ | |
| ' case "$subnet" in' \ | |
| ' 255.255.255.0) CIDR=24 ;;' \ | |
| ' 255.255.255.128) CIDR=25 ;;' \ | |
| ' 255.255.255.192) CIDR=26 ;;' \ | |
| ' 255.255.0.0) CIDR=16 ;;' \ | |
| ' 255.0.0.0) CIDR=8 ;;' \ | |
| ' esac' \ | |
| ' ip addr add $ip/$CIDR dev $interface' \ | |
| ' [ -n "$router" ] && ip route add default via $router dev $interface 2>/dev/null || true' \ | |
| ' if [ -n "$dns" ]; then' \ | |
| ' echo -n > /etc/resolv.conf' \ | |
| ' for i in $dns; do echo "nameserver $i" >> /etc/resolv.conf; done' \ | |
| ' fi' \ | |
| ' ;;' \ | |
| 'esac' \ | |
| 'exit 0' \ | |
| | sudo tee /mnt/busybox/etc/udhcpc/default.script > /dev/null | |
| sudo chmod +x /mnt/busybox/etc/udhcpc/default.script | |
| # Set TERM for serial console | |
| sudo mkdir -p /mnt/busybox/etc/profile.d | |
| echo 'export TERM=vt100' | sudo tee /mnt/busybox/etc/profile.d/term.sh | |
| sudo chmod +x /mnt/busybox/etc/profile.d/term.sh | |
| sudo umount /mnt/busybox | |
| rm -rf /tmp/apk-extract /tmp/apk-static.apk | |
| ls -lh images/busybox-1.35.ext4 | |
| - name: Build Ubuntu minimal rootfs | |
| run: | | |
| # Download official Ubuntu 24.04 cloud image (minimal rootfs) | |
| # Using cloud image instead of debootstrap - faster and more reliable | |
| UBUNTU_URL="https://cloud-images.ubuntu.com/minimal/releases/noble/release/ubuntu-24.04-minimal-cloudimg-amd64-root.tar.xz" | |
| echo "Downloading Ubuntu 24.04 minimal cloud image..." | |
| curl -fsSL -o /tmp/ubuntu-rootfs.tar.xz "$UBUNTU_URL" | |
| # Create ext4 image (800MB for Ubuntu minimal) | |
| dd if=/dev/zero of=images/ubuntu-24.04-minimal.ext4 bs=1M count=800 | |
| mkfs.ext4 -F images/ubuntu-24.04-minimal.ext4 | |
| # Mount and extract | |
| sudo mkdir -p /mnt/ubuntu | |
| sudo mount -o loop images/ubuntu-24.04-minimal.ext4 /mnt/ubuntu | |
| sudo tar -xJf /tmp/ubuntu-rootfs.tar.xz -C /mnt/ubuntu | |
| # Set root password | |
| sudo chroot /mnt/ubuntu /bin/sh -c 'export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; echo "root:root" | chpasswd' | |
| # Enable serial console | |
| sudo chroot /mnt/ubuntu /usr/bin/systemctl enable serial-getty@ttyS0.service || true | |
| # Set default TERM for serial console | |
| sudo mkdir -p /mnt/ubuntu/etc/profile.d | |
| echo 'export TERM=vt100' | sudo tee /mnt/ubuntu/etc/profile.d/term.sh | |
| sudo chmod +x /mnt/ubuntu/etc/profile.d/term.sh | |
| # Configure networking | |
| sudo tee /mnt/ubuntu/etc/netplan/01-dhcp.yaml > /dev/null <<'NETPLAN' | |
| network: | |
| version: 2 | |
| ethernets: | |
| eth0: | |
| dhcp4: true | |
| NETPLAN | |
| sudo umount /mnt/ubuntu | |
| # Clean up | |
| rm -f /tmp/ubuntu-rootfs.tar.xz | |
| ls -lh images/ubuntu-24.04-minimal.ext4 | |
| - name: Build Node.js function runtime | |
| run: | | |
| cd $GITHUB_WORKSPACE | |
| # Script auto-converts relative OUTPUT_IMAGE to absolute path | |
| sudo OUTPUT_IMAGE="images/node-runtime.ext4" bash scripts/runtime-images/build-node-runtime.sh | |
| ls -lh images/node-runtime.ext4 | |
| - name: Build Python function runtime | |
| run: | | |
| cd $GITHUB_WORKSPACE | |
| # Script auto-converts relative OUTPUT_IMAGE to absolute path | |
| sudo OUTPUT_IMAGE="images/python-runtime.ext4" bash scripts/runtime-images/build-python-runtime.sh | |
| ls -lh images/python-runtime.ext4 | |
| - name: Build Bun function runtime | |
| run: | | |
| cd $GITHUB_WORKSPACE | |
| # Script auto-converts relative OUTPUT_IMAGE to absolute path | |
| sudo OUTPUT_IMAGE="images/bun-runtime.ext4" bash scripts/runtime-images/build-bun-runtime.sh | |
| ls -lh images/bun-runtime.ext4 | |
| - name: Build container runtime (Alpine + Docker) | |
| run: | | |
| # Build container runtime for Docker-in-VM feature | |
| # Uses the same approach as scripts/build-container-runtime-v2.sh | |
| ALPINE_VERSION=3.18 | |
| WORK_DIR="$(pwd)/build-container-runtime" | |
| # Clean up any leftover build directory from previous failed runs | |
| sudo rm -rf "$WORK_DIR" || true | |
| mkdir -p "$WORK_DIR" | |
| cd "$WORK_DIR" | |
| # Download Alpine minirootfs | |
| curl -fsSL -o alpine-minirootfs.tar.gz \ | |
| "https://dl-cdn.alpinelinux.org/alpine/v${ALPINE_VERSION}/releases/x86_64/alpine-minirootfs-${ALPINE_VERSION}.0-x86_64.tar.gz" | |
| # Extract Alpine | |
| mkdir -p rootfs | |
| tar xzf alpine-minirootfs.tar.gz -C rootfs | |
| # Copy DNS config for package installation | |
| sudo cp /etc/resolv.conf rootfs/etc/resolv.conf | |
| # Create setup script inside rootfs | |
| cat > rootfs/tmp/setup.sh << 'SETUP_SCRIPT' | |
| #!/bin/sh | |
| set -e | |
| # chroot inherits the caller's PATH, which on some host distros (Arch) | |
| # omits /sbin and /usr/sbin where apk and most rootfs tooling live. | |
| export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin | |
| # Update package index | |
| apk update | |
| # Install Docker and OpenRC | |
| apk add --no-cache \ | |
| docker \ | |
| docker-openrc \ | |
| openrc \ | |
| util-linux \ | |
| coreutils \ | |
| bash \ | |
| curl \ | |
| ca-certificates | |
| # Configure OpenRC | |
| rc-update add devfs boot | |
| rc-update add procfs boot | |
| rc-update add sysfs boot | |
| rc-update add cgroups boot | |
| rc-update add networking boot | |
| rc-update add docker default | |
| echo "Docker version: $(docker --version)" | |
| SETUP_SCRIPT | |
| chmod +x rootfs/tmp/setup.sh | |
| sudo chroot rootfs /tmp/setup.sh | |
| sudo rm rootfs/tmp/setup.sh | |
| # Configure Docker daemon for OpenRC | |
| # All files are root-owned after chroot, so use sudo for all writes | |
| sudo mkdir -p rootfs/etc/docker rootfs/etc/conf.d | |
| # OpenRC configuration for Docker - enable TCP listener | |
| sudo tee rootfs/etc/conf.d/docker > /dev/null << 'EOF' | |
| # Docker daemon options for OpenRC | |
| DOCKER_OPTS="-H unix:///var/run/docker.sock -H tcp://0.0.0.0:2375" | |
| EOF | |
| # Docker daemon JSON config (storage settings only, hosts in DOCKER_OPTS) | |
| sudo tee rootfs/etc/docker/daemon.json > /dev/null << 'EOF' | |
| { | |
| "storage-driver": "overlay2", | |
| "log-driver": "json-file", | |
| "log-opts": { | |
| "max-size": "10m", | |
| "max-file": "3" | |
| } | |
| } | |
| EOF | |
| # Configure networking (DHCP) | |
| sudo tee rootfs/etc/network/interfaces > /dev/null << 'EOF' | |
| auto lo | |
| iface lo inet loopback | |
| auto eth0 | |
| iface eth0 inet dhcp | |
| EOF | |
| # Set hostname | |
| echo "container-runtime" | sudo tee rootfs/etc/hostname > /dev/null | |
| # Enable root login without password (for debugging) | |
| sudo sed -i 's/root:!:/root::/' rootfs/etc/shadow | |
| # Create ext4 filesystem directly from rootfs directory (matching dev script) | |
| # This is more reliable than mount/copy/umount approach | |
| sudo mkfs.ext4 -L container-runtime -d rootfs -E lazy_itable_init=0,lazy_journal_init=0 container-runtime.ext4 2200M | |
| # Move to images directory | |
| mv container-runtime.ext4 ../images/ | |
| # Cleanup | |
| cd .. | |
| sudo rm -rf "$WORK_DIR" | |
| ls -lh images/container-runtime.ext4 | |
| - name: Compress large images | |
| run: | | |
| # Compress larger images (>100MB) to reduce upload size | |
| cd images | |
| for img in ubuntu-24.04-minimal.ext4 node-runtime.ext4 python-runtime.ext4 bun-runtime.ext4 container-runtime.ext4; do | |
| if [ -f "$img" ]; then | |
| echo "Compressing $img..." | |
| gzip -9 -k "$img" | |
| ls -lh "${img}.gz" | |
| fi | |
| done | |
| echo "All images:" | |
| ls -lh | |
| - name: Upload images as artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: base-images | |
| path: | | |
| images/vmlinux-5.10.fc.bin | |
| images/alpine-3.18-minimal.ext4 | |
| images/busybox-1.35.ext4 | |
| images/ubuntu-24.04-minimal.ext4.gz | |
| images/node-runtime.ext4.gz | |
| images/python-runtime.ext4.gz | |
| images/bun-runtime.ext4.gz | |
| images/container-runtime.ext4.gz | |
| retention-days: 1 | |
| build-ui: | |
| name: Build UI | |
| needs: ci-gate | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20' | |
| - name: Install pnpm | |
| run: npm install -g pnpm | |
| - name: Cache pnpm | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.pnpm-store | |
| apps/ui/node_modules | |
| key: ${{ runner.os }}-pnpm-${{ hashFiles('apps/ui/pnpm-lock.yaml') }} | |
| restore-keys: | | |
| ${{ runner.os }}-pnpm- | |
| - name: Install UI dependencies | |
| working-directory: apps/ui | |
| run: pnpm install --frozen-lockfile | |
| - name: Build UI | |
| working-directory: apps/ui | |
| run: | | |
| # Remove .env to prevent baking development API URLs into production build | |
| rm -f .env .env.local .env.production .env.development | |
| pnpm build | |
| - name: Package UI | |
| run: | | |
| cd apps/ui | |
| # Verify standalone build exists | |
| if [[ ! -f .next/standalone/apps/ui/server.js ]]; then | |
| echo "ERROR: Standalone build not found at .next/standalone/apps/ui/server.js" | |
| echo "Make sure next.config.mjs has output: 'standalone'" | |
| exit 1 | |
| fi | |
| # Create a temporary directory to assemble the tarball contents | |
| TEMP_DIR=$(mktemp -d) | |
| # Copy standalone build output (includes server.js, .next/, node_modules/, package.json) | |
| cp -r .next/standalone/apps/ui/. "$TEMP_DIR/" | |
| # Remove any .env files that might have been included in standalone | |
| rm -f "$TEMP_DIR/.env" "$TEMP_DIR/.env.local" "$TEMP_DIR/.env.production" | |
| # Copy static assets from build root to standalone's .next/static | |
| # The standalone .next/static might be incomplete, so merge from build root | |
| if [[ -d .next/static ]]; then | |
| mkdir -p "$TEMP_DIR/.next/static" | |
| cp -r .next/static/. "$TEMP_DIR/.next/static/" | |
| fi | |
| # Copy public directory (Next.js standalone doesn't include this by default) | |
| if [[ -d public ]]; then | |
| cp -r public "$TEMP_DIR/" | |
| fi | |
| # Create tarball from the assembled directory | |
| tar czf ../../nqrust-ui.tar.gz -C "$TEMP_DIR" . | |
| # Cleanup | |
| rm -rf "$TEMP_DIR" | |
| echo "UI tarball created successfully" | |
| tar tzf ../../nqrust-ui.tar.gz | head -20 | |
| - name: Upload UI as artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ui-build | |
| path: nqrust-ui.tar.gz | |
| retention-days: 1 | |
| create-release: | |
| name: Create GitHub Release | |
| needs: [build-binaries, build-ui, build-images] | |
| # build-images is skipped on prerelease tags (alpha/beta/rc) — `always()` | |
| # together with `result != 'failure'` lets us proceed when build-images | |
| # is `skipped` while still blocking on actual failures. | |
| if: ${{ always() && needs.build-binaries.result == 'success' && needs.build-ui.result == 'success' && needs.build-images.result != 'failure' }} | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts/ | |
| - name: Decompress large images (except container-runtime which exceeds 2GB limit) | |
| # build-images job is skipped for alpha/beta/rc tags, so the | |
| # base-images artifact won't exist for those releases. | |
| if: ${{ needs.build-images.result == 'success' }} | |
| run: | | |
| cd artifacts/base-images | |
| for gz in *.gz; do | |
| if [ -f "$gz" ]; then | |
| # Skip container-runtime - it's >2GB uncompressed, keep it gzipped | |
| if [ "$gz" = "container-runtime.ext4.gz" ]; then | |
| echo "Keeping $gz compressed (>2GB uncompressed)" | |
| continue | |
| fi | |
| gunzip -k "$gz" | |
| fi | |
| done | |
| ls -lh | |
| - name: Package installer scripts | |
| run: | | |
| tar czf nqrust-installer.tar.gz \ | |
| scripts/install/install.sh \ | |
| scripts/install/uninstall.sh \ | |
| scripts/install/README.md \ | |
| scripts/install/lib/ \ | |
| scripts/install/systemd/ \ | |
| scripts/install/sudoers.d/ | |
| - name: Organize release files | |
| run: | | |
| mkdir -p release/ | |
| find artifacts/ -type f -name "nqrust-*" -exec cp {} release/ \; | |
| find artifacts/ -type f -name "nqr-installer-*" -exec cp {} release/ \; | |
| cp nqrust-installer.tar.gz release/ | |
| # Base images are only present when build-images ran. Skip the | |
| # copy when it didn't (alpha/beta/rc tags). Operators installing | |
| # a prerelease pull images from the prior stable release URL. | |
| if [ -d artifacts/base-images ]; then | |
| cp artifacts/base-images/vmlinux-5.10.fc.bin release/ | |
| cp artifacts/base-images/alpine-3.18-minimal.ext4 release/ | |
| cp artifacts/base-images/busybox-1.35.ext4 release/ | |
| cp artifacts/base-images/ubuntu-24.04-minimal.ext4 release/ | |
| cp artifacts/base-images/node-runtime.ext4 release/ | |
| cp artifacts/base-images/python-runtime.ext4 release/ | |
| cp artifacts/base-images/bun-runtime.ext4 release/ | |
| # Container runtime stays compressed (>2GB uncompressed exceeds GitHub limit) | |
| cp artifacts/base-images/container-runtime.ext4.gz release/ | |
| else | |
| echo "::notice::No base-images artifact (prerelease build) — release will not include kernel/rootfs files." | |
| fi | |
| ls -lh release/ | |
| - name: Generate checksums | |
| working-directory: release | |
| run: | | |
| sha256sum nqrust-manager-* > checksums.txt | |
| sha256sum nqrust-agent-* >> checksums.txt | |
| sha256sum nqrust-guest-agent-* >> checksums.txt | |
| sha256sum nqr-installer-* >> checksums.txt | |
| sha256sum nqrust-ui.tar.gz >> checksums.txt | |
| sha256sum nqrust-installer.tar.gz >> checksums.txt | |
| # Image artifacts are absent on prerelease builds — every line below | |
| # is a no-op then. | |
| sha256sum vmlinux-*.bin >> checksums.txt 2>/dev/null || true | |
| sha256sum *.ext4 >> checksums.txt 2>/dev/null || true | |
| sha256sum *.ext4.gz >> checksums.txt 2>/dev/null || true | |
| cat checksums.txt | |
| - name: Get version | |
| id: version | |
| run: | | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| VERSION="${{ github.event.inputs.version }}" | |
| else | |
| VERSION="${GITHUB_REF#refs/tags/}" | |
| fi | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| # Pre-release only when the tag explicitly says so (-alpha, -beta, -rc). | |
| # We dropped the blanket "v0.x.x is always prerelease" rule because | |
| # 0.x is the project's normal range — it shouldn't permanently | |
| # disqualify everything from being marked Latest on GitHub. | |
| if [[ "$VERSION" =~ -(alpha|beta|rc) ]]; then | |
| echo "prerelease=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "prerelease=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Create release manifest | |
| working-directory: release | |
| run: | | |
| cat > release-manifest.json <<EOF | |
| { | |
| "version": "${{ steps.version.outputs.version }}", | |
| "released_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", | |
| "min_compatible_version": "v0.1.0", | |
| "changelog": "See CHANGELOG.md for details", | |
| "components": { | |
| "manager": { | |
| "version": "${{ steps.version.outputs.version }}", | |
| "url": "https://github.com/${{ github.repository }}/releases/download/${{ steps.version.outputs.version }}/nqrust-manager-x86_64-linux-musl", | |
| "checksum": "$(sha256sum nqrust-manager-x86_64-linux-musl | cut -d' ' -f1)", | |
| "size": $(stat -f%z nqrust-manager-x86_64-linux-musl 2>/dev/null || stat -c%s nqrust-manager-x86_64-linux-musl) | |
| }, | |
| "agent": { | |
| "version": "${{ steps.version.outputs.version }}", | |
| "url": "https://github.com/${{ github.repository }}/releases/download/${{ steps.version.outputs.version }}/nqrust-agent-x86_64-linux-musl", | |
| "checksum": "$(sha256sum nqrust-agent-x86_64-linux-musl | cut -d' ' -f1)", | |
| "size": $(stat -f%z nqrust-agent-x86_64-linux-musl 2>/dev/null || stat -c%s nqrust-agent-x86_64-linux-musl) | |
| }, | |
| "guest-agent": { | |
| "version": "${{ steps.version.outputs.version }}", | |
| "url": "https://github.com/${{ github.repository }}/releases/download/${{ steps.version.outputs.version }}/nqrust-guest-agent-x86_64-linux-musl", | |
| "checksum": "$(sha256sum nqrust-guest-agent-x86_64-linux-musl | cut -d' ' -f1)", | |
| "size": $(stat -f%z nqrust-guest-agent-x86_64-linux-musl 2>/dev/null || stat -c%s nqrust-guest-agent-x86_64-linux-musl) | |
| }, | |
| "ui": { | |
| "version": "${{ steps.version.outputs.version }}", | |
| "url": "https://github.com/${{ github.repository }}/releases/download/${{ steps.version.outputs.version }}/nqrust-ui.tar.gz", | |
| "checksum": "$(sha256sum nqrust-ui.tar.gz | cut -d' ' -f1)", | |
| "size": $(stat -f%z nqrust-ui.tar.gz 2>/dev/null || stat -c%s nqrust-ui.tar.gz) | |
| }, | |
| "installer": { | |
| "version": "${{ steps.version.outputs.version }}", | |
| "url": "https://github.com/${{ github.repository }}/releases/download/${{ steps.version.outputs.version }}/nqrust-installer.tar.gz", | |
| "checksum": "$(sha256sum nqrust-installer.tar.gz | cut -d' ' -f1)", | |
| "size": $(stat -f%z nqrust-installer.tar.gz 2>/dev/null || stat -c%s nqrust-installer.tar.gz) | |
| } | |
| }, | |
| "migrations": { | |
| "from": "v0.1.0", | |
| "requires_downtime": false, | |
| "breaking_changes": false | |
| } | |
| } | |
| EOF | |
| cat release-manifest.json | |
| jq '.' release-manifest.json # Validate JSON | |
| - name: Generate release notes | |
| id: release_notes | |
| run: | | |
| cat > release_notes.md <<'EOF' | |
| ## NQRust-MicroVM ${{ steps.version.outputs.version }} | |
| ### Installation | |
| **Quick Install (Ubuntu 22.04+):** | |
| ```bash | |
| curl -fsSL https://raw.githubusercontent.com/${{ github.repository }}/main/scripts/install/install.sh | bash | |
| ``` | |
| **Manual Install:** | |
| ```bash | |
| # Download installer | |
| curl -LO https://github.com/${{ github.repository }}/releases/download/${{ steps.version.outputs.version }}/nqr-installer-x86_64-linux-musl | |
| chmod +x nqr-installer-x86_64-linux-musl | |
| # Run installer (interactive TUI) | |
| sudo ./nqr-installer-x86_64-linux-musl install | |
| # Or with options | |
| sudo ./nqr-installer-x86_64-linux-musl install --mode production --yes | |
| ``` | |
| ### Pre-built Binaries | |
| Download and install manually: | |
| - **Installer (Rust TUI)**: [nqr-installer-x86_64-linux-musl](https://github.com/${{ github.repository }}/releases/download/${{ steps.version.outputs.version }}/nqr-installer-x86_64-linux-musl) (Recommended) | |
| - Manager: [nqrust-manager-x86_64-linux-musl](https://github.com/${{ github.repository }}/releases/download/${{ steps.version.outputs.version }}/nqrust-manager-x86_64-linux-musl) | |
| - Agent: [nqrust-agent-x86_64-linux-musl](https://github.com/${{ github.repository }}/releases/download/${{ steps.version.outputs.version }}/nqrust-agent-x86_64-linux-musl) | |
| - Guest Agent: [nqrust-guest-agent-x86_64-linux-musl](https://github.com/${{ github.repository }}/releases/download/${{ steps.version.outputs.version }}/nqrust-guest-agent-x86_64-linux-musl) | |
| - UI: [nqrust-ui.tar.gz](https://github.com/${{ github.repository }}/releases/download/${{ steps.version.outputs.version }}/nqrust-ui.tar.gz) | |
| - Legacy Installer Scripts: [nqrust-installer.tar.gz](https://github.com/${{ github.repository }}/releases/download/${{ steps.version.outputs.version }}/nqrust-installer.tar.gz) | |
| ### Base Images (for VMs and Functions) | |
| The installer downloads these automatically, or download manually: | |
| - **Kernel**: [vmlinux-5.10.fc.bin](https://github.com/${{ github.repository }}/releases/download/${{ steps.version.outputs.version }}/vmlinux-5.10.fc.bin) - Firecracker kernel 5.10 | |
| - **Alpine**: [alpine-3.18-minimal.ext4](https://github.com/${{ github.repository }}/releases/download/${{ steps.version.outputs.version }}/alpine-3.18-minimal.ext4) - Alpine Linux 3.18 minimal | |
| - **BusyBox**: [busybox-1.35.ext4](https://github.com/${{ github.repository }}/releases/download/${{ steps.version.outputs.version }}/busybox-1.35.ext4) - BusyBox 1.35 | |
| - **Ubuntu**: [ubuntu-24.04-minimal.ext4](https://github.com/${{ github.repository }}/releases/download/${{ steps.version.outputs.version }}/ubuntu-24.04-minimal.ext4) - Ubuntu 24.04 minimal | |
| - **Node.js Runtime**: [node-runtime.ext4](https://github.com/${{ github.repository }}/releases/download/${{ steps.version.outputs.version }}/node-runtime.ext4) - For serverless functions | |
| - **Python Runtime**: [python-runtime.ext4](https://github.com/${{ github.repository }}/releases/download/${{ steps.version.outputs.version }}/python-runtime.ext4) - For serverless functions | |
| - **Bun Runtime**: [bun-runtime.ext4](https://github.com/${{ github.repository }}/releases/download/${{ steps.version.outputs.version }}/bun-runtime.ext4) - For serverless functions (TypeScript/JS) | |
| - **Container Runtime**: [container-runtime.ext4.gz](https://github.com/${{ github.repository }}/releases/download/${{ steps.version.outputs.version }}/container-runtime.ext4.gz) - Docker-in-VM (~2.2GB, gzipped) | |
| ### Air-Gapped Installation | |
| For servers without internet access, download the self-extracting bundle (built automatically after release): | |
| - **Air-Gapped Bundle**: [nqr-microvm-airgap-${{ steps.version.outputs.version }}.run](https://github.com/${{ github.repository }}/releases/download/${{ steps.version.outputs.version }}/nqr-microvm-airgap-${{ steps.version.outputs.version }}.run) (~3-5GB) | |
| ```bash | |
| chmod +x nqr-microvm-airgap-${{ steps.version.outputs.version }}.run | |
| sudo ./nqr-microvm-airgap-${{ steps.version.outputs.version }}.run | |
| ``` | |
| The bundle includes all binaries, images, .deb packages, Node.js, and the web UI. | |
| ### Checksums | |
| ``` | |
| $(cat release/checksums.txt) | |
| ``` | |
| ### Requirements | |
| - Ubuntu 22.04+ or Debian 11+ or RHEL 8+ | |
| - x86_64 CPU with KVM support | |
| - 2GB+ RAM | |
| - 20GB+ disk space | |
| ### What's New | |
| - See [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md) for details | |
| ### Documentation | |
| - [Installation Guide](https://github.com/${{ github.repository }}/blob/main/README.md) | |
| - [Quick Start](https://github.com/${{ github.repository }}/blob/main/RUN.md) | |
| - [Features](https://github.com/${{ github.repository }}/blob/main/FEATURES.md) | |
| EOF | |
| echo "notes_file=release_notes.md" >> $GITHUB_OUTPUT | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v1 | |
| with: | |
| tag_name: ${{ steps.version.outputs.version }} | |
| name: Release ${{ steps.version.outputs.version }} | |
| body_path: ${{ steps.release_notes.outputs.notes_file }} | |
| draft: false | |
| prerelease: ${{ steps.version.outputs.prerelease }} | |
| files: | | |
| release/nqrust-manager-x86_64-linux-musl | |
| release/nqrust-agent-x86_64-linux-musl | |
| release/nqrust-guest-agent-x86_64-linux-musl | |
| release/nqr-installer-x86_64-linux-musl | |
| release/nqrust-ui.tar.gz | |
| release/nqrust-installer.tar.gz | |
| release/checksums.txt | |
| release/release-manifest.json | |
| release/vmlinux-5.10.fc.bin | |
| release/alpine-3.18-minimal.ext4 | |
| release/busybox-1.35.ext4 | |
| release/ubuntu-24.04-minimal.ext4 | |
| release/node-runtime.ext4 | |
| release/python-runtime.ext4 | |
| release/bun-runtime.ext4 | |
| release/container-runtime.ext4.gz | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Upload release manifest as latest | |
| uses: softprops/action-gh-release@v1 | |
| with: | |
| tag_name: latest | |
| name: Latest Release | |
| body: "Latest release manifest for auto-updater" | |
| draft: false | |
| prerelease: false | |
| files: release/release-manifest.json | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # TODO: Re-enable installer tests in v0.1.1 after fixing disk space and permission issues | |
| # test-installer: | |
| # name: Test Installer | |
| # needs: create-release | |
| # runs-on: ubuntu-24.04 | |
| # strategy: | |
| # matrix: | |
| # mode: [production, dev, manager, agent] | |
| # | |
| # steps: | |
| # - name: Checkout code | |
| # uses: actions/checkout@v4 | |
| # | |
| # - name: Test installer (${{ matrix.mode }} mode) | |
| # run: | | |
| # # Test in non-interactive mode (installer will use sudo internally) | |
| # # Use /tmp for logs to avoid permission issues in CI | |
| # LOG_DIR=/tmp/nqrust-install bash scripts/install/install.sh \ | |
| # --mode ${{ matrix.mode }} \ | |
| # --non-interactive \ | |
| # --network-mode nat | |
| # | |
| # - name: Verify services | |
| # if: matrix.mode != 'agent' | |
| # run: | | |
| # systemctl is-active nqrust-manager | |
| # curl -f http://localhost:18080/health | |
| # | |
| # - name: Verify agent | |
| # if: matrix.mode != 'manager' | |
| # run: | | |
| # systemctl is-active nqrust-agent | |
| # curl -f http://localhost:9090/health | |
| # | |
| # - name: Show logs on failure | |
| # if: failure() | |
| # run: | | |
| # journalctl -u nqrust-manager -n 100 --no-pager || true | |
| # journalctl -u nqrust-agent -n 100 --no-pager || true | |
| # | |
| # - name: Test uninstaller | |
| # run: | | |
| # bash scripts/install/uninstall.sh \ | |
| # --force \ | |
| # --remove-all |