diff --git a/.github/actions/setup-action/action.yml b/.github/actions/setup-action/action.yml
deleted file mode 100644
index 63fc965e..00000000
--- a/.github/actions/setup-action/action.yml
+++ /dev/null
@@ -1,186 +0,0 @@
-name: setup-action
-description: Install the Unikraft CLI from GitHub releases
-inputs:
- version:
- description: Version to install (latest, dev, or a release tag)
- required: false
- default: latest
- github-token:
- description: GitHub token used by gh for release resolution/downloads
- required: false
- default: ${{ github.token }}
- token:
- description: Authentication token for unikraft login
- required: false
- default: ""
- organization:
- description: Organization to associate the login with
- required: false
- default: ""
-outputs:
- version:
- description: Resolved release tag that was installed
- value: ${{ steps.resolve.outputs.tag }}
-runs:
- using: composite
- steps:
- - name: Resolve release tag
- id: resolve
- shell: bash
- env:
- GH_TOKEN: ${{ inputs['github-token'] || github.token }}
- REPO: "unikraft/cli"
- VERSION: ${{ inputs.version }}
- run: |
- set -euo pipefail
-
- if [[ -z "${VERSION}" ]]; then
- VERSION="latest"
- fi
-
- if [[ "${VERSION}" == "latest" ]]; then
- tag=""
- api_err="$(mktemp)"
- if latest_json="$(gh api "repos/${REPO}/releases/latest" 2>"${api_err}")"; then
- tag="$(echo "${latest_json}" | jq -r '.tag_name // empty')"
- if [[ -z "${tag}" || "${tag}" == "null" ]]; then
- rm -f "${api_err}"
- echo "No latest release tag found in repository ${REPO}." >&2
- exit 1
- fi
- else
- err_msg="$(cat "${api_err}")"
- rm -f "${api_err}"
- if [[ -z "${err_msg}" ]]; then
- err_msg="unknown error"
- fi
- echo "Failed to fetch latest release from ${REPO}: ${err_msg}" >&2
- exit 1
- fi
- rm -f "${api_err}"
- elif [[ "${VERSION}" == "dev" ]]; then
- releases="$(gh release list -R "${REPO}" --limit 100 --json tagName,isDraft,isPrerelease)"
- tag="$(echo "${releases}" | jq -r '[.[] | select(.isDraft == false and .isPrerelease == true)][0].tagName // empty')"
- else
- if [[ "${VERSION}" =~ ^v ]]; then
- tag="${VERSION}"
- else
- tag="v${VERSION}"
- fi
-
- api_err="$(mktemp)"
- if ! gh api "repos/${REPO}/releases/tags/${tag}" >/dev/null 2>"${api_err}"; then
- err_msg="$(cat "${api_err}")"
- rm -f "${api_err}"
- if [[ "${err_msg}" == *"HTTP 404"* ]]; then
- echo "Release tag '${tag}' not found in repository ${REPO}." >&2
- elif [[ -n "${err_msg}" ]]; then
- echo "Failed to fetch release tag '${tag}' from ${REPO}: ${err_msg}" >&2
- else
- echo "Failed to fetch release tag '${tag}' from ${REPO}." >&2
- fi
- exit 1
- fi
- rm -f "${api_err}"
- fi
-
- if [[ -z "${tag}" || "${tag}" == "null" ]]; then
- echo "Failed to resolve a release tag for version '${VERSION}'." >&2
- exit 1
- fi
-
- echo "tag=${tag}" >> "${GITHUB_OUTPUT}"
-
- - name: Download release asset
- id: download
- shell: bash
- env:
- GH_TOKEN: ${{ inputs['github-token'] || github.token }}
- REPO: "unikraft/cli"
- TAG: ${{ steps.resolve.outputs.tag }}
- run: |
- set -euo pipefail
- os="$(uname -s | tr '[:upper:]' '[:lower:]')"
- arch="$(uname -m)"
-
- case "${arch}" in
- x86_64) arch="amd64" ;;
- aarch64|arm64) arch="arm64" ;;
- *)
- echo "Unsupported architecture: ${arch}" >&2
- exit 1
- ;;
- esac
-
- case "${os}" in
- linux|darwin) ;;
- *)
- echo "Unsupported OS: ${os}" >&2
- exit 1
- ;;
- esac
-
- version="${TAG#v}"
- filename="unikraft-cli_${version}_${os}_${arch}.tar.gz"
- download_dir="$(mktemp -d)"
- gh release download "${TAG}" --repo "${REPO}" --pattern "${filename}" --dir "${download_dir}"
- echo "archive=${download_dir}/${filename}" >> "${GITHUB_OUTPUT}"
-
- - name: Install Unikraft CLI
- shell: bash
- env:
- ARCHIVE: "${{ steps.download.outputs.archive }}"
- run: |
- set -euo pipefail
- install_dir="${RUNNER_TEMP}/unikraft-cli/bin"
- extract_dir="$(mktemp -d)"
- mkdir -p "${install_dir}"
-
- tar -xzf "${ARCHIVE}" -C "${extract_dir}"
- binary_path=""
- match_count=0
- while IFS= read -r match; do
- match_count=$((match_count + 1))
- if [[ ${match_count} -gt 1 ]]; then
- echo "Multiple unikraft binaries found in ${ARCHIVE}" >&2
- exit 1
- fi
- binary_path="${match}"
- done < <(find "${extract_dir}" -type f -name unikraft -perm /111)
- if [[ -z "${binary_path}" ]]; then
- echo "unikraft binary not found in ${ARCHIVE}" >&2
- exit 1
- fi
-
- install -m 0755 "${binary_path}" "${install_dir}/unikraft"
- echo "${install_dir}" >> "${GITHUB_PATH}"
-
- - name: Login to Unikraft Cloud
- if: ${{ inputs.token != '' || inputs.organization != '' }}
- shell: bash
- env:
- TOKEN_INPUT: ${{ inputs.token }}
- ORGANIZATION: ${{ inputs.organization }}
- run: |
- set -euo pipefail
-
- if [[ -z "${TOKEN_INPUT}" && -n "${ORGANIZATION}" ]]; then
- echo "setup-action: organization input requires token input" >&2
- exit 1
- fi
-
- if [[ -z "${TOKEN_INPUT}" ]]; then
- exit 0
- fi
-
- token_file="$(mktemp)"
- trap 'rm -f "${token_file}"' EXIT
- chmod 0600 "${token_file}"
- printf '%s' "${TOKEN_INPUT}" > "${token_file}"
-
- args=(login --token "${token_file}")
- if [[ -n "${ORGANIZATION}" ]]; then
- args+=(--organization "${ORGANIZATION}")
- fi
-
- unikraft "${args[@]}"
diff --git a/.github/workflows/test-setup-action.yaml b/.github/workflows/test-setup-action.yaml
deleted file mode 100644
index 092cfec0..00000000
--- a/.github/workflows/test-setup-action.yaml
+++ /dev/null
@@ -1,148 +0,0 @@
-name: test-setup-action
-permissions:
- contents: read
-
-on:
- pull_request:
- branches:
- - staging
- paths:
- - '.github/actions/setup-action/**'
- - '.github/workflows/test-setup-action.yaml'
- push:
- branches:
- - staging
- paths:
- - '.github/actions/setup-action/**'
- - '.github/workflows/test-setup-action.yaml'
- workflow_dispatch:
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.ref }}
- cancel-in-progress: true
-
-jobs:
- setup-action-install:
- runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'arc-unikraft-cli-ubuntu24.04-standard' }}
- steps:
- - uses: actions/checkout@v6
-
- - name: Install Unikraft CLI (default)
- uses: ./.github/actions/setup-action
- - name: Capture default version
- shell: bash
- run: |
- unikraft --version | tee "${RUNNER_TEMP}/unikraft-version-default.txt"
-
- - name: Install Unikraft CLI (latest)
- uses: ./.github/actions/setup-action
- with:
- version: latest
- - name: Capture latest version
- shell: bash
- run: |
- unikraft --version | tee "${RUNNER_TEMP}/unikraft-version-latest.txt"
-
- - name: Install Unikraft CLI (dev)
- uses: ./.github/actions/setup-action
- with:
- version: dev
- - name: Capture dev version
- shell: bash
- run: |
- unikraft --version | tee "${RUNNER_TEMP}/unikraft-version-dev.txt"
-
- - name: Install Unikraft CLI (pinned)
- uses: ./.github/actions/setup-action
- with:
- version: 0.1.0-staging.71
- - name: Capture pinned version
- shell: bash
- run: |
- unikraft --version | tee "${RUNNER_TEMP}/unikraft-version-pinned.txt"
-
- - name: Verify CLI versions
- shell: bash
- run: |
- default_output="$(cat "${RUNNER_TEMP}/unikraft-version-default.txt")"
- latest_output="$(cat "${RUNNER_TEMP}/unikraft-version-latest.txt")"
- dev_output="$(cat "${RUNNER_TEMP}/unikraft-version-dev.txt")"
- pinned_output="$(cat "${RUNNER_TEMP}/unikraft-version-pinned.txt")"
-
- if [[ -z "${default_output}" || -z "${latest_output}" || -z "${dev_output}" || -z "${pinned_output}" ]]; then
- echo "One or more version outputs are empty" >&2
- exit 1
- fi
-
- extract_version() {
- local label="$1"
- local output="$2"
- local version
-
- version="$(printf '%s\n' "${output}" | awk -F: '/^[[:space:]]*version[[:space:]]*:/ { gsub(/^[[:space:]]+/, "", $2); gsub(/[[:space:]]+$/, "", $2); print $2; exit }')"
- if [[ -z "${version}" ]]; then
- echo "${label} output did not include a version line: ${output}" >&2
- exit 1
- fi
-
- if [[ ! "${version}" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?([+][0-9A-Za-z.-]+)?$ ]]; then
- echo "${label} version has unexpected format: ${version}" >&2
- exit 1
- fi
-
- printf '%s' "${version}"
- }
-
- default_version="$(extract_version "Default" "${default_output}")"
- latest_version="$(extract_version "Latest" "${latest_output}")"
- dev_version="$(extract_version "Dev" "${dev_output}")"
- pinned_version="$(extract_version "Pinned" "${pinned_output}")"
- echo "Default version: ${default_version}"
- echo "Latest version: ${latest_version}"
- echo "Dev version: ${dev_version}"
- echo "Pinned version: ${pinned_version}"
-
- if [[ "${default_version}" != "${latest_version}" ]]; then
- echo "Expected default and latest versions to match: default=${default_version} latest=${latest_version}" >&2
- exit 1
- fi
-
- if [[ "${dev_version}" != *-* ]]; then
- echo "Expected dev version to be a prerelease: ${dev_version}" >&2
- exit 1
- fi
-
- if [[ "${pinned_version}" != *"0.1.0-staging.71"* ]]; then
- echo "Expected pinned version 0.1.0-staging.71 in output: ${pinned_output}" >&2
- exit 1
- fi
-
- setup-action-login:
- runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'arc-unikraft-cli-ubuntu24.04-standard' }}
- steps:
- - uses: actions/checkout@v6
-
- - name: Install Unikraft CLI (login)
- if: ${{ github.event_name == 'push' || !github.event.pull_request.head.repo.fork }}
- uses: ./.github/actions/setup-action
- with:
- version: dev
- token: ${{ secrets.UKC_TOKEN }}
- - name: Verify login with metro list
- if: ${{ github.event_name == 'push' || !github.event.pull_request.head.repo.fork }}
- shell: bash
- run: |
- set -euo pipefail
- metros="$(unikraft metro list -oquiet)"
- echo "${metros}"
- if [[ -z "${metros}" ]]; then
- echo "No metros returned from unikraft metro list" >&2
- exit 1
- fi
- while IFS= read -r line; do
- if [[ -z "${line}" ]]; then
- echo "Unexpected empty metro entry" >&2
- exit 1
- fi
- done <<< "${metros}"
-
diff --git a/README.md b/README.md
index 1df60cd9..5dde0c2a 100644
--- a/README.md
+++ b/README.md
@@ -88,6 +88,21 @@ yum install unikraft-cli
+
+GitHub Actions
+
+- Requires [GitHub Actions](https://docs.github.com/en/actions).
+
+Use the official [`unikraft/setup-action`](https://github.com/unikraft/setup-action)
+to install the CLI in your workflow:
+
+```yaml
+- name: Install Unikraft CLI
+ uses: unikraft/setup-action@v1
+```
+
+
+
From Source