|
| 1 | +name: Release NIF |
| 2 | + |
| 3 | +# Builds the precompiled NIF for each supported (variant × target) |
| 4 | +# cell and publishes the resulting tarballs. Two triggers: |
| 5 | +# |
| 6 | +# * `push` on a bare-semver tag (e.g. 0.3.0) — the publication run. |
| 7 | +# Tarballs and matching `.sha256` sidecars land on a draft GitHub |
| 8 | +# release named after the tag, at the URL mix.exs's |
| 9 | +# `compile.emily_nif` step fetches from. |
| 10 | +# |
| 11 | +# * `workflow_dispatch` — manual rebuild against any branch. Useful |
| 12 | +# for debugging or reproducing a release without cutting a new |
| 13 | +# tag. Artefacts go to workflow run storage (retention 90 days); |
| 14 | +# no GitHub release is touched. |
| 15 | +# |
| 16 | +# Consumers verify each tarball against the sidecar fetched from |
| 17 | +# the same release, so there is no checksum list to bake into |
| 18 | +# mix.exs on the maintainer side. |
| 19 | + |
| 20 | +on: |
| 21 | + push: |
| 22 | + tags: ['[0-9]+.[0-9]+.[0-9]+*'] |
| 23 | + workflow_dispatch: |
| 24 | + |
| 25 | +permissions: |
| 26 | + contents: write |
| 27 | + |
| 28 | +jobs: |
| 29 | + # Resolves the target version once and — on tag trigger only — |
| 30 | + # creates the draft release up front so the matrix cells don't race |
| 31 | + # on `gh release create`. On workflow_dispatch the release step is |
| 32 | + # a no-op; there's no tag to create one against. |
| 33 | + prepare-release: |
| 34 | + runs-on: ubuntu-latest |
| 35 | + outputs: |
| 36 | + version: ${{ steps.version.outputs.version }} |
| 37 | + steps: |
| 38 | + - uses: actions/checkout@v6 |
| 39 | + |
| 40 | + - name: Resolve version |
| 41 | + id: version |
| 42 | + run: | |
| 43 | + if [[ "${{ github.event_name }}" == "push" ]]; then |
| 44 | + VERSION="${GITHUB_REF_NAME}" |
| 45 | + else |
| 46 | + VERSION=$(grep -m1 '@version' mix.exs | awk -F'"' '{print $2}') |
| 47 | + if [[ -z "$VERSION" ]]; then |
| 48 | + echo "error: could not parse @version from mix.exs" >&2 |
| 49 | + exit 1 |
| 50 | + fi |
| 51 | + fi |
| 52 | + echo "version=$VERSION" >> "$GITHUB_OUTPUT" |
| 53 | + echo "Resolved version: $VERSION" |
| 54 | +
|
| 55 | + - name: Create or reuse draft release |
| 56 | + if: github.event_name == 'push' |
| 57 | + env: |
| 58 | + GH_TOKEN: ${{ github.token }} |
| 59 | + TAG: ${{ steps.version.outputs.version }} |
| 60 | + REPO: ${{ github.repository }} |
| 61 | + run: | |
| 62 | + if ! gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then |
| 63 | + gh release create "$TAG" \ |
| 64 | + --repo "$REPO" \ |
| 65 | + --draft \ |
| 66 | + --title "Emily $TAG" \ |
| 67 | + --notes "Precompiled NIF tarballs for macOS arm64 (AOT + JIT variants)." |
| 68 | + fi |
| 69 | +
|
| 70 | + build: |
| 71 | + needs: prepare-release |
| 72 | + strategy: |
| 73 | + fail-fast: false |
| 74 | + matrix: |
| 75 | + include: |
| 76 | + # AOT runs on macos-14 so the metallib works back to older |
| 77 | + # macOS; JIT runs on macos-26 because MLX's NAX kernel |
| 78 | + # sources transitively include |
| 79 | + # <MetalPerformancePrimitives/MetalPerformancePrimitives.h>, |
| 80 | + # first shipped in the macOS 26.2 SDK. |
| 81 | + - { variant: aot, target: macos-arm64, runs-on: macos-14 } |
| 82 | + - { variant: jit, target: macos-arm64, runs-on: macos-26 } |
| 83 | + runs-on: ${{ matrix.runs-on }} |
| 84 | + env: |
| 85 | + MIX_ENV: dev |
| 86 | + EMILY_MLX_VARIANT: ${{ matrix.variant }} |
| 87 | + VERSION: ${{ needs.prepare-release.outputs.version }} |
| 88 | + steps: |
| 89 | + - uses: actions/checkout@v6 |
| 90 | + |
| 91 | + - uses: erlef/setup-beam@v1 |
| 92 | + with: |
| 93 | + version-file: .tool-versions |
| 94 | + version-type: strict |
| 95 | + |
| 96 | + - name: Cache deps |
| 97 | + uses: actions/cache@v5 |
| 98 | + with: |
| 99 | + path: deps |
| 100 | + key: deps-${{ runner.os }}-${{ hashFiles('mix.lock') }} |
| 101 | + restore-keys: deps-${{ runner.os }}- |
| 102 | + |
| 103 | + # MLX install dir keyed by the NIF sources, Makefile, build |
| 104 | + # script, and the pinned MLX version in mix.exs. |
| 105 | + - name: Cache MLX install |
| 106 | + uses: actions/cache@v5 |
| 107 | + with: |
| 108 | + path: ~/Library/Caches/emily |
| 109 | + key: mlx-release-${{ runner.os }}-${{ matrix.variant }}-${{ hashFiles('c_src/**', 'Makefile', 'mix.exs', 'scripts/build-mlx.sh') }} |
| 110 | + restore-keys: mlx-release-${{ runner.os }}-${{ matrix.variant }}- |
| 111 | + |
| 112 | + - run: mix deps.get |
| 113 | + |
| 114 | + - run: mix compile |
| 115 | + |
| 116 | + - name: Package NIF tarball |
| 117 | + id: pack |
| 118 | + env: |
| 119 | + VARIANT: ${{ matrix.variant }} |
| 120 | + TARGET: ${{ matrix.target }} |
| 121 | + run: | |
| 122 | + ASSET="emily-nif-${VERSION}-${VARIANT}-${TARGET}.tar.gz" |
| 123 | + PRIV="_build/dev/lib/emily/priv" |
| 124 | +
|
| 125 | + if [[ ! -d "$PRIV" ]]; then |
| 126 | + echo "error: expected $PRIV after mix compile" >&2 |
| 127 | + exit 1 |
| 128 | + fi |
| 129 | +
|
| 130 | + # Flat tarball (no wrapping dir) — mix.exs's fetch_nif/1 |
| 131 | + # extracts with `tar -xzf ... -C priv` and expects files at |
| 132 | + # the top level. |
| 133 | + (cd "$PRIV" && tar czf "$GITHUB_WORKSPACE/$ASSET" libemily.* mlx.metallib) |
| 134 | +
|
| 135 | + shasum -a 256 "$ASSET" > "${ASSET}.sha256" |
| 136 | + echo "asset=$ASSET" >> "$GITHUB_OUTPUT" |
| 137 | +
|
| 138 | + { |
| 139 | + echo "## ${VARIANT}-${TARGET}" |
| 140 | + echo "" |
| 141 | + echo '```' |
| 142 | + cat "${ASSET}.sha256" |
| 143 | + echo '```' |
| 144 | + } >> "$GITHUB_STEP_SUMMARY" |
| 145 | +
|
| 146 | + # On tag push, publish to the draft GitHub release so consumers |
| 147 | + # can fetch. On workflow_dispatch, stash as workflow artefacts |
| 148 | + # for maintainer collection — no release side-effects. |
| 149 | + - name: Upload to draft release |
| 150 | + if: github.event_name == 'push' |
| 151 | + env: |
| 152 | + GH_TOKEN: ${{ github.token }} |
| 153 | + TAG: ${{ needs.prepare-release.outputs.version }} |
| 154 | + REPO: ${{ github.repository }} |
| 155 | + ASSET: ${{ steps.pack.outputs.asset }} |
| 156 | + run: gh release upload "$TAG" "$ASSET" "${ASSET}.sha256" --repo "$REPO" --clobber |
| 157 | + |
| 158 | + - name: Upload as workflow artefact |
| 159 | + if: github.event_name == 'workflow_dispatch' |
| 160 | + uses: actions/upload-artifact@v4 |
| 161 | + with: |
| 162 | + name: ${{ steps.pack.outputs.asset }} |
| 163 | + path: | |
| 164 | + ${{ steps.pack.outputs.asset }} |
| 165 | + ${{ steps.pack.outputs.asset }}.sha256 |
| 166 | + retention-days: 90 |
0 commit comments