Skip to content
Draft
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
35 changes: 19 additions & 16 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ jobs:
# The `wasm` leg is the Phase-0 WASI-component prototype (docs/WASM-COMPONENTS-PROPOSAL.md §6). It
# is a separate, opt-in sbt build in wasm/ (the scala-wasm fork of sbt-scalajs cannot share a
# meta-build with mainline sbt-scalajs) and has its own steps below: format check, the wall's unit
# tests (on Node) and scripts/wasm/e2e.sh in source mode, then publishLocal of the six wasm artifacts
# tests (on Node) and scripts/wasm/e2e.sh in source mode, then publishLocal of the five wasm artifacts
# and a plugin-only consumer build (scripts/wasm/consumer-check.sh).
#
# The wasm leg GATES `publish`, like jvm and js: the publish job also publishes the six wasm
# The wasm leg GATES `publish`, like jvm and js: the publish job also publishes the five wasm
# artifacts to Central (PUBLISH_WASM_TO_CENTRAL), so a broken wasm build must never reach a release.
test:
strategy:
Expand Down Expand Up @@ -147,20 +147,20 @@ jobs:
wget -q https://raw.githubusercontent.com/dapr/cli/v1.15.1/install/install.sh -O - | /bin/bash -s 1.15.1
dapr init --slim --runtime-version 1.17.7
echo "$HOME/.dapr/bin" >> "$GITHUB_PATH"
# The patched scala-wasm linker (scripts/wasm/build-patched-linker.sh: the 1.22.1-wasm.6 release
# commit + our export-params patch, published to ~/.ivy2/local). Rebuilt only when the script, the
# patch or the version in wasm/build.sbt changes.
# The patched scala-wasm linker (scripts/wasm/build-patched-linker.sh: the 1.22.1-wasm.6 release commit +
# our params-via-memory patch) is built by the `wasmLinker` project on demand; cache its output and the
# scala-wasm clone. Rebuilt only when the script or the patch changes.
- name: Cache the patched scala-wasm linker build
# The UNRELEASED scala-wasm the wasm/ build pins (1.22.1-wasm.7-dapr4s-local: scala-wasm's branch +
# the open upstream PRs dapr4s needs, see scripts/wasm/build-scala-wasm.sh). Built from source and
# publishLocal'ed; cached by the script's hash (it pins the commit SHAs and the version). Goes away
# with the next scala-wasm release on Central.
- name: Cache the scala-wasm snapshot build
if: matrix.platform == 'wasm'
id: cache-scala-wasm
uses: actions/cache@v4
with:
path: |
wasm/target/scala-wasm-linker
~/.cache/dapr4s/scala-wasm-src
key: scala-wasm-linker-${{ hashFiles('scripts/wasm/build-patched-linker.sh', 'scripts/wasm/patches/**') }}
path: ~/.ivy2/local/io.github.scala-wasm
key: scala-wasm-snapshot-${{ hashFiles('scripts/wasm/build-scala-wasm.sh') }}
- name: Build and publishLocal the scala-wasm snapshot
if: matrix.platform == 'wasm'
run: scripts/wasm/build-scala-wasm.sh
# Source mode: the wall over dapr4s's own src/shared (covers shared-code changes), unit tests + e2e
# (counter-raw, counter, imports-probe against daprd + Redis).
- name: Format check, wall unit tests (wasm/, source mode)
Expand All @@ -171,7 +171,7 @@ jobs:
if: matrix.platform == 'wasm'
timeout-minutes: 20
run: SBT_ARGS=-Ddapr4s.wasm.core=source scripts/wasm/e2e.sh
# Release mode (the default): the six artifacts as they would be published (the wall on the published
# Release mode (the default): the five artifacts as they would be published (the wall on the published
# dapr4s_sjs1_3), publishLocal'ed, then a fresh consumer build that has nothing but the plugin + one
# dependency builds test/wasm/counter and runs its e2e. dapr4s-examples consumes the same artifacts.
- name: publishLocal (release mode)
Expand Down Expand Up @@ -204,7 +204,10 @@ jobs:
# The switch for Central publishing of the wasm artifacts (ON since 2026-10-13, approved by the
# user). Setting it to 'false' stops only the wasm artifacts; core (dapr4s_3, dapr4s_sjs1_3)
# publishes regardless.
PUBLISH_WASM_TO_CENTRAL: 'true'
# OFF while wasm/ pins the unreleased scala-wasm 1.22.1-wasm.7-dapr4s-local
# (scripts/wasm/build-scala-wasm.sh): sbt-dapr4s-wasm's POM would name an sbt-scalajs version that
# is not on Central, so no consumer could resolve it. Back to 'true' with a scala-wasm release.
PUBLISH_WASM_TO_CENTRAL: 'false'
steps:
- uses: actions/checkout@v5
with:
Expand Down Expand Up @@ -246,7 +249,7 @@ jobs:
PUBLISH_SECRET_KEY: ${{ secrets.PUBLISH_SECRET_KEY }}
PGP_PASSPHRASE: ${{ secrets.PUBLISH_SECRET_KEY_PASSWORD }}
# ---- The wasm artifacts (wasm/ build): dapr4s-wasm, dapr4s-wasm-glue(-exports), dapr4s-wasm-scalalib-overlay,
# dapr4s-wasm-linker, sbt-dapr4s-wasm — at the same version as core, the wall depending on the dapr4s_sjs1_3
# sbt-dapr4s-wasm — at the same version as core, the wall depending on the dapr4s_sjs1_3
# of THIS version (published locally first so the wasm build resolves it before Central has it).
# Gated by PUBLISH_WASM_TO_CENTRAL (the wasm build also refuses `publish` unless that variable is 'true',
# so a local `sbt publish` in wasm/ can never reach Central by accident). See AGENTS.md "Wasm component layer rules".
Expand Down
83 changes: 48 additions & 35 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,83 +351,94 @@ The `wasm` platform builds a dapr4s app as a WASI **component** for
[dapr-wasm-components](https://github.com/sideeffffect/dapr-wasm-components) 0.7.5, with the
scala-wasm fork of Scala.js (`io.github.scala-wasm:sbt-scalajs`, `ModuleKind.WasmComponent`).
Status, blockers and numbers: `docs/WASM-COMPONENTS-PROPOSAL.md` §6 and
`wiki/scala-js/scala-wasm-component-spike.md`. The six artifacts are **published to Maven Central**
by the same `publish` job as core (snapshots on master pushes, releases on tags); `publishLocal` works.
`wiki/scala-js/scala-wasm-component-spike.md`. The five artifacts are **published to Maven Central**
by the same `publish` job as core (snapshots on master pushes, releases on tags) — but that switch is
OFF while `wasm/` pins an unreleased scala-wasm (below); `publishLocal` works.
Upstream gaps are worked around here AND fixed upstream (the earlier "no upstream PRs" rule was lifted on
2026-10-13). Open upstream PRs, each retiring one workaround: scala-wasm/scala-wasm
[#268](https://github.com/scala-wasm/scala-wasm/pull/268) (params via memory → patched linker),
[#269](https://github.com/scala-wasm/scala-wasm/pull/269) (WIT aliases → re-embed),
[#268](https://github.com/scala-wasm/scala-wasm/pull/268) (params via memory → patched linker; merged, RETIRED here),
[#269](https://github.com/scala-wasm/scala-wasm/pull/269) (WIT aliases → re-embed; RETIRED here),
[#270](https://github.com/scala-wasm/scala-wasm/pull/270) (pluggable System → println/clock rules),
[#271](https://github.com/scala-wasm/scala-wasm/pull/271) (core-lib exclusions),
[#271](https://github.com/scala-wasm/scala-wasm/pull/271) (core-lib exclusions; the org.scala-js ones RETIRED here),
scala/scala3 [#27300](https://github.com/scala/scala3/pull/27300) (unpickler crash → empty javadoc),
cquiroz/scala-java-locales [#640](https://github.com/cquiroz/scala-java-locales/pull/640) (java.time formatting).
**scala/scala3 and VirtusLab/scala-cli forbid LLM-written PR/issue text** (their `LLM_POLICY.md`): there,
agents push branches / open drafts with an EMPTY description; the user writes the text.

- **The pinned scala-wasm is an unreleased local build**: `1.22.1-wasm.7-dapr4s-local`
(`wasm/project/plugins.sbt`), built by `scripts/wasm/build-scala-wasm.sh` from scala-wasm's
`scala-wasm` branch (with #268) + a merge of the heads of #269 and #271, all pinned by SHA, and
`publishLocal`'ed to `~/.ivy2/local` (idempotent, stamp = the SHAs; ~10 min cold). Run it once
before the first `cd wasm && sbt ...`. The CI wasm leg runs it, cached by the script's hash. Bump the
SHAs (and the version, if the artifacts change) when the upstream PRs move. **RETIRES** with a
scala-wasm release that has all three: pin it, delete the script and its CI steps, and turn
`PUBLISH_WASM_TO_CENTRAL` back on (OFF meanwhile: `sbt-dapr4s-wasm`'s POM would name an
`sbt-scalajs` that is not on Central).
- **Opt-in by construction — a nested sbt build in `wasm/`** (`wasm/build.sbt`, `wasm/project/`,
`wasm/sbt-plugin/`). WHY: the scala-wasm plugin is a fork of `org.scala-js:sbt-scalajs` with the
same classes and keys, so one meta-build cannot load both, and switching the root build to the
fork would change `coreJS`'s published artifact. The root build does not know the wasm projects:
plain `sbt test` works without any wasm tool. Run it with `cd wasm && sbt ...`. `wasm/build.sbt`
reads `scala3Nightly`, `scalaJavaTime` and `munit` from the root `build.sbt` (single source of
truth); keep its `compilerFlags` in sync by hand.
- **The six artifacts** (all `com.github.sideeffffect`, at core's sbt-dynver version via
- **The five artifacts** (all `com.github.sideeffffect`, at core's sbt-dynver version via
sbt-ci-release, POM metadata as core's, normal suffixes):
| artifact | project | contains |
|---|---|---|
| `dapr4s-wasm_sjs1_3` | `wallWasm` | the Scala 3 adapters (`src/wasm/internal`: `WasmStateCapability`, `InvokeDispatcher`, `WasmRuntime`, the real `AppRoot`) + `dapr4s.wasm.DaprComponent` (`src/wasm/api`) + the inert callbacks (`src/wasm/inert`), the last two merged into its jar. Depends on dapr4s core (`dapr4s_sjs1_3`) + `dapr4s-wasm-glue` |
| `dapr4s-wasm-glue_sjs1_2.13` | `witGlue` | the wit-bindgen-scala bindings of world `app` + the hand-written 2.13 glue |
| `dapr4s-wasm-glue-exports_sjs1_2.13` | `witExports` | the hand-written `@WitExport` trampolines (a COMPONENT's dependency only; the plugin adds it) |
| `dapr4s-wasm-scalalib-overlay` | `scalalibOverlay` | the fork-patched stdlib `.sjsir` (unmodified, from `io.github.scala-wasm:scalajs-scalalib_2.13:3.8.3-RC1-wasm.4`) + LICENSE/NOTICE attribution |
| `dapr4s-wasm-linker_2.12` | `wasmLinker` | scala-wasm's 1.22.1-wasm.6 linker rebuilt with our patch (`scripts/wasm/patches/scala-wasm-params-via-memory.patch`), LICENSE/NOTICE, patched sources in the sources jar; depends on the released ir/linker-interface |
| `sbt-dapr4s-wasm` | `sbtDapr4sWasm` | the build logic (below) |
- **Core: published or source.** By default (`CoreRelease` in `wasm/build.sbt`, or
`-Ddapr4s.wasm.core=<v>`) the wall depends on the PUBLISHED `dapr4s_sjs1_3`, whose Scala.js IR the
scala-wasm linker links into the component (the JS wall is unreachable and dropped; mainline
org.scala-js library/javalib are excluded). Valid because the wasm platform traits
scala-wasm linker links into the component (the JS wall is unreachable and dropped; the scala-wasm
plugin excludes the mainline org.scala-js library/javalib). Valid because the wasm platform traits
(`src/wasm/core`) equal the JS ones (all empty). `-Ddapr4s.wasm.core=source` compiles `src/shared`
+ `src/wasm/core` as `coreWasm` instead, so shared-code changes are tested (nothing publishes in
that mode). A release publishes the wasm artifacts against the core of the SAME version (the
publish job passes it). Bump `CoreRelease` after each core release.
- **The consumer** needs only:
`addSbtPlugin("com.github.sideeffffect" % "sbt-dapr4s-wasm" % v)`,
`.enablePlugins(Dapr4sWasmComponentPlugin)`,
`libraryDependencies += "com.github.sideeffffect" %%% "dapr4s-wasm" % dapr4sWasmVersion`, its own
`wit/world.wit` (`include dapr-wasm-components:interfaces/app@0.7.5;`) + `wkg-config.toml` +
`wkg.lock`, and the fixed-name `object dapr4s.wasm.ComponentRoot { def component: DaprComponent = ... }`.
`libraryDependencies += "com.github.sideeffffect" %%% "dapr4s-wasm" % dapr4sWasmVersion` and the
fixed-name `object dapr4s.wasm.ComponentRoot { def component: DaprComponent = ... }`. No WIT files,
no wkg (the linker's component is used as is).
`scripts/wasm/consumer-check.sh` builds `test/wasm/counter` exactly so (fresh build, nothing else)
and runs its e2e; dapr4s-examples' `wasm/` build is the worked example.
- **The plugin** (`wasm/sbt-plugin/.../Dapr4sWasmPlugin.scala`, every workaround documented at its
definition with how it RETIRES): `Dapr4sWasmPlugin` (every nightly-compiled project) excludes the
2.13 stdlib and mainline `org.scala-js` library/javalib and sets the linker to
`dapr4s-wasm-linker` (`dapr4sWasmLinkerClasspath`); `Dapr4sWasmComponentPlugin` (every component)
fork's 2.13 stdlib (`io.github.scala-wasm:scalajs-scalalib_2.13`, which #271 leaves next to the
nightly's); `Dapr4sWasmComponentPlugin` (every component)
sets `ModuleKind.WasmComponent`, prepends `dapr4s-wasm-scalalib-overlay` to the linker classpath
(`dapr4sWasmScalalibOverlay`, resolved through the linker's GLOBAL `dependencyResolution` because
coursier's per-project one applies `excludeDependencies`), adds `dapr4s-wasm-glue-exports`
(`dapr4sWasmAddGlueExports`), runs `wkg wit fetch` and provides `dapr4sComponent` /
`dapr4sComponentFull` (re-embed the real WIT world with `--encoding utf16` + `component new`). The
(`dapr4sWasmAddGlueExports`) and provides `dapr4sComponent` / `dapr4sComponentFull` (copy the
linker's component as is). The
same source is compiled into `wasm/`'s meta-build (`wasm/project/plugin-sources.sbt`); `wasm/build.sbt`
points the two artifact-backed keys at its own `wasmLinker` / `scalalibOverlay` projects and sets
points the artifact-backed key at its own `scalalibOverlay` project and sets
`dapr4sWasmAddGlueExports := false` on its test components (they depend on `witExports` directly).
- **How each workaround retires**: patched linker → when a scala-wasm release passes WIT params through
linear memory on both sides, drop `dapr4s-wasm-linker` and the plugin's linker override; overlay → when
the nightly's library-js is Wasm-clean, drop `dapr4s-wasm-scalalib-overlay`; WIT re-embed → when the
linker keeps WIT aliases (or reads the WIT world), use its component as is; exclusions → when the
fork's library and mainline's can evict each other; the 2.13 glue → when Scala 3 gets WIT codegen.
- **The patched linker** (`scripts/wasm/build-patched-linker.sh`, run by `wasmLinker` on demand;
idempotent, stamp = commit + patch hash): the released linker leaves the EXPORT side of
params-via-memory (> 16 flat params) a `???` (`on-topic-event`, `on-timer` of the mandatory callbacks)
and silently emits NOTHING on the IMPORT side (`jobs.schedule`, `conversation.converse`,
`actors.register-reminder`/`-timer`; `component new` then fails "expected i32 but nothing on stack").
The patch implements both (canonical ABI `lift_flat_values` / `lower_flat_values`).
`test/wasm/imports-probe` calls the four imports; the e2e checks it imports exactly them and runs
them against daprd (daprd's answers name the component/actor the probe sent).
- **How each workaround retires**: the local scala-wasm build → a scala-wasm release with #268, #269,
#271; overlay → when the nightly's library-js is Wasm-clean, drop `dapr4s-wasm-scalalib-overlay`;
the fork-scalalib exclusion → when the scala-wasm plugin stops adding its 2.13 scalalib to Scala 3
projects; `useWitAliases` (`wasm/build.sbt`: rewrites the generated bindings so a `use`d alias such as
`type Metadata` points at `types.Metadata`, the only form #269 keeps) → when wit-bindgen-scala renders
`use`d aliases so; the fork's `scalajs-junit-test-runtime` on `wallWasm/Test` (#271 excludes the
upstream one munit needs, adds no replacement) → when the plugin swaps it; the scaladoc-less
`dapr4s-wasm` javadoc jar → a nightly with scala/scala3#27300 (none yet: the newest, 2026-10-15,
predates the merge); the 2.13 glue → when Scala 3 gets WIT codegen.
- **Retired** (2026-10-16, verified by `scripts/wasm/e2e.sh` + `consumer-check.sh`): the patched linker
`dapr4s-wasm-linker` (#268 lowers > 16 flat params through linear memory on both sides; the
`test/wasm/imports-probe` e2e runs `jobs.schedule`, `conversation.converse`,
`actors.register-reminder`/`-timer` against daprd), the WIT re-embed (`component embed --encoding
utf16` + `component new`; the linker's component, with #269 + `useWitAliases`, composes with the
providers as is) and the `org.scala-js` library/javalib exclusions (#271).
- **WIT only through OCI**: `world app { include dapr-wasm-components:interfaces/app@0.7.5; }`
+ the upstream `wkg-config.toml`; `wkg wit fetch --config wkg-config.toml` runs before
wit-bindgen-scala. `wkg.lock` is committed, `wasm/wit/deps/` is git-ignored. Never copy WIT from git.
- **Newest everything**: the glue tracks the WIT version (`wasm/wit/world.wit`, the `WitScope`
versions in the trampolines). Always the newest dapr-wasm-components WIT release (now 0.7.5), the
newest scala-wasm sbt plugin on Central (now 1.22.1-wasm.6) and the newest wit-bindgen-scala (0.1.0).
newest scala-wasm (now the local build above; on Central 1.22.1-wasm.6) and the newest wit-bindgen-scala (0.1.0).
- **The two-compiler constraint**: mainline Scala 3 has no WIT codegen (imports are lowered at
the call site by the compiler backend, `@WitExport` is compiler-emitted). Every import call and
every export must be compiled by Scala 2.13 + scala-wasm's compiler plugin. The 2.13 code is
Expand Down Expand Up @@ -455,17 +466,19 @@ agents push branches / open drafts with an EMPTY description; the user writes th
`dapr4s.internal.witglue.AppRoot`. The stubs are never published and are filtered out of the POMs
(on a linker classpath they would shadow the real objects: first IR file per class wins).
- **Central publishing switch**: the `publish` job of `.github/workflows/ci.yml` sets
`PUBLISH_WASM_TO_CENTRAL: 'true'` (ON since 2026-10-13, approved by the user); the wasm build keeps
`PUBLISH_WASM_TO_CENTRAL` (ON since 2026-10-13, approved by the user; `'false'` while the local
scala-wasm build is pinned); the wasm build keeps
`publish / skip` unless that variable is `'true'`, so local `sbt publish` in `wasm/` never reaches Central.
The job publishes `coreJS` locally and runs `sbt -Ddapr4s.wasm.core=<version> ci-release` in `wasm/`.
- **Shared code on the `wasm` path** must avoid `java.time` formatting/parsing, wall-clock
(`Instant.now`), `println` and `UUID.randomUUID`: they reach JS and fail to link.
- Commands (tools: `wkg`, `wasm-tools`, `wit-bindgen-scala` for the build; plus Docker, the dapr
- Commands (first `scripts/wasm/build-scala-wasm.sh`; tools: `wkg`, `wasm-tools`, `wit-bindgen-scala`
for the build; plus Docker, the dapr
CLI + daprd from `dapr init --slim`, `wasmtime` and `wac` for the e2e):
- `cd wasm && sbt wallWasm/test` (wall unit tests, plain Scala.js on Node; add
`-Ddapr4s.wasm.core=source` for the source mode)
- `cd wasm && sbt counter/dapr4sComponent` → `wasm/target/components/counter.wasm`
- `cd wasm && sbt publishLocal` (the six artifacts, for dapr4s-examples)
- `cd wasm && sbt publishLocal` (the five artifacts, for dapr4s-examples)
- `scripts/wasm/e2e.sh` (counter-raw, counter, imports-probe: imports check, compose, daprd + Redis, for the
probe also a `daprio/dapr` scheduler container and daprd's echo conversation component; the probe asserts
`converse` → `ok: <echo>`, `schedule` → `ok: scheduled …`, register-reminder/-timer on an unhosted actor type →
Expand Down Expand Up @@ -509,7 +522,7 @@ Four legs, exactly as CI runs them (`.github/workflows/ci.yml`, one `test` matri
`jvm` and a `js` include entry — a future Scala Native port is a new include entry, not a new
job; each leg runs compile + unit tests + integration tests). A third include entry, `wasm`, runs
the opt-in `wasm/` build (format check, compile, `wallWasm/test`, `scripts/wasm/e2e.sh`; see
"Wasm component layer rules"). It installs its pinned tools, builds and caches the patched linker,
"Wasm component layer rules"). It installs its pinned tools, builds and caches the pinned scala-wasm snapshot,
and gates `publish` like the other legs (the wasm artifacts are published by the same job).

- **JVM unit tests** (no Docker): `sbt coreJVM/test`. Shared
Expand Down
Loading
Loading