434 lines
15 KiB
YAML
434 lines
15 KiB
YAML
name: Release
|
|
|
|
on:
|
|
# Fires when release-please (or a maintainer) publishes a GitHub Release.
|
|
# Using the `release` event instead of `push: tags` is necessary because
|
|
# tags created by the release-please workflow use GITHUB_TOKEN, which by
|
|
# design does not trigger downstream workflows — that caused v0.9.4 to
|
|
# ship with no binary assets (see issue #424).
|
|
release:
|
|
types: [published]
|
|
# Manual backfill: run against a specific existing tag (e.g. v0.9.4) to
|
|
# upload assets to an already-published release.
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: "Existing tag to build and attach assets to (e.g. v0.9.4)"
|
|
required: true
|
|
type: string
|
|
|
|
permissions:
|
|
contents: write
|
|
id-token: write
|
|
actions: read
|
|
|
|
env:
|
|
CARGO_TERM_COLOR: always
|
|
BINARY: llmfit
|
|
|
|
jobs:
|
|
build:
|
|
strategy:
|
|
matrix:
|
|
include:
|
|
# Linux x86_64 (static musl)
|
|
- target: x86_64-unknown-linux-musl
|
|
os: ubuntu-latest
|
|
use-cross: true
|
|
|
|
# Linux ARM64 (static musl)
|
|
- target: aarch64-unknown-linux-musl
|
|
os: ubuntu-latest
|
|
use-cross: true
|
|
|
|
# Linux x86_64 (glibc)
|
|
- target: x86_64-unknown-linux-gnu
|
|
os: ubuntu-latest
|
|
use-cross: false
|
|
|
|
# Linux ARM64 (glibc)
|
|
- target: aarch64-unknown-linux-gnu
|
|
os: ubuntu-latest
|
|
use-cross: true
|
|
|
|
# Linux RISCV64 (glibc)
|
|
- target: riscv64gc-unknown-linux-gnu
|
|
os: ubuntu-latest
|
|
use-cross: true
|
|
|
|
# macOS Intel (cross-compiled from ARM64 runner)
|
|
- target: x86_64-apple-darwin
|
|
os: macos-latest
|
|
use-cross: false
|
|
|
|
# macOS Apple Silicon
|
|
- target: aarch64-apple-darwin
|
|
os: macos-latest
|
|
use-cross: false
|
|
|
|
# Windows x86_64
|
|
- target: x86_64-pc-windows-msvc
|
|
os: windows-latest
|
|
use-cross: false
|
|
|
|
# Windows ARM64
|
|
- target: aarch64-pc-windows-msvc
|
|
os: windows-latest
|
|
use-cross: false
|
|
|
|
runs-on: ${{ matrix.os }}
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
with:
|
|
ref: ${{ inputs.tag || github.ref }}
|
|
|
|
- name: Install Rust toolchain
|
|
uses: dtolnay/rust-toolchain@stable
|
|
with:
|
|
targets: ${{ matrix.target }}
|
|
|
|
- name: Build web dashboard assets
|
|
run: |
|
|
cd llmfit-web
|
|
npm ci
|
|
npm run build
|
|
|
|
- name: Install cross
|
|
if: matrix.use-cross
|
|
run: cargo install cross --version 0.2.5
|
|
|
|
- name: Build
|
|
shell: bash
|
|
run: |
|
|
if [ "${{ matrix.use-cross }}" = "true" ]; then
|
|
cross build --release --target ${{ matrix.target }}
|
|
else
|
|
cargo build --release --target ${{ matrix.target }}
|
|
fi
|
|
|
|
- name: Package
|
|
shell: bash
|
|
run: |
|
|
TAG="${{ inputs.tag || github.ref_name }}"
|
|
ASSET_NAME="${BINARY}-${TAG}-${{ matrix.target }}"
|
|
STAGING="${RUNNER_TEMP}/${ASSET_NAME}"
|
|
mkdir -p "${STAGING}"
|
|
|
|
if [[ "${{ matrix.target }}" == *"windows"* ]]; then
|
|
EXE_EXT=".exe"
|
|
ARCHIVE_EXT=".zip"
|
|
COMPRESS_CMD="7z a ${ASSET_NAME}${ARCHIVE_EXT} ${ASSET_NAME}"
|
|
else
|
|
EXE_EXT=""
|
|
ARCHIVE_EXT=".tar.gz"
|
|
COMPRESS_CMD="tar czf ${ASSET_NAME}${ARCHIVE_EXT} ${ASSET_NAME}"
|
|
fi
|
|
|
|
cp "target/${{ matrix.target }}/release/${BINARY}${EXE_EXT}" "${STAGING}/"
|
|
cp README.md LICENSE "${STAGING}/" 2>/dev/null || true
|
|
|
|
cd "${RUNNER_TEMP}"
|
|
$COMPRESS_CMD
|
|
|
|
# Generate per-asset SHA256 checksum file (consistent format for both tools)
|
|
if command -v sha256sum >/dev/null 2>&1; then
|
|
sha256sum "${ASSET_NAME}${ARCHIVE_EXT}" | awk '{print $1 " " $2}' > "${ASSET_NAME}${ARCHIVE_EXT}.sha256"
|
|
elif command -v shasum >/dev/null 2>&1; then
|
|
shasum -a 256 "${ASSET_NAME}${ARCHIVE_EXT}" | awk '{print $1 " " $2}' > "${ASSET_NAME}${ARCHIVE_EXT}.sha256"
|
|
fi
|
|
|
|
echo "ASSET=${ASSET_NAME}${ARCHIVE_EXT}" >> "$GITHUB_ENV"
|
|
echo "ASSET_PATH=${RUNNER_TEMP}/${ASSET_NAME}${ARCHIVE_EXT}" >> "$GITHUB_ENV"
|
|
echo "CHECKSUM_PATH=${RUNNER_TEMP}/${ASSET_NAME}${ARCHIVE_EXT}.sha256" >> "$GITHUB_ENV"
|
|
|
|
- name: Upload artifact
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: ${{ env.ASSET }}
|
|
path: |
|
|
${{ env.ASSET_PATH }}
|
|
${{ env.CHECKSUM_PATH }}
|
|
|
|
# Sign Windows executables via SignPath (Authenticode).
|
|
# Uses the test-signing policy initially; switch to release-signing
|
|
# after SignPath reviews the setup and provisions the production cert.
|
|
sign-windows:
|
|
needs: build
|
|
runs-on: ubuntu-latest
|
|
strategy:
|
|
matrix:
|
|
target:
|
|
- x86_64-pc-windows-msvc
|
|
- aarch64-pc-windows-msvc
|
|
steps:
|
|
- name: Determine asset name
|
|
id: asset-name
|
|
shell: bash
|
|
run: |
|
|
TAG="${{ inputs.tag || github.ref_name }}"
|
|
ASSET="llmfit-${TAG}-${{ matrix.target }}"
|
|
echo "zip=${ASSET}.zip" >> "$GITHUB_OUTPUT"
|
|
echo "dir=${ASSET}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Download unsigned artifact
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
name: ${{ steps.asset-name.outputs.zip }}
|
|
path: unsigned
|
|
|
|
- name: Extract exe from zip
|
|
shell: bash
|
|
run: |
|
|
cd unsigned
|
|
unzip "${{ steps.asset-name.outputs.zip }}"
|
|
# Move the exe to a clean directory for signing
|
|
mkdir -p ../to-sign
|
|
cp "${{ steps.asset-name.outputs.dir }}/llmfit.exe" ../to-sign/
|
|
|
|
- name: Upload exe for SignPath
|
|
id: upload-for-signing
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: ${{ steps.asset-name.outputs.zip }}-unsigned
|
|
path: to-sign/llmfit.exe
|
|
|
|
- name: Submit signing request to SignPath
|
|
uses: signpath/github-action-submit-signing-request@v2
|
|
with:
|
|
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
|
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
|
|
project-slug: ${{ secrets.SIGNPATH_PROJECT_SLUG }}
|
|
signing-policy-slug: ${{ secrets.SIGNPATH_SIGNING_POLICY_SLUG }}
|
|
artifact-configuration-slug: ${{ secrets.SIGNPATH_ARTIFACT_CONFIGURATION_SLUG }}
|
|
github-artifact-id: ${{ steps.upload-for-signing.outputs.artifact-id }}
|
|
wait-for-completion: true
|
|
output-artifact-directory: signed
|
|
|
|
- name: Repackage signed exe into zip
|
|
shell: bash
|
|
run: |
|
|
# Replace the unsigned exe with the signed one
|
|
cp signed/llmfit.exe "unsigned/${{ steps.asset-name.outputs.dir }}/llmfit.exe"
|
|
cd unsigned
|
|
zip -r "../${{ steps.asset-name.outputs.zip }}" "${{ steps.asset-name.outputs.dir }}"
|
|
|
|
- name: Replace unsigned artifact with signed one
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: ${{ steps.asset-name.outputs.zip }}
|
|
path: ${{ steps.asset-name.outputs.zip }}
|
|
overwrite: true
|
|
|
|
- name: Regenerate checksum for signed zip
|
|
shell: bash
|
|
run: |
|
|
sha256sum "${{ steps.asset-name.outputs.zip }}" | awk '{print $1 " " $2}' > "${{ steps.asset-name.outputs.zip }}.sha256"
|
|
|
|
- name: Upload signed checksum
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: ${{ steps.asset-name.outputs.zip }}.sha256
|
|
path: ${{ steps.asset-name.outputs.zip }}.sha256
|
|
overwrite: true
|
|
|
|
release:
|
|
needs: [build, sign-windows]
|
|
# Run even if sign-windows is skipped (e.g. secrets not yet configured)
|
|
if: always() && needs.build.result == 'success'
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
- name: Download all artifacts
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
path: artifacts
|
|
|
|
- name: Create GitHub Release
|
|
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
|
|
with:
|
|
tag_name: ${{ inputs.tag || github.ref_name }}
|
|
generate_release_notes: true
|
|
files: |
|
|
artifacts/**/*.tar.gz
|
|
artifacts/**/*.zip
|
|
artifacts/**/*.sha256
|
|
|
|
publish-python:
|
|
needs: release
|
|
# Run when release succeeds, even if upstream sign-windows was skipped or failed
|
|
if: always() && needs.release.result == 'success'
|
|
runs-on: ubuntu-latest
|
|
environment:
|
|
name: pypi
|
|
url: https://pypi.org/p/llmfit/
|
|
permissions:
|
|
id-token: write
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
with:
|
|
ref: ${{ inputs.tag || github.ref }}
|
|
|
|
- name: Set up uv
|
|
uses: astral-sh/setup-uv@v8.2.0
|
|
|
|
- name: Build Python wheels
|
|
shell: bash
|
|
env:
|
|
TAG: ${{ inputs.tag || github.ref_name }}
|
|
run: |
|
|
# Each entry: python_platform_tag:rust_target:binary_name:archive_ext
|
|
# Must stay in sync with TARGET_CONFIGS in llmfit-python/hatch_build.py.
|
|
TARGETS=(
|
|
"manylinux_2_17_x86_64:x86_64-unknown-linux-gnu:llmfit:tar.gz"
|
|
"manylinux_2_17_aarch64:aarch64-unknown-linux-gnu:llmfit:tar.gz"
|
|
"manylinux_2_39_riscv64:riscv64gc-unknown-linux-gnu:llmfit:tar.gz"
|
|
"musllinux_1_2_x86_64:x86_64-unknown-linux-musl:llmfit:tar.gz"
|
|
"musllinux_1_2_aarch64:aarch64-unknown-linux-musl:llmfit:tar.gz"
|
|
"macosx_10_12_x86_64:x86_64-apple-darwin:llmfit:tar.gz"
|
|
"macosx_11_0_arm64:aarch64-apple-darwin:llmfit:tar.gz"
|
|
"win_amd64:x86_64-pc-windows-msvc:llmfit.exe:zip"
|
|
"win_arm64:aarch64-pc-windows-msvc:llmfit.exe:zip"
|
|
)
|
|
BASE_URL="https://github.com/AlexsJones/llmfit/releases/download/${TAG}"
|
|
mkdir -p dist/
|
|
for entry in "${TARGETS[@]}"; do
|
|
IFS=: read -r py_tag rust_target binary_name archive_ext <<< "$entry"
|
|
ASSET="llmfit-${TAG}-${rust_target}"
|
|
ARCHIVE="${ASSET}.${archive_ext}"
|
|
EXPECTED=$(curl -fsSL "${BASE_URL}/${ARCHIVE}.sha256" | awk '{print $1}')
|
|
ACTUAL=$(curl -fsSL "${BASE_URL}/${ARCHIVE}" | tee "${ARCHIVE}" | sha256sum | awk '{print $1}')
|
|
if [ "${ACTUAL}" != "${EXPECTED}" ]; then
|
|
echo "Checksum mismatch for ${ARCHIVE}: expected ${EXPECTED}, got ${ACTUAL}"
|
|
exit 1
|
|
fi
|
|
echo "Checksum OK for ${ARCHIVE}"
|
|
if [ "${archive_ext}" = "tar.gz" ]; then
|
|
tar -xzf "${ARCHIVE}" "${ASSET}/${binary_name}"
|
|
else
|
|
unzip "${ARCHIVE}" "${ASSET}/${binary_name}"
|
|
fi
|
|
dest="target/${rust_target}/release"
|
|
mkdir -p "${dest}"
|
|
cp "${ASSET}/${binary_name}" "${dest}/${binary_name}"
|
|
chmod +x "${dest}/${binary_name}"
|
|
rm -f "${ARCHIVE}"
|
|
rm -rf "${ASSET}"
|
|
LLMFIT_PYTHON_PLATFORM_TAG="${py_tag}" uv build --wheel --out-dir=dist/ llmfit-python/
|
|
done
|
|
|
|
- name: Publish to PyPI
|
|
uses: pypa/gh-action-pypi-publish@release/v1
|
|
|
|
publish-crate:
|
|
needs: release
|
|
if: always() && needs.release.result == 'success'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
with:
|
|
ref: ${{ inputs.tag || github.ref }}
|
|
|
|
- name: Install Rust toolchain
|
|
uses: dtolnay/rust-toolchain@stable
|
|
|
|
# Pass the registry token via env rather than --token: argv is
|
|
# visible in the process listing on the runner, and GitHub secret
|
|
# masking only covers log output. cargo reads CARGO_REGISTRY_TOKEN
|
|
# automatically.
|
|
- name: Publish llmfit-core to crates.io
|
|
env:
|
|
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
|
run: cargo publish -p llmfit-core
|
|
|
|
- name: Wait for crates.io index update
|
|
run: sleep 30
|
|
|
|
- name: Publish llmfit to crates.io
|
|
env:
|
|
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
|
run: cargo publish -p llmfit
|
|
|
|
update-homebrew:
|
|
needs: release
|
|
if: always() && needs.release.result == 'success'
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
- name: Checkout tap
|
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
with:
|
|
repository: AlexsJones/homebrew-llmfit
|
|
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
|
path: homebrew-tap
|
|
|
|
- name: Download release assets and update formula
|
|
env:
|
|
TAG: ${{ inputs.tag || github.ref_name }}
|
|
run: |
|
|
VERSION="${TAG#v}"
|
|
|
|
# Download the four tarballs and compute SHA256
|
|
declare -A SHAS
|
|
for target in aarch64-apple-darwin x86_64-apple-darwin aarch64-unknown-linux-musl x86_64-unknown-linux-musl; do
|
|
URL="https://github.com/AlexsJones/llmfit/releases/download/${TAG}/llmfit-${TAG}-${target}.tar.gz"
|
|
echo "Downloading ${URL}..."
|
|
SHA=$(curl -fsSL "$URL" | shasum -a 256 | awk '{print $1}')
|
|
SHAS[$target]="$SHA"
|
|
echo "${target}: ${SHA}"
|
|
done
|
|
|
|
# Generate the formula
|
|
cat > homebrew-tap/Formula/llmfit.rb << RUBY
|
|
class Llmfit < Formula
|
|
desc "Terminal tool that right-sizes LLM models to your system hardware"
|
|
homepage "https://github.com/AlexsJones/llmfit"
|
|
version "${VERSION}"
|
|
license "MIT"
|
|
|
|
on_macos do
|
|
if Hardware::CPU.arm?
|
|
url "https://github.com/AlexsJones/llmfit/releases/download/v#{version}/llmfit-v#{version}-aarch64-apple-darwin.tar.gz"
|
|
sha256 "${SHAS[aarch64-apple-darwin]}"
|
|
else
|
|
url "https://github.com/AlexsJones/llmfit/releases/download/v#{version}/llmfit-v#{version}-x86_64-apple-darwin.tar.gz"
|
|
sha256 "${SHAS[x86_64-apple-darwin]}"
|
|
end
|
|
end
|
|
|
|
on_linux do
|
|
if Hardware::CPU.arm?
|
|
url "https://github.com/AlexsJones/llmfit/releases/download/v#{version}/llmfit-v#{version}-aarch64-unknown-linux-musl.tar.gz"
|
|
sha256 "${SHAS[aarch64-unknown-linux-musl]}"
|
|
else
|
|
url "https://github.com/AlexsJones/llmfit/releases/download/v#{version}/llmfit-v#{version}-x86_64-unknown-linux-musl.tar.gz"
|
|
sha256 "${SHAS[x86_64-unknown-linux-musl]}"
|
|
end
|
|
end
|
|
|
|
def install
|
|
bin.install "llmfit"
|
|
end
|
|
|
|
test do
|
|
assert_match "llmfit", shell_output("#{bin}/llmfit --help")
|
|
end
|
|
end
|
|
RUBY
|
|
|
|
# Fix heredoc indentation
|
|
sed -i 's/^ //' homebrew-tap/Formula/llmfit.rb
|
|
|
|
- name: Commit and push
|
|
run: |
|
|
cd homebrew-tap
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
git add Formula/llmfit.rb
|
|
git commit -m "Update llmfit to ${TAG}"
|
|
git push
|