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_TITLEやISSUE_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_titleとinputs.issue_bodyを環境変数として渡します。
ここで必要な権限はcontents: readとissues: 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タブでは、次のような初期画面が出ていました。

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が認識されると、表示は次のように変わります。

There are no workflow runs yet.
workflowは認識されたが、まだ一度も実行されていない状態です。左側にTriage issue with Flueが出ていれば、それを選び、Run workflowから手動実行できます。
手動実行では、次の値を入れました。

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_KEYとOPENAI_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_TITLEとISSUE_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まで進んでいます。

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へアクセス)が明確に記載されており、ダッシュボード機能が完全に利用できない重大な不具合。"
}
ローカル実行とラベル候補の細部は少し違いますが、severity、reproducible、labels、summaryを持つ構造化結果として返っているため、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トリアージのような外部入力処理ではかなり助かります。