linear-webhooks-events
Linearのウェブフックを設定・管理して、リアルタイムイベント処理を実現します。ウェブフック設定時、Linearイベント処理、またはリアルタイム連携構築時に活用できます。「linearウェブフック」「linearイベント」「linearリアルタイム」「linearウェブフック処理」「linearウェブフック設定」といったフレーズで呼び出せます。
description の原文を見る
Configure and handle Linear webhooks for real-time event processing. Use when setting up webhooks, handling Linear events, or building real-time integrations. Trigger with phrases like "linear webhooks", "linear events", "linear real-time", "handle linear webhook", "linear webhook setup".
SKILL.md 本文
Linear Webhooks & Events
Overview
Linear webhooksをセットアップして、リアルタイムイベント通知を処理します。
Prerequisites
- Linear workspace の管理者アクセス
- Webhook配信用のパブリックエンドポイント
- Webhook署名秘密鍵の設定
利用可能なイベントタイプ
| イベントタイプ | 説明 |
|---|---|
Issue | Issue の作成、更新、削除 |
IssueComment | コメントの追加または更新 |
Project | プロジェクトの変更 |
Cycle | Cycle(スプリント)の変更 |
Label | ラベルの変更 |
Reaction | 絵文字リアクション |
手順
ステップ 1: Webhook エンドポイントの作成
// api/webhooks/linear.ts (Vercel/Next.js style)
import crypto from "crypto";
import type { NextApiRequest, NextApiResponse } from "next";
export const config = {
api: {
bodyParser: false, // Need raw body for signature
},
};
async function getRawBody(req: NextApiRequest): Promise<string> {
const chunks: Buffer[] = [];
for await (const chunk of req) {
chunks.push(chunk);
}
return Buffer.concat(chunks).toString("utf8");
}
function verifySignature(payload: string, signature: string): boolean {
const secret = process.env.LINEAR_WEBHOOK_SECRET!;
const hmac = crypto.createHmac("sha256", secret);
const expectedSignature = hmac.update(payload).digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
const rawBody = await getRawBody(req);
const signature = req.headers["linear-signature"] as string;
if (!signature || !verifySignature(rawBody, signature)) {
return res.status(401).json({ error: "Invalid signature" });
}
const event = JSON.parse(rawBody);
// Process event
await processLinearEvent(event);
return res.status(200).json({ received: true });
}
ステップ 2: イベント処理ルーター
// lib/webhook-handlers.ts
interface LinearWebhookPayload {
action: "create" | "update" | "remove";
type: string;
data: Record<string, unknown>;
createdAt: string;
organizationId: string;
webhookTimestamp: number;
webhookId: string;
}
type EventHandler = (data: Record<string, unknown>, action: string) => Promise<void>;
const handlers: Record<string, EventHandler> = {
Issue: handleIssueEvent,
IssueComment: handleCommentEvent,
Project: handleProjectEvent,
Cycle: handleCycleEvent,
};
export async function processLinearEvent(payload: LinearWebhookPayload) {
const handler = handlers[payload.type];
if (!handler) {
console.log(`No handler for event type: ${payload.type}`);
return;
}
try {
await handler(payload.data, payload.action);
} catch (error) {
console.error(`Error processing ${payload.type} event:`, error);
throw error;
}
}
async function handleIssueEvent(data: Record<string, unknown>, action: string) {
const issue = data as {
id: string;
identifier: string;
title: string;
state: { name: string };
priority: number;
team: { key: string };
};
console.log(`Issue ${action}: ${issue.identifier} - ${issue.title}`);
switch (action) {
case "create":
await onIssueCreated(issue);
break;
case "update":
await onIssueUpdated(issue);
break;
case "remove":
await onIssueRemoved(issue.id);
break;
}
}
async function handleCommentEvent(data: Record<string, unknown>, action: string) {
const comment = data as {
id: string;
body: string;
issue: { identifier: string };
user: { name: string };
};
console.log(`Comment ${action} on ${comment.issue.identifier} by ${comment.user.name}`);
}
async function handleProjectEvent(data: Record<string, unknown>, action: string) {
console.log(`Project ${action}:`, data);
}
async function handleCycleEvent(data: Record<string, unknown>, action: string) {
console.log(`Cycle ${action}:`, data);
}
ステップ 3: ビジネスロジックハンドラー
// lib/linear-handlers.ts
import { sendSlackNotification } from "./slack";
import { syncToDatabase } from "./database";
async function onIssueCreated(issue: any) {
// Sync to local database
await syncToDatabase("issues", issue.id, issue);
// Notify Slack for high-priority issues
if (issue.priority <= 2) {
await sendSlackNotification({
channel: "#engineering-alerts",
text: `New high-priority issue: ${issue.identifier} - ${issue.title}`,
});
}
}
async function onIssueUpdated(issue: any) {
// Update local cache
await syncToDatabase("issues", issue.id, issue);
// Check for state changes
if (issue.state?.name === "Done") {
await celebrateCompletion(issue);
}
}
async function onIssueRemoved(issueId: string) {
await syncToDatabase("issues", issueId, null); // Soft delete
}
async function celebrateCompletion(issue: any) {
console.log(`Issue completed: ${issue.identifier}`);
}
ステップ 4: Linear に Webhook を登録
# Linear UI を使用する場合:
# 1. Settings > API > Webhooks に移動
# 2. 「Create webhook」をクリック
# 3. エンドポイント URL を入力
# 4. 受信するイベントを選択
# 5. 保存して署名秘密鍵をコピー
// または API 経由
import { LinearClient } from "@linear/sdk";
async function createWebhook() {
const client = new LinearClient({
apiKey: process.env.LINEAR_API_KEY!,
});
const result = await client.createWebhook({
url: "https://your-domain.com/api/webhooks/linear",
label: "My Integration Webhook",
teamId: "your-team-id", // Optional: limit to specific team
resourceTypes: ["Issue", "IssueComment", "Project"],
});
if (result.success) {
const webhook = await result.webhook;
console.log("Webhook created:", webhook?.id);
console.log("Secret (save this!):", webhook?.secret);
}
}
ステップ 5: ngrok を使用したローカル開発
# ローカルサーバーを起動
npm run dev # localhost:3000 で実行
# 別のターミナルで ngrok を起動
ngrok http 3000
# https URL をコピーして Linear webhook 設定に追加
# 例: https://abc123.ngrok.io/api/webhooks/linear
ステップ 6: べき等なイベント処理
// lib/idempotency.ts
import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL);
export async function processIdempotent(
webhookId: string,
processor: () => Promise<void>
): Promise<boolean> {
const key = `webhook:${webhookId}`;
// Check if already processed
const exists = await redis.exists(key);
if (exists) {
console.log(`Webhook ${webhookId} already processed, skipping`);
return false;
}
// Mark as processing
await redis.setex(key, 86400, "processing"); // 24 hour TTL
try {
await processor();
await redis.setex(key, 86400, "completed");
return true;
} catch (error) {
await redis.del(key); // Allow retry
throw error;
}
}
// Usage in webhook handler
await processIdempotent(payload.webhookId, async () => {
await processLinearEvent(payload);
});
エラーハンドリング
| エラー | 原因 | 解決策 |
|---|---|---|
Invalid signature | 秘密鍵が間違っているか改ざんされている | webhook 秘密鍵を確認 |
Timeout | 処理が遅い | 非同期キューを使用 |
Duplicate events | Webhook リトライ | べき等性を実装 |
Missing data | 部分的なイベント | グレースフルに処理 |
リソース
次のステップ
linear-performance-tuning でパフォーマンスを最適化します。
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- Brmbobo
- リポジトリ
- Brmbobo/Web2podcast
- ライセンス
- MIT
- 最終更新
- 2026/1/26
Source: https://github.com/Brmbobo/Web2podcast / ライセンス: MIT
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。