diff --git a/.github/workflows/publish-examples.yml b/.github/workflows/publish-examples.yml index 183652b..584b363 100644 --- a/.github/workflows/publish-examples.yml +++ b/.github/workflows/publish-examples.yml @@ -4,6 +4,7 @@ on: workflow_dispatch: push: branches: [main] + tags: ['v*'] paths: - 'examples/**' - 'runtimes/**' @@ -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: @@ -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 @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8b6212d --- /dev/null +++ b/.github/workflows/release.yml @@ -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" diff --git a/host/src/bin/pyhl.rs b/host/src/bin/pyhl.rs index ff5bdd1..13720ed 100644 --- a/host/src/bin/pyhl.rs +++ b/host/src/bin/pyhl.rs @@ -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; @@ -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, + /// 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, + /// Overwrite an existing installed image without prompting. #[arg(long)] force: bool, @@ -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, ) diff --git a/host/src/pyhl.rs b/host/src/pyhl.rs index c5199d8..2335941 100644 --- a/host/src/pyhl.rs +++ b/host/src/pyhl.rs @@ -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, @@ -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. @@ -164,18 +165,16 @@ pub fn install(opts: &InstallOptions<'_>) -> Result { 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) } }; @@ -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