つれづれなる Agent OPS
Flue

FlueのWorkflowをGitHub Actionsから呼び、IssueトリアージをCIに寄せる

Flue 1.0 Betaで作ったGitHub IssueトリアージWorkflowを、webhook常駐サーバーではなくGitHub Actionsのissues.openedからdry-run実行する検証ログ。

前回、Flue 1.0 BetaでGitHub Issueのタイトルと本文を受け取り、severity、再現可否、ラベル候補、要約を構造化して返すWorkflowを作りました。

Flue 1.0 BetaでGitHub Issueトリアージエージェントを動かしてみた https://llm-lab.dev/posts/flue-1-0-beta-issue-triage-agent/

前回の最後では、GitHub CLIで実Issueを1件読み、flue run triage-issueへ渡すところまで確認しました。今回はその続きとして、GitHub Actionsのissues.openedをトリガーにして、同じFlue WorkflowをCI上でdry-run実行する構成にしました。

まだIssueへコメントを書き戻したり、ラベルを付けたりはしません。まずはActionsログに分類結果を出すだけです。未信頼のIssue本文を扱うAgentに、最初から広いGitHub権限を渡さないためです。

検証では、前回記事の状態を残したブランチとは別に、GitHub Actions用のブランチを切って進めました。読者が再現する場合も、既存のトリアージWorkflowが動く状態を一度保存してから、Actions用の差分を別ブランチで足すと切り戻しやすいです。

git checkout -b flue-github-actions-issue-triage-workflow

なぜGitHub Actionsに寄せるのか

GitHub Issueを起点にAgentを動かす方法はいくつかあります。

  • webhookを受ける常駐サーバーを作る
  • GitHub Appを作る
  • GitHub Actionsでissues.openedを拾う
  • 手元のGitHub CLIでIssueを読んで手動実行する

本番運用だけを考えるなら、GitHub Appやwebhookサーバーが自然な場面もあります。ただ、今回の段階では「Issue本文をWorkflowへ渡して分類できるか」を確認したいだけです。そこで、外部公開するサーバーを作らず、GitHub Actionsの使い捨てランナーで実行する形に寄せました。

Issue本文は誰でも書けるテキストです。プロンプトインジェクションっぽい文面、長すぎるログ、内部URL、個人情報に近い情報が混ざる可能性があります。その入力を読むAgentに、いきなりコメント投稿権限やラベル更新権限を渡すと、検証段階としては境界が広すぎます。

今回は、Actionsログにstructured outputを出すだけにします。

Actions用の入口を作る

前回のCLI連携では、gh issue viewでIssueを読み、payloadを作ってflue runへ渡していました。GitHub Actionsでは、event payloadがGITHUB_EVENT_PATHにJSONファイルとして置かれます。そこで、Actions eventからIssue情報を読み、同じtriage-issue Workflowへ渡すスクリプトを追加しました。

import { readFileSync } from 'node:fs';
import { spawnSync } from 'node:child_process';

function readGitHubEvent() {
	const eventPath = process.env.GITHUB_EVENT_PATH;
	if (!eventPath) {
		return undefined;
	}

	return JSON.parse(readFileSync(eventPath, 'utf8'));
}

const event = readGitHubEvent();
const issue = event?.issue;

const title = issue?.title ?? process.env.ISSUE_TITLE;
const body = issue?.body ?? process.env.ISSUE_BODY ?? '';
const number = issue?.number ?? Number(process.env.ISSUE_NUMBER ?? 0);
const url = issue?.html_url ?? process.env.ISSUE_URL;
const repo = process.env.GITHUB_REPOSITORY ?? process.env.ISSUE_REPOSITORY;

if (!title) {
	console.error('Missing issue title. Provide GITHUB_EVENT_PATH or ISSUE_TITLE.');
	process.exit(1);
}

const payload = JSON.stringify({
	title,
	body,
	source: {
		repo,
		number,
		url,
	},
});

const result = spawnSync(
	'npx',
	['flue', 'run', 'triage-issue', '--target', 'node', '--payload', payload],
	{ stdio: 'inherit' },
);

process.exit(result.status ?? 1);

GITHUB_EVENT_PATHがあるときはActionsのevent payloadを読み、手元で試すときはISSUE_TITLEISSUE_BODYを環境変数で渡せるようにしています。これで、CI用の入口をローカルでも同じスクリプトで確認できます。

package.jsonには、次のscriptを追加しました。

{
  "scripts": {
    "triage:event": "node scripts/triage-github-event.mjs"
  }
}

workflowを書く

GitHub Actions側は、issues.openedで起動し、依存関係を入れてからnpm run triage:eventを呼ぶだけです。

name: Triage issue with Flue

on:
  issues:
    types: [opened]
  workflow_dispatch:
    inputs:
      issue_title:
        description: Issue title for manual dry-run
        required: true
        type: string
      issue_body:
        description: Issue body for manual dry-run
        required: false
        type: string

permissions:
  contents: read
  issues: read

jobs:
  triage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm

      - run: npm ci

      - name: Run Flue issue triage
        env:
          OPENAI_COMPAT_API_KEY: ${{ secrets.OPENAI_COMPAT_API_KEY }}
          OPENAI_COMPAT_BASE_URL: ${{ secrets.OPENAI_COMPAT_BASE_URL }}
          FLUE_MODEL: ${{ vars.FLUE_MODEL || 'openai/preview/Kimi-K2.6' }}
          ISSUE_TITLE: ${{ inputs.issue_title }}
          ISSUE_BODY: ${{ inputs.issue_body }}
        run: npm run triage:event

workflow_dispatchも足しておくと、Issueを作らなくても手動でdry-runできます。issues.openedで起動した場合はGITHUB_EVENT_PATHからIssueを読みます。手動実行の場合は、inputs.issue_titleinputs.issue_bodyを環境変数として渡します。

ここで必要な権限はcontents: readissues: readだけです。コメントやラベルを書き戻さないので、issues: writeはまだ付けません。

GitHub Actionsに認識させる

ここで一度詰まりました。.github/workflows/triage-issue.ymlを続編用ブランチにpushしただけでは、GitHubのActionsタブにworkflowが出ませんでした。

GitHub Actionsの手動実行に使うworkflow_dispatchは、基本的にデフォルトブランチ上にworkflowファイルが存在している必要があります。今回のように検証用ブランチでworkflowを作った場合、まずPull Requestを作ってデフォルトブランチへmergeします。

merge前のActionsタブでは、次のような初期画面が出ていました。

GitHub Actionsがまだworkflowを認識していない初期画面

Get started with GitHub Actions
Build, test, and deploy your code. Make code reviews, branch management, and issue triaging work the way you want. Select a workflow to get started.

PRをmergeしてworkflowが認識されると、表示は次のように変わります。

GitHub Actionsがworkflowを認識したが、まだ実行履歴がない画面

There are no workflow runs yet.

workflowは認識されたが、まだ一度も実行されていない状態です。左側にTriage issue with Flueが出ていれば、それを選び、Run workflowから手動実行できます。

手動実行では、次の値を入れました。

Run workflowから手動dry-run用のIssue情報を入力する画面

issue_title:
Dashboard is blank after login

issue_body:
Steps: log in, open /dashboard. Expected widgets. Actual blank white screen in Chrome 126.

実行前に、Repository secretsとしてOPENAI_COMPAT_API_KEYOPENAI_COMPAT_BASE_URLを設定しておきます。FLUE_MODELはRepository variablesで指定できますが、未設定ならworkflow内のデフォルトでopenai/preview/Kimi-K2.6を使います。

ローカルで同じ入口を動かす

Actionsへpushする前に、手元でtriage:eventを動かしました。

ISSUE_TITLE="Dashboard is blank after login" \
ISSUE_BODY="Steps: log in, open /dashboard. Expected widgets. Actual blank white screen in Chrome 126." \
npm run triage:event

上のようにコマンドの直前へ書く場合は、ISSUE_TITLEISSUE_BODYがそのコマンドの環境変数として渡されます。別々の行で先に値を入れる場合は、単なるシェル変数ではなく、子プロセスへ渡す環境変数にするためexportが必要です。

export ISSUE_TITLE="Dashboard is blank after login"
export ISSUE_BODY="Steps: log in, open /dashboard. Expected widgets. Actual blank white screen in Chrome 126."
npm run triage:event

最初はネットワーク制限のある実行環境で動かしたため、Workflow起動後にモデルAPIへの接続で失敗しました。

Error: Workflow failed: [internal_error] prompt failed: Connection error.
prompt failed: Connection error.

これは追加したtriage:event固有の問題ではありません。同じ環境で、前回からあるnpm run triageを実行しても同じ接続エラーになりました。つまり、payload生成やスクリプトの問題ではなく、モデルAPI接続の問題です。

ネットワーク接続を許可して再実行すると、triage:eventからflue runが起動し、Skill読み込みからfinishまで完了しました。

 ▗  flue run
 ▚  workflow triage-issue
 ▘  starting...
    run       run_...

tool activate_skill
tool done activate_skill  (547 chars)

tool finish
tool done finish
{
  "severity": "high",
  "reproducible": true,
  "labels": [
    "bug",
    "dashboard",
    "frontend"
  ],
  "summary": "ログイン後に `/dashboard` を開くと、Chrome 126 で期待されるウィジェットが表示されず、空白の白画面が表示される問題です。再現手順が明確に示されており、主要機能であるダッシュボードが利用不可となっています。"
}
done workflow completed

これで、Actions用の入口から既存のtriage-issue Workflowへpayloadを渡せることは確認できました。

Actions上で動かす

最後に、GitHub Actions上でも同じ入力で手動実行しました。ログでは、npm run triage:eventからflue runが起動し、activate_skillからfinishまで進んでいます。

GitHub Actions上でFlue Workflowが完了し、構造化JSONを出したログ

Run npm run triage:event

> flue@1.0.0 triage:event
> node scripts/triage-github-event.mjs

 ▗  flue run
 ▚  workflow triage-issue
 ▘  starting...
    run       run_...

tool activate_skill
tool done activate_skill  (547 chars)

tool finish
tool done finish
done workflow completed
{
  "severity": "high",
  "reproducible": true,
  "labels": [
    "bug",
    "dashboard",
    "ui"
  ],
  "summary": "Chrome 126でログイン後に/dashboardを開くと、ウィジェットが表示されるべき画面が真っ白になるという問題。再現手順(ログイン→/dashboardへアクセス)が明確に記載されており、ダッシュボード機能が完全に利用できない重大な不具合。"
}

ローカル実行とラベル候補の細部は少し違いますが、severityreproduciblelabelssummaryを持つ構造化結果として返っているため、Actions上でFlue Workflowを呼ぶところまでは確認できました。

ここまで来ると、次にやりたくなるのはIssueへのコメント投稿です。ただ、そこはまだ分けて考えたいです。

Issueへコメントを書くには、issues: write権限が必要になります。さらに、コメント重複を避ける、再実行時に同じコメントを増やさない、既存ラベルがあるか確認する、ラベル名を勝手に増やさない、といった運用上の論点が出ます。

分類結果を出すだけのdry-runと、GitHubへ副作用を起こす処理は別の段階です。今回のActions化は、あくまで「常駐サーバーなしで、Issue作成イベントからFlue Workflowを呼べるか」を見るところまでにしました。

まとめ

GitHub Issueを起点にFlue Agentを動かす最初の形として、GitHub Actionsはかなり扱いやすい入口でした。webhookサーバーを立てずに、issues.openedのpayloadをそのまま使い、flue run triage-issueへ渡せます。

一方で、Actionsで動くからといって、すぐにGitHubへの書き戻しまで進める必要はありません。未信頼のIssue本文を読む段階では、まずdry-runで構造化結果をログに出し、モデルAPI接続、Skill実行、finish到達、出力スキーマの安定性を確認する方がよいです。

FlueのWorkflowは、一回きりの処理としてCIに載せやすい形でした。常駐Agentを作る前に、CI上で入力と出力の境界を細く確認できるのは、Issueトリアージのような外部入力処理ではかなり助かります。

DUOps

Author

DUOps(デュオプス)

LLMOps、Agent、MCP、Langfuse、Cloudflare 周辺の実装と運用を、個人で試しながら記録しています。

Xを見る

Related