Theme / v1.5.0

Agent Reach

給 AI Agent 一鍵安裝網路讀寫能力

實戰範例

實戰範例 005:GitHub Release Notes 自動生成與跨平台社交廣播

探討如何利用 GitHub Webhooks 觸發自動化腳本,結合大型語言模型生成對用戶友好的 Release Notes,並穩健地推送至多個社群平台。

實戰範例 005:GitHub Release Notes 自動生成與跨平台社交廣播

簡介與場景

在軟體產品的開發生命週期中,每當發佈新版本 (Release) 時,我們通常需要將技術性較強的 Commit Messages 轉換為用戶友好 (User-friendly) 的 Release Notes,並推送到 Discord、Slack 或 Twitter 等社交平台。 本範例展示了如何構建一個「GitHub 發佈廣播代理」。我們將處理 GitHub Webhook 的 Payload,防禦重複的 Webhook 事件 (Idempotency),並確保在呼叫外部通訊軟體 API 時能夠優雅地處理速率限制 (Rate Limit) 與連線超時。

原始代碼:不具防護的 Webhook 處理器

初階開發者通常會用 Express.js 寫一個簡單的路由,收到請求就直接呼叫外部 API。這樣容易因為 Webhook 的重試機制導致發送多次重複訊息。

const express = require('express');
const axios = require('axios');
const app = express();

app.use(express.json());

app.post('/webhook/github', async (req, res) => {
    // 沒有驗證 Signature,也沒有處理重複發送
    const { action, release } = req.body;
    
    if (action === 'published') {
        const message = `🎉 新版本發佈!${release.name}\n${release.body}`;
        
        // 如果 Discord API 暫時卡住,這裡會超時,GitHub 會重試,導致發佈多次
        await axios.post('https://discord.com/api/webhooks/YOUR_WEBHOOK_URL', {
            content: message
        });
    }
    
    res.send('OK');
});

app.listen(3000);

開發者與 AI 的對話記錄

Ponytail (極簡主義者): 「GitHub Webhook 一接就通了,為什麼還要寫那麼多複雜的邏輯?代碼越少 Bug 越少。」

Agent Reach (容錯專家): 「網路請求沒有絕對的穩定。GitHub Webhook 要求你在 10 秒內回覆 200 OK,否則它會認為發送失敗並進行重試。你的代碼中,如果 axios.post 到 Discord 需要 15 秒,GitHub 就會重試,最終導致用戶的 Discord 頻道被相同的新版本通知洗版。」

Ponytail: 「了解。所以我應該先把收到的事件存下來,然後立刻回覆 200 OK,接著再慢慢發送通知對吧?」

Agent Reach: 「完全正確!這叫做『異步處理 (Asynchronous Processing)』與『事件冪等性 (Event Idempotency)』。此外,你也缺少了對 GitHub HMAC-SHA256 簽名 (Signature) 的校驗。若有惡意用戶隨意對這個 Endpoint 發送 POST 請求,你的系統就會發出偽造的更新公告。」

重構/優化後的代碼:加入防偽造、去重與異步隊列

重構後的系統加入了加密校驗與簡單的記憶體去重機制,並使用 setTimeout 模擬將任務放入背景異步處理。

const express = require('express');
const crypto = require('crypto');
const axios = require('axios');

const app = express();
const WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET;

// 用於記錄已處理的 Release ID (實務上應使用 Redis 設定過期時間)
const processedReleases = new Set();

// 必須使用 raw body 來計算 HMAC
app.use(express.json({
    verify: (req, res, buf) => { req.rawBody = buf; }
}));

function verifySignature(req) {
    const signature = req.headers['x-hub-signature-256'];
    if (!signature) return false;
    
    const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
    const digest = 'sha256=' + hmac.update(req.rawBody).digest('hex');
    
    return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest));
}

async function broadcastToDiscord(releaseData, retries = 3) {
    const message = `🚀 **新版本發佈: ${releaseData.name}**\n\n${releaseData.body}`;
    
    while (retries > 0) {
        try {
            await axios.post(process.env.DISCORD_WEBHOOK_URL, { content: message }, { timeout: 5000 });
            console.log(`[INFO] 成功廣播 Release: ${releaseData.id}`);
            return;
        } catch (error) {
            retries--;
            console.error(`[ERROR] Discord 廣播失敗,剩餘重試次數: ${retries}`);
            if (retries === 0) return;
            await new Promise(r => setTimeout(r, 2000)); // 退避重試
        }
    }
}

app.post('/webhook/github', (req, res) => {
    // 1. 安全防護:驗證請求合法性
    if (!verifySignature(req)) {
        console.warn("[WARN] 拒絕未經授權的 Webhook 請求");
        return res.status(401).send('Unauthorized');
    }

    const githubEvent = req.headers['x-github-event'];
    
    if (githubEvent === 'release' && req.body.action === 'published') {
        const release = req.body.release;
        
        // 2. 邊界檢查與去重 (Idempotency)
        if (processedReleases.has(release.id)) {
            console.log(`[INFO] 忽略重複發佈事件: ${release.id}`);
            return res.status(200).send('Already processed');
        }
        
        processedReleases.add(release.id);
        
        // 3. 異步處理:立刻回覆 200 OK 避免 GitHub Timeout 重試
        setTimeout(() => {
            broadcastToDiscord(release).catch(e => console.error(e));
        }, 0);
    }

    // 立即回應
    res.status(200).send('Accepted');
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    console.log(`[INFO] Webhook server listening on port ${PORT}`);
});

效益分析表格與解讀

防護與優化點 原始設計 重構後設計 (Agent Reach) 實際效益
請求防偽造 (Authentication) 無驗證,接受所有請求 x-hub-signature-256 HMAC 校驗 防止惡意競爭對手發送假公告,保障社群安全
網路逾時反應 同步等待 Discord API 立即回覆 200,異步背景發送 滿足 GitHub Webhook 的 10 秒回應規範,避免服務降級
冪等性 (去重) 每次收到請求都發送 processedReleases.has() 去重機制 根除重試機制帶來的「洗版」災難
API 容錯重試 失敗即丟棄 broadcastToDiscord 實作 Retry 增加消息送達的可靠性 (Reliability)

解讀: 本範例說明了 Webhook 服務的基本防禦。透過 Agent Reach 原則,我們不信任外部傳來的任何觸發器(加入了 HMAC 校驗),並且不預設網路通道永遠順暢(加入了異步處理與重試機制)。這才是建構生產環境事件驅動系統 (Event-driven System) 的標準姿態。