local emulator image optimization #68
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 & Publish QEMU Emulator Images | |
| on: | |
| push: | |
| branches: | |
| - main | |
| - dev | |
| pull_request: | |
| paths: | |
| - 'docker/local-emulator/**' | |
| - '.github/workflows/qemu-emulator-build.yaml' | |
| workflow_dispatch: | |
| inputs: | |
| publish: | |
| description: 'Publish images to GitHub Releases' | |
| type: boolean | |
| default: false | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/dev' }} | |
| env: | |
| EMULATOR_IMAGE_NAME: stack-local-emulator | |
| jobs: | |
| build: | |
| name: Build QEMU Image (${{ matrix.arch }}) | |
| runs-on: ubicloud-standard-8 | |
| timeout-minutes: 120 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - arch: amd64 | |
| - arch: arm64 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Set up QEMU user-mode emulation | |
| uses: docker/setup-qemu-action@v3 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Install QEMU dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y qemu-system-x86 qemu-system-arm qemu-kvm qemu-utils genisoimage socat qemu-efi-aarch64 | |
| - name: Enable KVM access | |
| 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 || true | |
| ls -la /dev/kvm || echo "no /dev/kvm present" | |
| if [ -w /dev/kvm ]; then | |
| echo "KVM is writable — hardware acceleration will be used" | |
| else | |
| echo "WARNING: /dev/kvm is not writable — will fall back to TCG (very slow)" | |
| fi | |
| - name: Build QEMU image | |
| run: | | |
| chmod +x docker/local-emulator/qemu/build-image.sh | |
| EMULATOR_PROVISION_TIMEOUT=6000 \ | |
| docker/local-emulator/qemu/build-image.sh ${{ matrix.arch }} | |
| - name: Generate emulator env | |
| run: node docker/local-emulator/generate-env-development.mjs | |
| - name: Start emulator and verify | |
| run: | | |
| chmod +x docker/local-emulator/qemu/run-emulator.sh | |
| EMULATOR_ARCH=${{ matrix.arch }} \ | |
| EMULATOR_READY_TIMEOUT=3200 \ | |
| docker/local-emulator/qemu/run-emulator.sh start | |
| - name: Verify services are healthy | |
| run: | | |
| EMULATOR_ARCH=${{ matrix.arch }} \ | |
| docker/local-emulator/qemu/run-emulator.sh status | |
| - name: Stop emulator | |
| if: always() | |
| run: | | |
| EMULATOR_ARCH=${{ matrix.arch }} \ | |
| docker/local-emulator/qemu/run-emulator.sh stop | |
| - name: Package image | |
| run: | | |
| BASE_IMG="docker/local-emulator/qemu/images/stack-emulator-${{ matrix.arch }}.qcow2" | |
| cp "$BASE_IMG" "stack-emulator-${{ matrix.arch }}.qcow2" | |
| - name: Upload image artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: qemu-emulator-${{ matrix.arch }} | |
| path: stack-emulator-${{ matrix.arch }}.qcow2 | |
| retention-days: 30 | |
| compression-level: 0 | |
| test: | |
| name: Smoke Test (${{ matrix.arch }}) | |
| needs: build | |
| runs-on: ubicloud-standard-8 | |
| timeout-minutes: 60 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - arch: amd64 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install QEMU dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y qemu-system-x86 qemu-utils genisoimage socat | |
| - name: Download built image | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: qemu-emulator-${{ matrix.arch }} | |
| path: docker/local-emulator/qemu/images/ | |
| - name: Generate emulator env | |
| run: node docker/local-emulator/generate-env-development.mjs | |
| - name: Start emulator from artifact | |
| run: | | |
| chmod +x docker/local-emulator/qemu/run-emulator.sh docker/local-emulator/qemu/common.sh | |
| EMULATOR_ARCH=${{ matrix.arch }} \ | |
| EMULATOR_READY_TIMEOUT=600 \ | |
| docker/local-emulator/qemu/run-emulator.sh start | |
| - name: Verify services are healthy | |
| run: | | |
| EMULATOR_ARCH=${{ matrix.arch }} \ | |
| docker/local-emulator/qemu/run-emulator.sh status | |
| - name: Smoke test — backend health | |
| run: curl -sf http://localhost:26701/health?db=1 | |
| - name: Smoke test — dashboard reachable | |
| run: curl -sf -o /dev/null -w "HTTP %{http_code}\n" http://localhost:26700/handler/sign-in | |
| - name: Smoke test — MinIO health | |
| run: curl -sf http://localhost:26702/minio/health/live | |
| - name: Smoke test — Inbucket reachable | |
| run: curl -sf -o /dev/null -w "HTTP %{http_code}\n" http://localhost:26703/ | |
| - name: Stop emulator | |
| if: always() | |
| run: | | |
| EMULATOR_ARCH=${{ matrix.arch }} \ | |
| docker/local-emulator/qemu/run-emulator.sh stop | |
| - name: Print serial log on failure | |
| if: failure() | |
| run: tail -100 docker/local-emulator/qemu/run/vm/serial.log 2>/dev/null || true | |
| publish: | |
| name: Publish to GitHub Releases | |
| needs: [build, test] | |
| if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' || (github.event_name == 'workflow_dispatch' && inputs.publish) | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts | |
| - name: Prepare release assets | |
| run: | | |
| mkdir -p release | |
| SHORT_SHA="${GITHUB_SHA:0:8}" | |
| BRANCH="${GITHUB_REF_NAME}" | |
| DATE="$(date -u +%Y%m%d)" | |
| TAG="emulator-${BRANCH}-${DATE}-${SHORT_SHA}" | |
| echo "RELEASE_TAG=${TAG}" >> "$GITHUB_ENV" | |
| echo "SHORT_SHA=${SHORT_SHA}" >> "$GITHUB_ENV" | |
| for f in artifacts/qemu-emulator-*/*.qcow2; do | |
| cp "$f" release/ | |
| done | |
| cat > release-notes.md <<EOF | |
| ## QEMU Emulator Images | |
| Built from \`${BRANCH}\` @ \`${GITHUB_SHA}\` | |
| ### Images | |
| | File | Description | | |
| |------|-------------| | |
| | \`stack-emulator-arm64.qcow2\` | ARM64 emulator image | | |
| | \`stack-emulator-amd64.qcow2\` | AMD64 emulator image | | |
| ### Usage | |
| \`\`\`bash | |
| stack emulator pull | |
| stack emulator start | |
| \`\`\` | |
| EOF | |
| ls -lh release/ | |
| - name: Create or update GitHub Release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| TITLE="QEMU Emulator — ${{ github.ref_name }} ($SHORT_SHA)" | |
| if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then | |
| gh release edit "$RELEASE_TAG" \ | |
| --title "$TITLE" \ | |
| --notes-file release-notes.md \ | |
| --prerelease | |
| gh release upload "$RELEASE_TAG" release/* --clobber | |
| else | |
| gh release create "$RELEASE_TAG" \ | |
| --title "$TITLE" \ | |
| --notes-file release-notes.md \ | |
| --prerelease \ | |
| release/* | |
| fi | |
| - name: Update latest tag for branch | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| LATEST_TAG="emulator-${{ github.ref_name }}-latest" | |
| TITLE="QEMU Emulator — ${{ github.ref_name }} (latest)" | |
| NOTES="Latest emulator images from \`${{ github.ref_name }}\`. Auto-updated on each build." | |
| if gh release view "$LATEST_TAG" >/dev/null 2>&1; then | |
| gh release edit "$LATEST_TAG" \ | |
| --draft \ | |
| --prerelease \ | |
| --target "${{ github.sha }}" \ | |
| --title "$TITLE" \ | |
| --notes "$NOTES" | |
| else | |
| gh release create "$LATEST_TAG" \ | |
| --draft \ | |
| --prerelease \ | |
| --target "${{ github.sha }}" \ | |
| --title "$TITLE" \ | |
| --notes "$NOTES" \ | |
| || gh release edit "$LATEST_TAG" \ | |
| --draft \ | |
| --prerelease \ | |
| --target "${{ github.sha }}" \ | |
| --title "$TITLE" \ | |
| --notes "$NOTES" | |
| fi | |
| gh release upload "$LATEST_TAG" release/* --clobber | |
| gh release edit "$LATEST_TAG" --draft=false --prerelease |