Build and Publish Dakota Live ISO #12
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: Build and Publish Dakota Live ISO | |
| on: | |
| push: | |
| branches: [main] | |
| paths: | |
| - 'dakota/**' | |
| - 'justfile' | |
| - '.github/workflows/build-iso.yml' | |
| schedule: | |
| # Rebuild daily at 03:00 UTC to pick up Dakota image updates | |
| - cron: '0 3 * * *' | |
| workflow_dispatch: | |
| inputs: | |
| skip_upload: | |
| description: 'Build ISO but skip uploading to R2 (dry run)' | |
| required: false | |
| default: false | |
| type: boolean | |
| jobs: | |
| build-and-publish: | |
| name: Build Dakota Live ISO | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| contents: read | |
| packages: read | |
| pull-requests: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Free disk space | |
| uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be | |
| with: | |
| tool-cache: true | |
| - name: Mount BTRFS for podman storage | |
| # Use 95% of /mnt for the loopback to maximise space for flatpak runtime | |
| # downloads, container image layers, OCI export, and squashfs assembly. | |
| # The podman build --mount=type=cache uses this filesystem, so any | |
| # GH Actions cache pointed at /var/lib/containers would just waste space. | |
| env: | |
| BTRFS_LOOPBACK_FREE: "0.95" | |
| run: bash .github/scripts/mount_btrfs.sh | |
| - name: Install dependencies | |
| run: | | |
| sudo apt-get update -qq | |
| sudo apt-get install -y just podman rclone mtools xorriso | |
| - name: Log in to GHCR | |
| run: | | |
| echo "${{ secrets.GITHUB_TOKEN }}" | \ | |
| podman login ghcr.io -u ${{ github.actor }} --password-stdin | |
| - name: Build Live ISO | |
| run: | | |
| sudo just installer_channel=dev output_dir=output iso-sd-boot dakota | |
| - name: Fix output directory ownership | |
| # sudo just creates output/ as root; subsequent steps run as the runner | |
| # user and cannot write into it without this ownership transfer. | |
| run: sudo chown -R "$USER:$(id -gn)" output/ | |
| - name: Compute ISO name | |
| id: iso | |
| run: | | |
| DATE=$(date -u +%Y%m%d) | |
| SHA=$(echo "${{ github.sha }}" | cut -c1-7) | |
| echo "dated=dakota-live-${DATE}-${SHA}.iso" >> "$GITHUB_OUTPUT" | |
| echo "path=output/dakota-live.iso" >> "$GITHUB_OUTPUT" | |
| - name: Generate SHA256 checksum | |
| id: checksum | |
| run: | | |
| ISO="${{ steps.iso.outputs.path }}" | |
| DATED="${{ steps.iso.outputs.dated }}" | |
| CHECKSUM_PATH="output/${DATED}-CHECKSUM" | |
| LATEST_CHECKSUM_PATH="output/dakota-live-latest.iso-CHECKSUM" | |
| # Full sha256sum format (hash + filename) so users can run: sha256sum -c | |
| # Two manifests: one with the dated filename, one with the "latest" alias. | |
| # They share the same hash but different recorded filenames so both work. | |
| sha256sum "$ISO" | sed "s|output/dakota-live.iso|${DATED}|" | tee "$CHECKSUM_PATH" | |
| sha256sum "$ISO" | sed "s|output/dakota-live.iso|dakota-live-latest.iso|" > "$LATEST_CHECKSUM_PATH" | |
| echo "path=${CHECKSUM_PATH}" >> "$GITHUB_OUTPUT" | |
| echo "name=${DATED}-CHECKSUM" >> "$GITHUB_OUTPUT" | |
| echo "latest_path=${LATEST_CHECKSUM_PATH}" >> "$GITHUB_OUTPUT" | |
| - name: Upload ISO to Cloudflare R2 | |
| if: ${{ inputs.skip_upload != true && github.event_name != 'pull_request' }} | |
| env: | |
| RCLONE_CONFIG_R2_TYPE: s3 | |
| RCLONE_CONFIG_R2_PROVIDER: Cloudflare | |
| RCLONE_CONFIG_R2_REGION: auto | |
| RCLONE_CONFIG_R2_ACCESS_KEY_ID: ${{ secrets.RCLONE_CONFIG_R2_ACCESS_KEY_ID }} | |
| RCLONE_CONFIG_R2_SECRET_ACCESS_KEY: ${{ secrets.RCLONE_CONFIG_R2_SECRET_ACCESS_KEY }} | |
| RCLONE_CONFIG_R2_ENDPOINT: ${{ secrets.RCLONE_CONFIG_R2_ENDPOINT }} | |
| run: | | |
| ISO="${{ steps.iso.outputs.path }}" | |
| DATED="${{ steps.iso.outputs.dated }}" | |
| BUCKET="${{ secrets.R2_BUCKET }}" | |
| echo "==> Uploading as $DATED ..." | |
| rclone copyto --log-level INFO --checksum --s3-no-check-bucket \ | |
| "$ISO" "R2:testing/${DATED}" | |
| echo "==> Updating dakota-live-latest.iso ..." | |
| rclone copyto --log-level INFO --s3-no-check-bucket \ | |
| "$ISO" "R2:testing/dakota-live-latest.iso" | |
| echo "==> Uploading checksum as ${{ steps.checksum.outputs.name }} ..." | |
| rclone copyto --log-level INFO --s3-no-check-bucket \ | |
| "${{ steps.checksum.outputs.path }}" \ | |
| "R2:testing/${{ steps.checksum.outputs.name }}" | |
| echo "==> Updating dakota-live-latest.iso-CHECKSUM ..." | |
| rclone copyto --log-level INFO --s3-no-check-bucket \ | |
| "${{ steps.checksum.outputs.latest_path }}" \ | |
| "R2:testing/dakota-live-latest.iso-CHECKSUM" | |
| echo "==> Done. Public URLs:" | |
| echo " https://projectbluefin.dev/dakota-live-latest.iso" | |
| echo " https://projectbluefin.dev/dakota-live-latest.iso-CHECKSUM" | |
| - name: Boot verification (UEFI + serial) | |
| timeout-minutes: 10 | |
| run: | | |
| sudo apt-get install -y qemu-system-x86 ovmf socat imagemagick -qq | |
| ISO="${{ steps.iso.outputs.path }}" | |
| echo "==> Booting $ISO ..." | |
| OVMF_CODE="" | |
| for f in /usr/share/OVMF/OVMF_CODE_4M.fd \ | |
| /usr/share/OVMF/OVMF_CODE.fd \ | |
| /usr/share/edk2/ovmf/OVMF_CODE.fd; do | |
| [ -f "$f" ] && OVMF_CODE="$f" && break | |
| done | |
| OVMF_VARS="" | |
| for f in /usr/share/OVMF/OVMF_VARS_4M.fd \ | |
| /usr/share/OVMF/OVMF_VARS.fd \ | |
| /usr/share/edk2/ovmf/OVMF_VARS.fd; do | |
| if [ -f "$f" ]; then | |
| cp "$f" /tmp/OVMF_VARS.fd | |
| OVMF_VARS="/tmp/OVMF_VARS.fd" | |
| break | |
| fi | |
| done | |
| PFLASH=() | |
| if [ -n "$OVMF_CODE" ] && [ -n "$OVMF_VARS" ]; then | |
| echo "==> UEFI firmware: $OVMF_CODE" | |
| PFLASH+=(-drive "if=pflash,format=raw,readonly=on,file=$OVMF_CODE") | |
| PFLASH+=(-drive "if=pflash,format=raw,file=$OVMF_VARS") | |
| fi | |
| sudo qemu-system-x86_64 \ | |
| -machine type=q35,accel=kvm \ | |
| -cpu host -m 4G -smp 2 \ | |
| -cdrom "$ISO" -boot d \ | |
| "${PFLASH[@]}" \ | |
| -monitor unix:/tmp/qemu-monitor.sock,server,nowait \ | |
| -serial file:/tmp/serial.log \ | |
| -display none \ | |
| -daemonize | |
| # Wait for monitor socket | |
| for i in $(seq 1 30); do | |
| [ -S /tmp/qemu-monitor.sock ] && break | |
| sleep 2 | |
| done | |
| echo "==> Waiting up to 5 minutes for live environment to be ready..." | |
| for i in $(seq 1 60); do | |
| if sudo grep -q "DAKOTA_LIVE_READY" /tmp/serial.log 2>/dev/null; then | |
| echo "==> Live environment ready after $((i * 5))s" | |
| break | |
| fi | |
| [ "$i" -eq 60 ] && echo "WARNING: DAKOTA_LIVE_READY not seen in serial log after 5m" || true | |
| sleep 5 | |
| done | |
| # Capture screenshot | |
| echo "screendump /tmp/screenshot.ppm" | \ | |
| sudo socat - UNIX-CONNECT:/tmp/qemu-monitor.sock || true | |
| sleep 2 | |
| if sudo test -f /tmp/screenshot.ppm; then | |
| sudo convert /tmp/screenshot.ppm screenshot.png 2>/dev/null || \ | |
| sudo cp /tmp/screenshot.ppm screenshot.png | |
| echo "==> Screenshot saved" | |
| fi | |
| echo "quit" | sudo socat - UNIX-CONNECT:/tmp/qemu-monitor.sock || true | |
| # Fail the job if the live environment never reached the ready state | |
| sudo grep -q "DAKOTA_LIVE_READY" /tmp/serial.log || \ | |
| { echo "ERROR: Live environment did not reach ready state"; sudo tail -50 /tmp/serial.log; exit 1; } | |
| - name: Upload ISO as artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ steps.iso.outputs.dated }} | |
| path: output/dakota-live.iso | |
| if-no-files-found: error | |
| retention-days: 7 | |
| - name: Upload SHA256 checksum as artifact | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: checksum-${{ steps.iso.outputs.dated }} | |
| path: ${{ steps.checksum.outputs.path }} | |
| if-no-files-found: warn | |
| retention-days: 7 | |
| - name: Upload boot screenshot as artifact | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: boot-screenshot | |
| path: screenshot.png | |
| if-no-files-found: warn |