Skip to content

feat: compile gh-aw compiler to WebAssembly for browser usage#16289

Merged
Mossaka merged 7 commits intomainfrom
feat/wasm-compiler
Feb 17, 2026
Merged

feat: compile gh-aw compiler to WebAssembly for browser usage#16289
Mossaka merged 7 commits intomainfrom
feat/wasm-compiler

Conversation

@Mossaka
Copy link
Collaborator

@Mossaka Mossaka commented Feb 17, 2026

Summary

  • Adds GOOS=js GOARCH=wasm build target (make build-wasm) that compiles the gh-aw workflow compiler to run entirely in the browser
  • Uses Go build tags to swap TUI libraries and os/exec-dependent code with lightweight stubs
  • Provides a Promise-based JS API: compileWorkflow(markdown){yaml, warnings, error}
  • Includes an interactive playground (wasm-playground/) and a live editor docs page (/reference/live-editor/)
  • Wasm binary (~17MB) is gitignored and generated at deploy time via scripts/bundle-wasm-docs.sh

What's included

Area Files Description
Build tags ~20 existing files !js && !wasm constraints on files using lipgloss, bubbletea, huh, os/exec, go-gh
Wasm stubs ~20 new _wasm.go files Plain-text/no-op implementations for browser builds
String API compiler_string_api.go ParseWorkflowString() + CompileToYAML() for in-memory compilation
Entry point cmd/gh-aw-wasm/main.go Registers compileWorkflow global via syscall/js
JS library wasm-playground/lib/, docs/public/wasm/ Browser loader, Web Worker wrapper
Playground wasm-playground/index.html Split-panel editor for testing
Docs reference/wasm-compilation.md, reference/live-editor.mdx Reference docs + live editor page

Test plan

  • GOOS=js GOARCH=wasm go build succeeds
  • Native go build ./cmd/gh-aw still works (no regressions)
  • Unit tests pass: pkg/tty, pkg/styles, pkg/console, pkg/workflow, pkg/parser
  • Playwright browser test: wasm loads, compiles sample workflow, produces valid YAML
  • make build-docs succeeds with new docs pages
  • Manual: open playground in browser, paste workflow markdown, verify YAML output

🤖 Generated with Claude Code

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>
Copilot AI review requested due to automatic review settings February 17, 2026 05:31
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-wasm Makefile target and a cmd/gh-aw-wasm entrypoint exporting a JS-facing compileWorkflow() Promise API.
  • Adds extensive js/wasm build-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.

Comment on lines +5 to +10
import (
"bytes"
"context"
"fmt"
"os/exec"
)
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 451 to 454
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 121 to 144
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 100 to 104
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 17 to 22
// 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")
}

Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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")
}

Copilot uses AI. Check for mistakes.
Comment on lines 25 to 42
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
- Go 1.23 or later
- Go 1.25 or later

Copilot uses AI. Check for mistakes.
Comment on lines 617 to 620
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 63 to 117
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +49
// 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)
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Mossaka and others added 4 commits February 17, 2026 05:55
- 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>
Mossaka

This comment was marked as resolved.

Mossaka and others added 2 commits February 17, 2026 06:41
- 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>
@Mossaka Mossaka merged commit 1a3cd12 into main Feb 17, 2026
52 checks passed
@Mossaka Mossaka deleted the feat/wasm-compiler branch February 17, 2026 06:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments