Publishing an example as a standalone repository¶
This guide turns one directory from examples/ into its
own self-contained GitHub repository, so a colleague can:
- download a prebuilt binary for Linux/macOS/Windows and run it, and
- optionally play it in the browser (a WebAssembly build on GitHub Pages).
It is the process used for the two reference repos — copy either as a template:
| Repo | Kind | Browser build? |
|---|---|---|
chrplr/Language-Localizer-French-audio |
audio experiment, ships .wav assets |
no |
chrplr/Rush-Hour |
mouse experiment, everything embedded | yes (Pages) |
Just need to hand off a zip, not publish a repo? Use the Makefile wrapper from the repo root:
make share-<ExampleName> # → _build/share/<ExampleName>/ make share-<ExampleName> VERSION=v0.12.5 # pin a specific goxpyriment releaseThat exports a standalone module (buildable on its own with
go run ., no workspace) to zip and email — seeexamples/README.mdin the repository. This guide goes further: it turns that output into a published repository with release CI and an optional in-browser build. Start fromshare.sh(Step 1), then add the pieces below.
When to use it¶
Publish an example this way when you want an external audience to run it without the goxpyriment workspace. Only publish a browser build for paradigms that tolerate browser timing (choice RT, memory, attention, puzzles) — not ones that need sub-millisecond onset control (rapid RSVP, subliminal priming). See WASM.md.
Prerequisites¶
- The
ghCLI, authenticated (gh auth status). - For a browser build: the example must embed all its assets with
//go:embed(no filesystem exists in the browser), and it should avoid APIs still stubbed on js — see WASM.md. - Use a goxpyriment release that contains the browser fixes (≥ v0.12.5):
the js
Save()no-op (one download at the end, not per block) and the mouse path fix (RenderCoordinatesFromWindow). Earlier releases crash or spam downloads in the browser. - Pin a release that delivers the session as a single
.zip. Until 2026-08-29 the browser build fired two downloads (the.csvand the-info.txt) milliseconds apart, and browsers set to ask where to save each file silently dropped the second one — sessions arrived with their metadata and no results. See "Why the results arrive as a .zip". Check which release you are pinning before you publish anything that will collect real data, and make sure yourweb/index.htmldescribes the download the pinned version actually produces.
Step 1 — Generate the standalone module¶
From the goxpyriment repo root, export the example with the latest release pinned (or pass a specific version):
bash examples/share.sh <ExampleName> <version> <output-parent-dir>
# e.g.
bash examples/share.sh Rush-Hour v0.12.5 ~/00_git
# → ~/00_git/Rush-Hour/ with a generated go.mod/go.sum requiring goxpyriment v0.12.5
share.sh copies the directory, drops the workspace go.mod/go.work coupling,
writes a fresh go.mod requiring the published goxpyriment, and runs
go mod tidy. The result builds on its own with go build ..
share.shstarts withrm -rfon its destination. Point the third argument at a parent directory where<ExampleName>/does not yet exist. Re-running it over a repository you have already started would delete the.gitdirectory and everything you added in the steps below.
make share-<ExampleName> is the convenient wrapper, but it always writes to
_build/share/<ExampleName>/; call share.sh directly with the third argument
when you want the module created straight where the repo will live (e.g.
~/00_git).
Step 2 — Prune goxpyriment-internal files¶
The copy still contains files that only make sense inside the monorepo. Remove:
cd ~/00_git/<ExampleName>
rm -rf .claude meta.yaml description.md # gallery/agent metadata, not needed standalone
Keep the .go sources, embedded assets, tests, and README.md.
If the example ships its own launcher page (web/index.html, as
examples/Reading-1 and examples/Memory_span do), share.sh has copied it
along with everything else — you do not need Rush-Hour's bare page. But it
was written for the monorepo, so grep it and web/README.md for references that
no longer resolve:
make wasm-<Name>-serve— that target exists only in the goxpyriment repo.docs/WASM.mdand other relative doc links — repoint them at https://chrplr.github.io/goxpyriment/WASM/.- comparisons to sibling examples ("unlike
Memory_span/web, …").
While you are in there, add a link back to the published repository: a demo page with no way to reach the source or the desktop build is a dead end for anyone who wants to actually run the experiment.
Step 3 — Add the repo scaffolding¶
Add these files (copy and adapt from the reference repos):
LICENSE— pick a license. goxpyriment itself is Apache-2.0, but you are its author, so you may license your example however you like (Rush-Hour is MIT). If you change the license, update the per-file header comments to match.build.sh/build.bat— a one-command local build (CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o <Name> .). These let a colleague build a locally-signed binary that avoids OS gatekeeper warnings..gitignore— ignore the built binary. Notego build .produces a binary named after the module (lowercase, e.g.rush-hour) andbuild.shproduces the-oname (e.g.Rush-Hour); ignore both, plusdist/and_build/.README.md— adapt the example's README: changego run ./examples/<Name>togo run ., and add "Binaries" (Releases) and, if applicable, "Play in your browser" sections. The reference READMEs include the standard notes about unsigned macOS/Windows binaries.- For a browser build only:
web/index.html— if the example already had one,share.shcopied it and Step 2 tells you what to rewrite in it; otherwise copy Rush-Hour's, which wires the canvas,sdl.js,wasm_exec.jsandmain.wasmand nothing else. A page that asks for a participant ID before starting (examples/Reading-1/web/index.html) is the fuller template: it writes the ID back as?s=<id>withhistory.replaceState, which is how the Go side receives it.
Step 4 — Add the CI workflows¶
.github/workflows/release.yml (native binaries)¶
Cross-compiles Linux/macOS/Windows on every push and, on a v* tag, publishes a
GitHub Release with archives + SHA256SUMS. CGO_ENABLED=0; go-sdl3 is pure Go
and embeds SDL for every platform, so all three cross-compile from Linux. This
path uses upstream go-sdl3 — no replace. Copy Rush-Hour's verbatim and
change the binary name.
.github/workflows/pages.yml (browser build — optional)¶
Builds the WebAssembly bundle and deploys it to GitHub Pages. Two things make it work (copy Rush-Hour's file and change nothing but the repo name in comments):
- It checks out the
chrplr/go-sdl3-wasmfork (branchwasm-render-fixes), which supplies the js/wasm SDL bindings and thewasmsdlbundler (shipssdl.js+sdl.wasm). - It runs
go mod edit -replace github.com/Zyko0/go-sdl3=../go-sdl3-wasmin CI only (never committed), so the browser build uses the fork while native/release builds keep using upstream go-sdl3.
Gotcha — branch ref, not pinned commit.
pages.ymltracks the fork's branch HEAD, whereas goxpyriment's owngo.modpins a specific fork pseudo-version. A fix that lives only in goxpyriment's vendored copy (or an uncommitted fork edit) will not reach your repo's browser build until it is committed and pushed towasm-render-fixes. This is the trap that broke Rush-Hour's first deploy.
Building the browser bundle locally¶
CI is not the only way to get a bundle, and you should get one before you publish — a Pages deploy is a slow way to discover that the page is broken. The standalone repo needs the same two ingredients the workflow uses:
cd ~/00_git/<ExampleName>
git clone -b wasm-render-fixes https://github.com/chrplr/go-sdl3-wasm ../go-sdl3-wasm
go mod edit -replace github.com/Zyko0/go-sdl3=../go-sdl3-wasm # NEVER commit this
(cd ../go-sdl3-wasm && go run ./cmd/wasmsdl serve \
-html "$PWD/../<ExampleName>/web/index.html" "$PWD/../<ExampleName>")
# → http://localhost:8080/?s=1, with the COOP/COEP headers
go mod edit -dropreplace github.com/Zyko0/go-sdl3 # before committing
Use wasmsdl build -out _build/wasm … instead of serve to inspect the bundle.
It is about 10 MB (roughly half sdl.wasm, half main.wasm), which is why
neither reference repo commits it: the workflow rebuilds it on every push, and
committing 10 MB per rebuild would bloat the history fast. Make sure .gitignore
covers wherever you put it.
Verify the replace is gone before you commit — git diff go.mod — or the
published module will point at a path that exists only on your machine.
Step 5 — Create the GitHub repo and enable Pages¶
cd ~/00_git/<ExampleName>
git init -b main && git add -A && git commit -m "Initial commit"
gh repo create chrplr/<ExampleName> --public --source=. --remote=origin --push
# Browser build only: enable Pages with GitHub Actions as the source
gh api -X POST repos/chrplr/<ExampleName>/pages -f build_type=workflow
The first push runs both workflows. The Pages URL is
https://chrplr.github.io/<ExampleName>/.
Step 6 — Cut a release¶
Binaries are published only on a version tag:
Native binaries are independent of the browser build — tag whenever the source is ready.
Updating the pinned goxpyriment later¶
When a newer goxpyriment fixes something you need, re-pin and redeploy:
go get github.com/chrplr/goxpyriment@vX.Y.Z && go mod tidy
git commit -am "Bump goxpyriment to vX.Y.Z" && git push # redeploys Pages
Gotcha — fresh tags and the checksum DB. Right after you push a goxpyriment tag,
sum.golang.orgmay not have indexed it yet andgo getfails with a500. Fetch straight from the tag once withGOPROXY=direct GOSUMDB=off go get github.com/chrplr/goxpyriment@vX.Y.Z; the correct hashes land in yourgo.sum, so CI (which readsgo.sum) is fine.
Verifying a browser build¶
Serve the bundle and open it in a real, focused, foreground tab:
# from the goxpyriment repo, before you split it out:
make wasm-<ExampleName>-serve # http://localhost:8080/?s=1
# or the built standalone repo's Pages URL after deploy
Sanity checks: the experiment renders and responds; a normal end triggers
exactly one download, a .zip holding the .csv and the -info.txt (not
one download per block, and not two files); on a crash you see the error overlay
and no download (see WASM.md). Check the browser console for
panics — each names the file:line of any still-stubbed binding.
Two things that will mislead you while checking by hand:
- An unfocused window throttles
requestAnimationFrameto ~1 Hz. goxpyriment notices and logsWARNING: measured refresh 1.00 Hz differs from nominal 60.00 Hz. A session driven from a background window is not a timing test, and a canvas that looks frozen is usually just being throttled. - A download that has not appeared is not necessarily lost. If the browser
asks where to save each file, the file lands only when the dialog is answered;
Chrome's own record is in the
downloadstable of~/.config/google-chrome/Default/History(copy it first — it is locked while Chrome runs), wherestate=2withinterrupt_reason=40and an emptytarget_pathmeans the dialog was never answered.
See also¶
examples/README.md(in the repository) —make share-NAME(the zip-and-email path)- WASM.md — how the browser build works, timing, and known gaps
- How_to_package_your_experiment.md — the GoReleaser alternative for native binaries