Skip to content

Release bundles, smoke test, Homebrew formula and release workflow - #36

Merged
krishhgg merged 11 commits into
mainfrom
packaging/3-release
Oct 6, 2026
Merged

krishhgg merged 11 commits into
mainfrom
packaging/3-release

Conversation

@krishhgg

@krishhgg krishhgg commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

PR 3 of 4 for packaging. PRs 1 (#34) and 2 (#35) are merged, so this targets main. It builds the release bundles, tests them, renders the Homebrew formula and adds the workflow that publishes a release from a tag.

What a user gets

  • 2 bundles, picked by the formula. brew install picks one from MacOS.full_version.
    • The macOS 26 bundle (66.9 MB) is for macOS 26.2 and later. Its MLX build has the neural-accelerator kernels that make sys1d's round 3 defaults fast.
    • The macOS 14 bundle (45.1 MB) covers macOS 14.0 through 26.1 with the standard MLX build.
  • Bundle layout. bin/sys1rust, lib/ with libmlx.dylib, libjaccl.dylib and mlx.metallib from the official mlx-metal 0.32.2 wheels, plus LICENSE, NOTICE and licenses/. The binary finds its libraries through @executable_path/../lib, so it runs through Homebrew's bin symlink.
  • License notices. licenses/ carries the Rust crate licenses, MLX's license and 16 pinned texts for third-party code compiled into the MLX libraries. Among them are nlohmann/json, gguflib, metal-cpp, PocketFFT and PyTorch's fp8 conversion. NOTICE names each text and explains the 2 exclusions. fmt's license exempts object code, and ThreadPool's zlib terms ask for a notice only in source copies.
  • A brew services entry that runs sys1rust serve at login.

What build.sh checks

packaging/build.sh macos26|macos14 OUT fails unless:

  • Rust matches packaging/rust-toolchain-version (1.95.0).
  • The wheel matches its sha256 in packaging/mlx-wheels.lock.
  • Every vendored license text exists and NOTICE names it.
  • The binary links only into the bundle and the OS, with exactly 1 rpath.
  • No Mach-O file needs a newer macOS than the bundle's target.
  • codesign --verify passes.
  • --version prints the expected string.

The smoke test

packaging/smoke.py starts the bundle's serve --offline --port 0 and sends the 24 smoke requests (120 questions). It compares each answer with bench/reference/typed-decisions/smoke.jsonl using bench/harness/compare.py's rules. Each reply must match the reference's shape, as in live.rs: the same question ids, answer fields, probability labels and legend, in order. Probabilities and confidences must be finite and in [0, 1], and confidences must stay within the same 0.05 drift limit. It also requires a clean shutdown on SIGINT, and it never leaves a server running, even when interrupted during startup.

On a base M5, both bundles had 0 failures. The largest probability drift was 0.0008 (macOS 26 bundle) and 0.0009 (macOS 14 bundle).

Homebrew

  • packaging/sys1rust.rb.in plus render_formula.sh render the formula for a tag and the 2 tarball hashes.
  • A local tap install from a rendered formula passed brew install, brew test and the smoke test through Homebrew's path.
  • brew style and brew audit --strict --online are clean.

The release workflow

.github/workflows/release.yml runs on v* tags. A pull request that touches packaging/ or the workflow runs only the build and smoke jobs.

  • build (macos-26) checks the tag against the workspace version and builds both bundles.
  • smoke runs the macOS 26 bundle on macos-26 and the macOS 14 bundle on macos-15. Each job pulls typed-decisions into a cached HF_HUB_CACHE, then runs smoke.py. GitHub retires its macos-14 runner on 2026-11-02, so no job uses it. macos-15 still runs the macOS 14 bundle on an OS older than 26.2, which is the case that bundle exists for.
  • publish (tags only):
    • It checks that TAP_TOKEN can push to the tap, with git push --dry-run, before anything goes public.
    • It publishes the release with SHA256SUMS. A -rcN tag makes a prerelease and leaves the tap alone.
    • A final tag then commits the rendered formula to krishhgg/homebrew-tap.
    • Reruns are safe. A leftover draft from a cancelled run stops the job with instructions, since deleting it automatically could race a manual publish. A published release is accepted only when its tarballs and SHA256SUMS match this run. A tap with a newer version is left alone. Publish jobs from different tags share a concurrency group.

packaging/RELEASE.md is the checklist. It covers the local strict suite in a dedicated cache holding only the pinned snapshots, smoke on both bundles, an rc tag, a throwaway local tap install, the final tag, and the expired-token recovery.

Prerequisites (done)

  • The tap repo krishhgg/homebrew-tap exists.
  • The TAP_TOKEN secret is set, a fine-grained token with contents read and write on that repo only.

Tests

  • Both bundles built and smoke-tested on a base M5 as above. A build from a checkout path containing spaces works.
  • actionlint 1.7.12 with shellcheck is clean on the workflow.
  • The publish steps ran against a fake gh and a local git server, 10 release cases plus the tap cases:
    • a run cancelled partway through upload, and a draft published by hand during the rerun
    • an expired token (fails before publishing)
    • a read-only token
    • a newer formula already in the tap
    • a rerun with no changes
  • render_formula.sh gives identical output with GNU and BSD tools, including URLs that contain &, | and \.
  • This PR's first workflow run passed on GitHub's hosted runners. The build took 5 minutes. Both smoke jobs downloaded typed-decisions, ran on the runner's paravirtual Metal GPU with MLX's gemms, and had 0 failures with a largest drift of 0.0010. The macOS 14 bundle ran on macOS 15.7.9 and the macOS 26 bundle on 26.6.2.

🤖 Generated with Claude Code

RetriggerConfidence Score: 5/5

The PR appears safe to merge; both previous findings are addressed and no new blocking issue was found.

What we checked:

  • Correct replies rejected by order: The strict test already requires the same ordered fields and probability labels. The smoke test now follows that contract.
  • Valid probabilities rejected: The server normalizes probabilities and clamps entropy confidence to [0, 1]. The new bounds match those calculations.

Summary

Adds two macOS release bundles, license notices, a Homebrew formula, and a workflow that builds, smoke-tests, and publishes tagged releases.

  • The latest changes make smoke.py check reply shape, missing fields, and confidence differences.
  • The release checklist now states the GPU requirement for the strict suite.
  • Both previous findings are addressed. No new actionable issues were found.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Trigger["Packaging PR or version tag"] --> Build["Build both macOS bundles"]
  Build --> Smoke14["Smoke macOS 14 bundle on macOS 15"]
  Build --> Smoke26["Smoke macOS 26 bundle on macOS 26"]
  Smoke14 --> Gate{"Tag and both tests pass?"}
  Smoke26 --> Gate
  Gate -->|Yes| Publish["Publish release and checksums"]
  Publish --> Final{"Final release?"}
  Final -->|Yes| Tap["Update Homebrew formula"]
  Final -->|No| RC["Keep prerelease out of tap"]
Loading

Reviews (2) · Last reviewed commit: "Require every reference field in the smo..."

krishhgg and others added 10 commits October 5, 2026 16:57
packaging/build.sh macos14|macos26 builds sys1rust against the pinned
MLX wheel for that macOS target, points its rpath at lib/ next to
bin/, signs it ad hoc, checks linkage, minimum macOS, signature and
--version, and tars it with libmlx, libjaccl, the metallib and the
license files.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Starts a bundle's sys1rust serve, sends the 24 smoke requests and
checks the answers against the fp32 CPU reference with compare.py's
rules, then requires a clean SIGINT shutdown.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The formula picks the macOS 26 bundle on 26.2 and later and the
macOS 14 bundle otherwise, installs into libexec with a bin symlink,
and declares a brew services entry for sys1rust serve.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The smoke test now checks each answer against the reference's type and
fields before it allows a near-tie disagreement, and it requires
probabilities wherever the reference has them so the drift check always
runs. Each request runs in a worker thread with a 120 s deadline, and a
transport error, timeout or bad JSON records a FAIL line with its stage.
SIGINT and SIGTERM handlers route through the cleanup, which waits up to
10 s after SIGKILL and reports a server that is still alive.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
packaging/licenses holds pinned license texts for the third-party code that MLX 0.32.2
compiles into libmlx.dylib, libjaccl.dylib and mlx.metallib. build.sh copies them into the
bundle's licenses/, appends packaging/licenses/NOTICE to the bundle NOTICE and fails if a file
is missing.

build.sh also reads the whole LC_RPATH path, so a checkout path with spaces builds, requires the
Rust version in packaging/rust-toolchain-version and prints tarball sizes in decimal MB.
third_party.py runs cargo metadata only with that Rust version. render_formula.sh escapes \, &
and | so BASE_URL and the version land in the formula literally.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…test

malformed() now fails an answer whose probability map lacks a label from the reference's map.
compare.py counts a missing label as 0, so dropping a low-probability label stayed under the
drift limit. Extra labels still pass, as in compare.py.

A SIGINT or SIGTERM that arrives while Popen starts the server now waits until `proc` holds the
child, so the cleanup in `finally` stops it. The docstrings now name the actor.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A v* tag builds both bundles on macos-26 with the Rust in
packaging/rust-toolchain-version, pulls typed-decisions and
smoke-tests them on macOS 14, 15 and 26 runners, and publishes the
release with SHA256SUMS. A final tag also pushes the rendered formula
to krishhgg/homebrew-tap with TAP_TOKEN. An -rc tag publishes a
prerelease only. Pull requests that touch packaging run the build and
smoke jobs.

The publish job clones the tap before it creates the release, so an
empty or expired token stops it before anything goes public. A rerun
keeps a release that holds the same bundles and skips the tap commit
when the formula is unchanged. packaging/RELEASE.md is the checklist,
including the lock file refresh after a version bump.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
lib/ carries more adapted third-party code than the first inventory listed. packaging/licenses
now holds pinned texts for 8 more sources, packaging/licenses/NOTICE names each one, and
build.sh fails if any is missing.

- erf and erfinv by njuffa on Stack Overflow, in Metal erf.h and the CPU erfinv. MLX's
  constants match answer 35148199 revision 4 (2022) and answer 49743348 revision 3 (2021),
  both CC BY-SA 4.0 with no separate license.
- PyTorch, for the fp8 e4m3 conversion in Metal fp8.h and CPU unary_ops.h and for the CPU erf.
- supervised-lda's NumpyFormat.hpp, for the .npy reader and writer in io/load.cpp.
- Julien Pommier's neon_mathfun.h (zlib) with simd_utils' FMA change (BSD 2-Clause), for the
  CPU sincos, which libmlx.dylib also keeps as C++ source in its CPU JIT preamble.
- fastermath, for the CPU exp.
- NumPy, TensorFlow and JAX, whose einsum path search, cumulative logsumexp gradient and
  truncated normal sampler MLX translates.

NOTICE ends with what needs no notice and why. fmt, ThreadPool and Cephes are listed there,
along with the papers and algorithms MLX implements in its own code.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Smoke-test the macOS 14 bundle on macos-15 only. GitHub retires the
macos-14 image on 2026-11-02 and its brownouts already fail jobs.
macos-15 runs that bundle on an OS older than 26.2, the case it exists
for, and the macOS 26 bundle stays on macos-26.

The publish job now:
- deletes, by id, every draft release of the tag that a cancelled
  gh release create left, then creates the release again. A rerun
  accepts a published release only if both bundles and SHA256SUMS
  match this run's files.
- runs git push --dry-run against the tap before it creates the
  release. The public tap clones without the token, so the clone never
  checked it. The dry run asks GitHub for git-receive-pack, which
  GitHub serves only to a token it accepts with push access to the
  tap. It sends no commits and does not check branch rules.
- leaves the tap alone with a notice when its formula has a newer
  version (sort -V), so a retried older tag cannot downgrade it. Every
  tag's publish job shares the tap-update concurrency group.

RELEASE.md pulls the 3 pinned models into a fresh HF_HUB_CACHE under
packaging/.work before the strict suite. The laya-mlx tests load the
snapshot that refs/main names, else the only cached one, and sys1rust
pull writes no refs/main, so a shared cache could test another
revision. The candidate tag lives in rc, so tagging, the SHA256SUMS
download and the test formula all follow a bump to rc2.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The publish job deleted, by id, every draft release of the tag that a
cancelled gh release create left. Publishing a draft keeps its id, so a
maintainer who published the draft between the list and the delete
would lose a live release, and the concurrency group cannot coordinate
with a manual publish. The job now stops on such a draft, prints its
URL and asks for a manual delete before a rerun. The published-release
guard stays as it was.

Also drop the last comment that mentioned the macos-14 runner.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment thread packaging/smoke.py Outdated
Comment thread packaging/RELEASE.md Outdated
…suite's GPU

smoke.py now checks a reply's shape the way live.rs does. Question ids, answer fields and
probability labels must match the reference's lists in order, and the legend must equal the
reference's. noul, the confidences, act_probability and every probability must be numbers in
[0, 1], and confidence, answer_confidence and act_probability must stay within MAX_DRIFT. A
reply without confidence, answer_confidence, act_probability or legend used to pass.

RELEASE.md says the strict suite needs an M5-class GPU on macOS 26.2 or later, because the
laya-mlx settings tests need MLX's NAX gemms, and that the smoke tests run on any Apple silicon
Mac.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@krishhgg
krishhgg merged commit 61df8e7 into main Oct 6, 2026
5 checks passed
@krishhgg
krishhgg deleted the packaging/3-release branch October 6, 2026 19:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant