From 64ff85b41f03a80e216843fc637c673f337e56db Mon Sep 17 00:00:00 2001 From: Max042004 Date: Tue, 9 Jun 2026 11:31:10 +0800 Subject: [PATCH] ci: add self-hosted macOS HVF runtime job Hypervisor.framework needs real Apple Silicon hardware, so the hosted build-macos job can only compile elfuse and check the entitlement, never boot a guest. Add a runtime-macos job on a self-hosted Apple Silicon runner ([self-hosted, macOS, arm64]) that actually exercises the VM under HVF. The job is scoped to the upstream repository with github.repository == 'sysprog21/elfuse', so it runs for pushes to main and for pull_requests targeting main -- including PRs opened from a collaborator's fork -- but never inside a fork's own Actions context, so a branch pushed to a fork without an upstream PR triggers nothing. Untrusted outside fork PRs are held by the repository's "require approval for outside collaborators" setting, so a maintainer still gates every outside run without needing a head-ref guard or a label. The job runs after build-macos, serializes per ref through a concurrency group, and cancels in-progress runs only for pull requests. It asserts the host is arm64, reports kern.hv_support, caches and installs just the missing Homebrew packages (binutils, qemu), and fails early with install guidance when Rosetta for Linux is absent. After make elfuse it confirms the com.apple.security.hypervisor entitlement is codesigned into build/elfuse, then runs test-hello, test-multi-vcpu, make check, and tests/test-matrix.sh all. --- .github/workflows/main.yml | 148 +++++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 54f286f..0a14c71 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -259,3 +259,151 @@ jobs: path: build/scan-build retention-days: 7 if-no-files-found: ignore + runtime-macos: + name: Runtime (macOS Apple Silicon, HVF) + needs: build-macos + if: > + github.repository == 'sysprog21/elfuse' && + (github.event_name == 'push' || github.event_name == 'pull_request') + runs-on: [self-hosted, macOS, arm64] + timeout-minutes: 20 + + # contents: read for the checkout; pull-requests: read so the guard can + # query the PR's current HEAD. (actions: write would let the guard + # cancel the run instead of failing it, but repo policy caps the token + # at actions: read, so the guard fails fast with a clear reason instead.) + permissions: + contents: read + pull-requests: read + + concurrency: + group: runtime-macos-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + + env: + LINUX_TOOLCHAIN: /opt/toolchain/aarch64-linux-gnu + GNU_OBJCOPY: /opt/homebrew/opt/binutils/bin/objcopy + HOMEBREW_NO_INSTALL_CLEANUP: 1 + HOMEBREW_NO_AUTO_UPDATE: 1 + BREW_PKGS: binutils qemu + + steps: + # Fail fast if this run targets a commit that is no longer the PR's + # HEAD. cancel-in-progress covers "commit 2 pushed while commit 1 is + # still running", but NOT a manual "Re-run jobs" on an old run: a + # re-run replays the original event payload (a frozen head.sha) + # against this single self-hosted runner, which would otherwise burn + # the full job timeout re-testing stale code. Compare the frozen + # head.sha against the live PR HEAD; when they differ, exit 1 with a + # clear "commit is no longer the latest" message. We fail (rather than + # cancel) because repo policy caps the token at actions: read, so the + # cancel API is unavailable. exit 1 also stops the job, so the later + # steps are skipped automatically -- no per-step guard needed. The + # lookup fails open: if HEAD can't be determined the job runs. + - name: Fail fast if superseded by a newer PR commit + if: github.event_name == 'pull_request' + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + RUN_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -uo pipefail + # curl and system python3 are always present on macOS; jq/gh are + # not guaranteed on a self-hosted runner, so don't depend on them. + latest=$(curl -fsSL \ + -H "Authorization: Bearer $GH_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$REPO/pulls/$PR_NUMBER" \ + | python3 -c 'import json,sys; print(json.load(sys.stdin)["head"]["sha"])') \ + || latest="" + echo "Run targets : $RUN_SHA" + echo "PR HEAD now : ${latest:-}" + if [ -n "$latest" ] && [ "$latest" != "$RUN_SHA" ]; then + echo "::error::This run targets $RUN_SHA, but PR #$PR_NUMBER HEAD is now $latest -- the commit is no longer the latest. Failing instead of re-testing stale code on the self-hosted runner; re-run CI on the current commit." + exit 1 + fi + + - name: Checkout + uses: actions/checkout@v6 + + - name: Host info + run: | + sw_vers + uname -a + uname -m + sysctl kern.hv_support || true + test "$(uname -m)" = "arm64" + + - name: Cache Homebrew downloads + uses: actions/cache@v5 + with: + path: ~/Library/Caches/Homebrew/downloads + key: brew-runtime-${{ runner.os }}-${{ runner.arch }}-${{ env.BREW_PKGS }} + + - name: Install missing Homebrew packages + run: | + missing=() + + for pkg in $BREW_PKGS; do + if ! brew list --formula "$pkg" >/dev/null 2>&1; then + missing+=("$pkg") + fi + done + + if [ "${#missing[@]}" -gt 0 ]; then + brew install --quiet "${missing[@]}" + else + echo "All Homebrew packages are already installed: $BREW_PKGS" + fi + + - name: Tool versions + run: | + command -v make + command -v "$GNU_OBJCOPY" + make -V .MAKE.VERSION 2>/dev/null || true + "$GNU_OBJCOPY" --version | head -1 + qemu-aarch64 --version | head -1 || true + python3 --version + + - name: Check Rosetta for Linux + run: | + ROSETTA=/Library/Apple/usr/libexec/oah/RosettaLinux/rosetta + + if [ ! -x "$ROSETTA" ]; then + echo "::error::Rosetta for Linux runtime was not found at $ROSETTA" + echo + echo "Install Rosetta on the self-hosted Mac runner first:" + echo " sudo softwareupdate --install-rosetta --agree-to-license" + echo + echo "Current /Library/Apple/usr/libexec/oah contents:" + ls -R /Library/Apple/usr/libexec/oah || true + exit 1 + fi + + ls -l "$ROSETTA" + + - name: Build elfuse + run: | + make elfuse + + - name: Verify HVF entitlement is embedded + run: | + codesign -d --entitlements - build/elfuse 2>&1 \ + | grep -q 'com\.apple\.security\.hypervisor' + + - name: test-hello + run: | + make test-hello + + - name: test-multi-vcpu + run: | + make test-multi-vcpu + + - name: make check + run: | + make check + + - name: Test matrix + run: | + bash tests/test-matrix.sh all