Skip to content
Draft
Show file tree
Hide file tree
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
202 changes: 202 additions & 0 deletions .github/workflows/RegenSnapshotGoldens.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json

# Publish snapshot goldens to
# ghcr.io/hyperlight-dev/hyperlight-snapshot-goldens.
#
# Runs automatically when a merge to main changes GOLDENS_VERSION (the
# version string lives in
# src/hyperlight_host/tests/snapshot_goldens/platform.rs). The resolve
# job reads that version and checks whether it is already published. If
# it is not, the matrix walks every (hv, cpu, config) cell, dumps the
# canonical init and call snapshots, and pushes each one to GHCR as its
# own tag named `{version}-{hv}-{cpu}-{profile}-{kind}`.
#
# An already-published version is left untouched, so a merge that does
# not bump the version, or a re-run of the same version, is a no-op.
# Manual dispatch with `force: true` overwrites an existing version and
# exists for recovery only.
#
# See docs/snapshot-versioning.md

name: Regenerate Snapshot Goldens

on:
push:
branches: [main]
paths:
- src/hyperlight_host/tests/snapshot_goldens/platform.rs
workflow_dispatch:
inputs:
version:
description: Goldens version string. Must match GOLDENS_VERSION in source (e.g. "v1.0").
required: true
type: string
force:
description: Overwrite tags even if the version is already published (recovery only).
type: boolean
default: false

env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: full
GHCR_IMAGE: ghcr.io/hyperlight-dev/hyperlight-snapshot-goldens

permissions:
contents: read
packages: write

concurrency:
group: regen-snapshot-goldens-${{ github.ref }}
cancel-in-progress: false

defaults:
run:
shell: bash

jobs:
resolve:
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
outputs:
version: ${{ steps.decide.outputs.version }}
needs_publish: ${{ steps.decide.outputs.needs_publish }}
steps:
- uses: actions/checkout@v6

- name: Install oras
uses: oras-project/setup-oras@38de303aac69abb66f3e6255b7198bff35f323e3 # v2.0.0
with:
version: 1.3.2

- name: Decide version and whether to publish
id: decide
env:
EVENT_NAME: ${{ github.event_name }}
INPUT_VERSION: ${{ inputs.version }}
FORCE: ${{ inputs.force }}
GHCR_USER: ${{ github.actor }}
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
SRC=$(grep -oE 'GOLDENS_VERSION: &str = "[^"]+"' src/hyperlight_host/tests/snapshot_goldens/platform.rs | head -n1 | sed -E 's/.*"([^"]+)".*/\1/')
if ! [[ "${SRC}" =~ ^v[0-9]+\.[0-9]+$ ]]; then
echo "::error::GOLDENS_VERSION in source must match ^v[0-9]+\.[0-9]+$ (e.g. v1.0), found '${SRC}'"
exit 1
fi

# On manual dispatch the input must name the version that the
# dispatched ref actually carries. This catches a stale input.
if [ "${EVENT_NAME}" = "workflow_dispatch" ] && [ "${INPUT_VERSION}" != "${SRC}" ]; then
echo "::error::version input '${INPUT_VERSION}' does not match GOLDENS_VERSION in source '${SRC}'"
exit 1
fi

echo "version=${SRC}" >> "$GITHUB_OUTPUT"

if [ "${EVENT_NAME}" = "workflow_dispatch" ] && [ "${FORCE}" = "true" ]; then
echo "force requested: will publish ${SRC} even if it already exists"
echo "needs_publish=true" >> "$GITHUB_OUTPUT"
exit 0
fi

# A version is frozen once any of its tags exist on GHCR.
# Publishing only when the set is absent makes the workflow
# idempotent and never clobbers a published baseline.
echo "${GHCR_TOKEN}" | oras login ghcr.io -u "${GHCR_USER}" --password-stdin
EXISTING=$(oras repo tags "${GHCR_IMAGE}" 2>/dev/null | grep -c "^${SRC}-" || true)
if [ "${EXISTING}" -gt 0 ]; then
echo "${SRC} already published (${EXISTING} tags). Nothing to do."
echo "needs_publish=false" >> "$GITHUB_OUTPUT"
else
echo "${SRC} not published yet. Will publish."
echo "needs_publish=true" >> "$GITHUB_OUTPUT"
fi

build-guests:
needs: resolve
if: needs.resolve.outputs.needs_publish == 'true'
strategy:
matrix:
config: [debug, release]
uses: ./.github/workflows/dep_build_guests.yml
with:
config: ${{ matrix.config }}
secrets: inherit

dump-and-push:
needs: [resolve, build-guests]
if: needs.resolve.outputs.needs_publish == 'true'
strategy:
fail-fast: false
matrix:
hypervisor: [kvm, mshv3, hyperv-ws2025]
cpu: [amd, intel]
config: [debug, release]
runs-on: ${{ fromJson(
format('["self-hosted", "{0}", "X64", "1ES.Pool=hld-{1}-{2}", "JobId=regen-goldens-{3}-{4}-{5}-{6}"]',
matrix.hypervisor == 'hyperv-ws2025' && 'Windows' || 'Linux',
matrix.hypervisor == 'hyperv-ws2025' && 'win2025' || matrix.hypervisor == 'mshv3' && 'azlinux3-mshv' || matrix.hypervisor,
matrix.cpu,
matrix.config,
github.run_id,
github.run_number,
github.run_attempt)) }}
steps:
- uses: actions/checkout@v6

- uses: hyperlight-dev/ci-setup-workflow@v1.9.0
with:
rust-toolchain: "1.94"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Fix cargo home permissions
if: runner.os == 'Linux'
run: sudo chown -R $(id -u):$(id -g) /opt/cargo || true

- name: Download Rust guests
uses: actions/download-artifact@v7
with:
name: rust-guests-${{ matrix.config }}
path: src/tests/rust_guests/bin/${{ matrix.config }}/

- name: Install oras
uses: oras-project/setup-oras@38de303aac69abb66f3e6255b7198bff35f323e3 # v2.0.0
with:
version: 1.3.2

- name: Confirm source matches resolved version
env:
RESOLVED_VERSION: ${{ needs.resolve.outputs.version }}
run: |
set -euo pipefail
SRC=$(grep -oE 'GOLDENS_VERSION: &str = "[^"]+"' src/hyperlight_host/tests/snapshot_goldens/platform.rs | head -n1 | sed -E 's/.*"([^"]+)".*/\1/')
if [ "${SRC}" != "${RESOLVED_VERSION}" ]; then
echo "::error::source GOLDENS_VERSION '${SRC}' does not match resolved '${RESOLVED_VERSION}'"
exit 1
fi

- name: Generate snapshots
run: just snapshot-goldens-generate ${{ matrix.config }}

- name: Log in to GHCR
env:
GHCR_USER: ${{ github.actor }}
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "${GHCR_TOKEN}" | oras login ghcr.io -u "${GHCR_USER}" --password-stdin

- name: Push goldens to GHCR
env:
GOLDENS_VERSION: ${{ needs.resolve.outputs.version }}
run: |
set -euo pipefail
GOLDENS_DIR="target/snapshot-goldens/${GOLDENS_VERSION}"
for layout in "$GOLDENS_DIR"/*/; do
tag=$(basename "$layout")
echo "::group::push ${tag}"
oras cp --from-oci-layout "${layout%/}:${tag}" "${GHCR_IMAGE}:${tag}"
echo "::endgroup::"
done
19 changes: 18 additions & 1 deletion .github/workflows/ValidatePullRequest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,17 +79,33 @@ jobs:
with:
docs_only: ${{ needs.docs-pr.outputs.docs-only }}

# Pick the goldens mode. The `regen-goldens` label means regenerate. No label means pull.
goldens-mode:
runs-on: ubuntu-latest
outputs:
regen: ${{ steps.check.outputs.regen }}
steps:
- id: check
if: github.event_name == 'pull_request'
env:
GH_TOKEN: ${{ github.token }}
run: |
gh pr view ${{ github.event.pull_request.number }} --repo ${{ github.repository }} \
--json labels -q '.labels[].name' | grep -qx regen-goldens \
&& echo "regen=true" >> "$GITHUB_OUTPUT" || echo "regen=false" >> "$GITHUB_OUTPUT"

# Build and test - needs guest artifacts
build-test:
needs:
- docs-pr
- build-guests
- goldens-mode
# Required because update-guest-locks is skipped on non-dependabot PRs,
# and a skipped dependency transitively skips all downstream jobs.
# See: https://github.com/actions/runner/issues/2205
if: ${{ !cancelled() && !failure() }}
strategy:
fail-fast: true
fail-fast: false
matrix:
hypervisor: ['hyperv-ws2025', mshv3, kvm]
cpu: [amd, intel]
Expand All @@ -101,6 +117,7 @@ jobs:
hypervisor: ${{ matrix.hypervisor }}
cpu: ${{ matrix.cpu }}
config: ${{ matrix.config }}
regen_goldens: ${{ needs.goldens-mode.outputs.regen }}

# Run examples - needs guest artifacts, runs in parallel with build-test
run-examples:
Expand Down
32 changes: 32 additions & 0 deletions .github/workflows/dep_build_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,19 @@ on:
description: CPU architecture for the build (passed from caller matrix)
required: true
type: string
regen_goldens:
description: Regenerate snapshot goldens from the branch and skip pulling published ones
required: false
type: string
default: "false"

env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: full

permissions:
contents: read
packages: read

defaults:
run:
Expand Down Expand Up @@ -132,3 +138,29 @@ jobs:
env:
RUST_LOG: debug
run: just test-rust-tracing ${{ inputs.config }}

- name: Install oras
if: ${{ inputs.regen_goldens != 'true' }}
uses: oras-project/setup-oras@38de303aac69abb66f3e6255b7198bff35f323e3 # v2.0.0
with:
version: 1.3.2

# Pull the published goldens for this cell and load them with the
# branch. A missing tag fails the job and flags a format break.
- name: Snapshot goldens (pull and verify)
if: ${{ inputs.regen_goldens != 'true' }}
env:
GHCR_USER: ${{ github.actor }}
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "${GHCR_TOKEN}" | oras login ghcr.io -u "${GHCR_USER}" --password-stdin
just snapshot-goldens-pull ghcr.io/hyperlight-dev/hyperlight-snapshot-goldens ${{ inputs.config }}
just snapshot-goldens-verify ${{ inputs.config }}

# Label path: generate the goldens from the branch and load them
# back. Used when no published tag set exists yet.
- name: Snapshot goldens (regenerate and verify)
if: ${{ inputs.regen_goldens == 'true' }}
run: |
just snapshot-goldens-generate ${{ inputs.config }}
just snapshot-goldens-verify ${{ inputs.config }}
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Prerelease] - Unreleased

### Added
* `Snapshot::to_oci`, `Snapshot::from_oci`, and `Snapshot::from_oci_unchecked` for persisting and loading sandbox snapshots as OCI Image Layout directories by @ludfjig in https://github.com/hyperlight-dev/hyperlight/pull/1465

### Changed
* **Breaking:** `MultiUseSandbox::map_file_cow` and `UninitializedSandbox::map_file_cow` no longer take a label argument. The APIs now accept only `(file_path, guest_base)`.

Expand Down
Loading
Loading