v1.1.6 #134
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |