npx skills add ...
npx skills add github/gh-aw --skill javascript-refactoring
Split large JavaScript files into maintainable modules safely.
npx skills add github/gh-aw --skill javascript-refactoring
Use this guide to split JavaScript into maintainable CommonJS modules in gh-aw without drifting into dead embedding patterns.
The current gh-aw architecture is action-centric:
pkg/workflow/js/*.cjs and actions/setup/js/*.cjsactions/<action-name>/src/actions/<action-name>/index.jsmake actions-build, gh aw actions-build) and dependency maps such as pkg/cli/actions_build_command.gopkg/workflow/js.go is a stub; it no longer owns the runtime JavaScript shipping path for the main workflowsIf you are refactoring a workflow utility, prefer the current action/module architecture over any older //go:embed pattern.
Top-level .cjs scripts executed directly in workflows follow this pattern:
✅ Correct Pattern - Export main, but don't call it:
❌ Incorrect Pattern - Don't call main in the file:
Why this pattern?
await main() at execution timeChoose the correct location for the module before writing code:
pkg/workflow/js/actions/<action-name>/src/ or actions/setup/js/actions/<action-name>/index.jsFile naming convention:
sanitize_content.cjs, load_agent_output.cjs).cjs for CommonJS modulesExample file structure:
Key points:
// @ts-check for TypeScript checking/// <reference types="@actions/github-script" /> when the module is used with GitHub Actions scriptsmodule.exports = { ... }@actions/core or @actions/github directly unless the module is running in an action context that explicitly expects itCreate a matching test beside the module using the same base name plus .test.cjs:
Example: pkg/workflow/js/my_module.test.cjs
Testing guidelines:
core and github globals as neededawait import()) to allow module setup at test timebeforeEachRun tests:
Do not add a new //go:embed mapping just to ship a new runtime script. The current repo ships JavaScript through the action-generation/build pipeline.
Use this checklist:
pkg/cli/actions_build_command.goactions/<action-name>/src/make actions-buildpkg/workflow/js/ and update the action or workflow definition that consumes itExample design:
Run the relevant checks for the area you changed:
Before committing your refactor:
.cjs file created in the correct source directory.test.cjs file createdmake test-js or the targeted Vitest suiterequire() statements work correctly in other JS filesmake fmt-cjsmake lint-cjs or make test-unitFiles like sanitize_content.cjs or load_agent_output.cjs are best kept under pkg/workflow/js/ or actions/setup/js/ and consumed by other JS modules via require().
When the JavaScript belongs to a single action, keep it under actions/<action-name>/src/ and regenerate the output bundle with make actions-build.
If the script is executed directly in a workflow, export main and omit the direct await main() call. The host build/runtime step handles execution.
Cause: Action bundle was not rebuilt after editing the source file
Solution:
core is not definedCause: Missing global mocks
Solution:
Cause: It was added to the wrong layer
Solution: Move it to the action-specific source tree instead of creating a broad workflow-level registry entry.
actions/README.md - current action-generation/build workflowpkg/cli/actions_build_command.go - action dependency mappingpkg/workflow/js/*.cjs - existing shared module patternsactions/setup/js/*.cjs - action runtime/source examplesasync function main() {
// Script logic here
core.info("Running the script");
}
await main(); // ❌ Don't do this!
module.exports = { main };// @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) {
return input;
}
module.exports = {
myFunction,
};import { describe, it, expect, beforeEach, vi } from "vitest";
const mockCore = {
debug: vi.fn(),
info: vi.fn(),
warning: vi.fn(),
error: vi.fn(),
setFailed: vi.fn(),
setOutput: vi.fn(),
};
global.core = mockCore;
describe("myFunction", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("handles a normal input", async () => {
const { myFunction } = await import("./my_module.cjs");
expect(myFunction("test input")).toBe("expected output");
});
it("handles empty input", async () => {
const { myFunction } = await import("./my_module.cjs");
expect(myFunction("")).toBe("");
});
});make test-jsconst { myFunction } = require("./my_module.cjs");
async function main() {
const result = myFunction("some input");
core.info(`Result: ${result}`);
}
module.exports = { main };make fmt-cjs
make lint-cjs
make test-js
make test-unit
make actions-buildmake actions-buildglobal.core = mockCore;