v0.0.27 #1
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: | |
| release: | |
| types: [published] | |
| workflow_dispatch: | |
| inputs: | |
| release_tag: | |
| description: 'Release tag to upload artifacts to (e.g. v0.1.0)' | |
| required: false | |
| # Default-deny top-level permissions; each job grants only what it needs. | |
| permissions: {} | |
| jobs: | |
| # ============================================================================ | |
| # macOS ARM64 (Apple Silicon) build | |
| # ============================================================================ | |
| build-macos: | |
| name: Build macOS (Apple Silicon) | |
| runs-on: self-hosted-macos-26-arm64 # Self-hosted Apple Silicon runner with macOS 26 SDK + Metal 4 | |
| environment: packaging | |
| timeout-minutes: 90 | |
| # Block forks from invoking this workflow against the canonical self-hosted | |
| # runner pool. `workflow_dispatch` can be triggered from any branch the | |
| # actor has push access to, so this guard is the last line of defense | |
| # against a forked copy of the repo dispatching a build with the | |
| # canonical-org runner labels. | |
| if: github.repository == 'lablup/mlxcel' | |
| permissions: | |
| contents: write | |
| env: | |
| MACOSX_DEPLOYMENT_TARGET: "14.0" | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| with: | |
| # Don't leave GITHUB_TOKEN in .git/config; release jobs only fetch. | |
| persist-credentials: false | |
| # Use persistent cache paths for self-hosted runner (outside workspace) | |
| - name: Setup persistent cache paths (self-hosted) | |
| run: | | |
| # Set persistent cargo target directory to enable incremental builds | |
| # on self-hosted runner (avoids recompiling from scratch every run) | |
| CARGO_TARGET="$HOME/.cargo-target/mlxcel" | |
| mkdir -p "$CARGO_TARGET" | |
| echo "CARGO_TARGET_DIR=$CARGO_TARGET" >> $GITHUB_ENV | |
| # Prune stale cargo target cache if older than 7 days | |
| # Prevents unbounded disk growth from accumulated build artifacts | |
| MARKER="$CARGO_TARGET/.last-clean" | |
| SHOULD_CLEAN=false | |
| if [ ! -f "$MARKER" ]; then | |
| SHOULD_CLEAN=true | |
| elif [ "$(find "$MARKER" -mtime +7 2>/dev/null)" ]; then | |
| SHOULD_CLEAN=true | |
| fi | |
| if [ "$SHOULD_CLEAN" = true ]; then | |
| echo "Cargo target cache older than 7 days — cleaning" | |
| rm -rf "$CARGO_TARGET" | |
| mkdir -p "$CARGO_TARGET" | |
| date -u +"%Y-%m-%dT%H:%M:%SZ" > "$MARKER" | |
| else | |
| AGE_DAYS=$(( ($(date +%s) - $(stat -f %m "$MARKER")) / 86400 )) | |
| echo "Cargo target cache age: ${AGE_DAYS}d — keeping" | |
| fi | |
| - name: Ensure build tools | |
| run: | | |
| # cmake is required by mlxcel-core and sentencepiece-sys build scripts | |
| if ! command -v cmake &>/dev/null; then | |
| echo "cmake not found, installing via Homebrew..." | |
| brew install cmake | |
| fi | |
| echo "cmake: $(cmake --version | head -1)" | |
| echo "git: $(git --version)" | |
| - name: Verify build environment | |
| run: | | |
| echo "=== macOS ===" | |
| sw_vers | |
| echo "" | |
| echo "=== Xcode SDK ===" | |
| xcrun --show-sdk-version | |
| echo "" | |
| echo "=== Metal version ===" | |
| echo "__METAL_VERSION__" | xcrun -sdk macosx metal -E -x metal -P - | tail -1 | |
| echo "" | |
| echo "=== Metal compiler ===" | |
| xcrun -sdk macosx metal --version | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: aarch64-apple-darwin | |
| - name: Build release binaries | |
| run: cargo build --release --target aarch64-apple-darwin --locked | |
| env: | |
| RUSTFLAGS: "-C target-cpu=apple-m1" | |
| - name: Prepare signing certificate (rcodesign) | |
| env: | |
| APPLE_CERTIFICATE: ${{ secrets.DEV_ID_CERT_P12 }} | |
| APPLE_CERTIFICATE_PASSWORD: ${{ secrets.DEV_ID_CERT_PASSWORD }} | |
| run: | | |
| # Use rcodesign instead of Apple's codesign to bypass macOS 26 keychain | |
| # session issues on self-hosted runners. rcodesign reads PEM directly — | |
| # no keychain, no SecurityAgent, no GUI session required. | |
| echo "$APPLE_CERTIFICATE" | base64 --decode > $RUNNER_TEMP/original.p12 | |
| # Extract to unencrypted PEM (cert + private key) using -legacy flag | |
| # for p12 files with legacy RC2-40-CBC encryption. | |
| openssl pkcs12 -in $RUNNER_TEMP/original.p12 \ | |
| -passin "pass:$APPLE_CERTIFICATE_PASSWORD" \ | |
| -legacy -nodes -out $RUNNER_TEMP/signing.pem 2>/dev/null | |
| rm -f $RUNNER_TEMP/original.p12 | |
| if [ ! -s "$RUNNER_TEMP/signing.pem" ]; then | |
| echo "::error::Failed to extract certificate from p12" | |
| exit 1 | |
| fi | |
| echo "=== Certificate details ===" | |
| openssl x509 -in $RUNNER_TEMP/signing.pem -noout -subject -dates 2>/dev/null || true | |
| echo "PEM_FILE=$RUNNER_TEMP/signing.pem" >> $GITHUB_ENV | |
| - name: Install rcodesign | |
| run: | | |
| RCODESIGN_VERSION="0.29.0" | |
| RCODESIGN_DIR="$HOME/.rcodesign" | |
| RCODESIGN_BIN="$RCODESIGN_DIR/rcodesign" | |
| if [ -f "$RCODESIGN_BIN" ] && "$RCODESIGN_BIN" --version 2>/dev/null | grep -q "$RCODESIGN_VERSION"; then | |
| echo "rcodesign $RCODESIGN_VERSION already installed" | |
| else | |
| echo "Installing rcodesign $RCODESIGN_VERSION..." | |
| mkdir -p "$RCODESIGN_DIR" | |
| curl -sL "https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F${RCODESIGN_VERSION}/apple-codesign-${RCODESIGN_VERSION}-aarch64-apple-darwin.tar.gz" \ | |
| | tar xz -C "$RCODESIGN_DIR" --strip-components=1 | |
| fi | |
| echo "$RCODESIGN_DIR" >> $GITHUB_PATH | |
| - name: Code sign macOS binaries | |
| run: | | |
| BIN_DIR="$CARGO_TARGET_DIR/aarch64-apple-darwin/release" | |
| for BIN in mlxcel mlxcel-server; do | |
| echo "Signing $BIN..." | |
| rcodesign sign \ | |
| --pem-file "$PEM_FILE" \ | |
| --code-signature-flags runtime \ | |
| "$BIN_DIR/$BIN" | |
| done | |
| - name: Locate mlx.metallib | |
| run: | | |
| METALLIB=$(find "$CARGO_TARGET_DIR/aarch64-apple-darwin/release/build/mlxcel-core-"*/out/build/lib -name "mlx.metallib" 2>/dev/null | head -1) | |
| if [ -z "$METALLIB" ]; then | |
| echo "::error::mlx.metallib not found in build output" | |
| exit 1 | |
| fi | |
| echo "METALLIB_PATH=$METALLIB" >> $GITHUB_ENV | |
| echo "Found mlx.metallib at: $METALLIB ($(du -h "$METALLIB" | cut -f1))" | |
| - name: Package binaries | |
| run: | | |
| BIN_DIR="$CARGO_TARGET_DIR/aarch64-apple-darwin/release" | |
| mkdir -p package | |
| cp "$BIN_DIR/mlxcel" package/ | |
| cp "$BIN_DIR/mlxcel-server" package/ | |
| cp "$METALLIB_PATH" package/mlx.metallib | |
| ditto -c -k --sequesterRsrc package mlxcel-macos-aarch64.zip | |
| - name: Generate checksum | |
| run: shasum -a 256 mlxcel-macos-aarch64.zip > mlxcel-macos-aarch64.zip.sha256 | |
| - name: Upload release artifacts | |
| if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' | |
| uses: softprops/action-gh-release@v3 | |
| with: | |
| tag_name: ${{ github.event.release.tag_name || github.event.inputs.release_tag }} | |
| files: | | |
| mlxcel-macos-aarch64.zip | |
| mlxcel-macos-aarch64.zip.sha256 | |
| # ============================================================================ | |
| # Linux ARM64 + CUDA builds (self-hosted GB10 runner) | |
| # ============================================================================ | |
| build-linux-cuda: | |
| name: Build Linux ARM64 CUDA (${{ matrix.variant }}) | |
| runs-on: GB10 | |
| environment: packaging | |
| if: github.repository == 'lablup/mlxcel' | |
| permissions: | |
| contents: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # NVIDIA DGX Spark / GB10 (Blackwell, SM 121) | |
| - variant: gb10 | |
| cuda_arch: "121" | |
| asset_name: mlxcel-linux-aarch64-cuda13-gb10 | |
| # NVIDIA GH200 (Grace Hopper, SM 90a) — cross-compiled on GB10 runner | |
| - variant: gh200 | |
| cuda_arch: "90a" | |
| asset_name: mlxcel-linux-aarch64-cuda13-gh200 | |
| timeout-minutes: 120 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| with: | |
| # Don't leave GITHUB_TOKEN in .git/config; release jobs only fetch. | |
| persist-credentials: false | |
| # Use persistent cache paths for self-hosted runner (outside workspace) | |
| - name: Setup persistent cache paths (self-hosted) | |
| run: | | |
| # Set persistent cargo target directory to enable incremental builds | |
| # on self-hosted runner (avoids recompiling from scratch every run) | |
| CARGO_TARGET="$HOME/.cargo-target/mlxcel-cuda-${{ matrix.variant }}" | |
| mkdir -p "$CARGO_TARGET" | |
| echo "CARGO_TARGET_DIR=$CARGO_TARGET" >> $GITHUB_ENV | |
| # Prune stale cargo target cache if older than 7 days | |
| MARKER="$CARGO_TARGET/.last-clean" | |
| SHOULD_CLEAN=false | |
| if [ ! -f "$MARKER" ]; then | |
| SHOULD_CLEAN=true | |
| elif [ "$(find "$MARKER" -mtime +7 2>/dev/null)" ]; then | |
| SHOULD_CLEAN=true | |
| fi | |
| if [ "$SHOULD_CLEAN" = true ]; then | |
| echo "Cargo target cache older than 7 days — cleaning" | |
| rm -rf "$CARGO_TARGET" | |
| mkdir -p "$CARGO_TARGET" | |
| date -u +"%Y-%m-%dT%H:%M:%SZ" > "$MARKER" | |
| else | |
| AGE_DAYS=$(( ($(date +%s) - $(stat -c %Y "$MARKER")) / 86400 )) | |
| echo "Cargo target cache age: ${AGE_DAYS}d — keeping" | |
| fi | |
| - name: Validate MLX build cache | |
| env: | |
| # Must match GIT_TAG in src/lib/mlx-cpp/CMakeLists.txt and | |
| # MLX_EXPECTED_COMMIT in src/lib/mlxcel-core/build.rs | |
| MLX_EXPECTED_COMMIT: "84961223c02925bef6bef95d3a0a046779bde935" | |
| run: | | |
| # Check every _deps directory for a valid .mlx-build-commit marker. | |
| # If the marker is missing or doesn't match, purge that _deps/ entirely. | |
| # This ensures stale MLX source/objects never survive across MLX upgrades. | |
| FOUND=0 | |
| PURGED=0 | |
| for deps_dir in $(find "$CARGO_TARGET_DIR" -path "*/_deps" -type d 2>/dev/null); do | |
| FOUND=$((FOUND + 1)) | |
| MARKER="$deps_dir/.mlx-build-commit" | |
| if [ -f "$MARKER" ] && [ "$(cat "$MARKER")" = "$MLX_EXPECTED_COMMIT" ]; then | |
| echo "Valid cache: $deps_dir" | |
| else | |
| echo "Stale cache (marker=$(cat "$MARKER" 2>/dev/null || echo 'missing')): purging $deps_dir" | |
| rm -rf "$deps_dir" | |
| PURGED=$((PURGED + 1)) | |
| fi | |
| done | |
| # Also clean workspace target/ from any previous actions/cache restore | |
| for deps_dir in $(find target -path "*/_deps" -type d 2>/dev/null); do | |
| rm -rf "$deps_dir" | |
| PURGED=$((PURGED + 1)) | |
| done | |
| echo "Checked $FOUND cache(s), purged $PURGED" | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Build release binaries | |
| run: cargo build --release --features cuda --locked | |
| env: | |
| MLX_CUDA_ARCHITECTURES: ${{ matrix.cuda_arch }} | |
| - name: Package binaries | |
| run: | | |
| BIN_DIR="$CARGO_TARGET_DIR/release" | |
| mkdir -p package | |
| cp "$BIN_DIR/mlxcel" package/ | |
| cp "$BIN_DIR/mlxcel-server" package/ | |
| cd package && zip -r "../${{ matrix.asset_name }}.zip" . && cd .. | |
| - name: Generate checksum | |
| run: sha256sum "${{ matrix.asset_name }}.zip" > "${{ matrix.asset_name }}.zip.sha256" | |
| - name: Upload release artifacts | |
| if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' | |
| uses: softprops/action-gh-release@v3 | |
| with: | |
| tag_name: ${{ github.event.release.tag_name || github.event.inputs.release_tag }} | |
| files: | | |
| ${{ matrix.asset_name }}.zip | |
| ${{ matrix.asset_name }}.zip.sha256 | |
| # ============================================================================ | |
| # Microsoft Teams release notification (Power Automate Workflows webhook) | |
| # ============================================================================ | |
| notify-teams: | |
| name: Notify Teams on release | |
| needs: [build-macos, build-linux-cuda] | |
| if: github.repository == 'lablup/mlxcel' && github.event_name == 'release' | |
| runs-on: ubuntu-latest | |
| permissions: {} | |
| steps: | |
| - name: Build Adaptive Card payload | |
| env: | |
| TAG: ${{ github.event.release.tag_name }} | |
| NAME: ${{ github.event.release.name }} | |
| URL: ${{ github.event.release.html_url }} | |
| BODY: ${{ github.event.release.body }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| TRIMMED=$(printf '%s' "$BODY" | head -c 2000) | |
| jq -n \ | |
| --arg tag "$TAG" --arg name "$NAME" \ | |
| --arg url "$URL" --arg body "$TRIMMED" --arg repo "$REPO" ' | |
| { | |
| type: "message", | |
| attachments: [{ | |
| contentType: "application/vnd.microsoft.card.adaptive", | |
| content: { | |
| "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", | |
| type: "AdaptiveCard", | |
| version: "1.5", | |
| body: [ | |
| { type: "TextBlock", size: "Large", weight: "Bolder", | |
| text: ("🚀 " + $repo + " " + $tag + " released") }, | |
| { type: "TextBlock", text: $name, wrap: true, isSubtle: true }, | |
| { type: "TextBlock", text: $body, wrap: true } | |
| ], | |
| actions: [ | |
| { type: "Action.OpenUrl", title: "View release", url: $url } | |
| ] | |
| } | |
| }] | |
| }' > card.json | |
| - name: POST to Teams workflow | |
| if: env.WEBHOOK_URL != '' | |
| env: | |
| WEBHOOK_URL: ${{ secrets.TEAMS_RELEASE_NOTIFICATION_WORKFLOW_URL }} | |
| run: | | |
| curl -sSf -X POST \ | |
| -H "Content-Type: application/json" \ | |
| --data-binary @card.json \ | |
| "$WEBHOOK_URL" | |
| # ============================================================================ | |
| # Promote pre-release to full release after all jobs succeed | |
| # ============================================================================ | |
| promote-release: | |
| name: Promote to full release | |
| needs: [build-macos, build-linux-cuda] | |
| if: always() && github.repository == 'lablup/mlxcel' && github.event_name == 'release' && github.event.release.prerelease && needs.build-macos.result == 'success' && needs.build-linux-cuda.result == 'success' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Mark release as non-prerelease | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| gh release edit "${{ github.event.release.tag_name }}" \ | |
| --repo "${{ github.repository }}" \ | |
| --prerelease=false |