Skip to content

Commit a13793a

Browse files
authored
Merge pull request #28 from hyperlight-dev/ghcr-tagged-pulls
Support pinned GHCR image tags and release workflow
2 parents ad90d23 + d4b0467 commit a13793a

4 files changed

Lines changed: 128 additions & 24 deletions

File tree

.github/workflows/publish-examples.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ on:
44
workflow_dispatch:
55
push:
66
branches: [main]
7+
tags: ['v*']
78
paths:
89
- 'examples/**'
910
- 'runtimes/**'
@@ -48,6 +49,12 @@ jobs:
4849
-f runtimes/${{ matrix.runtime }}.Dockerfile \
4950
-t $IMAGE runtimes/
5051
docker push $IMAGE
52+
# Also tag with the release version when triggered by a git tag
53+
if [[ "${{ github.ref_type }}" == "tag" ]]; then
54+
VERSION_TAG=${{ env.IMAGE_BASE }}/${{ matrix.runtime }}-base:${{ github.ref_name }}
55+
docker tag $IMAGE $VERSION_TAG
56+
docker push $VERSION_TAG
57+
fi
5158
5259
# Build and publish kernels for each example
5360
publish-kernels:
@@ -152,6 +159,11 @@ jobs:
152159
DEOF
153160
docker build -f /tmp/Dockerfile.kernel -t $IMAGE .
154161
docker push $IMAGE
162+
if [[ "${{ github.ref_type }}" == "tag" ]]; then
163+
VERSION_TAG=${{ env.IMAGE_BASE }}/${{ matrix.example.name }}-kernel:${{ github.ref_name }}
164+
docker tag $IMAGE $VERSION_TAG
165+
docker push $VERSION_TAG
166+
fi
155167
156168
# Publish the python-agent-driver rootfs CPIO as its own image so
157169
# `pyhl setup` can pull the initrd (and kernel, above) from GHCR
@@ -205,3 +217,8 @@ jobs:
205217
DEOF
206218
docker build -f /tmp/Dockerfile.initrd -t $IMAGE .
207219
docker push $IMAGE
220+
if [[ "${{ github.ref_type }}" == "tag" ]]; then
221+
VERSION_TAG=${{ env.IMAGE_BASE }}/python-agent-driver-initrd:${{ github.ref_name }}
222+
docker tag $IMAGE $VERSION_TAG
223+
docker push $VERSION_TAG
224+
fi

.github/workflows/release.yml

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
name: Create release
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
version:
7+
description: 'Release version (e.g., 0.2.0) — without the v prefix'
8+
required: true
9+
type: string
10+
11+
concurrency:
12+
group: release
13+
cancel-in-progress: false
14+
15+
jobs:
16+
release:
17+
runs-on: ubuntu-latest
18+
permissions:
19+
contents: write
20+
packages: write
21+
steps:
22+
- uses: actions/checkout@v4
23+
with:
24+
ref: main
25+
26+
- name: Validate version input
27+
run: |
28+
VERSION="${{ github.event.inputs.version }}"
29+
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
30+
echo "::error::Invalid version '$VERSION' — expected semver like 1.2.3"
31+
exit 1
32+
fi
33+
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
34+
echo "::error::Tag v$VERSION already exists"
35+
exit 1
36+
fi
37+
38+
- uses: Swatinem/rust-cache@v2
39+
with:
40+
workspaces: host -> target
41+
42+
- name: Bump Cargo.toml version
43+
working-directory: host
44+
run: |
45+
VERSION="${{ github.event.inputs.version }}"
46+
sed -i "s/^version = \".*\"/version = \"$VERSION\"/" Cargo.toml
47+
cargo generate-lockfile
48+
grep "^version" Cargo.toml
49+
50+
- name: Commit version bump
51+
run: |
52+
VERSION="${{ github.event.inputs.version }}"
53+
git config user.name "github-actions[bot]"
54+
git config user.email "github-actions[bot]@users.noreply.github.com"
55+
git add host/Cargo.toml host/Cargo.lock
56+
git commit -m "release: v$VERSION"
57+
git push origin main
58+
59+
- name: Create tag and GitHub Release
60+
env:
61+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
62+
run: |
63+
VERSION="${{ github.event.inputs.version }}"
64+
git tag "v$VERSION"
65+
git push origin "v$VERSION"
66+
gh release create "v$VERSION" \
67+
--title "v$VERSION" \
68+
--generate-notes
69+
70+
- name: Trigger GHCR image publish
71+
env:
72+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
73+
run: |
74+
VERSION="${{ github.event.inputs.version }}"
75+
gh workflow run publish-examples.yml --ref "v$VERSION"

host/src/bin/pyhl.rs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
use anyhow::{anyhow, bail, Context, Result};
2424
use clap::{Args, Parser, Subcommand};
2525
use hyperlight_unikraft::pyhl::{
26-
copy_replace, discover_source_artifacts, extract_from_ghcr, GHCR_INITRD_IMAGE,
27-
GHCR_KERNEL_IMAGE,
26+
copy_replace, discover_source_artifacts, extract_from_ghcr, ghcr_image_ref, GHCR_INITRD_REPO,
27+
GHCR_KERNEL_REPO,
2828
};
2929
use hyperlight_unikraft::{AllowList, BlockList, ListenPorts, NetworkPolicy, Preopen, Sandbox};
3030
use std::fs;
@@ -157,9 +157,14 @@ struct SetupArgs {
157157
///
158158
/// Without --from, pyhl pulls the pre-published image from GHCR (requires
159159
/// docker or podman on $PATH).
160-
#[arg(long, value_name = "DIR")]
160+
#[arg(long, value_name = "DIR", conflicts_with = "tag")]
161161
from: Option<PathBuf>,
162162

163+
/// Pin a specific GHCR release tag (e.g., "v0.3.1") instead of
164+
/// pulling :latest. Ignored when --from is used.
165+
#[arg(long, value_name = "TAG", conflicts_with = "from")]
166+
tag: Option<String>,
167+
163168
/// Overwrite an existing installed image without prompting.
164169
#[arg(long)]
165170
force: bool,
@@ -373,15 +378,18 @@ fn cmd_setup(args: SetupArgs) -> Result<()> {
373378
// No --from: pull from GHCR. Uses docker or podman under the hood
374379
// because that's the standard OCI client everyone has and avoids
375380
// linking an oci-distribution client into pyhl.
381+
let tag = args.tag.as_deref();
382+
let kernel_image = ghcr_image_ref(GHCR_KERNEL_REPO, tag);
383+
let initrd_image = ghcr_image_ref(GHCR_INITRD_REPO, tag);
376384
eprintln!("pyhl: downloading image from GHCR…");
377385
let tmp = home.join(".pyhl.download");
378386
fs::create_dir_all(&tmp)?;
379387
let kernel_path = tmp.join("kernel");
380388
let initrd_path = tmp.join("initrd.cpio");
381-
extract_from_ghcr(GHCR_KERNEL_IMAGE, "/kernel", &kernel_path)?;
382-
extract_from_ghcr(GHCR_INITRD_IMAGE, "/initrd.cpio", &initrd_path)?;
389+
extract_from_ghcr(&kernel_image, "/kernel", &kernel_path)?;
390+
extract_from_ghcr(&initrd_image, "/initrd.cpio", &initrd_path)?;
383391
(
384-
format!("{GHCR_KERNEL_IMAGE} + {GHCR_INITRD_IMAGE}"),
392+
format!("{kernel_image} + {initrd_image}"),
385393
kernel_path,
386394
initrd_path,
387395
)

host/src/pyhl.rs

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
//! // One-time install (no-op if already present).
2626
//! pyhl::install(&pyhl::InstallOptions {
2727
//! home,
28-
//! source: pyhl::InstallSource::Ghcr,
28+
//! source: pyhl::InstallSource::Ghcr { tag: None }, // None = :latest
2929
//! mounts: &[],
3030
//! network: None,
3131
//! listen_ports: None,
@@ -104,8 +104,9 @@ pub struct InstallOptions<'a> {
104104
/// Where `install` pulls its kernel and CPIO from.
105105
#[derive(Debug)]
106106
pub enum InstallSource<'a> {
107-
/// Pull the default published image from GHCR via docker/podman.
108-
Ghcr,
107+
/// Pull the published image from GHCR via docker/podman.
108+
/// `tag`: `None` pulls `:latest`; `Some("v0.3.1")` pins a release.
109+
Ghcr { tag: Option<&'a str> },
109110
/// Copy from a local python-agent-driver build tree.
110111
LocalDir(&'a Path),
111112
/// Explicit files — useful for custom image pipelines.
@@ -164,18 +165,16 @@ pub fn install(opts: &InstallOptions<'_>) -> Result<InstallReport> {
164165
kernel.to_path_buf(),
165166
initrd.to_path_buf(),
166167
),
167-
InstallSource::Ghcr => {
168+
InstallSource::Ghcr { tag } => {
169+
let kernel_image = ghcr_image_ref(GHCR_KERNEL_REPO, *tag);
170+
let initrd_image = ghcr_image_ref(GHCR_INITRD_REPO, *tag);
168171
let scratch = home.join(".pyhl.download");
169172
fs::create_dir_all(&scratch)?;
170173
let k = scratch.join("kernel");
171174
let i = scratch.join("initrd.cpio");
172-
extract_from_ghcr(GHCR_KERNEL_IMAGE, "/kernel", &k)?;
173-
extract_from_ghcr(GHCR_INITRD_IMAGE, "/initrd.cpio", &i)?;
174-
(
175-
format!("ghcr: {GHCR_KERNEL_IMAGE} + {GHCR_INITRD_IMAGE}"),
176-
k,
177-
i,
178-
)
175+
extract_from_ghcr(&kernel_image, "/kernel", &k)?;
176+
extract_from_ghcr(&initrd_image, "/initrd.cpio", &i)?;
177+
(format!("ghcr: {kernel_image} + {initrd_image}"), k, i)
179178
}
180179
};
181180

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

397-
/// OCI image references published by `.github/workflows/publish-examples.yml`.
398-
/// Both images are FROM-scratch payloads: kernel image has a single /kernel,
399-
/// initrd image has a single /initrd.cpio.
400-
pub const GHCR_KERNEL_IMAGE: &str =
401-
"ghcr.io/hyperlight-dev/hyperlight-unikraft/python-agent-driver-kernel:latest";
402-
pub const GHCR_INITRD_IMAGE: &str =
403-
"ghcr.io/hyperlight-dev/hyperlight-unikraft/python-agent-driver-initrd:latest";
396+
/// GHCR repository base (without tag) for the kernel image.
397+
pub const GHCR_KERNEL_REPO: &str =
398+
"ghcr.io/hyperlight-dev/hyperlight-unikraft/python-agent-driver-kernel";
399+
/// GHCR repository base (without tag) for the initrd image.
400+
pub const GHCR_INITRD_REPO: &str =
401+
"ghcr.io/hyperlight-dev/hyperlight-unikraft/python-agent-driver-initrd";
402+
403+
/// Build a full OCI image reference from a repo base and an optional tag.
404+
/// `None` resolves to `:latest`.
405+
pub fn ghcr_image_ref(repo: &str, tag: Option<&str>) -> String {
406+
format!("{}:{}", repo, tag.unwrap_or("latest"))
407+
}
404408

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

0 commit comments

Comments
 (0)