From 4e8f5e2a3102b657391e26cf2f907cee1245b5eb Mon Sep 17 00:00:00 2001 From: Pedro Gomes Branquinho Date: Wed, 21 Oct 2026 17:55:08 -0300 Subject: [PATCH 1/8] chore(mutation): drop the stale print.zig cmp_gt_to_ge equivalence row a3c1212f replaced the keys-then-get walk in print.zig with an entry walk, so the `countOf(ks) > 0` loop the proof covered no longer exists. The scheduled sweep on 32125e33 scored 100% (11 killed, 0 survived) but exited 1 on this one STALE row. --- .dev/mutation_equivalent.jsonl | 1 - 1 file changed, 1 deletion(-) diff --git a/.dev/mutation_equivalent.jsonl b/.dev/mutation_equivalent.jsonl index 785b93983..3426a058d 100644 --- a/.dev/mutation_equivalent.jsonl +++ b/.dev/mutation_equivalent.jsonl @@ -3,4 +3,3 @@ # can distinguish the mutant. run.sh reports an entry that matches nothing as STALE. {"file": "src/runtime/collection/vector.zig", "op": "const_bump_in_arith", "before": "const new_tail_leaf = arrayFor(old, old.count - 2);", "reason": "Reached only when the tail holds exactly one element, i.e. count = 32m+1. arrayFor selects a 32-element leaf by i >> SHIFT_BITS, and (32m-1) >> 5 == (32m-2) >> 5, so `count - 2` and `count - 3` always name the same leaf. No input distinguishes them."} {"file": "src/runtime/collection/vector.zig", "op": "const_bump_in_arith", "before": "const sub_index = ((new_count - 1) >> @as(u5, @intCast(level))) & MASK;", "reason": "popTail is entered only with new_count = 32m (a full-leaf boundary) and level >= SHIFT_BITS. new_count-1 and new_count-2 differ only in bit 0, which every level's >> level discards, so sub_index is identical at every level of the descent."} -{"file": "src/runtime/print.zig", "op": "cmp_gt_to_ge", "before": "while (ks.tag() == .list and list_collection.countOf(ks) > 0) : (ks = list_collection.rest(ks)) {", "reason": "Ranges of every callee in the predicate, which is what decides this. map_collection.keys returns Value.nil_val for an empty map (the .array_map arm via keysArrayMap's `am.count == 0` guard, the .hash_map arm via `phm.root orelse`, and the .nil arm directly), and otherwise a chain built only by list_mod.consHeap seeded from Value.nil_val; sorted_collection.keys is the same shape, seeding keysInto with Value.nil_val. consHeap sets .count = countPrepending(tail), whose whole range is {1, n+1, COUNT_UNKNOWN} - every value >= 1. list_collection.rest of such a cell returns cell.rest, which in that chain is either another consHeap cell or the nil_val seed. So for every ks this loop can hold, ks.tag() == .list implies countOf(ks) >= 1: the conjunct is never the reason the loop stops, and >= 0 cannot change the iteration count. NOT the general claim that no empty .list exists - one does (rt.empty_list, list.zig, .count = 0, which is what '() evaluates to; verified: (list? '()) is true and (count '()) is 0). The equivalence is specifically that keys never returns it. If keys is ever changed to return the empty-list singleton for an empty map, re-derive this. One entry covers both loops (.array_map/.hash_map and .sorted_map) - the line text is identical and matching is on file+op+before."} From 29a81ba953eaa1b76bc7064a80a33957fc41c149 Mon Sep 17 00:00:00 2001 From: Pedro Gomes Branquinho Date: Wed, 21 Oct 2026 19:48:58 -0300 Subject: [PATCH 2/8] ci(release): bump the homebrew tap only from BuddhiLW/ClojureWasm The upstream mirror clojurewasm/ClojureWasm carries this workflow, so merging #18 there cut its own v1.15.1 build and its tap job tried to rewrite Formula/cljw.rb with that build's checksums. The formula's urls name BuddhiLW/ClojureWasm releases, whose artifacts hash differently (linux cb7ad4a5 here vs 586396f7 upstream), so a successful push would have broken brew install. It failed instead with a 403: the mirror has no TAP_DEPLOY_KEY. Guard the job on github.repository; it is skipped, not failed, elsewhere. --- .github/workflows/release.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 431f89dcd..f4c6cc464 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -272,9 +272,17 @@ jobs: # # Auth is a deploy key on BuddhiLW/homebrew-tap (secret TAP_DEPLOY_KEY), not a # PAT: write access reaches that one repository and nothing else in the account. + # + # Only the repository the formula downloads from may bump it. The formula's + # urls name BuddhiLW/ClojureWasm releases, and an upstream mirror + # (clojurewasm/ClojureWasm) that carries this workflow cuts its own build of + # the same tag with different checksums: pushing those would break + # `brew install`. The mirror also has no TAP_DEPLOY_KEY, so the job failed + # there with a 403 on every release (v1.15.1, 8621cd0b). tap: name: bump homebrew tap needs: [preflight, build] + if: github.repository == 'BuddhiLW/ClojureWasm' runs-on: ubuntu-22.04 timeout-minutes: 10 steps: From c0fc59281e2a2447ed94f09912333a4218411171 Mon Sep 17 00:00:00 2001 From: Pedro Gomes Branquinho Date: Wed, 21 Oct 2026 19:58:41 -0300 Subject: [PATCH 3/8] docs(changelog): note the release workflow's tap repository guard --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 084d6f929..39a093895 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ first stable `1.0.0` tag; pre-1.0 `alpha` / `rc` tags may still change surfaces. ## [Unreleased] +### Fixed + +- **The release workflow bumps the Homebrew tap only from + BuddhiLW/ClojureWasm.** An upstream mirror carrying the workflow cut its own + build of v1.15.1 and tried to write that build's checksums into + `Formula/cljw.rb`, whose urls name this repository's release assets. The push + failed with a 403; had it succeeded, `brew install` would have rejected the + download. The `tap` job is now skipped in any other repository. + ## [1.15.1] - 2026-10-21 ### Added From 5289c89d50868bb3b18f03a1ed1faa2ae3e8a4b8 Mon Sep 17 00:00:00 2001 From: clojurewasm-release Date: Wed, 21 Oct 2026 23:28:52 +0000 Subject: [PATCH 4/8] chore(release): v1.15.2 --- CHANGELOG.md | 2 ++ build.zig.zon | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39a093895..a86532ddd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ first stable `1.0.0` tag; pre-1.0 `alpha` / `rc` tags may still change surfaces. ## [Unreleased] +## [1.15.2] - 2026-10-21 + ### Fixed - **The release workflow bumps the Homebrew tap only from diff --git a/build.zig.zon b/build.zig.zon index 8bea727f0..c72e81b04 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .cljw, - .version = "1.15.1", + .version = "1.15.2", .fingerprint = 0x1869d207073beffa, .minimum_zig_version = "0.16.0", .dependencies = .{ From d0b7075934bfcd65a991a93c7c7d97d736799696 Mon Sep 17 00:00:00 2001 From: Pedro Gomes Branquinho Date: Wed, 21 Oct 2026 20:31:50 -0300 Subject: [PATCH 5/8] ci(release): owner-relative tap bump, signed provenance, SHA256SUMS Each repository now bumps /homebrew-tap and points the formula at its OWN release assets, so BuddhiLW/ClojureWasm keeps buddhilw/tap and clojurewasm/ClojureWasm drives clojurewasm/tap (deploy key added there). The two repositories build each tag separately and hash differently; a formula never pairs one's urls with the other's checksums. Preflight reports whether TAP_DEPLOY_KEY exists, and a fork without it skips the job instead of failing. Each tarball gets a Sigstore build attestation (actions/attest-build-provenance v4.2.2): `gh attestation verify` proves it was built by this workflow from a given commit, which a .sha256 next to the asset cannot. A SHA256SUMS file collects the per-asset sums. README advertises clojurewasm/tap/cljw and the verify command. --- .github/workflows/release.yml | 75 +++++++++++++++++++++++++++++------ README.md | 22 ++++++---- docs/architecture.md | 2 +- 3 files changed, 78 insertions(+), 21 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f4c6cc464..4abad4d14 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -75,7 +75,16 @@ jobs: tag: ${{ steps.resolve.outputs.tag }} sha: ${{ steps.resolve.outputs.sha }} proceed: ${{ steps.resolve.outputs.proceed }} + tap-key: ${{ steps.tap-key.outputs.present }} steps: + # A job-level `if` cannot read secrets, so the tap job gates on this. + - name: Is there a tap deploy key + id: tap-key + env: + PRESENT: ${{ secrets.TAP_DEPLOY_KEY != '' }} + shell: bash + run: echo "present=$PRESENT" >> "$GITHUB_OUTPUT" + - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: @@ -194,6 +203,12 @@ jobs: if: needs.preflight.outputs.proceed == 'true' runs-on: ${{ matrix.runs-on }} timeout-minutes: 60 + # id-token + attestations: the Sigstore provenance step below. A job-level + # block replaces the workflow's, so contents: write is restated. + permissions: + contents: write + id-token: write + attestations: write strategy: fail-fast: true matrix: @@ -238,6 +253,15 @@ jobs: tar -C zig-out/bin -czf "dist/${{ matrix.artifact }}.tar.gz" cljw ( cd dist && shasum -a 256 "${{ matrix.artifact }}.tar.gz" > "${{ matrix.artifact }}.tar.gz.sha256" ) + # Signed build provenance (Sigstore, keyless): binds the tarball's digest + # to this repository, this workflow and the commit it was built from. + # A .sha256 next to the asset only catches corruption; this catches a + # substituted binary. Verify: gh attestation verify -R /ClojureWasm + - name: Attest build provenance + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-path: dist/${{ matrix.artifact }}.tar.gz + - name: Attach to GitHub Release env: GH_TOKEN: ${{ github.token }} @@ -265,24 +289,47 @@ jobs: || gh release view "$TAG_NAME" >/dev/null 2>&1 gh release upload "$TAG_NAME" dist/* --clobber - # Point `brew install buddhilw/tap/cljw` at the release this run just built. + # One SHA256SUMS per release, from the per-asset sidecars the build legs + # uploaded, so `sha256sum -c SHA256SUMS --ignore-missing` checks any download. + sums: + name: SHA256SUMS + needs: [preflight, build] + runs-on: ubuntu-22.04 + timeout-minutes: 10 + steps: + - name: Collect and upload SHA256SUMS + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + TAG_NAME: ${{ needs.preflight.outputs.tag }} + shell: bash + run: | + set -euo pipefail + mkdir -p sums && cd sums + gh release download "$TAG_NAME" --pattern '*.tar.gz.sha256' --clobber + cat ./*.tar.gz.sha256 | sort -k2 > SHA256SUMS + test "$(wc -l < SHA256SUMS)" -eq 2 + cat SHA256SUMS + gh release upload "$TAG_NAME" SHA256SUMS --clobber + + # Point `brew install /tap/cljw` at the release this run just built. # Runs after BOTH matrix legs so the formula never names an asset that does # not exist yet; the checksums come from the .sha256 sidecars the build wrote, # not from a re-hash here, so the formula pins exactly what was uploaded. # - # Auth is a deploy key on BuddhiLW/homebrew-tap (secret TAP_DEPLOY_KEY), not a - # PAT: write access reaches that one repository and nothing else in the account. + # Owner-relative: each repository bumps `/homebrew-tap` and points + # the formula at its OWN release assets. BuddhiLW/ClojureWasm and + # clojurewasm/ClojureWasm build the same tag separately and the artifacts hash + # differently, so a formula must never pair one repository's urls with the + # other's checksums (the v1.15.1 mirror run tried exactly that, 8621cd0b). # - # Only the repository the formula downloads from may bump it. The formula's - # urls name BuddhiLW/ClojureWasm releases, and an upstream mirror - # (clojurewasm/ClojureWasm) that carries this workflow cuts its own build of - # the same tag with different checksums: pushing those would break - # `brew install`. The mirror also has no TAP_DEPLOY_KEY, so the job failed - # there with a 403 on every release (v1.15.1, 8621cd0b). + # Auth is a deploy key on the tap (secret TAP_DEPLOY_KEY), not a PAT: write + # access reaches that one repository and nothing else. A repository without + # the secret (any other fork) skips this job instead of failing it. tap: name: bump homebrew tap needs: [preflight, build] - if: github.repository == 'BuddhiLW/ClojureWasm' + if: needs.preflight.outputs.tap-key == 'true' runs-on: ubuntu-22.04 timeout-minutes: 10 steps: @@ -308,7 +355,7 @@ jobs: - name: Check out the tap uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: - repository: BuddhiLW/homebrew-tap + repository: ${{ github.repository_owner }}/homebrew-tap ssh-key: ${{ secrets.TAP_DEPLOY_KEY }} path: tap @@ -326,8 +373,10 @@ jobs: tag = os.environ["TAG_NAME"] version = tag.lstrip("v") # The formula carries no `version` line: brew scans it from the URL, - # and `brew audit --strict` rejects stating it twice. - s = re.sub(r'/download/v[^/]+/', '/download/%s/' % tag, s) + # and `brew audit --strict` rejects stating it twice. The url names + # this repository, whose release the checksums below were read from. + s = re.sub(r'https://github\.com/[^/"]+/[^/"]+/releases/download/v[^/]+/', + 'https://github.com/%s/releases/download/%s/' % (os.environ["GITHUB_REPOSITORY"], tag), s) # Each sha256 sits under its own url line, so replace positionally. for host, sha in (("macos-aarch64", os.environ["MAC_SHA256"]), ("linux-x86_64", os.environ["LINUX_SHA256"])): diff --git a/README.md b/README.md index dd7e248a4..5a09a6005 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@

Latest release CI - Homebrew tap + Homebrew tap Zig 0.16.0 License: EPL 2.0

@@ -36,12 +36,19 @@ you set. ## Install ```sh -brew install buddhilw/tap/cljw # macOS arm64, Linux x86_64 +brew install clojurewasm/tap/cljw # macOS arm64, Linux x86_64 zig build -Dwasm -Doptimize=ReleaseSafe # or from source (Zig 0.16) ``` -Prebuilt binaries are on [Releases](https://github.com/BuddhiLW/ClojureWasm/releases). -They are not code-signed; on macOS, `xattr -d com.apple.quarantine "$(which cljw)"` +Prebuilt binaries are on [Releases](https://github.com/BuddhiLW/ClojureWasm/releases), +each with a `SHA256SUMS` file and a signed build attestation. To check that a +tarball was built by this repository's release workflow: + +```sh +gh attestation verify cljw-linux-x86_64.tar.gz -R BuddhiLW/ClojureWasm +``` + +The binaries are not code-signed; on macOS, `xattr -d com.apple.quarantine "$(which cljw)"` once. ```sh @@ -215,7 +222,8 @@ Created by [Shota Kudo (@chaploud)](https://github.com/chaploud), who built it from nothing to a working JVM-free Clojure with a Wasm FFI through v1.10.1. In 2026-10 the project passed to [@BuddhiLW](https://github.com/BuddhiLW); the fork's work (v1.11 to v1.15.0) is merged back into -[clojurewasm/ClojureWasm](https://github.com/clojurewasm/ClojureWasm), while -releases and the Homebrew tap publish from -[BuddhiLW/ClojureWasm](https://github.com/BuddhiLW/ClojureWasm). The engine is +[clojurewasm/ClojureWasm](https://github.com/clojurewasm/ClojureWasm). Releases +are cut on [BuddhiLW/ClojureWasm](https://github.com/BuddhiLW/ClojureWasm) and +again when they reach clojurewasm/ClojureWasm; each repository bumps its own +owner's Homebrew tap from its own build. The engine is [zwasm](https://github.com/zwasm/zwasm). diff --git a/docs/architecture.md b/docs/architecture.md index 3cf463e8e..196a31a0e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -125,7 +125,7 @@ the interpreter. Shipped through the tag the release badge points at; `CHANGELOG.md` is the record. Releases are cut by the release workflow on a push to `main` and ship as single binaries for macOS arm64 and Linux x86_64 plus the Homebrew tap -(`brew install buddhilw/tap/cljw`). +(`brew install clojurewasm/tap/cljw`). Built and exercised end to end: the reader, analyzer, both backends, the error system, persistent collections and the GC; the numeric tower (`Long` From 6084098c10ec3b7f436b0a663859792bd24ebb79 Mon Sep 17 00:00:00 2001 From: Pedro Gomes Branquinho Date: Wed, 21 Oct 2026 20:48:42 -0300 Subject: [PATCH 6/8] docs(changelog): signed provenance, SHA256SUMS, owner-relative taps --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a86532ddd..25da0d9a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,21 @@ first stable `1.0.0` tag; pre-1.0 `alpha` / `rc` tags may still change surfaces. ## [Unreleased] +### Added + +- **Release binaries carry signed build provenance and a `SHA256SUMS` file.** + Each tarball gets a Sigstore attestation from the release workflow, so + `gh attestation verify cljw-linux-x86_64.tar.gz -R BuddhiLW/ClojureWasm` + proves it was built by that workflow from a given commit. `SHA256SUMS` + collects the per-asset sums for `sha256sum -c`. + +### Changed + +- **`brew install clojurewasm/tap/cljw` is the advertised install.** Each + repository's release now bumps its own owner's tap from its own build: + BuddhiLW/ClojureWasm keeps `buddhilw/tap`, clojurewasm/ClojureWasm drives + `clojurewasm/tap`. A fork without a `TAP_DEPLOY_KEY` skips the tap job. + ## [1.15.2] - 2026-10-21 ### Fixed From 26d89900c77049ed50b3316e0e9a7123a3ae7468 Mon Sep 17 00:00:00 2001 From: clojurewasm-release Date: Thu, 22 Oct 2026 00:08:30 +0000 Subject: [PATCH 7/8] chore(release): v1.15.3 --- CHANGELOG.md | 2 ++ build.zig.zon | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25da0d9a8..e3c08c07a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ first stable `1.0.0` tag; pre-1.0 `alpha` / `rc` tags may still change surfaces. ## [Unreleased] +## [1.15.3] - 2026-10-22 + ### Added - **Release binaries carry signed build provenance and a `SHA256SUMS` file.** diff --git a/build.zig.zon b/build.zig.zon index c72e81b04..20cbb2185 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .cljw, - .version = "1.15.2", + .version = "1.15.3", .fingerprint = 0x1869d207073beffa, .minimum_zig_version = "0.16.0", .dependencies = .{ From 4d3ab76785e7632fe3cb4711f98047999c1762f3 Mon Sep 17 00:00:00 2001 From: Pedro Gomes Branquinho Date: Wed, 21 Oct 2026 21:15:40 -0300 Subject: [PATCH 8/8] docs(readme): link the playground and Bookshelf demos at cljw.buddhilw.com --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 5a09a6005..8d70f1c3f 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@

Install · + Demos · Call C from Clojure · How it works · Numbers · @@ -59,6 +60,16 @@ cljw nrepl --port 7888 # nREPL, CIDER-compatible cljw build script.clj -o app # self-contained native binary ``` +## Try it online + +Both demos are a `cljw` server and a ClojureScript page, one container each, +tracking the latest release. + +| Demo | What it shows | Source | +|---|---|---| +| [Playground](https://cljw.buddhilw.com/playground/) | Evaluate Clojure under a step, time and heap budget; call Rust and Go Wasm modules | [cw-playground](https://github.com/clojurewasm/cw-playground) | +| [Bookshelf](https://cljw.buddhilw.com/bookshelf/) | SQLite compiled to Wasm as the store, a Rust module for cover colours | [cw-serverless-demo](https://github.com/clojurewasm/cw-serverless-demo) | + ## Call C from Clojure A C function, compiled to Wasm, called with ordinary Clojure interop.