Skip to content
Merged
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
17 changes: 17 additions & 0 deletions .github/workflows/publish-examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
workflow_dispatch:
push:
branches: [main]
tags: ['v*']
paths:
- 'examples/**'
- 'runtimes/**'
Expand Down Expand Up @@ -48,6 +49,12 @@ jobs:
-f runtimes/${{ matrix.runtime }}.Dockerfile \
-t $IMAGE runtimes/
docker push $IMAGE
# Also tag with the release version when triggered by a git tag
if [[ "${{ github.ref_type }}" == "tag" ]]; then
VERSION_TAG=${{ env.IMAGE_BASE }}/${{ matrix.runtime }}-base:${{ github.ref_name }}
docker tag $IMAGE $VERSION_TAG
docker push $VERSION_TAG
fi

# Build and publish kernels for each example
publish-kernels:
Expand Down Expand Up @@ -152,6 +159,11 @@ jobs:
DEOF
docker build -f /tmp/Dockerfile.kernel -t $IMAGE .
docker push $IMAGE
if [[ "${{ github.ref_type }}" == "tag" ]]; then
VERSION_TAG=${{ env.IMAGE_BASE }}/${{ matrix.example.name }}-kernel:${{ github.ref_name }}
docker tag $IMAGE $VERSION_TAG
docker push $VERSION_TAG
fi

# Publish the python-agent-driver rootfs CPIO as its own image so
# `pyhl setup` can pull the initrd (and kernel, above) from GHCR
Expand Down Expand Up @@ -205,3 +217,8 @@ jobs:
DEOF
docker build -f /tmp/Dockerfile.initrd -t $IMAGE .
docker push $IMAGE
if [[ "${{ github.ref_type }}" == "tag" ]]; then
VERSION_TAG=${{ env.IMAGE_BASE }}/python-agent-driver-initrd:${{ github.ref_name }}
docker tag $IMAGE $VERSION_TAG
docker push $VERSION_TAG
fi
75 changes: 75 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: Create release

on:
workflow_dispatch:
inputs:
version:
description: 'Release version (e.g., 0.2.0) — without the v prefix'
required: true
type: string

concurrency:
group: release
cancel-in-progress: false

jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
steps:
- uses: actions/checkout@v4
with:
ref: main

- name: Validate version input
run: |
VERSION="${{ github.event.inputs.version }}"
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Invalid version '$VERSION' — expected semver like 1.2.3"
exit 1
fi
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
echo "::error::Tag v$VERSION already exists"
exit 1
fi

- uses: Swatinem/rust-cache@v2
with:
workspaces: host -> target

- name: Bump Cargo.toml version
working-directory: host
run: |
VERSION="${{ github.event.inputs.version }}"
sed -i "s/^version = \".*\"/version = \"$VERSION\"/" Cargo.toml
cargo generate-lockfile
grep "^version" Cargo.toml

- name: Commit version bump
run: |
VERSION="${{ github.event.inputs.version }}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add host/Cargo.toml host/Cargo.lock
git commit -m "release: v$VERSION"
git push origin main

- name: Create tag and GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ github.event.inputs.version }}"
git tag "v$VERSION"
git push origin "v$VERSION"
gh release create "v$VERSION" \
--title "v$VERSION" \
--generate-notes

- name: Trigger GHCR image publish
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ github.event.inputs.version }}"
gh workflow run publish-examples.yml --ref "v$VERSION"
20 changes: 14 additions & 6 deletions host/src/bin/pyhl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
use anyhow::{anyhow, bail, Context, Result};
use clap::{Args, Parser, Subcommand};
use hyperlight_unikraft::pyhl::{
copy_replace, discover_source_artifacts, extract_from_ghcr, GHCR_INITRD_IMAGE,
GHCR_KERNEL_IMAGE,
copy_replace, discover_source_artifacts, extract_from_ghcr, ghcr_image_ref, GHCR_INITRD_REPO,
GHCR_KERNEL_REPO,
};
use hyperlight_unikraft::{AllowList, BlockList, ListenPorts, NetworkPolicy, Preopen, Sandbox};
use std::fs;
Expand Down Expand Up @@ -157,9 +157,14 @@ struct SetupArgs {
///
/// Without --from, pyhl pulls the pre-published image from GHCR (requires
/// docker or podman on $PATH).
#[arg(long, value_name = "DIR")]
#[arg(long, value_name = "DIR", conflicts_with = "tag")]
from: Option<PathBuf>,

/// Pin a specific GHCR release tag (e.g., "v0.3.1") instead of
/// pulling :latest. Ignored when --from is used.
#[arg(long, value_name = "TAG", conflicts_with = "from")]
tag: Option<String>,

/// Overwrite an existing installed image without prompting.
#[arg(long)]
force: bool,
Expand Down Expand Up @@ -373,15 +378,18 @@ fn cmd_setup(args: SetupArgs) -> Result<()> {
// No --from: pull from GHCR. Uses docker or podman under the hood
// because that's the standard OCI client everyone has and avoids
// linking an oci-distribution client into pyhl.
let tag = args.tag.as_deref();
let kernel_image = ghcr_image_ref(GHCR_KERNEL_REPO, tag);
let initrd_image = ghcr_image_ref(GHCR_INITRD_REPO, tag);
eprintln!("pyhl: downloading image from GHCR…");
let tmp = home.join(".pyhl.download");
fs::create_dir_all(&tmp)?;
let kernel_path = tmp.join("kernel");
let initrd_path = tmp.join("initrd.cpio");
extract_from_ghcr(GHCR_KERNEL_IMAGE, "/kernel", &kernel_path)?;
extract_from_ghcr(GHCR_INITRD_IMAGE, "/initrd.cpio", &initrd_path)?;
extract_from_ghcr(&kernel_image, "/kernel", &kernel_path)?;
extract_from_ghcr(&initrd_image, "/initrd.cpio", &initrd_path)?;
(
format!("{GHCR_KERNEL_IMAGE} + {GHCR_INITRD_IMAGE}"),
format!("{kernel_image} + {initrd_image}"),
kernel_path,
initrd_path,
)
Expand Down
40 changes: 22 additions & 18 deletions host/src/pyhl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
//! // One-time install (no-op if already present).
//! pyhl::install(&pyhl::InstallOptions {
//! home,
//! source: pyhl::InstallSource::Ghcr,
//! source: pyhl::InstallSource::Ghcr { tag: None }, // None = :latest
//! mounts: &[],
//! network: None,
//! listen_ports: None,
Expand Down Expand Up @@ -104,8 +104,9 @@ pub struct InstallOptions<'a> {
/// Where `install` pulls its kernel and CPIO from.
#[derive(Debug)]
pub enum InstallSource<'a> {
/// Pull the default published image from GHCR via docker/podman.
Ghcr,
/// Pull the published image from GHCR via docker/podman.
/// `tag`: `None` pulls `:latest`; `Some("v0.3.1")` pins a release.
Ghcr { tag: Option<&'a str> },
/// Copy from a local python-agent-driver build tree.
LocalDir(&'a Path),
/// Explicit files — useful for custom image pipelines.
Expand Down Expand Up @@ -164,18 +165,16 @@ pub fn install(opts: &InstallOptions<'_>) -> Result<InstallReport> {
kernel.to_path_buf(),
initrd.to_path_buf(),
),
InstallSource::Ghcr => {
InstallSource::Ghcr { tag } => {
let kernel_image = ghcr_image_ref(GHCR_KERNEL_REPO, *tag);
let initrd_image = ghcr_image_ref(GHCR_INITRD_REPO, *tag);
let scratch = home.join(".pyhl.download");
fs::create_dir_all(&scratch)?;
let k = scratch.join("kernel");
let i = scratch.join("initrd.cpio");
extract_from_ghcr(GHCR_KERNEL_IMAGE, "/kernel", &k)?;
extract_from_ghcr(GHCR_INITRD_IMAGE, "/initrd.cpio", &i)?;
(
format!("ghcr: {GHCR_KERNEL_IMAGE} + {GHCR_INITRD_IMAGE}"),
k,
i,
)
extract_from_ghcr(&kernel_image, "/kernel", &k)?;
extract_from_ghcr(&initrd_image, "/initrd.cpio", &i)?;
(format!("ghcr: {kernel_image} + {initrd_image}"), k, i)
}
};

Expand Down Expand Up @@ -394,13 +393,18 @@ pub fn discover_source_artifacts(dir: &Path) -> Result<(PathBuf, PathBuf)> {
// through InstallSource::Ghcr.
// ---------------------------------------------------------------------------

/// OCI image references published by `.github/workflows/publish-examples.yml`.
/// Both images are FROM-scratch payloads: kernel image has a single /kernel,
/// initrd image has a single /initrd.cpio.
pub const GHCR_KERNEL_IMAGE: &str =
"ghcr.io/hyperlight-dev/hyperlight-unikraft/python-agent-driver-kernel:latest";
pub const GHCR_INITRD_IMAGE: &str =
"ghcr.io/hyperlight-dev/hyperlight-unikraft/python-agent-driver-initrd:latest";
/// GHCR repository base (without tag) for the kernel image.
pub const GHCR_KERNEL_REPO: &str =
"ghcr.io/hyperlight-dev/hyperlight-unikraft/python-agent-driver-kernel";
/// GHCR repository base (without tag) for the initrd image.
pub const GHCR_INITRD_REPO: &str =
"ghcr.io/hyperlight-dev/hyperlight-unikraft/python-agent-driver-initrd";

/// Build a full OCI image reference from a repo base and an optional tag.
/// `None` resolves to `:latest`.
pub fn ghcr_image_ref(repo: &str, tag: Option<&str>) -> String {
format!("{}:{}", repo, tag.unwrap_or("latest"))
}

/// Pull a single file out of an OCI image hosted on GHCR. Uses whichever
/// of `docker` / `podman` is on `$PATH`. The published images are
Expand Down
Loading