Skip to content

Release

Release #104

Workflow file for this run

name: Release
# Auto-release after CI passes on main.
#
# This used to trigger directly on push, which meant the release built and published
# concurrently with CI Pipeline and did not depend on it — a red pipeline still shipped a
# binary. workflow_run defers the release until CI Pipeline has concluded, and the job
# guard below requires that conclusion to be success.
#
# To be clear about what this does not fix: b674 shipped a Gemma 4 regression while CI
# was green on that commit, because no job loads gemma-4-e2b-it-4bit. Gating stops a
# release when CI fails; it cannot stop one when CI passes on an untested path.
#
# The opencode job is no longer continue-on-error, so a failure there now flips CI
# Pipeline to failure and stops the release. That coupling is intentional: the job tests
# the request shape opencode sends rather than shelling out to its npm CLI, so a failure
# means SwiftLM misbehaved.
on:
workflow_dispatch:
inputs:
create_release:
description: 'Create new release'
required: true
type: boolean
workflow_run:
workflows: ["CI Pipeline"]
types: [completed]
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: write
jobs:
build-and-release:
runs-on: macos-15
# A manual dispatch always runs; an automatic one only after CI succeeded.
if: >-
github.event_name == 'workflow_dispatch'
|| github.event.workflow_run.conclusion == 'success'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for build number
submodules: recursive
# Build exactly the commit CI validated. workflow_run would otherwise check out
# the branch tip, which may already have moved on to an untested commit.
ref: ${{ github.event.workflow_run.head_sha || github.ref }}
- name: Install Metal Toolchain
run: xcodebuild -downloadComponent MetalToolchain || true
# workflow_run carries no path filter, so the old `paths:` list is enforced here.
# Without this a docs-only commit would publish a release, because CI runs on every
# push to main. Submodule pointer moves count as source: the mlx-swift-lm bump is
# how the Gemma 4 fix reached users.
- name: Check for source changes since the last release
id: changes
run: |
LAST_TAG="$(git tag --list 'b[0-9]*' --sort=-creatordate | head -1)"
if [ -z "$LAST_TAG" ]; then
echo "No previous release tag; treating as source change."
echo "source=true" >> $GITHUB_OUTPUT
exit 0
fi
CHANGED="$(git diff --name-only "$LAST_TAG"..HEAD || true)"
echo "Changed since $LAST_TAG:"; echo "$CHANGED" | head -40
if echo "$CHANGED" | grep -qE '\.(swift|c|cpp|h|hpp|m|mm|metal)$|^Package\.(swift|resolved)$|^\.github/workflows/release\.yml$|^mlx-swift(-lm)?$'; then
echo "source=true" >> $GITHUB_OUTPUT
else
echo "No source changes since $LAST_TAG — build will run, publish will be skipped."
echo "source=false" >> $GITHUB_OUTPUT
fi
- name: Determine tag name
id: tag
run: |
BUILD_NUMBER="$(git rev-list --count HEAD)"
SHORT_HASH="$(git rev-parse --short=7 HEAD)"
echo "name=b${BUILD_NUMBER}" >> $GITHUB_OUTPUT
echo "full=b${BUILD_NUMBER}-${SHORT_HASH}" >> $GITHUB_OUTPUT
echo "Build: b${BUILD_NUMBER} (${SHORT_HASH})"
- name: Generate changelog
id: changelog
run: |
# Find the previous release tag
PREV_TAG=$(git tag --sort=-creatordate | head -1 2>/dev/null || echo "")
if [ -z "$PREV_TAG" ]; then
# First release — all commits
CHANGELOG=$(git log --pretty=format:"- %s (%h)" --no-merges -20)
else
CHANGELOG=$(git log "${PREV_TAG}..HEAD" --pretty=format:"- %s (%h)" --no-merges)
fi
# Write to file (multi-line safe)
echo "$CHANGELOG" > /tmp/changelog.txt
echo "Generated changelog with $(echo "$CHANGELOG" | wc -l | tr -d ' ') entries"
- name: Cache Swift packages
uses: actions/cache@v4
with:
path: .build
key: ${{ runner.os }}-spm-release-${{ hashFiles('Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-release-
- name: Build (Release)
run: |
swift package resolve
swift build -c release
- name: Install MLX Metal library
run: |
python3 -m venv /tmp/mlx_venv
/tmp/mlx_venv/bin/pip install --quiet mlx
MLX_LIB=$(find /tmp/mlx_venv -name "mlx.metallib" | head -n 1)
cp "$MLX_LIB" .build/release/mlx.metallib
- name: Verify binary
run: |
ls -lh .build/release/SwiftLM
file .build/release/SwiftLM
.build/release/SwiftLM --help || true
- name: Package binary
run: |
mkdir -p release
cp .build/release/SwiftLM release/
cp .build/release/mlx.metallib release/
cp LICENSE README.md release/
cd release
# Verify the tarball will be self-contained before archiving
ls -lh
tar -czvf ../SwiftLM-${{ steps.tag.outputs.name }}-macos-arm64.tar.gz .
- name: Build macOS DMG Wrapper
run: |
# Previously hardcoded 1.0/1 inside generate_xcodeproj.py, so every
# release ever built reported the same app version regardless of
# which commit produced it. SWIFTBUDDY_BUILD_NUMBER reuses the same
# `git rev-list --count HEAD` counter this workflow's bNNN release
# tags already use (step "Determine tag name" above), so the app's
# build number and its release tag always agree. Marketing version
# is date-based (always distinct, no manual "when do we bump this"
# decision needed for a pipeline that releases roughly daily).
export SWIFTBUDDY_MARKETING_VERSION="$(date -u +%Y.%m.%d)"
export SWIFTBUDDY_BUILD_NUMBER="$(git rev-list --count HEAD)"
echo "App version: ${SWIFTBUDDY_MARKETING_VERSION} (${SWIFTBUDDY_BUILD_NUMBER})"
cd SwiftBuddy && python3 generate_xcodeproj.py
cd ..
xcodebuild clean build \
-project SwiftBuddy/SwiftBuddy.xcodeproj \
-scheme SwiftBuddy \
-destination "generic/platform=macOS" \
-configuration Release \
ARCHS=arm64 ONLY_ACTIVE_ARCH=NO \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGN_ENTITLEMENTS="" \
CODE_SIGNING_ALLOWED=NO \
TARGET_BUILD_DIR="$RUNNER_TEMP/build" \
BUILT_PRODUCTS_DIR="$RUNNER_TEMP/build"
brew install create-dmg
chmod +x scripts/build_dmg.sh
./scripts/build_dmg.sh "$RUNNER_TEMP/build/SwiftBuddy.app"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: SwiftLM-${{ steps.tag.outputs.name }}-macos-arm64
path: SwiftLM-${{ steps.tag.outputs.name }}-macos-arm64.tar.gz
retention-days: 90
- name: Prepare release notes
id: notes
run: |
CHANGELOG=$(cat /tmp/changelog.txt)
cat > /tmp/release_notes.md << 'RELEASE_EOF'
## SwiftLM ${{ steps.tag.outputs.full }}
<details open>
${{ github.event.head_commit.message }}
</details>
### Changelog
RELEASE_EOF
cat /tmp/changelog.txt >> /tmp/release_notes.md
cat >> /tmp/release_notes.md << 'RELEASE_EOF'
### Download
- **CLI Server**: [macOS Apple Silicon (arm64)](https://github.com/SharpAI/SwiftLM/releases/download/${{ steps.tag.outputs.name }}/SwiftLM-${{ steps.tag.outputs.name }}-macos-arm64.tar.gz)
- **GUI Desktop App**: Download the attached DMG file below!
### Quick Start
**For GUI Users (SwiftBuddy)**:
1. Download the attached DMG and open it.
2. Drag `SwiftBuddy.app` into your Applications folder natively or run directly.
3. When launched, click "Model Options" to select or download an MLX local model to chat with.
**For CLI Users (SwiftLM)**:
Please refer to the [Getting Started](https://github.com/SharpAI/SwiftLM#getting-started) section in the README.
> **Note:** `mlx.metallib` is bundled in the tar archive. Keep it in the same directory as the `SwiftLM` binary — Metal GPU compute will fail if it is missing.
RELEASE_EOF
- name: Create release
if: >-
${{ (github.event_name == 'workflow_run' && steps.changes.outputs.source == 'true')
|| github.event.inputs.create_release == 'true' }}
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.tag.outputs.name }}
name: "SwiftLM ${{ steps.tag.outputs.name }}"
body_path: /tmp/release_notes.md
files: |
SwiftLM-${{ steps.tag.outputs.name }}-macos-arm64.tar.gz
output/*.dmg
draft: false
prerelease: false