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
166 changes: 166 additions & 0 deletions .claude/skills/release/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
---
name: release
description: Cut a wasm-cxx-shim release — version bump, CHANGELOG, doc-staleness sweep, PR (and post-merge, tag + GitHub Release)
user-invocable: true
---

# Release — wasm-cxx-shim

Drives the mechanical parts of a release end-to-end. Pauses at each
human-decision point (CHANGELOG content review, PR body sign-off,
post-merge tag) rather than barreling through.

Read [`CLAUDE.md`](../../../CLAUDE.md) first — the "Commit history
policy" and "All changes to main go through PRs" sections set the
ground rules this skill operates within.

## Arguments

- `<version>` (required): the version being released, no leading `v`.
Examples: `0.4.0`, `0.4.0-alpha.1`, `0.4.0-alpha.1+5f95a3ac`. The
CMake `project(VERSION ...)` line accepts only `MAJOR.MINOR.PATCH`,
so for pre-releases the alpha/build-metadata segments live only on
the git tag and the CHANGELOG title — CMakeLists.txt gets the bare
numeric version.
- `prep`: run only phases 1-5 (version bump, CHANGELOG, sweep, build
verify, push branch + open PR). Stop before the post-merge phases.
- `tag`: run only phases 6-7 (assumes the PR has merged; tags the
merge commit, pushes, creates the GitHub Release).
- No phase flag: run `prep` (default).

## Pre-flight checks

Before doing anything, verify:

1. **On a feature branch**, not `main`. If on `main`, stop and tell
the user to `git checkout -b release/v<version>` first.
2. **Working tree is clean** (`git status --porcelain` empty). If not,
stop and surface the dirty paths.
3. **The version isn't already tagged.** `git rev-parse v<version>`
should fail with "unknown revision." If it succeeds, stop —
tagging the same version twice would be a bug.
4. **`gh` is authenticated** (`gh auth status`). The skill needs it
for PR + release creation.

## Phases

### 1. Version bump

Update `CMakeLists.txt`'s `project(... VERSION X.Y.Z ...)` line. For
pre-releases, drop the alpha/build-metadata segments — CMake doesn't
parse them. Verify with `grep VERSION CMakeLists.txt | head -3`.

### 2. CHANGELOG

Update `CHANGELOG.md`:

- **Move the `## Unreleased` section** to a new versioned section
(`## v<version> — <one-line summary> (<YYYY-MM-DD>)`). The
one-line summary is yours to draft based on what's in the
Unreleased section; surface it to the user for sign-off before
finalizing.
- **Re-add an empty `## Unreleased`** above the new versioned
section.
- **Update the "Tested upstream combinations" table** at the top
with a new row for this version. Pull the manifold pin from
`cmake/WasmCxxShimManifold.cmake`'s
`_wasm_cxx_shim_manifold_default_manifold_tag` default, the
Clipper2 pin (where applicable — for v0.4.0+ it's "inherits
manifold's pin"), and the patch count (e.g., "1 (verbatim diff
of #1690)").

### 3. Doc-staleness sweep

Run the sweep from the review skill's Cat 8:

```sh
grep -rn -E "v0\.[0-9]|pre-CI|TBD|TODO|coming|not started|in flight|NEXT" \
--include='*.md' --exclude=CHANGELOG.md --exclude-dir=build .
```

Plus the API/patch-rename audit from Cat 8: if this release renames
or drops any `wasm_cxx_shim_add_manifold()` arguments, or shipped
patches under `cmake/manifold-patches/`, grep for the old names
across all `*.md` and surface hits.

Stop and walk the user through each hit: intentional historical
narrative or stale text? Apply the user's calls.

### 4. Build verify

Don't tag a release that doesn't build. Run:

```sh
rm -rf build/wasm32 && cmake --preset wasm32 -DWASM_CXX_SHIM_BUILD_MANIFOLD_LINK=ON \
&& cmake --build --preset wasm32 -j \
&& ctest --preset wasm32
```

All ctest entries must pass, including manifold-link and
manifold-tests. If anything fails, stop and surface — releasing on
red is never the right call.

### 5. PR

Per CLAUDE.md's "one session, one commit" + "All changes to main go
through PRs":

- If the branch has multiple commits, propose squashing them
(`git reset --soft <last-pre-session> && git commit`). Stop and
ask before force-resetting.
- Push the branch (`git push -u origin <branch>` for new branches,
`git push --force-with-lease` for amended ones).
- Open the PR via `gh pr create`. The PR title is "Release
v<version>" or similar; the body is **extracted from the
CHANGELOG section you just wrote** (the versioned `## v<version>`
block you moved Unreleased into). Surface the body for user
sign-off before submitting.

**Stop here.** Per CLAUDE.md's "merge guardrail" — don't run `gh pr
merge`. Wait for the user to merge via the GitHub UI or their own
`gh pr merge`. CI must be green; a green CI is the END of the
prep flow, not a green light to merge.

### 6. Tag (post-merge)

After the user confirms the merge happened:

- `git checkout main && git pull && git remote prune origin`.
- `git tag v<version> <merge-commit-sha>`. Use the actual merge
commit, not whatever `HEAD` happens to be.
- `git push origin v<version>`.

### 7. GitHub Release

```sh
gh release create v<version> \
--title "v<version>" \
--notes-file <(awk '/^## v<version>/,/^## v[^.]+\.[^.]+\.[^.]+/' CHANGELOG.md | head -n -1)
```

The awk extracts the section between `## v<version>` and the next
`## v...` header. Verify the extracted notes look right before
submitting; offer the user the rendered preview.

## Notes / gotchas

- **Pre-release versions**: CMake's `project(VERSION ...)` doesn't
accept `0.4.0-alpha.1+5f95a3ac` — only `0.4.0`. The alpha/build
metadata lives on the git tag (`v0.4.0-alpha.1+5f95a3ac`) and in
the CHANGELOG title. Inside the project, version comparisons use
the bare numeric version.
- **Tested-combinations table**: the column meanings shifted at
v0.4.0-alpha.1. Pre-v0.4: the shim pre-declared Clipper2 with
its own pin. v0.4+: manifold owns Clipper2's declaration, so the
Clipper2 column reads "inherits manifold's pin" instead of a SHA.
When updating, match the convention of the row above to stay
consistent.
- **Carry-patch SHA in tested-combinations**: when a release
vendors an upstream PR's diff (e.g., elalish/manifold#1690 in
v0.4.0-alpha.1), the manifold pin includes the upstream commit
the patch was generated against. Format: `<sha>` (master +
vendored elalish/manifold#<n>).
- **Don't tag before merge.** If the branch hasn't merged yet,
there's no merge commit to tag against. Tagging a feature branch
ahead of merge produces a tag that points at a commit that
later disappears from `main`'s history.
32 changes: 32 additions & 0 deletions .claude/skills/review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,26 @@ For any change touching `libc/src/dlmalloc/malloc.c`,
installed prefix.
- **Install rules cover headers.** `libm/include/` must be installed,
or downstream consumers can't `#include <math.h>`.
- **`FetchContent_Declare(... PATCH_COMMAND ...)` is idempotent.**
Re-configures (e.g., from any CMakeLists.txt edit) re-trigger
PATCH_COMMAND; vanilla `git apply` then fails on the already-patched
source tree. Acceptable mitigations: `UPDATE_DISCONNECTED TRUE` on
the FetchContent_Declare (short-circuits the patch step on
subsequent configures), or a wrapper `cmake -P` script that does
`git apply --reverse --check` first. A bare `PATCH_COMMAND git
apply ${PATCH}` with neither mitigation is an **error** — works
on a clean tree, breaks the moment the user re-configures.
- **Order-of-operations on derived options across nested
`FetchContent_MakeAvailable` calls.** When a consumer pre-declares
package A before package B's MakeAvailable runs, B's CMakeLists
runs after A is already populated. Any cache var B sets to
influence A's option resolution arrives too late. If a helper has
a pre-declare, audit whether a transitive dep of the
pre-declared package needs to consume an option set by an
intermediate CMakeLists — if so, drop the pre-declare or set the
cache var explicitly in the helper. **Warning** if a
consumer-pre-declared dep also has cache-var dependencies coming
from a nested package.

### 6. Toolchain & build infrastructure

Expand Down Expand Up @@ -420,6 +440,18 @@ For any change touching `libc/src/dlmalloc/malloc.c`,
READMEs that drift when test coverage extends. Either keep
abstract ("a slice of its tests passes") or commit to updating
on every test addition. Drift here is a **note**.
- **API renames / removals propagated to docstrings + caller sites.**
When a CMake helper drops or renames an argument (e.g.,
`wasm_cxx_shim_add_manifold(CLIPPER2_GIT_TAG ...)` → no longer
accepted), audit: (a) the helper's own docstring at the top of the
file, (b) every caller site (`grep -rn 'wasm_cxx_shim_add_manifold'`),
(c) any README that documents the helper's parameters by name. Same
for renamed/removed shipped patches: when `0001-foo.patch` becomes
`0001-bar.patch` (or three patches collapse to one), grep all `*.md`
for the old patch filenames. A docstring that lists a removed
argument is a **warning**; a README that names a removed patch is
also a **warning** since a future agent will look for it and be
confused.

### 9. Style for hand-written code

Expand Down
67 changes: 62 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ pin of manifold + Clipper2 with the carry-patches that make them link
on `wasm32-unknown-unknown`. Each tagged shim release verifies the
combination in CI:

| Shim version | manifold | Clipper2 | Patches shipped |
|--------------|----------------|----------------------------------------------|-----------------------|
| v0.3.0 | `v3.4.1` | `46f639177fe418f9689e8ddb74f08a870c71f5b4` | 0001 + 0002 + 0003 |
| v0.2.0 | `v3.4.1` | `46f639177fe418f9689e8ddb74f08a870c71f5b4` | 0001 + 0002 + 0003 (carried in `test/manifold-link/patches/`; helper not yet present) |
| Shim version | manifold | Clipper2 | Patches shipped |
|-----------------------|-----------------------------------------------|----------------------------------------------|-----------------------|
| v0.4.0-alpha.1 | `5f95a3ac` (master + vendored elalish/manifold#1690) | inherits manifold's pin (manifold owns the FetchContent_Declare) | 1 (verbatim diff of #1690) |
| v0.3.0 | `v3.4.1` | `46f639177fe418f9689e8ddb74f08a870c71f5b4` | 0001 + 0002 + 0003 |
| v0.2.0 | `v3.4.1` | `46f639177fe418f9689e8ddb74f08a870c71f5b4` | 0001 + 0002 + 0003 (carried in `test/manifold-link/patches/`; helper not yet present) |

Consumers calling `wasm_cxx_shim_add_manifold()` (introduced in v0.3.0)
with no arguments get the row matching their installed shim version.
Expand All @@ -32,7 +33,63 @@ version where the macro guard appears natively.

## Unreleased

(no changes since v0.3.0)
(no changes since v0.4.0-alpha.1)

## v0.4.0-alpha.1 — manifold pin bump + #1690 carry-patch (2026-05-03)

Pre-release. Pins manifold to a current upstream master commit and
folds the three shim-side iostream patches into a single vendored
diff of [elalish/manifold#1690][pr1690] (the upstream PR that adds
`MANIFOLD_NO_IOSTREAM` natively). Once #1690 lands and the shim's
manifold pin moves past it, the carry-patch drops entirely and a
v0.4.0 (non-alpha) release follows.

[pr1690]: https://github.com/elalish/manifold/pull/1690

### Added

- `cmake/manifold-patches/0001-manifold-no-iostream.patch` — verbatim
diff of #1690 against the pinned upstream manifold commit. Replaces
the three previously-shipped patches.
- Six additional manifold test files now run on the shim:
`boolean_complex_test`, `manifoldc_test`, `smooth_test` (plus the
existing `boolean_test`, `cross_section_test`, `sdf_test`). Total
test count: **121** (up from 71).

### Changed

- `wasm_cxx_shim_add_manifold()`: refactored for the post-#1690
world. Drops the Clipper2 pre-declaration, sets
`MANIFOLD_NO_IOSTREAM=ON` as a CMake cache var, and lets manifold's
carry-patched option chain propagate `MANIFOLD_NO_FILESYSTEM` and
`CLIPPER2_NO_IOSTREAM` as PUBLIC compile defs.
- API: removed `CLIPPER2_GIT_TAG` and `EXTRA_CLIPPER2_PATCHES`
parameters (manifold owns the Clipper2 declaration; shim no longer
has a say). `MANIFOLD_GIT_TAG`, `EXTRA_MANIFOLD_PATCHES`, and
`SKIP_BUILTIN_PATCHES` continue to work.
- Default `MANIFOLD_GIT_TAG` bumped from `v3.4.1` to a master commit
(`5f95a3ac`) so the carry-patch applies cleanly.
- `manifold_tests_size_budget` raised from 1.10 MB to 1.40 MB to
accommodate the expanded test set.

### Removed

- The three shim-side iostream patches
(`0001-clipper2-strip-iostream`, `0002-manifold-ifdef-iostream`,
`0003-manifold-test-main-ifdef-filesystem`). Replaced by the
single vendored #1690 diff.

### Notes for downstream consumers

- This is an **alpha pre-release** pinned to a specific manifold
commit. The carry-patch may be re-rolled if #1690 evolves in
review (would land as alpha.2, alpha.3, etc.). The non-alpha
v0.4.0 ships once #1690 merges.
- Consumers calling `wasm_cxx_shim_add_manifold()` without
overrides will be on the bumped pin automatically. Override
with `MANIFOLD_GIT_TAG <ref>` if you need to stay on the v0.3.0
combination — pair with `EXTRA_MANIFOLD_PATCHES` to supply your
own iostream patches in that case.

## v0.3.0 — CMake integration helper (2026-05-02)

Expand Down
44 changes: 44 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,38 @@ them.
"patches not found at expected path" with no obvious connection to
the bug, so future helpers should follow the captured-at-top
pattern.
- **`FetchContent` `PATCH_COMMAND` is not idempotent by default.** The
patch step re-runs on every CMake re-configure (any CMakeLists.txt
edit triggers it). Vanilla `git apply` then fails on the
already-patched source tree with "patch does not apply" — and the
failure mode is mysterious because nothing in the diff explains why
`cmake --build` started failing after a trivial edit. Two known fixes:
1. `UPDATE_DISCONNECTED TRUE` on the `FetchContent_Declare(...)`
call short-circuits the patch step on subsequent configures.
Simplest when you control the declare site (e.g., the shim's
own `wasm_cxx_shim_add_manifold()` helper uses this).
2. A wrapper `cmake -P` script that does `git apply --reverse
--check` first and only applies if not already applied. Use this
when `UPDATE_DISCONNECTED` isn't appropriate (e.g., a library
wants its own FetchContent_Declare to be re-applyable on every
configure for development workflows). Pattern lives at
`manifold-upstream/cmake/patches/apply-clipper2-patch.cmake` in
the in-flight #1690.
- **Order-of-operations with `FetchContent_MakeAvailable` and a
consumer pre-declared transitive dep.** When a consumer (the shim)
pre-declares package A (Clipper2) before package B (manifold) does
`FetchContent_MakeAvailable(B)`, B's CMakeLists runs *after* A is
already populated and added as a subdirectory. Any cache var that B
sets to influence A's option resolution arrives *too late* — A's
`option()`s have already evaluated to their defaults. We hit this
trying to make `MANIFOLD_NO_IOSTREAM=ON` propagate to
`CLIPPER2_NO_IOSTREAM=ON` (manifold #1690 sets the latter from the
former in `manifoldDeps.cmake`); shim's pre-declare meant manifold's
derivation never reached Clipper2's option. Fix: drop the
pre-declare and let the inner FetchContent declaration win, so the
derivation happens in the right order. Pre-declaration is a
workaround for injecting patches that the upstream doesn't carry —
once those patches move upstream, the workaround stops being needed.

### wasm-ld and test wasms

Expand Down Expand Up @@ -441,6 +473,18 @@ them.
commit to updating on every test addition. Detailed counts live
in `test/manifold-tests/README.md` where they're maintained
alongside the test list itself.
- **Don't claim a test failure is "pre-existing" without verifying on
a default build.** When a test fails under a non-default config
(e.g., `MANIFOLD_NO_IOSTREAM=ON`), it's tempting to assume the
failure is upstream-pre-existing and unrelated. Verify by running
the same test on a default build *before* claiming the failure is
unrelated. Cost of getting this wrong: a real bug that I introduced
propagated from an open upstream PR through the shim's integration
test and into the alpha-tagging plan, until the user explicitly
questioned the claim. Specific case: `BooleanComplex.CraycloudBool`
passes on default builds; only failed because the asymmetric stub
for `ReadTestOBJ` returned an empty `Manifold`, breaking the test's
assertion that the boolean output was non-empty.

## Things to NEVER do

Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

cmake_minimum_required(VERSION 3.25)
project(wasm-cxx-shim
VERSION 0.3.0
VERSION 0.4.0
DESCRIPTION "Minimal C/C++ runtime shim for wasm32-unknown-unknown"
LANGUAGES C CXX
)
Expand Down
Loading
Loading