| name | javascript-refactoring |
| description | Instructions for refactoring JavaScript code into separate files |
JavaScript Code Refactoring Guide
This guide explains how to refactor JavaScript code into a separate .cjs file in the gh-aw repository. Follow these steps when extracting shared functionality or creating new JavaScript modules.
Overview
The gh-aw project uses CommonJS modules (.cjs files) for JavaScript code that runs in GitHub Actions workflows. These files are:
- Embedded in the Go binary using
//go:embeddirectives - Bundled using a custom JavaScript bundler that inlines local
require()calls - Executed in GitHub Actions using
actions/github-script@v8
Step 1: Create the New .cjs File
Create your new file in /home/runner/work/gh-aw/gh-aw/pkg/workflow/js/ with a descriptive name:
File naming convention:
- Use snake_case for filenames (e.g.,
sanitize_content.cjs,load_agent_output.cjs) - Use
.cjsextension (CommonJS module) - Choose names that clearly describe the module's purpose
Example file structure:
// @ts-check
/// <reference types="@actions/github-script" />
/**
* Brief description of what this module does
*/
/**
* Function documentation
* @param {string} input - Description of parameter
* @returns {string} Description of return value
*/
function myFunction(input) {
// Implementation
return input;
}
// Export the function(s)
module.exports = {
myFunction,
};
Key points:
- Include
// @ts-checkfor TypeScript checking - Include
/// <reference types="@actions/github-script" />for GitHub Actions types - Use JSDoc comments for documentation
- Export functions using
module.exports = { ... } - Do NOT import
@actions/coreor@actions/github- these are available globally in GitHub Actions
Step 2: Add Tests
Create a test file with the same base name plus .test.cjs:
Example: pkg/workflow/js/my_module.test.cjs
import { describe, it, expect, beforeEach, vi } from "vitest";
// Mock the global objects that GitHub Actions provides
const mockCore = {
debug: vi.fn(),
info: vi.fn(),
warning: vi.fn(),
error: vi.fn(),
setFailed: vi.fn(),
setOutput: vi.fn(),
summary: {
addRaw: vi.fn().mockReturnThis(),
write: vi.fn().mockResolvedValue(),
},
};
// Set up global mocks before importing the module
global.core = mockCore;
describe("myFunction", () => {
beforeEach(() => {
// Reset mocks before each test
vi.clearAllMocks();
});
it("should handle basic input", async () => {
// Import the module to test
const { myFunction } = await import("./my_module.cjs");
const result = myFunction("test input");
expect(result).toBe("expected output");
});
it("should handle edge cases", async () => {
const { myFunction } = await import("./my_module.cjs");
const result = myFunction("");
expect(result).toBe("");
});
});
Testing guidelines:
- Use vitest for testing framework
- Mock
coreandgithubglobals as needed - Use dynamic imports (
await import()) to allow mocking before module load - Clear mocks in
beforeEachto ensure test isolation - Test both success cases and error handling
- Follow existing test patterns in
pkg/workflow/js/*.test.cjsfiles
Run tests:
make test-js
Step 3: Add Embedded Variable in Go
Add an //go:embed directive and variable in the appropriate Go file:
For shared utility functions (used by multiple scripts):
Add to pkg/workflow/js.go:
//go:embed js/my_module.cjs
var myModuleScript string
Then add to the GetJavaScriptSources() function:
func GetJavaScriptSources() map[string]string {
return map[string]string{
"sanitize_content.cjs": sanitizeContentScript,
"sanitize_label_content.cjs": sanitizeLabelContentScript,
"sanitize_workflow_name.cjs": sanitizeWorkflowNameScript,
"load_agent_output.cjs": loadAgentOutputScript,
"staged_preview.cjs": stagedPreviewScript,
"is_truthy.cjs": isTruthyScript,
"my_module.cjs": myModuleScript, // Add this line
}
}
For main scripts (top-level scripts that use bundling):
Add to pkg/workflow/scripts.go:
//go:embed js/my_script.cjs
var myScriptSource string
Then create a getter function with bundling:
var (
myScript string
myScriptOnce sync.Once
)
// getMyScript returns the bundled my_script script
// Bundling is performed on first access and cached for subsequent calls
func getMyScript() string {
myScriptOnce.Do(func() {
sources := GetJavaScriptSources()
bundled, err := BundleJavaScriptFromSources(myScriptSource, sources, "")
if err != nil {
scriptsLog.Printf("Bundling failed for my_script, using source as-is: %v", err)
// If bundling fails, use the source as-is
myScript = myScriptSource
} else {
myScript = bundled
}
})
return myScript
}
Important:
- Variables in
js.goare for shared utilities that get bundled into other scripts - Variables in
scripts.goare for main scripts that use the bundler to inline dependencies - Use
sync.Oncepattern for lazy bundling inscripts.go - The bundler will inline all local
require()calls at runtime
Step 4: Register in the Bundler (if creating a shared utility)
If you're creating a shared utility that will be used by other scripts via require(), it's automatically available through the GetJavaScriptSources() map (Step 3).
The bundler will:
- Detect
require('./my_module.cjs')in any script - Look up the file in the
GetJavaScriptSources()map - Inline the required module's content
- Remove the
require()statement - Deduplicate if the same module is required multiple times
No additional bundler registration needed - just ensure the file is in the GetJavaScriptSources() map.
Step 5: Use Local Require in Other JavaScript Files
To use your new module in other JavaScript files, use CommonJS require():
Example usage in another .cjs file:
// @ts-check
/// <reference types="@actions/github-script" />
const { myFunction } = require("./my_module.cjs");
async function main() {
const result = myFunction("some input");
core.info(`Result: ${result}`);
}
await main();
Require guidelines:
- Use relative paths starting with
./ - Include the
.cjsextension - Use destructuring to import specific functions
- The bundler will inline the required module at compile time
Multiple requires example:
const { sanitizeContent } = require("./sanitize_content.cjs");
const { loadAgentOutput } = require("./load_agent_output.cjs");
const { generateStagedPreview } = require("./staged_preview.cjs");
Complete Example: Creating a New Utility Module
Let's walk through creating a new format_timestamp.cjs utility:
1. Create the file: pkg/workflow/js/format_timestamp.cjs
// @ts-check
/// <reference types="@actions/github-script" />
/**
* Formats a timestamp to ISO 8601 format
* @param {Date|string|number} timestamp - Timestamp to format
* @returns {string} ISO 8601 formatted timestamp
*/
function formatTimestamp(timestamp) {
const date = timestamp instanceof Date ? timestamp : new Date(timestamp);
return date.toISOString();
}
/**
* Formats a timestamp to a human-readable string
* @param {Date|string|number} timestamp - Timestamp to format
* @returns {string} Human-readable timestamp
*/
function formatTimestampHuman(timestamp) {
const date = timestamp instanceof Date ? timestamp : new Date(timestamp);
return date.toLocaleString('en-US', {
dateStyle: 'medium',
timeStyle: 'short'
});
}
module.exports = {
formatTimestamp,
formatTimestampHuman,
};
2. Create tests: pkg/workflow/js/format_timestamp.test.cjs
import { describe, it, expect } from "vitest";
describe("formatTimestamp", () => {
it("should format Date object to ISO 8601", async () => {
const { formatTimestamp } = await import("./format_timestamp.cjs");
const date = new Date('2024-01-15T12:30:00Z');
const result = formatTimestamp(date);
expect(result).toBe('2024-01-15T12:30:00.000Z');
});
it("should format timestamp number to ISO 8601", async () => {
const { formatTimestamp } = await import("./format_timestamp.cjs");
const timestamp = 1705323000000; // Jan 15, 2024 12:30:00 UTC
const result = formatTimestamp(timestamp);
expect(result).toBe('2024-01-15T12:30:00.000Z');
});
});
describe("formatTimestampHuman", () => {
it("should format Date object to human-readable string", async () => {
const { formatTimestampHuman } = await import("./format_timestamp.cjs");
const date = new Date('2024-01-15T12:30:00Z');
const result = formatTimestampHuman(date);
expect(result).toContain('Jan');
expect(result).toContain('15');
expect(result).toContain('2024');
});
});
3. Add to pkg/workflow/js.go:
//go:embed js/format_timestamp.cjs
var formatTimestampScript string
func GetJavaScriptSources() map[string]string {
return map[string]string{
// ... existing entries ...
"format_timestamp.cjs": formatTimestampScript,
}
}
4. Use in another script:
// @ts-check
/// <reference types="@actions/github-script" />
const { formatTimestamp } = require("./format_timestamp.cjs");
async function main() {
const now = new Date();
core.info(`Current time: ${formatTimestamp(now)}`);
}
await main();
5. Build and test:
# Format the code
make fmt-cjs
# Run JavaScript tests
make test-js
# Run Go tests (includes bundler tests)
make test-unit
# Build the binary (embeds JavaScript files)
make build
Verification Checklist
Before committing your refactored code:
- New
.cjsfile created inpkg/workflow/js/ - Tests created in corresponding
.test.cjsfile - Tests pass with
make test-js - Embedded variable added in
pkg/workflow/js.goorpkg/workflow/scripts.go - If utility: Added to
GetJavaScriptSources()map - If main script: Created bundling getter function with
sync.Once - Local
require()statements work correctly in other files - Code formatted with
make fmt-cjs - Code linted with
make lint-cjs - All Go tests pass with
make test-unit - Build succeeds with
make build
Common Patterns
Pattern 1: Shared Utility Function
Files like sanitize_content.cjs, load_agent_output.cjs that provide reusable functions:
- Add to
js.gowith//go:embed - Add to
GetJavaScriptSources()map - Use via
require()in other scripts
Pattern 2: Main Workflow Script
Files like create_issue.cjs, add_labels.cjs that are top-level scripts:
- Add to
scripts.gowith//go:embedasxxxSourcevariable - Create bundling getter function with
sync.Oncepattern - These scripts can
require()utilities fromGetJavaScriptSources()
Pattern 3: Log Parser
Files like parse_claude_log.cjs that parse AI engine logs:
- Add to
js.gowith//go:embed - Add case in
GetLogParserScript()function - Used by workflow compilation system
Troubleshooting
Issue: "required file not found in sources"
Cause: File not added to GetJavaScriptSources() map
Solution: Add the file to the map in pkg/workflow/js.go
Issue: Tests fail with "core is not defined"
Cause: Missing global mocks
Solution: Add proper mocks before importing the module:
global.core = mockCore;
global.github = mockGithub;
Issue: Bundler fails with circular dependency
Cause: File A requires File B which requires File A
Solution: Restructure to break the circular dependency, or combine the modules
Issue: Changes not reflected after rebuild
Cause: Go build cache not recognizing embedded file changes
Solution:
make clean
make build
References
- Bundler implementation:
pkg/workflow/bundler.go - JavaScript sources registry:
pkg/workflow/js.go - Script bundling:
pkg/workflow/scripts.go - Existing test examples:
pkg/workflow/js/*.test.cjs - GitHub Actions script documentation: actions/toolkit