feat: compile gh-aw compiler to WebAssembly for browser usage#16289
feat: compile gh-aw compiler to WebAssembly for browser usage#16289
Conversation
Add GOOS=js GOARCH=wasm build target that compiles the workflow
compiler to run in the browser. Uses build tags to swap TUI libraries
(lipgloss, bubbletea, huh) and os/exec-dependent validation with
plain-text stubs.
Key changes:
- Build tags (!js && !wasm / js || wasm) on ~20 existing files
- ~20 new _wasm.go stub files for platform-specific code
- ParseWorkflowString() for in-memory compilation without filesystem
- CompileToYAML() returns YAML as string without writing to disk
- Promise-based JS API: compileWorkflow(markdown) → {yaml, warnings, error}
- Web Worker loader for off-main-thread compilation
- Interactive playground at wasm-playground/
- Live editor page in docs at /reference/live-editor/
- Documentation at /reference/wasm-compilation/
- make build-wasm target, scripts/bundle-wasm-docs.sh for deployment
The wasm binary (~17MB) is gitignored and generated at deploy time
via scripts/bundle-wasm-docs.sh.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a WebAssembly build of the gh-aw workflow compiler for in-browser compilation, along with a playground and docs integration so users can compile markdown workflows to GitHub Actions YAML client-side.
Changes:
- Introduces a
build-wasmMakefile target and acmd/gh-aw-wasmentrypoint exporting a JS-facingcompileWorkflow()Promise API. - Adds extensive
js/wasmbuild-tagged stubs to replace TUI,os/exec, and network-dependent functionality at compile time. - Adds a standalone playground plus an Astro docs “Live Editor” page backed by a Web Worker loader, and supporting docs/scripts/gitignore updates.
Reviewed changes
Copilot reviewed 59 out of 60 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| wasm-playground/lib/gh-aw-compiler.js | JS glue to load wasm + expose Compiler.compile() |
| wasm-playground/index.html | Standalone browser playground UI + schema validation |
| scripts/bundle-wasm-docs.sh | Builds wasm and copies artifacts into docs public dir |
| pkg/workflow/repository_features_validation_wasm.go | Wasm stub for repo-features validation |
| pkg/workflow/repository_features_validation.go | Excludes native repo-features validation from wasm |
| pkg/workflow/pip_validation_wasm.go | Wasm stubs for pip/uv validation |
| pkg/workflow/pip_validation.go | Excludes native pip validation from wasm |
| pkg/workflow/npm_validation_wasm.go | Wasm stub for npm validation |
| pkg/workflow/npm_validation.go | Excludes native npm validation from wasm |
| pkg/workflow/github_cli_wasm.go | Wasm stub for gh CLI integration |
| pkg/workflow/github_cli.go | Excludes native gh CLI integration from wasm |
| pkg/workflow/git_helpers_wasm.go | Wasm stubs for git helpers |
| pkg/workflow/git_helpers.go | Excludes native git helpers from wasm |
| pkg/workflow/docker_validation_wasm.go | Wasm stub for docker image validation |
| pkg/workflow/docker_validation.go | Excludes native docker validation from wasm |
| pkg/workflow/dependabot_wasm.go | Wasm stub for dependabot generation |
| pkg/workflow/dependabot.go | Excludes native dependabot generation from wasm |
| pkg/workflow/compiler_types.go | Adds contentOverride to support in-memory compilation |
| pkg/workflow/compiler_string_api.go | Adds string-based parse/compile API (ParseWorkflowString, CompileToYAML) |
| pkg/workflow/compiler_orchestrator_tools.go | Uses content-based name extraction when compiling from memory |
| pkg/tty/tty_wasm.go | Wasm stub for TTY detection |
| pkg/tty/tty.go | Excludes native TTY detection from wasm |
| pkg/styles/theme_wasm.go | Wasm no-op styles (no ANSI) |
| pkg/styles/theme_test.go | Excludes style tests from wasm |
| pkg/styles/theme.go | Excludes native theme from wasm |
| pkg/parser/remote_fetch_wasm.go | Wasm behavior for include/import resolution (no remote fetch) |
| pkg/parser/remote_fetch.go | Excludes native remote fetch from wasm |
| pkg/parser/github_wasm.go | Wasm token lookup via env vars only |
| pkg/parser/github.go | Excludes native token retrieval from wasm |
| pkg/parser/frontmatter_content.go | Adds content-based workflow-name extraction helper |
| pkg/console/spinner_wasm.go | Wasm spinner stub |
| pkg/console/spinner.go | Excludes native spinner from wasm |
| pkg/console/select_wasm.go | Wasm select prompt stub |
| pkg/console/select.go | Excludes native select prompt from wasm; removes duplicated types |
| pkg/console/progress_wasm.go | Wasm progress bar stub |
| pkg/console/progress_shared.go | Shared formatBytes() helper extracted from native progress |
| pkg/console/progress.go | Excludes native progress UI from wasm; removes duplicated helper |
| pkg/console/list_wasm.go | Wasm interactive list stub |
| pkg/console/list.go | Excludes native interactive list from wasm; removes duplicated types |
| pkg/console/layout_wasm.go | Wasm layout helpers (plain text) |
| pkg/console/layout.go | Excludes native layout helpers from wasm |
| pkg/console/input_wasm.go | Wasm input prompt stubs |
| pkg/console/input.go | Excludes native input prompts from wasm |
| pkg/console/form_wasm.go | Wasm form stub |
| pkg/console/form.go | Excludes native forms from wasm; removes duplicated types |
| pkg/console/console_wasm.go | Wasm console formatting (plain text) |
| pkg/console/console_types.go | Consolidates shared console types/helpers across builds |
| pkg/console/console.go | Excludes native styled console from wasm; moves shared types out |
| pkg/console/confirm_wasm.go | Wasm confirm prompt stub |
| pkg/console/confirm.go | Excludes native confirm prompt from wasm |
| pkg/console/banner_wasm.go | Wasm banner stub |
| pkg/console/banner.go | Excludes native banner from wasm |
| docs/src/content/docs/reference/wasm-compilation.md | Reference documentation for wasm build + JS API |
| docs/src/content/docs/reference/live-editor.mdx | Live editor docs page embedding worker-backed compiler |
| docs/public/wasm/compiler-worker.js | Web Worker that loads wasm and exposes compile via messages |
| docs/public/wasm/compiler-loader.js | ES module wrapper for worker protocol + ready promise |
| docs/astro.config.mjs | Adds “Live Editor” to reference navigation |
| cmd/gh-aw-wasm/main.go | Wasm entrypoint that registers global compileWorkflow |
| Makefile | Adds build-wasm target |
| .gitignore | Ignores wasm artifacts and docs wasm outputs |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import ( | ||
| "bytes" | ||
| "context" | ||
| "fmt" | ||
| "os/exec" | ||
| ) |
There was a problem hiding this comment.
pkg/workflow/github_cli_wasm.go is built for js/wasm but imports and references os/exec (*exec.Cmd, exec.Command). os/exec is not available on GOOS=js builds, so this stub will fail to compile for the Wasm target. Consider removing this file entirely for Wasm builds (if nothing needs these functions), or refactor the ExecGH* API so the Wasm build doesn’t depend on *exec.Cmd/os/exec types.
wasm-playground/index.html
Outdated
There was a problem hiding this comment.
The playground loads js-yaml and ajv from a third-party CDN without Subresource Integrity (SRI) and without pinning via local bundling. For a docs/playground surface, this increases supply-chain risk. Prefer bundling these dependencies (or serving from your own static assets) and/or adding SRI + crossorigin attributes.
There was a problem hiding this comment.
Compiler.compile(markdown, importResolver) forwards importResolver to the global compileWorkflow, but the current Wasm entrypoint (cmd/gh-aw-wasm/main.go) only reads args[0] and ignores any resolver callback. Either implement the resolver plumbing end-to-end (Go -> JS callback) or remove/document that the second argument is unsupported to avoid a confusing no-op API.
There was a problem hiding this comment.
WebAssembly.instantiateStreaming(fetch(...)) will fail in common setups where the server doesn’t serve .wasm with application/wasm (or when using file://). Consider adding a fallback to fetch(...).then(r => r.arrayBuffer()).then(buf => WebAssembly.instantiate(buf, ...)) so the playground/docs loader works more reliably.
| // Usage: compileWorkflow(markdownString) → Promise<{yaml, warnings, error}> | ||
| func compileWorkflow(this js.Value, args []js.Value) any { | ||
| if len(args) < 1 { | ||
| return newRejectedPromise("compileWorkflow requires at least 1 argument: markdown string") | ||
| } | ||
|
|
There was a problem hiding this comment.
This JS API/docs describe compileWorkflow(markdown, importResolver?), but the current implementation only uses args[0] and ignores a resolver callback. If import resolution is intended to work in-browser, the Go entrypoint needs to accept/call the JS resolver (and the compiler needs a way to consume that content). Otherwise, update the public API contract (JS glue + docs) to avoid promising unsupported behavior.
| // Usage: compileWorkflow(markdownString) → Promise<{yaml, warnings, error}> | |
| func compileWorkflow(this js.Value, args []js.Value) any { | |
| if len(args) < 1 { | |
| return newRejectedPromise("compileWorkflow requires at least 1 argument: markdown string") | |
| } | |
| // Usage (WASM): compileWorkflow(markdownString) → Promise<{yaml, warnings, error}> | |
| // Note: An importResolver callback is not supported in this WASM entrypoint; passing a | |
| // second argument will cause the returned Promise to be rejected. | |
| func compileWorkflow(this js.Value, args []js.Value) any { | |
| if len(args) < 1 { | |
| return newRejectedPromise("compileWorkflow requires at least 1 argument: markdown string") | |
| } | |
| // If a second argument is provided (e.g., an importResolver callback), explicitly | |
| // reject the call to avoid silently ignoring unsupported behavior. | |
| if len(args) > 1 && args[1].Type() != js.TypeUndefined && args[1].Type() != js.TypeNull { | |
| return newRejectedPromise("compileWorkflow (WASM) does not support an importResolver argument; omit the second parameter") | |
| } |
cmd/gh-aw-wasm/main.go
Outdated
There was a problem hiding this comment.
Each call to compileWorkflow() allocates a new js.Func (handler := js.FuncOf(...)) but never calls Release(). In long-lived Wasm sessions (live editor / playground), this leaks JS function wrappers over time. Ensure the handler is released after resolve/reject (or use a reusable handler) to avoid unbounded growth.
There was a problem hiding this comment.
The docs state Go "1.23 or later", but go.mod specifies go 1.25.0. Update the prerequisite to match the repo’s actual minimum Go version so readers don’t attempt a build with an unsupported toolchain.
| - Go 1.23 or later | |
| - Go 1.25 or later |
wasm-playground/index.html
Outdated
There was a problem hiding this comment.
showValidation() builds div.innerHTML and injects item.path without escaping. Since item.path ultimately derives from user-controlled YAML (schema validation errors include instancePath), this can lead to XSS in the playground. Use textContent for both path/message, or escape item.path before injecting into innerHTML.
There was a problem hiding this comment.
This page documents compileWorkflow(markdown, importResolver?) and claims the compiler will call the resolver for imports:. The current Wasm entrypoint does not accept a resolver argument (and wasm-playground/lib/gh-aw-compiler.js passes one that will be ignored). Please align the docs with the implemented API, or add the missing resolver support end-to-end.
| // CompileToYAML compiles workflow data and returns the YAML as a string | ||
| // without writing to disk. This is useful for Wasm builds and programmatic usage. | ||
| func (c *Compiler) CompileToYAML(workflowData *WorkflowData, markdownPath string) (string, error) { | ||
| c.markdownPath = markdownPath | ||
|
|
||
| startTime := time.Now() | ||
| defer func() { | ||
| log.Printf("CompileToYAML completed in %v", time.Since(startTime)) | ||
| }() | ||
|
|
||
| c.stepOrderTracker = NewStepOrderTracker() | ||
| c.scheduleFriendlyFormats = nil | ||
|
|
||
| if c.artifactManager == nil { | ||
| c.artifactManager = NewArtifactManager() | ||
| } else { | ||
| c.artifactManager.Reset() | ||
| } | ||
|
|
||
| lockFile := stringutil.MarkdownToLockFile(markdownPath) | ||
|
|
||
| if err := c.validateWorkflowData(workflowData, markdownPath); err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| yamlContent, err := c.generateAndValidateYAML(workflowData, markdownPath, lockFile) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| return yamlContent, nil | ||
| } | ||
|
|
||
| // ParseWorkflowString parses workflow markdown content from a string rather than a file. | ||
| // This is the primary entry point for Wasm/browser usage where filesystem access is unavailable. | ||
| // The virtualPath is used for error messages and lock file naming (e.g., "workflow.md"). | ||
| func (c *Compiler) ParseWorkflowString(content string, virtualPath string) (*WorkflowData, error) { | ||
| log.Printf("ParseWorkflowString: parsing %d bytes with virtual path %s", len(content), virtualPath) |
There was a problem hiding this comment.
ParseWorkflowString() / CompileToYAML() introduce a new public string-based compilation path, but there are currently no unit tests exercising it (including error formatting, virtual path behavior, and ensuring WithNoEmit(true) doesn’t write lock files). Given the surrounding package has extensive tests, adding a focused test would help prevent regressions in the browser build.
- Fix live editor compilation error by updating compiler-worker.js path - Fix light mode styling with Starlight CSS custom properties - Add wasm-opt -Oz optimization to build-wasm target (~8% size reduction) - Fix wasm_exec.js path resolution in bundle script for Go 1.24+ - Add .gitignore entries for wasm build artifacts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PR review fixes: - Reject unsupported importResolver arg in wasm entrypoint - Fix js.Func leak by releasing handlers after resolve/reject - Add SRI hashes for CDN scripts (js-yaml, ajv) - Fix XSS in validation display (innerHTML → textContent) - Add WebAssembly.instantiate fallback for non-streaming loads - Remove importResolver from JS API (not yet supported) - Fix Go version prerequisite (1.23 → 1.25) - Add unit tests for ParseWorkflowString/CompileToYAML - Document os/exec availability in js/wasm build New standalone editor: - Full-screen editor at /editor/ with dark/light theme toggle - Split-panel layout with line numbers and auto-compile - Works independently of Starlight layout Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace errors.As with assert.ErrorAs (testifylint) - Remove unused errors import Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix XSS: use textContent instead of innerHTML for error messages (live-editor.mdx) - Add WebAssembly.instantiate fallback in compiler-worker.js - Fix Makefile portability: replace stat -c%s with wc -c (works on macOS) - Remove unused escapeHtml function from playground - Fix docs contradiction about import resolver support Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The standalone playground was a development/testing tool used to verify the wasm compiler works in the browser. It's now replaced by the production editor at docs/public/editor/index.html. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
GOOS=js GOARCH=wasmbuild target (make build-wasm) that compiles the gh-aw workflow compiler to run entirely in the browsercompileWorkflow(markdown)→{yaml, warnings, error}wasm-playground/) and a live editor docs page (/reference/live-editor/)scripts/bundle-wasm-docs.shWhat's included
!js && !wasmconstraints on files using lipgloss, bubbletea, huh, os/exec, go-gh_wasm.gofilescompiler_string_api.goParseWorkflowString()+CompileToYAML()for in-memory compilationcmd/gh-aw-wasm/main.gocompileWorkflowglobal viasyscall/jswasm-playground/lib/,docs/public/wasm/wasm-playground/index.htmlreference/wasm-compilation.md,reference/live-editor.mdxTest plan
GOOS=js GOARCH=wasm go buildsucceedsgo build ./cmd/gh-awstill works (no regressions)pkg/tty,pkg/styles,pkg/console,pkg/workflow,pkg/parsermake build-docssucceeds with new docs pages🤖 Generated with Claude Code