ts-morphとClaude Code ActionsでFeature Flagクリーンアップ作業を半自動化
はじめに
まず、TimeTreeのWeb版ではOpenFeatureによるトランクベース開発をしています。
詳しくは弊社のエンジニアのメンバーがまとめてくれた記事がありますので参考にしてみてください。
Feature Flagを運用していくことで起きる課題
- 新機能の開発やバグ修正と違い、Feature Flagの削除はユーザーに直接価値を届けないの
で誰も積極的にやろうと思わない - Feature Flagを削除するためのタスクを作成するのも面倒
結果として、コードベースが複雑化し、メンテナンスコストが増大する可能性があります。
実現したいこと
実現したことは極力人的リソースを使用せずに削除作業を行い、Github Actionsで定期的にFlag毎にPRを作成して欲しいです。
実際にFeature Flagを削除するまでの流れを確認する
開発チームやツールによってはこのステップは変わるだろうとは思いますが、以下のようになるかなと思います。
- 削除対象Flagの特定
- 特定したFlagがアプリケーション内でどのように参照されているのかを特定
- 常にFlagがonの状態 = 削除可能
- そもそも参照されていないものは削除
- 特定後、不必要な処理などを削除
- Flagを管理しているファイルから該当のFlagを削除(弊社では
flagConfig.tsというファイルで管理されています) - Flagに関連するコメントの削除
- Flagによる分岐
- Flagの値をコンポーネントに渡している場合は、そのコンポーネントのPropsからFlagを削除
- 不要になったスタイルの削除
- 不要になったテストの削除
- Flagを管理しているファイルから該当のFlagを削除(弊社では
どのようにやるか?
ts-morphを使用して、構文解析をして不要なコードを削除・変更。
ts-morphは、TypeScriptのソースコードをプログラムから操作するためのライブラリです。TypeScriptのコンパイラAPIをラップし、より直感的で簡単にコードの解析、変更、生成をすることができます。
ts-morphのみで、やれないことはないですが膨大な量且つ複雑なコードになりそうかなと思い、アプローチを考えました。
処理の重みをつける
先ほどの処理の流れを参考にそれぞれの重みをつけます。
| 機能 | 複雑度 |
|---|---|
| 削除対象Flagの特定 | 低 ~ 中 |
| 特定したFlagがアプリケーション内でどのように参照されているかを特定 | 低 ~ 中 |
| 特定後、不必要な処理を削除・変更 | 高 |
特定後、不必要な処理を削除・変更の部分が割と重めかなという印象です。
この部分をどうメンテナンスの観点も踏まえて実装するのか?を検討しました。
結論からClaude Code GitHub Actionsを利用することに決めました。
Claude Code GitHub Actionsに関する説明については以下を参照してください。
それぞれの役割を整理する
削除の流れを再掲します。
- 削除対象Flagの特定 (ts-morph)
- 特定したFlagがアプリケーション内でどのように参照されているのかを特定 (ts-morph)
- 常にFlagがonの状態 = 削除可能
- そもそも参照されていないものは削除
- 特定後、不必要な処理などを削除(Claude Code GitHub Actions)
- Flagを管理しているファイルから該当のFlagを削除(弊社では
flagConfig.tsというファイルで管理されています) - Flagに関連するコメントの削除
- Flagによる分岐
- Flagの値をコンポーネントに渡している場合は、そのコンポーネントのPropsからFlagを削除
- 不要になったスタイルの削除
- 不要になったテストの削除
- Flagを管理しているファイルから該当のFlagを削除(弊社では
ts-morphには、どのFlagが削除可能か?という削除可能なFlagの検出する役割を担い、Claude Code GitHub Actionsは、検出したFlagから実際に不要な処理などを削除する役割を担います。
自分たちの開発組織・フローに合うより良い運用を考える
TimeTreeのWeb版での開発では、大きく二つのチームが存在しています。
- 公開カレンダーに関わる機能を開発をするチーム
- 共有カレンダーに関わる機能を開発をするチーム
実現したいことのところでも言及しましたが、定期的にPRを作成して欲しいという点に加えてPRを作成した際に、管理しているFlagを適切なチームをアサインしてくれるとより便利そうです。
チームのアサインを実現するためにFlagごとにメタ情報を付与してあげます。また更に有効期限なども設定することで、有効期限が切れたFlagを削除対象とすることが可能となります。
以下のようにexpirationDateとteamNameを付与しています。
type TeamName = "team1" | "team2"; // githubで設定されているチーム名
type FlagMeta = {
expirationDate?: string; // (YYYY-MM-DD)
teamName?: TeamName;
};
type Flag = FlagConfiguration[keyof FlagConfiguration] & FlagMeta;
// beta環境に表示するFlag設定
const createBetaFlag = (
overrides: Omit<Partial<Flag>, "expirationDate" | "teamName"> & FlagMeta = {},
): Flag => ({
disabled: false,
variants: {
on: true,
off: false,
},
defaultVariant: "off",
...overrides,
});
export const OPEN_FEATURE_CONFIG = {
"feature-a": createBetaFlag({
defaultVariant: "on",
contextEvaluator: () => "on",
expirationDate: "2025-09-23",
teamName: "team1",
}),
"feature-b": createBetaFlag({
defaultVariant: "on",
contextEvaluator: () => "on",
expirationDate: "2025-09-23",
teamName: "team2",
}),
} as const satisfies FlagConfiguration;
こうすることでどのチームがどのFeature Flagを管理しているのか?という情報を持つことが可能になります。
削除対象Flag, 参照を特定する
一部のコードになりますが、以下のような処理があります。
const getFlagConfigObjectLiteral = (
project: Project,
): ObjectLiteralExpression => {
const flagConfigFile = project.getSourceFileOrThrow(FLAG_CONFIG_PATH);
const configDeclaration =
flagConfigFile.getVariableDeclarationOrThrow(FLAG_CONFIG_VAR_NAME);
const initializer = configDeclaration.getInitializer();
if (!initializer) {
throw new Error(`${FLAG_CONFIG_VAR_NAME} has no initializer`);
}
const objectLiteral = Node.isObjectLiteralExpression(initializer)
? initializer
: initializer.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression);
if (!objectLiteral) {
throw new Error(`${FLAG_CONFIG_VAR_NAME} must be an object literal`);
}
return objectLiteral;
};
プロジェクトから機能Flag設定オブジェクトを抽出します。
const analyzeFeatureFlags = (project: Project) => {
const objectLiteral = getFlagConfigObjectLiteral(project);
const flagsWithExpiration = getFlagsWithExpiration(objectLiteral);
const usedFlags = getUsedFlags(project);
const removableFlags = flagsWithExpiration
.filter((flag) => isFlagRemovable(flag, usedFlags))
.map((flag) => flag.name);
return { flagsWithExpiration, usedFlags, removableFlags };
};
有効期限切れで削除可能なFlagを特定する関数です。プロジェクト全体をスキャンして使用状況を調査し、未使用または定数値のFlagを検出します。
if (removableFlags.length === 0) {
console.info("削除可能な機能Flagは見つかりませんでした。");
} else {
console.info("\n削除可能な機能Flag:");
for (const flagName of removableFlags) {
const reasonInfo = getRemovalReasonInfo(
flagName,
flagsWithExpiration,
usedFlags,
);
console.info(formatRemovalReason(reasonInfo));
}
process.exitCode = 1;
}
削除可能な機能Flagの結果を表示し、該当するFlagがある場合は詳細な削除理由(未使用/定数値、有効期限、チーム名など)を出力する処理です。
npm-scriptsを用意し実行すると以下のようなログが出力されます。
=== 機能Flag削除対象チェック ===
有効期限が設定されているFlag数: 3
有効期限が切れているFlag数: 3
削除可能な機能Flag:
- feature3 (理由: 未使用, 有効期限: 2025-09-23, チーム: team2)
- feature5 (理由: 定数値, 有効期限: 2025-09-23, チーム: team1)
- feature10 (理由: 定数値, 有効期限: 2025-09-23, チーム: team1)
このログを次のフローで利用します。
不必要なコードを削除・修正し、PRを作成する
ポイントとしては、先ほど出力した削除対象のFlagの一覧のログをClaude Code Actionに読み取ってもらうという点です。それ以降は、削除に関する具体的な指示を出します。
name: Cleanup Unused Feature Flags
on:
schedule:
- cron: "0 1 1,15 * *"
workflow_dispatch:
permissions:
contents: read
defaults:
run:
working-directory: frontend
jobs:
cleanup-unused-feature-flags:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Setup Node.js
uses: ./.github/actions/setup_node
- name: Cleanup unused feature flags via Claude Code Action
uses: anthropics/claude-code-action@v1.0.8
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.JW_MACHINE_PAT }}
claude_args: |
--allowedTools Bash,Read,Write,Edit
prompt: |
## タスク
リポジトリから削除可能な機能Flagを特定し、Flagごとに個別のPRを作成して削除してください。
### 手順
1) まず `yarn find-removable-feature-flags` を実行して削除可能なFlagを特定
2) 削除可能なFlagが存在する場合、各Flagに対して以下を実行:
- Flagごとに個別のブランチを作成(ブランチ名: `chore/feature-flag/<Flag名>`)
- 同一ブランチ名が既に存在し、15日以上レビューされていない場合は既存のPRを更新
- `frontend/src/lib/openFeature/flagConfig.ts` から該当Flagの定義を削除
- `frontend/src/` 配下・`frontend/src/components/` 配下・`frontend/src/*.test.tsx` から当該Flagの参照を削除し、条件分岐を簡略化
- 不要なインポートの整理とコードフォーマットを実施
- 削除された条件分岐に関連する不要なスタイル(vanilla-extractのCSS)がある場合は削除
- Flagごとに個別のPRを作成し、チーム名に基づいてレビュワーをアサイン
- PRの説明文は日本語で記述してください
3) 削除可能なFlagがない場合は何もしない(PR も作成しない)
4) ユーザー確認を求めず自動で適用する
### 安全性
- 当該Flagが常に true/false の場合は "有効側" の分岐のみ残してください
- TypeScript の型エラーやビルドエラーが出ないように修正してください
- PR作成時には、yarn test がpassしているようにしてください
- 削除された条件分岐で使用されていたスタイル(vanilla-extractのCSS)は、他の箇所で使用されていない場合のみ削除してください
- 各Flagを個別に処理し、問題が発生した場合はそのFlagのみスキップして続行
チームアサインの実装方法:
```bash
# PR作成後にチームをレビューアーとしてアサイン
if [ -n "$TEAM_NAME" ]; then
echo "Attempting to assign team: $TEAM_NAME to PR: $PR_NUMBER"
gh api repos/${{ github.repository }}/pulls/$PR_NUMBER/requested_reviewers \
--method POST \
--field "team_reviewers[]=$TEAM_NAME" || {
echo "Failed to assign team $TEAM_NAME, continuing with next flag"
}
fi
```
実際に作成されたPRを見てみる
PRはFlag毎に作成され、意図したチームとメンバーがアサインされていることを確認できました。

コードの中身を確認しましたが、割と意図した通りの変更になっていました。
より良いものにしていくために
Claude Code Actionsは、Job Summaryを生成してくれます。
(以下の部分)
簡単にいうとワークフローの実行結果をまとめたレポートを、ジョブの実行ページに表示する機能です。
Job Summaryについての詳細は以下を参考にしてください。
Job Summaryのログを活用することで、より不要な処理をしていないか?などを見て改善していくことが可能になります。また実行した際に掛かったコストも最終的に残してくれています。
最終的にClaude Code Actionsが作成したPRをレビューする必要性がありますが、どのくらい意図した挙動になっているか?などは、運用する中でよりチームに合う方法を探っていこうと思います。
まとめ
人件費を節約でき別のことにリソースを割ける
一つ当たりのFlagを削除してPRを作成するまでに要する時間でいうと30min~1hくらい掛かります(もっと早く終わるものもあれば時間がかかるものもあります)。Job SummaryのログからClaude Code ActionsがPRを1つ作成するまでにかかるコストは1$くらいです。本格的な運用はこれからなので、コスト感については安直に言及できませんが、肌感としては安いなという感覚です。
なくても何とかなるものに
今回はFeature Flagの削除を自動化したいというものでした。これはあったらより便利になるというくらいで極論なくても絶望的に困ることはありません。今回の例だけでなく、捨てても問題ないケースで不確実性の高く複雑なものは応用が効きそうかなと思います。
まだフローが構築しただけで実際の運用はこれからなので運用してみた結果どうなったか?は別の記事で結果報告できればなと思います。
TimeTreeのエンジニアによる記事です。メンバーのインタビューはこちらで発信中! note.com/timetree_inc/m/m4735531db852
Discussion