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
5 changes: 5 additions & 0 deletions cmd/gh-aw-wasm/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package main

import (
"strings"
"syscall/js"

"github.com/github/gh-aw/pkg/parser"
Expand Down Expand Up @@ -87,9 +88,13 @@ func doCompile(markdown string, files map[string][]byte, filename string) (js.Va
defer parser.ClearVirtualFiles()
}

// Derive workflow identifier from filename for fuzzy cron schedule scattering
identifier := strings.TrimSuffix(filename, ".md")

compiler := workflow.NewCompiler(
workflow.WithNoEmit(true),
workflow.WithSkipValidation(true),
workflow.WithWorkflowIdentifier(identifier),
)

// Parse directly from string — no temp files needed
Expand Down
7 changes: 7 additions & 0 deletions pkg/parser/virtual_fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,10 @@ import "os"
// In wasm builds, this is overridden to read from a virtual filesystem
// populated by the browser via SetVirtualFiles.
var readFileFunc = os.ReadFile

// ReadFile reads a file using the parser's file reading function, which
// checks the virtual filesystem first in wasm builds. Use this instead of
// os.ReadFile when reading files that may be provided as virtual files.
func ReadFile(path string) ([]byte, error) {
return readFileFunc(path)
}
14 changes: 11 additions & 3 deletions pkg/workflow/compiler_jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,18 @@ func (c *Compiler) getReferencedCustomJobs(content string, customJobs map[string
func (c *Compiler) buildJobs(data *WorkflowData, markdownPath string) error {
compilerJobsLog.Printf("Building jobs for workflow: %s", markdownPath)

// Try to read frontmatter to determine event types for safe events check
// Try to read frontmatter to determine event types for safe events check.
// Use contentOverride first (set by ParseWorkflowString for wasm/string API mode),
// then fall back to reading from disk.
var frontmatter map[string]any
if content, err := os.ReadFile(markdownPath); err == nil {
if result, err := parser.ExtractFrontmatterFromContent(string(content)); err == nil {
var rawContent string
if c.contentOverride != "" {
rawContent = c.contentOverride
} else if diskContent, err := os.ReadFile(markdownPath); err == nil {
rawContent = string(diskContent)
}
if rawContent != "" {
if result, err := parser.ExtractFrontmatterFromContent(rawContent); err == nil {
frontmatter = result.Frontmatter
}
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/compiler_orchestrator_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func (c *Compiler) setupEngineAndImports(result *parser.FrontmatterResult, clean
fmt.Fprintf(os.Stderr, "WARNING: Skipping security scan for unresolvable import '%s': %v\n", importedFile, resolveErr)
continue
}
importContent, readErr := os.ReadFile(fullPath)
importContent, readErr := parser.ReadFile(fullPath)
if readErr != nil {
orchestratorEngineLog.Printf("Skipping security scan for unreadable import: %s: %v", fullPath, readErr)
fmt.Fprintf(os.Stderr, "WARNING: Skipping security scan for unreadable import '%s' (resolved path: %s): %v\n", importedFile, fullPath, readErr)
Expand Down
6 changes: 4 additions & 2 deletions pkg/workflow/compiler_string_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
func (c *Compiler) CompileToYAML(workflowData *WorkflowData, markdownPath string) (string, error) {
c.markdownPath = markdownPath
c.skipHeader = true
// Clear contentOverride after compilation (set by ParseWorkflowString)
defer func() { c.contentOverride = "" }()

startTime := time.Now()
defer func() {
Expand Down Expand Up @@ -51,9 +53,9 @@ func (c *Compiler) ParseWorkflowString(content string, virtualPath string) (*Wor

cleanPath := filepath.Clean(virtualPath)

// Store content so downstream code can use it instead of reading from disk
// Store content so downstream code can use it instead of reading from disk.
// Cleared in CompileToYAML after compilation completes.
c.contentOverride = content
defer func() { c.contentOverride = "" }()

// Enable inline prompt mode for string-based compilation (Wasm/browser)
// since runtime-import macros cannot resolve without filesystem access
Expand Down
92 changes: 92 additions & 0 deletions pkg/workflow/testdata/wasm_golden/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Wasm Golden Tests

Golden file tests that verify the wasm compiler (string API) produces correct YAML output.

## Directory structure

```
wasm_golden/
fixtures/ # Input .md workflow files
basic-copilot.md # Synthetic fixtures (stable)
smoke-claude.md # Smoke workflow fixtures (from .github/workflows/)
shared/ # Shared components for import resolution
mood.md
reporting.md
...
TestWasmGolden_CompileFixtures/ # Golden output files (auto-generated)
basic-copilot.golden
smoke-claude.golden
...
```

## How it works

1. Each `.md` file in `fixtures/` is compiled via `ParseWorkflowString()` + `CompileToYAML()` — the same code path used by the wasm binary
2. The output is compared byte-for-byte against the corresponding `.golden` file
3. The Node.js wasm test (`scripts/test-wasm-golden.mjs`) also builds the actual wasm binary and verifies its output matches the same golden files

## Common tasks

### Regenerate golden files after compiler changes

If you change the compiler and golden tests fail, regenerate the expected output:

```bash
make update-wasm-golden
```

This runs `go test ./pkg/workflow -run='^TestWasmGolden_' -update` which overwrites the `.golden` files with current compiler output. Review the diff before committing.

### Add a new fixture

1. Create a `.md` file in `fixtures/` with valid frontmatter (`name`, `on`, `engine`)
2. If it uses `imports:`, add the shared components to `fixtures/shared/`
3. Generate the golden file:
```bash
make update-wasm-golden
```
4. Commit the new `.md` file and its `.golden` file together

### Run just the wasm golden tests

```bash
# Go string API tests (fast, ~0.5s)
make test-wasm-golden

# Full wasm binary test via Node.js (builds wasm first, ~30s)
make test-wasm
```

### Fix a failing golden test

Golden tests fail when the compiler output changes. This is expected after compiler changes — the test is doing its job.

1. Run the failing test to see the diff:
```bash
go test -v -timeout=5m -run='^TestWasmGolden_CompileFixtures/basic-copilot$' ./pkg/workflow
```
2. If the change is intentional, regenerate:
```bash
make update-wasm-golden
```
3. Review the golden file diff (`git diff`) to confirm the changes are expected
4. Commit the updated `.golden` files with your compiler change

### Fix a wasm-specific divergence

If the Node.js wasm test fails but the Go golden test passes, the wasm binary is producing different output than the native string API. Common causes:

- **File reading**: Code using `os.ReadFile` instead of `parser.ReadFile` — the wasm build overrides `parser.ReadFile` to check virtual files
- **contentOverride**: The `Compiler.contentOverride` field must remain set until `CompileToYAML` completes (cleared in its defer, not in `ParseWorkflowString`)
- **Missing build tag stubs**: New code that calls OS functions needs a wasm stub (see `*_wasm.go` files in `pkg/workflow/` and `pkg/parser/`)

## Design decisions

**Why not use production workflows as fixtures?**
Production workflows in `.github/workflows/` change frequently. Using them as fixtures would break the golden tests on every mundane workflow edit, creating friction. The fixtures are self-contained copies that only change when the compiler changes.

**Why smoke workflows?**
The 4 smoke workflows (claude, codex, copilot, test-tools) provide real-world coverage with imports, safe-outputs, tools, network config, and MCP servers — features the synthetic fixtures don't exercise.

**Why exact match (no tolerance)?**
The golden test verifies compiler determinism. Any output difference — even a single character — indicates a real change in compilation behavior that should be reviewed.
Loading
Loading