Skip to content
Closed
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
145 changes: 65 additions & 80 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -337,9 +337,13 @@ jobs:
runtime-macos:
name: Runtime (${{ matrix.name }})
needs: build-macos
# Never run guest code or touch the persistent runner for pull requests;
# only trusted main-branch pushes and an explicitly selected main dispatch
# may enter the self-hosted HVF lane.
if: >
github.repository == 'sysprog21/elfuse' &&
(github.event_name == 'push' || github.event_name == 'pull_request')
((github.event_name == 'push' && github.ref == 'refs/heads/main') ||
(github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main'))
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
runs-on: [self-hosted, macOS, arm64]
# Sanitizer builds run several times slower than the release build, so the
# job budget and the per-test TEST_TIMEOUT are widened per leg. Without
Expand Down Expand Up @@ -397,17 +401,13 @@ jobs:
check_target: check-sanitizer
brew_pkgs: binutils

# 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

# Serialize each trusted sanitizer lane without cancelling main runs.
concurrency:
group: runtime-macos-${{ matrix.sanitizer }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
cancel-in-progress: false

env:
LINUX_TOOLCHAIN: /opt/toolchain/aarch64-linux-gnu
Expand All @@ -428,69 +428,25 @@ jobs:
MAKEFLAGS: -j8

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:-<unknown>}"
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@v7

- name: Restore cached test fixtures
# Only the release leg needs fixtures: the sanitizer legs run the
# fixture-free check-sanitizer subset.
if: ${{ matrix.run_matrix }}
# actions/checkout's default clean:true runs `git clean -ffdx`, which
# wipes externals/test-fixtures (gitignored) on this self-hosted
# runner even though its disk otherwise persists across runs.
# fetch-fixtures.sh is already idempotent -- it skips re-downloading
# Alpine packages when externals/test-fixtures/versions.lock still
# matches -- so stash that tree outside the workspace and restore it
# here as a real directory. The qemu lane in tests/test-matrix.sh
# shares the workspace root with the guest over virtio-9p, and a
# symlink pointing outside that root does not resolve inside the
# guest, so this must be a real copy, not a symlink.
run: |
cache="$HOME/.cache/elfuse-ci/test-fixtures"
if [ -d "$cache" ]; then
mkdir -p externals
rm -rf externals/test-fixtures
cp -Rc "$cache" externals/test-fixtures
echo "Restored test fixtures ($(du -sh externals/test-fixtures | cut -f1), lock: $(head -1 externals/test-fixtures/versions.lock 2>/dev/null || echo none))"
else
echo "No fixtures cache at $cache; tests fetch on demand"
fi
if: >
matrix.run_matrix && github.ref == 'refs/heads/main' &&
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
(github.event_name == 'push' || github.event_name == 'workflow_dispatch')
# GitHub scopes caches by ref and prevents low-trust PR runs from
# writing into the default branch's cache scope. Moving fixtures off
# the runner's persistent HOME also makes every legacy local cache
# ineligible for restore after this rollout.
uses: actions/cache/restore@v5
with:
path: externals/test-fixtures
key: fixtures-trusted-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('tests/fetch-fixtures.sh', 'tests/lib/bash-compat.sh') }}-lookup
restore-keys: |
fixtures-trusted-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('tests/fetch-fixtures.sh', 'tests/lib/bash-compat.sh') }}-

- name: Host info
run: |
Expand Down Expand Up @@ -531,6 +487,16 @@ jobs:
qemu-aarch64 --version | head -1 || true
python3 --version

- name: Prepare trusted test fixtures
id: prepare-fixtures
if: >
matrix.run_matrix && github.ref == 'refs/heads/main' &&
(github.event_name == 'push' || github.event_name == 'workflow_dispatch')
run: |
set -euo pipefail
INCLUDE_X86_64=1 bash tests/fetch-fixtures.sh
echo "ready=true" >> "$GITHUB_OUTPUT"

- name: Check Rosetta for Linux
# Rosetta is exercised only by test-matrix (release leg); the
# check-sanitizer subset has no x86_64-via-Rosetta tests.
Expand Down Expand Up @@ -594,20 +560,39 @@ jobs:
retention-days: 7
if-no-files-found: warn

- name: Save test fixtures cache
# Persist externals/test-fixtures outside the workspace so the next
# run's "Restore cached test fixtures" step can skip re-downloading
# unchanged Alpine packages. Runs even if an earlier step failed, as
# long as the job wasn't cancelled, so a fixture-unrelated test
# failure doesn't cost the next run its cache.
if: ${{ !cancelled() && matrix.sanitizer == 'release' }}
- name: Remove generated fixture credentials
id: sanitize-fixtures
# Default-branch caches are readable by PR workflows, so generated SSH
# credentials must be removed before the fixture tree is persisted.
# Drop the initramfs containing the corresponding public key as well;
# the trusted preparation step rebuilds it with a fresh key next time.
if: >
success() && matrix.sanitizer == 'release' &&
github.ref == 'refs/heads/main' &&
(github.event_name == 'push' || github.event_name == 'workflow_dispatch') &&
steps.prepare-fixtures.outputs.ready == 'true'
run: |
if [ -d externals/test-fixtures ]; then
cache="$HOME/.cache/elfuse-ci/test-fixtures"
mkdir -p "$(dirname "$cache")"
rm -rf "$cache"
cp -Rc externals/test-fixtures "$cache"
echo "Saved test fixtures ($(du -sh "$cache" | cut -f1))"
else
echo "No externals/test-fixtures to save"
fi
set -euo pipefail
test -s externals/test-fixtures/versions.lock
test -s externals/test-fixtures/aarch64-musl/staticbin/bin/busybox
test -s externals/test-fixtures/x86_64-musl/staticbin/bin/busybox
test -s externals/test-fixtures/initramfs.cpio.gz
rm -f externals/test-fixtures/keys/ssh_key \
externals/test-fixtures/keys/ssh_key.pub \
externals/test-fixtures/initramfs.cpio.gz
echo "ready=true" >> "$GITHUB_OUTPUT"

- name: Save test fixtures cache
# The resolved-version suffix creates a new immutable entry only when
# the selected fixture set changes. The trusted main ref is the sole
# writer; restore-keys above selects its newest matching generation.
if: >
success() && matrix.sanitizer == 'release' &&
github.ref == 'refs/heads/main' &&
(github.event_name == 'push' || github.event_name == 'workflow_dispatch') &&
steps.prepare-fixtures.outputs.ready == 'true' &&
steps.sanitize-fixtures.outputs.ready == 'true'
uses: actions/cache/save@v5
with:
path: externals/test-fixtures
key: fixtures-trusted-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('tests/fetch-fixtures.sh', 'tests/lib/bash-compat.sh') }}-${{ hashFiles('externals/test-fixtures/versions.lock') }}
Loading