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 コードのリファクタリングガイド
このガイドでは、gh-aw の JavaScript を個別の .cjs ファイルにリファクタリングする方法を説明します。
概要
gh-aw は、GitHub Actions のワークフロー内で JavaScript に CommonJS モジュール(.cjs)を使用します。これらのファイルは次のように扱われます。
//go:embedディレクティブを使用して Go バイナリに埋め込まれます- ローカルの
require()呼び出しをインライン化するカスタム JavaScript バンドラーを使用してバンドルされます actions/github-script@v8を使用して GitHub Actions で実行されます
トップレベルスクリプトのパターン
ワークフローで直接実行するトップレベルの .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 のイシューを作成しますadd_comment.cjs- イシューや PR にコメントを追加しますadd_labels.cjs- イシューや 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,
};
重要なポイント:
- TypeScript のチェックに
// @ts-checkを含めます - GitHub Actions の型に
/// <reference types="@actions/github-script" />を含めます - ドキュメントには 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() を介して他のスクリプトで使用する共有ユーティリティを作成する場合は、ステップ 3 で GetJavaScriptSources() のマップに追加することで、自動的に利用可能になります。
バンドラーの処理:
- 任意のスクリプト内にある
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したモジュールをインライン化します
複数の 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
検証チェックリスト
リファクタリングしたコードをコミットする前に、次の項目を確認してください。
- 新しい
.cjsファイルをpkg/workflow/js/に作成した - 対応する
.test.cjsファイルにテストを作成した -
make test-jsでテストが成功する -
pkg/workflow/js.goまたはpkg/workflow/scripts.goに埋め込み変数を追加した - ユーティリティの場合:
GetJavaScriptSources()マップに追加した - メインスクリプトの場合:
sync.Onceを使ったバンドリング用ゲッター関数を作成した - ほかのファイルでローカルの
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パターンを使ったバンドリング用ゲッター関数を作成します- これらのスクリプトでは、
GetJavaScriptSources()にあるユーティリティをrequire()できます main関数をエクスポートする必要がありますが、呼び出してはいけません。実行時にバンドラーがawait main()を挿入します
パターン 3:ログパーサー
AI エンジンのログを解析する parse_claude_log.cjs などのファイル:
//go:embedを使ってjs.goに追加しますGetLogParserScript()関数にケースを追加します- ワークフローのコンパイルシステムで使用されます
トラブルシューティング
問題:「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