关于 javascript-refactoring
面向 gh-aw 工作流 JavaScript 重构的开发指南,介绍 CommonJS `.cjs` 模块的组织方式,以及如何在 Go 中嵌入、打包和测试。
主要功能
- 01CommonJS 模块约定
- 02Go 嵌入与打包
- 03工具模块和脚本模式
- 04Vitest 测试指南
- 05格式化与验证清单
适用场景
- 01将工作流 JavaScript 拆分为专注模块
- 02添加共享工具模块或顶层工作流脚本
- 03测试并验证 `.cjs` 变更
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ github/ gh-aw --skill javascript-refactoring安装后在 Claude Code 中输入/javascript-refactoring确认已加载。
详细介绍
JavaScript 代码重构指南
使用本指南将 JavaScript 重构为 gh-aw 中独立的 .cjs 文件。
概览
gh-aw 在 GitHub Actions 工作流中使用 CommonJS 模块(.cjs)编写 JavaScript。这些文件会:
- 使用
//go:embed指令嵌入 Go 二进制文件 - 使用自定义 JavaScript 打包器打包,该打包器会内联本地
require()调用 - 在 GitHub Actions 中通过
actions/github-script@v8执行
顶层脚本模式
在工作流中直接执行的顶层 .cjs 脚本应遵循以下模式:
✅ 正确模式——导出 main,但不要调用它:
async function main() {
// Script logic here
core.info("Running the script");
}
module.exports = { main };
❌ 错误模式——不要在文件中调用 main:
async function main() {
// Script logic here
core.info("Running the script");
}
await main(); // ❌ Don't do this!
module.exports = { main };
为什么要采用这种模式?
- 打包器会在 GitHub Actions 中内联执行时自动注入
await main() - 这样脚本既可以导入(用于测试),也可以执行(用于工作流)
- 这种模式能清晰地区分模块定义和执行
- 测试可以导入 main 并使用模拟对象调用它,从而更便于测试
顶层脚本示例:
create_issue.cjs- 创建 GitHub issueadd_comment.cjs- 为 issue/PR 添加评论add_labels.cjs- 为 issue/PR 添加标签update_project.cjs- 更新 GitHub Projects
这些文件都会导出 main,但不会直接调用它。
第 1 步:创建新的 .cjs 文件
在 /home/runner/work/gh-aw/gh-aw/pkg/workflow/js/ 中创建新文件,并使用描述性名称:
文件命名约定:
- 文件名使用 snake_case(例如,
sanitize_content.cjs、load_agent_output.cjs) - 使用
.cjs扩展名(CommonJS 模块) - 选择能够清晰描述模块用途的名称
文件结构示例:
// @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,
};
要点:
- 添加
// @ts-check以启用 TypeScript 检查 - 添加
/// <reference types="@actions/github-script" />以引用 GitHub Actions 类型 - 使用 JSDoc 注释编写文档
- 使用
module.exports = { ... }导出函数 - 不要导入
@actions/core或@actions/github——这些在 GitHub Actions 中可全局使用
第 2 步:添加测试
创建一个测试文件,其基本名称与源文件相同,并以 .test.cjs 结尾:
示例: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("");
});
});
测试指南:
- 使用 vitest 作为测试框架
- 根据需要模拟
core和github全局对象 - 使用动态导入(
await import()),以便在模块加载前进行模拟 - 在
beforeEach中清除模拟,以确保测试隔离 - 测试成功场景和错误处理
- 遵循
pkg/workflow/js/*.test.cjs文件中的现有测试模式
运行测试:
make test-js
第 3 步:在 Go 中添加嵌入变量
在相应的 Go 文件中添加 //go:embed 指令和变量:
对于共享工具函数(由多个脚本使用):
添加到 pkg/workflow/js.go:
//go:embed js/my_module.cjs
var myModuleScript string
然后添加到 GetJavaScriptSources() 函数中:
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
}
}
对于主脚本(使用打包的顶层脚本):
添加到 pkg/workflow/scripts.go:
//go:embed js/my_script.cjs
var myScriptSource string
然后创建一个带打包功能的 getter 函数:
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
}
重要提示:
js.go中的变量用于打包到其他脚本中的共享工具scripts.go中的变量用于通过打包器内联依赖项的主脚本- 在
scripts.go中使用sync.Once模式进行延迟打包 - 打包器会在运行时内联所有本地
require()调用
第 4 步:在打包器中注册(如果要创建共享工具)
如果要创建供其他脚本通过 require() 使用的共享工具,它会通过 GetJavaScriptSources() 映射(第 3 步)自动提供。
打包器会:
- 检测任何脚本中的
require('./my_module.cjs') - 在
GetJavaScriptSources()映射中查找该文件 - 内联所需模块的内容
- 移除
require()语句 - 如果同一模块被多次引入,则进行去重
无需额外注册打包器——只需确保文件位于 GetJavaScriptSources() 映射中。
步骤 5:在其他 JavaScript 文件中使用本地 require
要在其他 JavaScript 文件中使用新模块,请使用 CommonJS require():
在另一个 .cjs 文件中的用法示例:
// @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}`);
}
module.exports = { main };
重要:顶层脚本应导出 main,但不得直接调用它。打包器会在 GitHub Actions 中内联执行时注入 await main()。
require 指南:
- 使用以
./开头的相对路径 - 包含
.cjs扩展名 - 使用解构导入特定函数
- 打包器会在编译时内联所需模块
多个 require 的示例:
const { sanitizeContent } = require("./sanitize_content.cjs");
const { loadAgentOutput } = require("./load_agent_output.cjs");
const { generateStagedPreview } = require("./staged_preview.cjs");
完整示例:创建新的实用工具模块
下面介绍如何创建一个新的 format_timestamp.cjs 实用工具:
1. 创建文件: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. 创建测试: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. 添加到 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. 在另一个脚本中使用:
// @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)}`);
}
module.exports = { main };
**注意:**该脚本会导出 main,但不会调用它。脚本在 GitHub Actions 中以内联方式执行时,打包器会注入 await main()。
5. 构建并测试:
# 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
验证清单
提交重构后的代码前:
- 在
pkg/workflow/js/中创建新的.cjs文件 - 在对应的
.test.cjs文件中创建测试 - 使用
make test-js运行测试并确保通过 - 在
pkg/workflow/js.go或pkg/workflow/scripts.go中添加嵌入变量 - 如果是实用工具:添加到
GetJavaScriptSources()映射中 - 如果是主脚本:使用
sync.Once创建打包 getter 函数 - 在其他文件中的本地
require()语句正常工作 - 使用
make fmt-cjs格式化代码 - 使用
make lint-cjs检查代码 - 使用
make test-unit确保所有 Go 测试通过 - 使用
make build确保构建成功
常见模式
模式 1:共享实用工具函数
像 sanitize_content.cjs、load_agent_output.cjs 这样的文件会提供可重用的函数:
- 使用
//go:embed添加到js.go - 添加到
GetJavaScriptSources()映射中 - 在其他脚本中通过
require()使用
模式 2:主工作流脚本
像 create_issue.cjs、add_labels.cjs 这样的文件属于顶层脚本:
- 使用
//go:embed作为xxxSource变量添加到scripts.go - 使用
sync.Once模式创建打包 getter 函数 - 这些脚本可以通过
require()使用GetJavaScriptSources()中的实用工具 - 必须导出
main函数,但不得调用它——打包器会在执行时注入await main()
模式 3:日志解析器
像 parse_claude_log.cjs 这样的文件用于解析 AI 引擎日志:
- 使用
//go:embed添加到js.go - 在
GetLogParserScript()函数中添加对应的 case - 由工作流编译系统使用
故障排除
问题:“required file not found in sources”
**原因:**文件未添加到 GetJavaScriptSources() 映射中
**解决方案:**将文件添加到 pkg/workflow/js.go 中的映射
问题:测试失败并提示 “core is not defined”
**原因:**缺少全局模拟对象
**解决方案:**在导入模块前添加适当的模拟对象:
global.core = mockCore;
global.github = mockGithub;
问题:打包器因循环依赖而失败
**原因:**文件 A 引用了文件 B,而文件 B 又引用了文件 A
**解决方案:**调整结构以打破循环依赖,或合并这些模块
问题:重建后更改未生效
**原因:**Go 构建缓存未识别嵌入文件的更改
解决方案:
make clean
make build
参考资料
- 打包器实现:
pkg/workflow/bundler.go - JavaScript 源文件注册表:
pkg/workflow/js.go - 脚本打包:
pkg/workflow/scripts.go - 现有测试示例:
pkg/workflow/js/*.test.cjs - GitHub Actions 脚本文档:actions/toolkit