---
title: "FlueのWorkflowをGitHub Actionsから呼び、IssueトリアージをCIに寄せる"
description: "Flue 1.0 Betaで作ったGitHub IssueトリアージWorkflowを、webhook常駐サーバーではなくGitHub Actionsのissues.openedからdry-run実行する検証ログ。"
lang: "ja"
canonical: "https://llm-lab.dev/posts/flue-github-actions-issue-triage-workflow/"
source: "https://llm-lab.dev/posts/flue-github-actions-issue-triage-workflow.md"
publishedAt: "2026-06-20"
updatedAt: "2026-06-20"
category: "Flue"
tags:
  - "agent"
  - "github-actions"
  - "workflow"
  - "ci"
---

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

import LinkCard from "../../components/LinkCard.astro";

> [!NOTE]
> この記事で確認したこと: FlueのIssueトリアージWorkflowは、GitHub Actionsの`issues.opened`イベントpayloadを読み取り、`flue run`へ渡すdry-runとして実行できました。重要なのは境界を細く保つことで、まずは`contents: read`と`issues: read`だけで構造化結果をログへ出し、コメント投稿やラベル更新は重複対策、権限設計、未信頼Issue本文の扱いを決めてから分けて進める方が安全です。

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

<LinkCard
  href="https://llm-lab.dev/posts/flue-1-0-beta-issue-triage-agent/"
  title="Flue 1.0 BetaでGitHub Issueトリアージエージェントを動かしてみた"
  description=""
  siteName=""
  image="/images/posts/flue-1-0-beta-issue-triage-agent/heroImage.webp"
/>

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

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

検証では、前回記事のトリアージWorkflowをそのまま使い、GitHub Actionsから呼び出すための入口だけを追加しました。再現する場合も、まずローカルでWorkflowが動く状態を確認してから、CI用の差分を足すと切り分けやすいです。

## なぜ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へ渡すスクリプトを追加しました。

```js
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を追加しました。

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

## workflowを書く

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

```yaml
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タブでは、次のような初期画面が出ていました。

![GitHub Actionsがまだworkflowを認識していない初期画面](/images/posts/flue-github-actions-issue-triage-workflow/01-actions-get-started.webp)

```txt
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を認識したが、まだ実行履歴がない画面](/images/posts/flue-github-actions-issue-triage-workflow/02-actions-no-runs-yet.webp)

```txt
There are no workflow runs yet.
```

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

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

![Run workflowから手動dry-run用のIssue情報を入力する画面](/images/posts/flue-github-actions-issue-triage-workflow/03-actions-run-workflow-form.webp)

```txt
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`を動かしました。

```sh
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`が必要です。

```sh
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への接続で失敗しました。

```txt
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`まで完了しました。

```txt
 ▗  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を出したログ](/images/posts/flue-github-actions-issue-triage-workflow/04-actions-triage-success-log.webp)

```txt
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トリアージのような外部入力処理ではかなり助かります。

この次の論点は、CIやローカルで動いたWorkflowを後からどう追うかです。`run_...`、モデル名、成功/失敗、structured outputをLangfuseへ送る検証は、別記事で整理しました。

<LinkCard
  href="https://llm-lab.dev/posts/flue-langfuse-observability-issue-triage/"
  title="FlueのobserveイベントをLangfuseへ流し、IssueトリアージAgentを観測する"
  description="FlueのobserveイベントをLangfuseへ送り、IssueトリアージWorkflowの成功ケースとfinish失敗ケースを追った検証ログ。"
  siteName="つれづれなる Agent OPS"
  image="/images/posts/flue-langfuse-observability-issue-triage/heroImage.webp"
/>
