
DevRev Snap-in で Ticket のコメントから自動で Article を作成する
はじめに
「ブログ記事の執筆を Ticket で管理し、記事を書き終えたら Ticket を Resolved にする。それだけで記事の内容が DevRev の Article (ナレッジベース記事) に自動で取り込まれる。」そんな仕組みを DevRev の Snap-in で構築しました。 タスクを進めるだけで DevRev 側にもナレッジが蓄積されていく運用の自動化が目的です。
Snap-in は DevRev 上で TypeScript の関数を実行できる開発者向け拡張機能です。GUI ベースの Workflows と異なり、外部 HTTP リクエストなど自由度の高い処理を実装できます。なお、先に Workflows での構築を試みましたが HTTP リクエストのレスポンスサイズ上限により断念しました。その詳細な経緯は 別記事 にまとめています。
対象読者
- DevRev の Snap-in を使った自動化に興味があるエンジニア
- DevRev で Ticket と Article の連携を検討している方
- Snap-in の開発フロー (init → 実装 → デプロイ → アクティベート) を知りたい方
参考
完成したワークフローの全体像
Ticket の customer messages にブログの URL をコメントし、ステージを Resolved にすると、記事の内容が Article として自動登録されます。
開発環境のセットアップ
DevRev CLI で認証し、テンプレートを生成します。
devrev profiles authenticate -o <org_slug> -u <email>
devrev snap_in_version init
生成されるプロジェクトの構成は以下の通りです。
プロジェクト構成
devrev-snaps-typescript-template/
├── manifest.yaml # イベントソースと関数の定義
└── code/
├── package.json
├── tsconfig.json
└── src/
├── function-factory.ts # 関数の登録
├── index.ts
└── functions/
└── on_work_creation/ # テンプレートの関数 (後で差し替え)
└── index.ts
テンプレートには work_created イベントに応答するサンプル関数が含まれています。これを work_updated に変更して実装を進めます。
マニフェストの設定
マニフェストでは、イベントソース、関数、オートメーションの 3 つを定義します。event_sources で work_updated イベントを購読し、automations でイベントと関数を紐付けます。ステージの判定は関数側のコードで行います。
manifest.yaml
version: "2"
name: "Ticket to Article"
description: "Ticket のステージが resolved に変更されたとき Article を作成する"
service_account:
display_name: DevRev Bot
event_sources:
organization:
- name: devrev-event-source
description: Ticket のステージ変更を監視するイベントソース
display_name: Ticket stage change listener
type: devrev-webhook
config:
event_types:
- work_updated
functions:
- name: on_ticket_resolved
description: Ticket が resolved になったときに Article を作成する関数
automations:
- name: handle-ticket-resolved
source: devrev-event-source
event_types:
- work_updated
function: on_ticket_resolved
関数の実装 (初版: Description から URL を抽出)
最初の実装では、Ticket の Description から URL を正規表現で抽出し、work_updated イベントのペイロードからステージ変更を検出する構成にしました。event.context.secrets.service_account_token でサービスアカウントのトークンが自動的に渡されるため、認証情報の管理は不要です。
on_ticket_resolved/index.ts (初版の抜粋)
function extractUrl(text: string): string | null {
const urlRegex = /https?:\/\/[^\s<>"{}|\\^`\[\]]+/g;
const matches = text.match(urlRegex);
return matches ? matches[0] : null;
}
export async function handleEvent(event: any) {
const devrevPAT = event.context.secrets.service_account_token;
const APIBase = event.execution_metadata.devrev_endpoint;
const devrevSDK = client.setup({
endpoint: APIBase,
token: devrevPAT,
});
const payload = event.payload;
const work = payload.work_updated.work;
// Ticket かつ work_in_progress → resolved の遷移のみ処理
if (work.type !== "ticket") return;
if (work.stage?.name !== "resolved") return;
if (payload.work_updated.old_work?.stage?.name !== "work_in_progress") return;
const url = extractUrl(work.body || "");
// ... URL 先のコンテンツを取得し Article を作成
}
この初版でもワークフロー自体は正常に動作しました。
仕様変更: customer messages から URL を取得する
初版を動かしてみたところ、チームメンバーから 「Description ではなく customer messages から URL を拾う方が運用しやすいのでは」 という指摘を受けました。
確かに、Description は Ticket 作成時に書くものです。後からブログ URL を追記するのは不自然ですし、他の内容と混在してしまいます。コメントで URL を共有してから Resolved にする方が、自然な運用フローになります。なおコメントには URL が複数含まれる可能性があるので、最後に投稿されたものを正とする方向で進めることになりました。
また、参照する URL についても、今回は DevelopersIO に限定する必要があったため、その修正を入れる必要がありました。
変更点をまとめると以下の通りです。
- 対象ドメインの限定:
dev.classmethod.jpのみにマッチするよう変更 - visibility フィルター:
PublicとExternalを指定し、customer messages のみ取得。internal discussions や events は対象外 - ページネーション:
cursorを使って全件走査し、最後に見つかった URL を返す
on_ticket_resolved/index.ts (仕様変更後の主要部分)
function extractDevClassmethodUrl(text: string): string | null {
const regex = /https?:\/\/dev\.classmethod\.jp\/[^\s<>"{}|\\^`\[\]]*/g;
const matches = text.match(regex);
return matches ? matches[matches.length - 1] : null;
}
async function findLastDevClassmethodUrl(
devrevSDK: publicSDK.Api<unknown>,
workId: string
): Promise<string | null> {
let lastUrl: string | null = null;
let cursor: string | undefined = undefined;
do {
const request: publicSDK.TimelineEntriesListRequest = {
object: workId,
limit: 50,
visibility: [
publicSDK.TimelineEntryVisibility.Public,
publicSDK.TimelineEntryVisibility.External,
],
...(cursor ? { cursor, mode: publicSDK.ListMode.After } : {}),
};
const response = await devrevSDK.timelineEntriesListPost(request);
const entries = response.data?.timeline_entries || [];
for (const entry of entries) {
const body = (entry as any).body;
if (typeof body === "string") {
const found = extractDevClassmethodUrl(body);
if (found) {
lastUrl = found;
}
}
}
cursor = response.data?.next_cursor || undefined;
} while (cursor);
return lastUrl;
}
エラー時は Ticket にコメントで通知し、処理をスキップします。タイムライン取得の失敗、URL が見つからない場合、コンテンツ取得の失敗のそれぞれに対応しています。
HTML パースと Article 作成
URL 先のページから <title> と <article> タグ (なければ <body>) の内容を抽出し、createArticle で Article を作成します。resource.url に元の URL を設定することで、Article から元記事への参照を保持します。
HTML パースと Article 作成
function parseHtml(html: string): { title: string; body: string } {
const titleMatch = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
const title = titleMatch ? titleMatch[1].trim() : "Untitled";
const articleMatch = html.match(/<article[^>]*>([\s\S]*?)<\/article>/i);
const bodyMatch = html.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
let rawBody = articleMatch ? articleMatch[1] : (bodyMatch ? bodyMatch[1] : "");
const body = rawBody
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "")
.replace(/<[^>]+>/g, "")
.replace(/\s+/g, " ")
.trim();
return { title, body };
}
// Article 作成
const articleRequest: publicSDK.ArticlesCreateRequest = {
title: title,
description: body,
owned_by: [work.owned_by?.[0]?.id || work.created_by?.id],
resource: { url: url },
status: publicSDK.ArticleStatus.Published,
};
const articleResponse = await devrevSDK.createArticle(articleRequest);
デプロイと動作確認
デプロイは以下の 3 ステップです。
# 1. パッケージ作成 (初回のみ)
devrev snap_in_package create-one --slug ticket-to-article
# 2. バージョン作成 (ビルド・アップロードが自動実行される)
devrev snap_in_version create-one --path . --package <package_id> -w 5
# 3. ドラフト作成とアクティベート
devrev snap_in draft --snap_in_version <version_id>
devrev snap_in activate <snap_in_id>
動作確認は、Ticket を作成してコメントに DevelopersIO の記事 URL を投稿し、ステージを Work in Progress → Resolved に変更します。数秒後、Ticket に以下のコメントが自動投稿されました。
Article を作成しました: ART-7
タイトル: DevRev Workflows を最小構成で動かす | DevelopersIO
URL: https://dev.classmethod.jp/articles/devrev-workflows-minimal-chatbot-flow/

おわりに
当初は Description から URL を抽出する設計でしたが、チームメンバーからのフィードバックを受けて customer messages から取得する方式に変更しました。コメントで URL を共有してから Resolved にする方が自然な運用フローであり、Description を汚さずに済みます。
Snap-in は外部 HTTP リクエストや複雑なデータ加工が必要なケースに適しています。DevRev 内部のオブジェクト操作のみで完結するフローであれば GUI で構築できる Workflows が適しており、用途に応じて使い分けるのがよいでしょう。









