Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions .github/workflows/forkdiff-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Fork Diff Check

# Fails a PR whose `fork.yaml` no longer describes this fork.
#
# The fork-diff page (https://layr-labs.github.io/mlx-swift/, rendered by
# protolambda/forkdiff from `fork.yaml`, deployed by forkdiff-pages.yml) is
# only useful while it is true, and two things make it go stale silently:
#
# 1. A rebase onto newer upstream. `base.hash` keeps pointing at the old
# upstream commit, so the page shows upstream's own changes as the fork's.
# `scripts/check_forkdiff.py` requires base.hash == merge-base(HEAD,
# upstream/main); a rebase moves the merge-base and the gate stays red
# until the hash — and the sections — are brought up to date.
# 2. A fork change nobody described. Every file in the base..HEAD diff must
# match a section glob (or a global ignore), and every glob must still
# match something.
#
# The page is also rendered here so a fork.yaml that forkdiff itself rejects
# cannot merge. See FORKDIFF.md.

on:
pull_request:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read

jobs:
analysis-up-to-date:
name: fork.yaml describes the fork
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0 # full history: merge-base with upstream, and forkdiff diffs against base.hash
# Submodules stay unchecked-out on purpose: the diff only needs the gitlinks.

- name: Fetch upstream main
run: |
git fetch --no-tags --quiet https://github.com/ml-explore/mlx-swift.git \
main:refs/remotes/upstream/main
echo "upstream/main = $(git rev-parse --short refs/remotes/upstream/main)"
echo "merge-base = $(git merge-base HEAD refs/remotes/upstream/main | cut -c1-12)"

- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"

- name: Verify fork.yaml against the real diff
run: |
python3 -m pip install --quiet pyyaml
python3 scripts/check_forkdiff.py --upstream-ref refs/remotes/upstream/main

- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
go-version: "1.24"
cache: false

- name: Render the fork-diff page
# fork.yaml names refs/heads/main; on a PR the checkout is a detached
# merge commit, so point the local branch at what we are checking.
run: |
git update-ref refs/heads/main HEAD
mkdir -p tmp/pages
go run github.com/protolambda/forkdiff@v0.1.1 \
-repo . -fork fork.yaml -out tmp/pages/index.html
echo "rendered $(wc -c < tmp/pages/index.html) bytes"

- name: Upload rendered page
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: forkdiff-page
path: tmp/pages/index.html
retention-days: 7
99 changes: 99 additions & 0 deletions .github/workflows/forkdiff-pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
name: Deploy Fork Diff

# Renders `fork.yaml` with protolambda/forkdiff and publishes the result as the
# repository's GitHub Pages site — the same setup ethereum-optimism/op-geth
# uses for its go-ethereum fork diff. Runs the analysis gate first, so a page
# that lies about the fork is never published.

on:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: "pages"
cancel-in-progress: true

jobs:
deploy:
name: Render and deploy
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0 # forkdiff diffs against base.hash, deep in history

- name: Fetch upstream main
run: |
git fetch --no-tags --quiet https://github.com/ml-explore/mlx-swift.git \
main:refs/remotes/upstream/main

- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"

- name: Refuse to publish a stale analysis
run: |
python3 -m pip install --quiet pyyaml
python3 scripts/check_forkdiff.py --upstream-ref refs/remotes/upstream/main

- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
go-version: "1.24"
cache: false

- name: Build forkdiff
run: |
mkdir -p tmp/pages
go run github.com/protolambda/forkdiff@v0.1.1 \
-repo . -fork fork.yaml -out tmp/pages/index.html
touch tmp/pages/.nojekyll

- name: Is GitHub Pages enabled?
# Pages must be switched on once by a repo admin (Settings → Pages →
# Source: "GitHub Actions"). The workflow token cannot do that itself
# (`configure-pages` enablement needs an admin PAT), so until then the
# page is built and kept as an artifact but not deployed — a notice,
# not a red run.
id: pages
env:
GH_TOKEN: ${{ github.token }}
run: |
if gh api "repos/${GITHUB_REPOSITORY}/pages" --silent 2>/dev/null; then
echo "enabled=true" >> "$GITHUB_OUTPUT"
else
echo "enabled=false" >> "$GITHUB_OUTPUT"
echo "::notice::GitHub Pages is not enabled for ${GITHUB_REPOSITORY}; built the page but skipped the deploy. Enable it once under Settings → Pages (Source: GitHub Actions) and re-run this workflow."
fi

- name: Keep the rendered page as an artifact
if: steps.pages.outputs.enabled != 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: forkdiff-page
path: tmp/pages/index.html
retention-days: 30

- name: Setup Pages
if: steps.pages.outputs.enabled == 'true'
uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0

- name: Upload artifact
if: steps.pages.outputs.enabled == 'true'
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1
with:
path: tmp/pages

- name: Deploy to GitHub Pages
if: steps.pages.outputs.enabled == 'true'
id: deployment
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5
81 changes: 81 additions & 0 deletions FORKDIFF.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Fork diff: what this fork changes, and keeping that page honest

This repository is a fork of [`ml-explore/mlx-swift`](https://github.com/ml-explore/mlx-swift).
Everything it changes relative to upstream is published as a browsable page:

**https://layr-labs.github.io/mlx-swift/**

The page is rendered by [`protolambda/forkdiff`](https://github.com/protolambda/forkdiff)
from [`fork.yaml`](fork.yaml) at the repo root, in the style of
[op-geth's go-ethereum fork diff](https://op-geth.optimism.io/). `fork.yaml`
groups the changed files into sections with a paragraph each, names the exact
upstream commit the fork sits on (`base.hash`), and lists files that are not
code (`ignore`). Every push to `main` re-renders and redeploys it
(`.github/workflows/forkdiff-pages.yml`).

**One-time setup.** GitHub Pages has to be switched on by a repo admin:
Settings → Pages → Source: **GitHub Actions**. The workflow token cannot do
this itself. Until it's done the deploy workflow still builds the page (kept
as a run artifact, `forkdiff-page`) and exits with a notice instead of failing.

## The gate

A fork-diff page is only useful while it is true, and two things make it go
stale silently. `scripts/check_forkdiff.py` runs on every PR
(`.github/workflows/forkdiff-check.yml`) and fails on both:

| Drift | Check | Why it matters |
|-------|-------|----------------|
| **Rebase onto newer upstream** | `base.hash` must equal `git merge-base HEAD upstream/main` and be an ancestor of upstream `main` | After a rebase the old base makes upstream's own commits look like fork changes. |
| **New fork change nobody described** | every path in `git diff --name-only base.hash HEAD` must match a section glob or a global `ignore` | An undescribed file is a change the page can't explain. |
| **Section describing code we no longer carry** | every glob must match at least one changed path | Stale sections are as misleading as missing ones. |

The check also renders the page, so a `fork.yaml` that forkdiff itself rejects
cannot merge. The deploy workflow runs the same check before publishing, so a
stale analysis is never served.

## Day to day

**Adding or changing fork files in a PR.** If the check lists uncovered files,
add each to the section that explains it in `fork.yaml` (or start a new
section with a short description). Files that are not code — CI, lockfiles —
go under the top-level `ignore`. Keep globs specific: a `Source/**` catch-all
would swallow upstream's changes after a bad rebase and defeat the gate. The
one broad glob here, `Source/Cmlx/mlx-generated/**`, is deliberate: that tree
is regenerated wholesale from the core pin and has no hand-written content.

**Rebasing onto newer upstream.** The gate will fail with the new merge-base
in its message:

```
git fetch https://github.com/ml-explore/mlx-swift.git main:refs/remotes/upstream/main
git merge-base HEAD refs/remotes/upstream/main # → new base.hash
```

Set `base.hash` to that value, then run the check locally and fix what it
reports — usually files upstream absorbed (stale globs to delete) and files
that moved (globs to rename):

```
python3 -m pip install pyyaml
python3 scripts/check_forkdiff.py --upstream-ref refs/remotes/upstream/main
```

**Previewing the page locally** (Go 1.21+):

```
go run github.com/protolambda/forkdiff@v0.1.1 -repo . -fork fork.yaml -out tmp/index.html
open tmp/index.html
```

## Design notes

- `base.hash` is a full 40-hex commit id, never a branch name: a symbolic base
would move underneath the page and the gate alike.
- Submodule paths (`Source/Cmlx/mlx`, `Source/Cmlx/mlx-c`) appear in the diff
as plain paths whose content is the commit pointer; they are globbed as such.
- The glob semantics are forkdiff's: `*` and `?` stop at `/`, `**` spans
directories (and may match none), `[!x]` negates a class.
- The check is pure git + PyYAML so the same command runs locally and in CI.
It is shared verbatim across the Layr-Labs MLX forks (`mlx`, `mlx-c`,
`mlx-swift`, `mlx-swift-lm`).
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

[**Installation**](#installation) | [**Documentation**](https://swiftpackageindex.com/ml-explore/mlx-swift/main/documentation/mlx) | [**Examples**](#examples)

> **This is a fork.** `Layr-Labs/mlx-swift` tracks [`ml-explore/mlx-swift`](https://github.com/ml-explore/mlx-swift) and is the Swift layer of Layr-Labs' Apple-silicon inference stack (`mlx` → `mlx-c` → `mlx-swift` → `mlx-swift-lm`). Everything changed relative to upstream is published as a fork diff at **https://layr-labs.github.io/mlx-swift/**, described in [`fork.yaml`](fork.yaml) and kept honest by CI — see [FORKDIFF.md](FORKDIFF.md).

MLX Swift is a Swift API for [MLX](https://ml-explore.github.io/mlx/build/html/index.html).

MLX is an array framework for machine learning on Apple silicon. MLX Swift
Expand Down
Loading
Loading