diff --git a/.github/workflows/modctl-action.yml b/.github/workflows/modctl-action.yml new file mode 100644 index 00000000..fc7fdb8b --- /dev/null +++ b/.github/workflows/modctl-action.yml @@ -0,0 +1,126 @@ +name: modctl Action + +on: + push: + branches: [main, release-*] + paths: + - action.yml + - README.md + - docs/getting-started.md + - .github/workflows/modctl-action.yml + pull_request: + branches: [main, release-*] + paths: + - action.yml + - README.md + - docs/getting-started.md + - .github/workflows/modctl-action.yml + +permissions: + contents: read + +jobs: + action-build: + name: "Action Build (version: ${{ matrix.modctl_version_label }})" + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - modctl_version: "" + modctl_version_label: latest + - modctl_version: "0.2.0" + modctl_version_label: "0.2.0" + steps: + - name: Checkout code + uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v4.3.1 + + - name: Prepare fixture + run: | + set -euo pipefail + fixture_dir="${RUNNER_TEMP}/modctl-action-fixture" + mkdir -p "${fixture_dir}" + + cat > "${fixture_dir}/Modelfile" << 'EOF' + NAME tiny-model + ARCH transformer + FAMILY tiny + FORMAT safetensors + PARAMSIZE 1 + PRECISION fp16 + CONFIG config.json + MODEL model.safetensors + DOC README.md + EOF + + printf '{}' > "${fixture_dir}/config.json" + printf 'tiny-weights' > "${fixture_dir}/model.safetensors" + printf '# tiny fixture\n' > "${fixture_dir}/README.md" + + artifact_ref="ghcr.io/modelpack/modctl-action-local:${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${{ matrix.modctl_version_label }}" + echo "FIXTURE_DIR=${fixture_dir}" >> "${GITHUB_ENV}" + echo "ARTIFACT_REF=${artifact_ref}" >> "${GITHUB_ENV}" + + - name: Run modctl action + uses: ./ + with: + modctl_version: ${{ matrix.modctl_version }} + modelfile_path: ${{ env.FIXTURE_DIR }}/Modelfile + artifact_name: ${{ env.ARTIFACT_REF }} + context_path: ${{ env.FIXTURE_DIR }} + + - name: Verify artifact exists locally + run: | + set -euo pipefail + modctl inspect "${ARTIFACT_REF}" + + action-registry-login: + name: Action Registry Login Path + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout code + uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v4.3.1 + + - name: Prepare fixture + run: | + set -euo pipefail + fixture_dir="${RUNNER_TEMP}/modctl-action-fixture-login" + mkdir -p "${fixture_dir}" + + cat > "${fixture_dir}/Modelfile" << 'EOF' + NAME tiny-model + ARCH transformer + FAMILY tiny + FORMAT safetensors + PARAMSIZE 1 + PRECISION fp16 + CONFIG config.json + MODEL model.safetensors + DOC README.md + EOF + + printf '{}' > "${fixture_dir}/config.json" + printf 'tiny-weights' > "${fixture_dir}/model.safetensors" + printf '# tiny fixture\n' > "${fixture_dir}/README.md" + + artifact_ref="ghcr.io/modelpack/modctl-action-login:${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" + echo "FIXTURE_DIR=${fixture_dir}" >> "${GITHUB_ENV}" + echo "ARTIFACT_REF=${artifact_ref}" >> "${GITHUB_ENV}" + + - name: Run modctl action with optional registry integration + uses: ./ + with: + modelfile_path: ${{ env.FIXTURE_DIR }}/Modelfile + artifact_name: ${{ env.ARTIFACT_REF }} + context_path: ${{ env.FIXTURE_DIR }} + registry: ghcr.io + registry_username: ${{ github.actor }} + registry_password: ${{ github.token }} + + - name: Verify artifact exists locally + run: | + set -euo pipefail + modctl inspect "${ARTIFACT_REF}" diff --git a/README.md b/README.md index 02e5aca3..0a6c0836 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,21 @@ It offers commands such as `build`, `pull`, `push`, and more, making it easy for You can find the full documentation on the [getting started](./docs/getting-started.md). +## GitHub Action + +Use the built-in action to install `modctl` and build a model artifact in GitHub Actions: + +```yaml +- name: Build model artifact + uses: modelpack/modctl@main + with: + artifact_name: ghcr.io/${{ github.repository_owner }}/my-model:latest + modelfile_path: ./Modelfile + context_path: . +``` + +For full inputs, optional version pinning, and optional registry integration, see [GitHub Action usage](./docs/getting-started.md#github-action). + ## Copyright Copyright © contributors to ModelPack, established as ModelPack a Series of LF Projects, LLC. diff --git a/action.yml b/action.yml new file mode 100644 index 00000000..f1c253c7 --- /dev/null +++ b/action.yml @@ -0,0 +1,219 @@ +name: "modctl build" +description: "Install modctl and build a model artifact, with optional registry login." +author: "ModelPack" + +inputs: + modctl_version: + description: "modctl version to install (for example: 0.2.0 or v0.2.0). Empty means latest release." + required: false + default: "" + modelfile_path: + description: "Path to the Modelfile used by modctl build." + required: false + default: "Modelfile" + artifact_name: + description: "Model artifact reference passed to modctl build --target." + required: true + context_path: + description: "Build context path passed as the final modctl build argument." + required: false + default: "." + output_remote: + description: "Set to true to pass --output-remote to modctl build." + required: false + default: "false" + plain_http: + description: "Set to true to use plain HTTP for optional login and build operations." + required: false + default: "false" + insecure: + description: "Set to true to disable TLS verification for optional login and build operations." + required: false + default: "false" + registry: + description: "Optional registry host for modctl login (for example: ghcr.io)." + required: false + default: "" + registry_username: + description: "Optional registry username for modctl login." + required: false + default: "" + registry_password: + description: "Optional registry password or token for modctl login." + required: false + default: "" + +outputs: + modctl-version: + description: "Installed modctl version without the leading v prefix." + value: ${{ steps.install.outputs.modctl-version }} + +runs: + using: "composite" + steps: + - name: Validate action inputs + shell: bash + env: + ARTIFACT_NAME: ${{ inputs.artifact_name }} + MODELFILE_PATH: ${{ inputs.modelfile_path }} + CONTEXT_PATH: ${{ inputs.context_path }} + OUTPUT_REMOTE: ${{ inputs.output_remote }} + PLAIN_HTTP: ${{ inputs.plain_http }} + INSECURE: ${{ inputs.insecure }} + REGISTRY: ${{ inputs.registry }} + REGISTRY_USERNAME: ${{ inputs.registry_username }} + REGISTRY_PASSWORD: ${{ inputs.registry_password }} + run: | + set -euo pipefail + + validate_bool() { + local name="$1" + local value="$2" + if [[ "$value" != "true" && "$value" != "false" ]]; then + echo "Input '${name}' must be 'true' or 'false', got '${value}'." >&2 + exit 1 + fi + } + + if [[ -z "${ARTIFACT_NAME}" ]]; then + echo "Input 'artifact_name' is required." >&2 + exit 1 + fi + + if [[ ! -f "${MODELFILE_PATH}" ]]; then + echo "Modelfile not found at '${MODELFILE_PATH}'." >&2 + exit 1 + fi + + if [[ ! -e "${CONTEXT_PATH}" ]]; then + echo "Context path '${CONTEXT_PATH}' does not exist." >&2 + exit 1 + fi + + validate_bool "output_remote" "${OUTPUT_REMOTE}" + validate_bool "plain_http" "${PLAIN_HTTP}" + validate_bool "insecure" "${INSECURE}" + + if [[ -n "${REGISTRY}" || -n "${REGISTRY_USERNAME}" || -n "${REGISTRY_PASSWORD}" ]]; then + if [[ -z "${REGISTRY}" || -z "${REGISTRY_USERNAME}" || -z "${REGISTRY_PASSWORD}" ]]; then + echo "Optional registry integration requires 'registry', 'registry_username', and 'registry_password' together." >&2 + exit 1 + fi + fi + + - name: Install modctl + id: install + shell: bash + env: + GH_TOKEN: ${{ github.token }} + MODCTL_VERSION: ${{ inputs.modctl_version }} + run: | + set -euo pipefail + + if [[ -z "${MODCTL_VERSION}" ]]; then + if ! command -v gh >/dev/null 2>&1; then + echo "GitHub CLI 'gh' is required to resolve the latest modctl release." >&2 + exit 1 + fi + release_tag="$(gh api repos/modelpack/modctl/releases/latest --jq '.tag_name')" + if [[ -z "${release_tag}" || "${release_tag}" == "null" ]]; then + echo "Unable to resolve latest modctl release tag via gh api." >&2 + exit 1 + fi + else + if [[ "${MODCTL_VERSION}" == v* ]]; then + release_tag="${MODCTL_VERSION}" + else + release_tag="v${MODCTL_VERSION}" + fi + fi + + version="${release_tag#v}" + + case "$(uname -s)" in + Linux) os="linux" ;; + Darwin) os="darwin" ;; + *) + echo "Unsupported runner OS: $(uname -s)" >&2 + exit 1 + ;; + esac + + case "$(uname -m)" in + x86_64|amd64) arch="amd64" ;; + arm64|aarch64) arch="arm64" ;; + *) + echo "Unsupported runner architecture: $(uname -m)" >&2 + exit 1 + ;; + esac + + asset="modctl-${version}-${os}-${arch}.tar.gz" + download_url="https://github.com/modelpack/modctl/releases/download/${release_tag}/${asset}" + tmp_dir="$(mktemp -d)" + trap 'rm -rf "$tmp_dir"' EXIT + + curl -fsSL "$download_url" -o "${tmp_dir}/${asset}" + tar -xzf "${tmp_dir}/${asset}" -C "$tmp_dir" + + if [[ ! -f "${tmp_dir}/modctl" ]]; then + echo "Downloaded release archive did not contain a modctl binary." >&2 + exit 1 + fi + + install_dir="${RUNNER_TEMP}/modctl-bin" + mkdir -p "$install_dir" + mv "${tmp_dir}/modctl" "${install_dir}/modctl" + chmod +x "${install_dir}/modctl" + + echo "$install_dir" >> "$GITHUB_PATH" + echo "modctl-version=${version}" >> "$GITHUB_OUTPUT" + + - name: Login to registry + if: ${{ inputs.registry != '' && inputs.registry_username != '' && inputs.registry_password != '' }} + shell: bash + env: + REGISTRY: ${{ inputs.registry }} + REGISTRY_USERNAME: ${{ inputs.registry_username }} + REGISTRY_PASSWORD: ${{ inputs.registry_password }} + PLAIN_HTTP: ${{ inputs.plain_http }} + INSECURE: ${{ inputs.insecure }} + run: | + set -euo pipefail + + login_cmd=(modctl login -u "${REGISTRY_USERNAME}" -p "${REGISTRY_PASSWORD}") + if [[ "${PLAIN_HTTP}" == "true" ]]; then + login_cmd+=(--plain-http) + fi + if [[ "${INSECURE}" == "true" ]]; then + login_cmd+=(--insecure) + fi + login_cmd+=("${REGISTRY}") + + "${login_cmd[@]}" + + - name: Build model artifact + shell: bash + env: + MODELFILE_PATH: ${{ inputs.modelfile_path }} + ARTIFACT_NAME: ${{ inputs.artifact_name }} + CONTEXT_PATH: ${{ inputs.context_path }} + OUTPUT_REMOTE: ${{ inputs.output_remote }} + PLAIN_HTTP: ${{ inputs.plain_http }} + INSECURE: ${{ inputs.insecure }} + run: | + set -euo pipefail + + build_cmd=(modctl build -f "${MODELFILE_PATH}" -t "${ARTIFACT_NAME}") + if [[ "${OUTPUT_REMOTE}" == "true" ]]; then + build_cmd+=(--output-remote) + fi + if [[ "${PLAIN_HTTP}" == "true" ]]; then + build_cmd+=(--plain-http) + fi + if [[ "${INSECURE}" == "true" ]]; then + build_cmd+=(--insecure) + fi + build_cmd+=("${CONTEXT_PATH}") + + "${build_cmd[@]}" diff --git a/docs/getting-started.md b/docs/getting-started.md index 3c19b00a..fff977a1 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -16,6 +16,86 @@ $ make $ ./output/modctl -h ``` +## GitHub Action + +`modctl` provides a composite GitHub Action at the repository root, so you can call it as: + +```yaml +uses: modelpack/modctl@ +``` + +### Basic usage + +```yaml +name: Build model with modctl + +on: + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build model artifact + uses: modelpack/modctl@main + with: + artifact_name: ghcr.io/${{ github.repository_owner }}/my-model:latest + modelfile_path: ./Modelfile + context_path: . +``` + +### Inputs + +| Input | Required | Default | Description | +| --- | --- | --- | --- | +| `artifact_name` | Yes | N/A | Target model artifact reference for `modctl build --target`. | +| `modelfile_path` | No | `Modelfile` | Path to the Modelfile passed to `modctl build -f`. | +| `context_path` | No | `.` | Build context path argument for `modctl build`. | +| `modctl_version` | No | latest | `modctl` release version to install (supports `0.2.0` and `v0.2.0`). | +| `output_remote` | No | `false` | When `true`, adds `--output-remote` to `modctl build`. | +| `plain_http` | No | `false` | When `true`, adds `--plain-http` to optional `login` and `build`. | +| `insecure` | No | `false` | When `true`, adds `--insecure` to optional `login` and `build`. | +| `registry` | No | empty | Optional registry host used for `modctl login`. | +| `registry_username` | No | empty | Optional username for registry login. | +| `registry_password` | No | empty | Optional password/token for registry login. | + +### Optional version pinning + +```yaml +- name: Build with a specific modctl version + uses: modelpack/modctl@main + with: + modctl_version: 0.2.0 + artifact_name: ghcr.io/${{ github.repository_owner }}/my-model:latest +``` + +### Optional container registry integration + +Registry login is conditional. If any of `registry`, `registry_username`, or `registry_password` is provided, all three must be provided. + +```yaml +permissions: + contents: read + packages: write + +steps: + - uses: actions/checkout@v4 + - name: Build and push directly to a registry + uses: modelpack/modctl@main + with: + artifact_name: ghcr.io/${{ github.repository_owner }}/my-model:latest + modelfile_path: ./Modelfile + context_path: . + output_remote: "true" + registry: ghcr.io + registry_username: ${{ github.actor }} + registry_password: ${{ github.token }} + +``` + +When a dedicated action release tag is available, replace `@main` with that tag. + ## Usage ### Modelfile