一覧に戻る
    CLIの実行結果、ログに流すだけで満足していませんか?
    開発ラボ
    PRこの記事には広告が含まれています

    CLIの実行結果、ログに流すだけで満足していませんか?

    12 分で読める

    自動化ツールのCLIコマンドを作っていると、「動いた、ログも出てる、OK」で終わらせがちです。

    正直、自分もそうでした。

    ところがある日、CI/CDパイプラインの設定を見ていて気づいたんです。

    「結果ファイルを保存する」設定が書いてあるのに、肝心のファイルを誰も作っていない。

    今回は、この「ログは流れるけどファイルが残らない」問題を解消するために、CLIコマンドにJSON出力機能を追加した記録です。実装自体はシンプルなのですが、「どう作るか」の判断が意外と肝だったので、そのあたりを中心に書き残しておきます。

    そもそも何が困るのか

    もう少し具体的に状況を説明します。

    開発している自動化ツールには、GitHub Issueを検品して自動クローズするCLIコマンドがあります。AIエージェントがIssueの内容を読み、「これはもう閉じてよさそう」と判断したものを自動でクローズしてくれる仕組みです。

    このコマンドの実行結果は、これまでログにしか出ていませんでした。ターミナルには流れるけど、ファイルとしてはどこにも残らない。

    一方で、CI/CDパイプライン(Jenkins)側には、こんな設定がすでに入っていました。

    // 実行結果をビルド成果物として保存する設定
    archiveArtifacts artifacts: 'results.json'

    これは「results.json というファイルがあれば、ビルドの成果物として保存しておいてね」という指示です。

    でも、コマンドがそのファイルを生成していないので、設定だけあって中身がない状態になっていた。

    動作上は問題になりません。ファイルがなくてもエラーにはしない設定にしていたので。

    ただ、こうなると困る場面があります。

    • 「先週のあの実行、何件クローズしたんだっけ?」→ ログを遡るしかない
    • 「このIssueがクローズされた理由は?」→ ログが流れていて追えない
    • 監査的に「いつ、どんな条件で、何をしたか」を残したい → 残っていない

    ログは「今」を確認するもの。ファイルは「後から」振り返るもの。 役割が違うんですよね。

    どう作るか:2つの選択肢

    実装方法は大きく2つありました。

    案A:標準出力にJSONを流す

    --json フラグひとつで、結果をJSON形式で標準出力に出す方法。実装は楽です。

    案B:ファイルに書き出す(既存パターン踏襲)

    --output-file <path> でパスを指定すると、結果をJSONファイルとして保存する方法。同じツール内の別コマンド(Issue自動起票コマンド)がすでにこの方式を採用していました。

    迷った末に、案Bを選びました。

    決め手は 「CLIのオプション体系を揃えておくこと」 でした。

    コマンドAでは --output-file なのにコマンドBでは --json となると、使う側(=未来の自分とCI/CD)が混乱します。「あれ、このコマンドはどっちだっけ?」が地味にストレスになる。

    SRE時代に、統一されていない設定ファイルの山と格闘した記憶がここで効いています。揃えておくと後が楽。

    実装で意識した3つのこと

    5ファイルにまたがる変更になりましたが、全部を詳しく書くと長くなるので、設計判断として意識した3点に絞ります。

    1. JSON出力のスキーマに「実行条件」を含める

    出力するJSONの構造はこんなイメージです。

    {
      "execution": {
        "timestamp": "2026-02-16T06:54:00.000Z",
        "repository": "your-org/your-repo",
        "category": "followup",
        "dryRun": false,
        "confidenceThreshold": 0.7
      },
      "summary": {
        "totalInspected": 10,
        "recommendedClose": 3,
        "actualClosed": 3,
        "skipped": 5,
        "errors": 2
      },
      "issues": [
        {
          "issueNumber": 42,
          "title": "Fix login bug",
          "action": "closed",
          "inspection": {
            "recommendation": "close",
            "confidence": 0.95,
            "reasoning": "..."
          }
        }
      ]
    }

    ポイントは execution セクションです。結果だけでなく「どういう条件で実行されたのか」を一緒に保存しています。

    これがないと、後からJSONを見たとき「これドライランの結果?本番の結果?」がわからない。ログを遡って条件を確認する、という本末転倒が起きます。結果ファイルは、それ単体で文脈がわかるようにしておく。 これは地味だけど大事な設計判断だと思っています。

    2. 「構築」と「書き出し」を分離する

    出力モジュールは、2つの関数に分けました。

    • buildJsonPayload() — 結果データからJSONの構造を組み立てる(純粋関数)
    • writeOutputFile() — JSONをファイルに書き出す(副作用あり)

    なぜ分けるか。テストしやすさです。

    buildJsonPayload() はファイルシステムに触らないので、モックなしでテストできます。「10件の結果を渡したらサマリーの数値は合っているか」みたいなテストがシンプルに書ける。ファイル書き出しのテストは writeOutputFile() 側だけfsをモックすればいい。

    既存の出力モジュールがすでにこの分離をしていたので踏襲したのですが、改めて「いいパターンだな」と感じました。

    3. 既存の動作を壊さない

    コマンド本体への統合はこうなります。

    // 通常のログ出力(既存処理)
    reportResults(results, options);
    
    // JSON出力(--output-file が指定されたときだけ動く)
    await exportJsonIfRequested(results, options, repository);

    --output-file を指定しなければ何も起きない。既存の動作に一切影響しません。

    オプションのパースでは path.resolve() を使って絶対パスに変換しています。CLIがどのディレクトリから呼ばれても確実にファイルが作られるようにするためです。相対パスのまま渡すと、カレントディレクトリが変わったときに意図しない場所にファイルができる。CI/CD環境だとこれがハマりどころになりがちです。

    CI/CDパイプラインとの連携

    Jenkins側の変更は、コマンド呼び出しに --output-file results.json を追加するだけ。

    // Before: ファイルが生成されない
    sh 'node dist/index.js auto-close-issue --category followup'
    
    // After: 結果がJSONファイルに保存される
    sh 'node dist/index.js auto-close-issue --category followup --output-file results.json'

    もともと archiveArtifacts の設定はあったので、コマンドがファイルを生成するようになった瞬間、パイプラインの成果物保存がそのまま動き出します。「期待」と「実装」がようやく噛み合った、という感じです。

    検証結果

    ビルド、型チェック、CLIの --help 表示、すべて問題なし。

    テストスイートで既存の失敗が数件ありましたが、今回の変更に起因するものはゼロ。既存失敗の原因は前回コミットのオプション変更がテスト側で追従できていなかったものと、ローカル環境の権限問題で、別途対応予定です。

    振り返り

    やったこと自体は「CLIにJSON出力オプションを足す」というシンプルな話です。

    でも振り返ると、判断の大半は「既存パターンに揃えるか、もっと楽な方法にするか」に集約されていました。今回は「揃える」を選んだ。5ファイルに変更が散らばって一見面倒に見えるけど、将来的に別のコマンドにも同じ仕組みを入れるとき、パターンが確立していれば迷わない。

    最初のコマンドで出力モジュールのパターンをちゃんと作っておいてくれた過去の自分に感謝、という気持ちです。

    ちなみにこのCLIツール、devloop-runner.app 上でJenkinsパイプラインから呼び出す形で運用しています。こういう自動化の仕組みをひとつずつ整えていく過程が、個人的には一番おもしろいところです。

    まだ残っていること

    • JSON出力のユニットテスト追加
    • 既存テストの追従(前回コミットのオプション変更分)
    • 出力フォーマットのバージョニング(今は入れていないが、外部から読むツールが増えたら必要になりそう)

    参考書籍

    Jenkinsパイプラインの設計やCI/CDの実践について体系的に学びたい方には、以下の書籍が参考になります。

    CI/CDの全体像やビルド成果物の管理を含めた自動化戦略を理解するには、こちらの定番書も役立ちます。

    AIエージェントを使った開発自動化に興味がある方に。

    この記事は役に立ちましたか?

    Coffee cup

    この記事が、何かの整理につながったら

    コーヒー1杯分の応援をもらえると嬉しいです。

    ※ これは応援とは別の話ですが、

    同じようなテーマを自分の文脈で整理したい場合は、 (文章だけだと詰まりやすい人向けに) 思考整理の壁打ちという形で対話の時間も取っています。

    対話の時間について