DevRev snap-inで外部データをTicket化してみた

DevRev snap-inで外部データをTicket化してみた

DevRev の snap-in を使うと、外部 API と連携する仕組みをカスタマイズできます。例として BigQuery の公開サンプルを読み込み、 Ticket 化する手順を紹介します。
2026.01.25

はじめに

DevRev を導入検討するとき、既存システムとどう繋げられるかが判断材料になります。既存のワークフローを大きく変えずに DevRev 側へ情報を流せるかが重要です。

DevRev は snap-in (拡張機能) によって、外部システムの API を呼び出し、DevRev 側のオブジェクトを作成する仕組みを提供します。本記事では、外部データの例として BigQuery の公開サンプルを使い、snap-in を 1 本作ります。その後自前のコマンドを実行し、DevRev の Ticket が作成されるところまでを確認します。

検証環境

  • Windows 11
  • Git Bash 2.47.1.windows.1
  • DevRev CLI v0.4.13
  • node v24.13.0
  • npm 11.6.2

対象読者

  • DevRev の導入を検討しており、snap-in による外部連携の実現性を短時間で確認したい方
  • DevRev の dev org を用意済みで、Admin 権限を持つ方
  • Windows + Git Bash の環境で、Node.js と TypeScript の基本操作に慣れている方

参考

snap-in を作成する

前提

  • DevRev の dev org を用意済みで、Admin 権限を持つこと (snap-in を draft インストールするため)
  • GCP のサービスアカウント JSON は用意済みで、service-account.json としてローカルに置けること
  • Ticket を紐付ける Part は作成済みで、owner (owned_by) が 1 人以上設定されていること

DevRev CLI の準備

snap-in を作成するには DevRev CLI を使います。Windows では、DevRev CLI の zip を展開し、devrev.exe へのパスを PATH に追加します。

devrev --version

実行結果の例

devrev version v0.4.13 fe3410c6efb59f249e3cc29ba113d382357e15ce 2025-11-10T17:15:52Z
gateway version d7a6c473fc2e1c1827f6bcbff51f33ecd57cfc02

DevRev にログインする

下記のコマンドで DevRev ワークスペースにログインします。ブラウザが開くので認証します。

devrev profiles authenticate -o <dev-org-slug> -u <your-email@example.com>

TypeScript テンプレートを初期化する

snap-in の TypeScript テンプレートを生成します。

devrev snap_in_version init

実行後、 devrev-snaps-typescript-template ディレクトリが作られます。

snap-in package を作成する

snap-in package を作ります。slug はグローバルで一意になるようにします。

devrev snap_in_package create-one --slug <globally-unique-slug>

manifest.yaml を更新する

この検証では、Discussions の command から function を呼び出せれば十分です。manifest には functionscommands を定義します。command の surfacesdiscussions にし、object_typespart にします。

namespace は、同名 command がある場合の区別に使います。普段の実行で namespace を入力する必要はありません。

devrev-snaps-typescript-template/manifest.yaml を次のようにします。 command 名は任意です。本記事では bqimport とします。

version: "2"

name: "External Ticket Import Demo"
description: "Demo: run a command and create tickets from an external system."

service_account:
  display_name: "External Import Bot"

developer_keyrings:
  - name: gcp_service_account_json
    description: "GCP service account JSON for external query"
    display_name: "GCP Service Account JSON"

functions:
  - name: bq_import
    description: "Command handler: query external system and create tickets"

commands:
  - name: bqimport
    namespace: demo
    description: "Create tickets from an external system."
    surfaces:
      - surface: discussions
        object_types:
          - part
    usage_hint: "[limit] (e.g. 10)"
    function: bq_import

developer_keyrings は、開発者が提供する秘密情報です。snap-in のインストーラには見えません。

developer keyring を作成する

DevRev では、秘密情報を keyring として保持できます。developer keyring の snap-in secret は plain text を格納できます。CLI では、devrev developer_keyring コマンドで認証情報 (例: service-account.json) を登録します。

cat service-account.json | devrev developer_keyring create snap-in-secret gcp_service_account_json

function を追加する

テンプレートでは function-factory.ts に function を登録します。bq_import を追加します。

devrev-snaps-typescript-template/code/src/function-factory.ts を更新します。

import on_work_creation from './functions/on_work_creation';
import bq_import from './functions/bq_import';

export const functionFactory = {
  // Add your functions here
  on_work_creation,
  bq_import,
} as const;

export type FunctionFactoryType = keyof typeof functionFactory;

次に、devrev-snaps-typescript-template/code/src/functions/bq_import/index.ts を作成します。

  • keyring からサービスアカウント JSON を取得 (例では event.input_data.keyrings.* を参照)
  • DevRev SDK は service_account_token で初期化
  • command の実行元は event.payload.source_id から取得
  • Ticket は works.create で作成し、applies_to_partowned_by を必ず指定
  • owned_by には Part の owner を parts.get で取得して使用
devrev-snaps-typescript-template/code/src/functions/bq_import/index.ts 全文
import { BigQuery } from "@google-cloud/bigquery";
import { client, publicSDK } from "@devrev/typescript-sdk";

const DEFAULT_LIMIT = 10;
const MAX_LIMIT = 200;

const SQL = (limit: number) => `
SELECT word, SUM(word_count) AS cnt
FROM \`bigquery-public-data.samples.shakespeare\`
GROUP BY word
ORDER BY cnt DESC
LIMIT ${limit};
`;

function parseLimit(event: any): number {
  const raw =
    event?.payload?.parameters ??
    event?.payload?.params ??
    event?.payload?.command_params ??
    "";

  const n = Number(String(raw).trim().split(/\s+/)[0] || DEFAULT_LIMIT);
  if (!Number.isFinite(n)) return DEFAULT_LIMIT;
  return Math.max(1, Math.min(MAX_LIMIT, Math.floor(n)));
}

async function getPartOwnerIds(partId: string, devrevSDK: any): Promise<string[]> {
  const resp = await devrevSDK.partsGet({ id: partId });
  const owned = resp?.data?.part?.owned_by ?? [];
  return owned.map((u: any) => u?.id).filter(Boolean);
}

export const run = async (events: any[]) => {
  // TS7030 対策: events が空でも必ず return
  if (!events || events.length === 0) {
    return { created: 0, reason: "no events" };
  }

  // コマンドは通常 1 件なので 1 件だけ処理
  const event = events[0];

  console.info("event.payload keys:", Object.keys(event?.payload ?? {}));

  const endpoint = event.execution_metadata.devrev_endpoint;
  const token = event.context.secrets.service_account_token;
  const devrevSDK = client.setup({ endpoint, token });

  // developer keyring
  const saJson = event.input_data.keyrings.gcp_service_account_json;
  if (!saJson) {
    throw new Error("Missing developer keyring: gcp_service_account_json");
  }

  const creds = JSON.parse(saJson);
  const bigquery = new BigQuery({
    projectId: creds.project_id,
    credentials: {
      client_email: creds.client_email,
      private_key: creds.private_key,
    },
  });

  const limit = parseLimit(event);

  const [rows] = await bigquery.query({
    query: SQL(limit),
    location: "US",
    // TS2769 対策: maximumBytesBilled は string
    maximumBytesBilled: String(100 * 1024 * 1024),
    useLegacySql: false,
  });

  const partId = event.payload.source_id;
  if (!partId) {
    throw new Error("Missing payload.source_id (expected Part id).");
  }

  const ownedBy = await getPartOwnerIds(partId, devrevSDK);
  if (ownedBy.length === 0) {
    throw new Error("Part has no owners (owned_by). Please set an owner on the Part.");
  }

  let created = 0;
  for (const r of rows as any[]) {
    const word = String(r.word);
    const cnt = Number(r.cnt);

    await devrevSDK.worksCreate({
      type: publicSDK.WorkType.Ticket,
      title: `[BQ] shakespeare: ${word}`,
      body: `count=${cnt}`,
      applies_to_part: partId,
      owned_by: ownedBy,
    });
    created += 1;
  }

  return { created, part: partId, limit };
};

export default run;

依存関係をインストールしてビルドする

テンプレートのビルドとパッケージングには Node.js を使います。

npm install
npm install @google-cloud/bigquery
npm run build
npm run package

snap-in version を作成し、draft をインストールする

テンプレートのルートに戻ります。

cd /path/to/devrev-snaps-typescript-template

snap-in version を作成します。

devrev snap_in_version create-one --path .

manifest に developer_keyrings があるため、CLI は keyring のマッピングを要求します。ここでは gcp_service_account_json に対して、先ほど作った同名の developer keyring を選びます。

続けて、snap-in を draft としてインストールします。draft インストールには Admin 権限が必要です。

devrev snap_in draft

DevRev の UI 上で snap-in が active になることを確認します。

activated snap-in

動作確認

manifest で surface: discussionsobject_types: part を指定したため、Part の Discussions で command を実行できるようになっています。

product page

試しに次のコマンドを実行してみました。

/bqimport 10

実行後、Part に紐づく Ticket が 10 件作成されました。

created tickets

snap-in のログは CLI で確認できます。

devrev snap_in_package logs

実行結果の例

{
    "incomplete_results": false,
    "log_summary": {
        "total": {
            "relation": "eq",
            "value": 3
        }
    },
    "logs": [
        {
            "id": "****",
            "level": "info",
            "msg": "END RequestId: **** Execution time: 5255.87 ms",
            "process": "function",
            "snap_in_package": "don:integration:dvrv-us-1:devo/****:snap_in_package/****",
            "timestamp": "2026-01-25T08:48:01.45Z"
        },
        {
            "dev_org": "don:identity:dvrv-us-1:devo/****",
            "id": "****",
            "level": "info",
            "msg": "event.payload keys: [\n  'actor_id',\n  'command_id',\n  'dev_org',\n  'parameters',\n  'parent_id',\n  'request_id',\n  'source_id',\n  'surface'\n]",
            "process": "function",
            "snap_in_package": "don:integration:dvrv-us-1:devo/****:snap_in_package/****",
            "snap_in_version": "don:integration:dvrv-us-1:devo/****:snap_in_package/****:snap_in_version/****",
            "source_type": "command",
            "timestamp": "2026-01-25T08:47:58.091Z"
        },
        {
            "id": "****",
            "level": "info",
            "msg": "START RequestId: ****",
            "process": "function",
            "snap_in_package": "don:integration:dvrv-us-1:devo/****:snap_in_package/****",
            "timestamp": "2026-01-25T08:47:56.194Z"
        }
    ],
    "next_cursor": "****",
    "prev_cursor": "****"
}

まとめ

snap-in を使うと、DevRev の UI 操作 (Discussions の command) をトリガーにし、外部システムへアクセスし、DevRev 側に Ticket を作成できます。本記事の手順は BigQuery を例にしましたが、keyring で秘密情報を渡し、function 内で外部 API と works.create を呼ぶ構造は共通です。DevRev 導入検討において、既存システム連携の実現可能性を確認する材料になれば幸いです。

この記事をシェアする

FacebookHatena blogX

関連記事