CI 掛掉時,打開主控台日誌、用肉眼追原因這件事,其實很耗精神。
本文會透過實際程式碼,解說如何把 偵測 Jenkins 失敗 → n8n 回收日誌 → Claude 摘要原因 →
通知到 Slack
這整套流程完全自動化。

重點是 徹底分工。Jenkins 端只負責丟出「失敗這件事」以及「日誌在哪裡」。
日誌回收、AI 調查、通知全部交給 n8n。

前提環境

後半段會出現一些 Kubernetes 特有的內容(例如改寫成內部 service),所以先分享一下筆者的環境。
如果換成其他環境,理解後也能套用到地端 VM 或雲端代管環境。

  • Jenkins:運行在 Kubernetes 上。對外公開為 https://jenkins.example.info,從內部可透過
    K8s service jenkins-app.jenkins.svc.cluster.local:8080 存取。
  • n8n:同樣運行在 Kubernetes 叢集上。
    • 使用內含 Claude CLI(claude 指令)的自製映像檔
    • 已啟用 Execute Command 節點,可以在 pod 內執行 shell。
    • pod 映像檔內沒有 curl(預設使用 busybox 的 wget)。
  • 秘密資訊管理:由 HashiCorp Vault 提供,並以 pod 的環境變數形式注入。
    • CLAUDE_CODE_OAUTH_TOKEN(Claude CLI 驗證)
    • Jenkins 的 API 使用者 / token(用於抓取主控台日誌的 Basic 驗證)
  • 公開 URL 保護:Jenkins 的公開主機位於 Cloudflare Access 之下,n8n pod 即使直接打
    公開 URL 也會被阻擋。因此後面需要做 URL 改寫。

Kubernetes / Vault 不是必需的。只要滿足以下 3 點即可:
「n8n 能連到 Jenkins 的內部端點」、「pod 內能執行 claude」、「能安全地傳遞 Jenkins 的認證資訊」,
其餘架構都可以。

全體像

分工原則如下:

  • 回收、調查、通知都由 n8n 負責。Jenkins 只通知「失敗了」以及日誌位置。
  • Jenkins 端的共用處理集中在 Shared Library 的 1 個檔案中,各個 pipeline 只需 1 行呼叫。

這樣切分之後,即使 AI 解析邏輯改了,Jenkins 端也完全不用動。

Jenkins 端:通知失敗的 Shared Library

在各個 pipeline 的 post { failure { } } 中,只要呼叫共用函式 1 行即可。

post {
    failure {
        notifyN8nFailure()
    }
}

內容放在 Shared Library(vars/notifyN8nFailure.groovy)裡。設計重點有 3 個。

1. 在失敗處理器內絕對不要 throw

這個函式是從 failure {} 裡呼叫,也就是「建置已經失敗之後的後處理」。
如果這裡再拋例外,可能會把後處理本身弄壞,所以 任何失敗都直接吞掉,只輸出 warn 日誌
通知採取 best-effort 方針(就算沒送到,建置結果也照樣以 FAILURE 結束)。

def call(Map args = [:]) {
    def urlCredId   = args.urlCredentialId   ?: 'n8n-failure-webhook-url'
    def tokenCredId = args.tokenCredentialId ?: 'n8n-failure-webhook-token'

    try {
        def payload = buildPayload(args.extra instanceof Map ? args.extra : [:])
        def payloadFile = '.n8n-failure-payload.json'
        writeFile file: payloadFile, text: payload
        // ...(送出流程)
    } catch (err) {
        // 不要因為失敗通知本身失敗,就破壞建置後處理。
        echo "[WARN] 傳送 n8n 失敗通知時發生錯誤,已忽略: ${err}"
    }
}

2. 不要把 Credential 洩漏到日誌

Webhook URL / 認證 token 都從 Jenkins Credentials 取得。
這裡 不要用 Groovy 字串插值("${...}")直接塞進 curl 指令
因為一旦插值,有時會繞過遮罩機制,讓明文出現在日誌中。
應該用 withCredentials 綁定成環境變數,再在 shell 裡以 $N8N_WEBHOOK_URL 的方式存取。

withCredentials([string(credentialsId: urlCredId, variable: 'N8N_WEBHOOK_URL')]) {
    sh """#!/bin/bash
        set -uo pipefail
        URL="\${N8N_WEBHOOK_URL:-}"
        if [ -z "\$URL" ]; then
          echo "[WARN] n8n-failure-webhook-url 為空,略過失敗通知。" >&2
          exit 0
        fi
        TOKEN="\${N8N_WEBHOOK_TOKEN:-}"
        AUTH_HEADER=()
        if [ -n "\$TOKEN" ]; then
          AUTH_HEADER=(-H "X-N8N-Webhook-Token: \$TOKEN")
        fi
        HTTP=\$(curl -sS -o /dev/null -w '%{http_code}' \\
          --connect-timeout 10 --max-time 30 \\
          -X POST \\
          -H 'Content-Type: application/json; charset=utf-8' \\
          "\${AUTH_HEADER[@]}" \\
          --data @${payloadFile} \\
          "\$URL" || echo 000)
        # 非 2xx 也只記 warn,繼續往下跑
    """
}

URL 是必填、token 是選填,這個非對稱設計也很重要。
即使沒有 token credential 也希望通知能繼續,所以 token 會另外嘗試取得;如果沒有,就不加 header 直接送出。

3. 傳給 n8n 的不是日誌本體,而是日誌位置

Payload 只包含失敗的中繼資訊。不直接傳送主控台日誌本體
改為傳 consoleUrlBUILD_URL + consoleText),回收交給 n8n。

private String buildPayload(Map extra) {
    def cb = currentBuild
    def data = [
        job        : env.JOB_NAME       ?: '',
        build      : env.BUILD_NUMBER   ?: '',
        buildUrl   : env.BUILD_URL      ?: '',
        consoleUrl : env.BUILD_URL ? "${env.BUILD_URL}consoleText" : '',
        branch     : env.BRANCH_NAME    ?: (env.GIT_BRANCH ?: ''),
        node       : env.NODE_NAME      ?: '',
        jenkinsUrl : env.JENKINS_URL    ?: '',
        result     : (cb?.currentResult ?: 'FAILURE'),
        durationMs : (cb?.duration ?: 0),
    ]
    if (extra) { data.extra = extra }
    return groovy.json.JsonOutput.toJson(data)
}

送出的 JSON 會長這樣:

{
  "job": "GitHub/n8n-i18n-japanese-pr-review",
  "build": "67",
  "buildUrl": "https://jenkins.example.info/job/.../67/",
  "consoleUrl": "https://jenkins.example.info/job/.../67/consoleText",
  "branch": "main",
  "result": "FAILURE",
  "durationMs": 12345
}

需要的 Jenkins Credentials

Credential ID種類是否必需用途n8n-failure-webhook-urlSecret text必需n8n Webhook 的正式 URLn8n-failure-webhook-tokenSecret text選填Header Auth token(X-N8N-Webhook-Token)如果 URL 尚未註冊或為空,就會跳過通知,因此可以分階段導入。

n8n 端:回收 → Claude 調查 → Slack 通知

n8n 的工作流程由 4 個節點組成,依序來看。

1. Webhook(接收)

POST /webhook/jenkins-failure 接收來自 Jenkins 的 JSON。
可以選擇性設定 Header Auth,並與 Jenkins 端的 n8n-failure-webhook-token 一致,降低外部直接呼叫的風險。

2. Build Claude Input(URL 的驗證與改寫)

這一步雖然不起眼,但很重要。收到的 consoleUrl 不能直接拿來用,而是要:

  1. 嚴格驗證 URL(擋掉非預期格式)
  2. 把公開主機改寫成 內部 Kubernetes service
  3. 轉成 base64 安全地傳給 shell

公開 URL(例如在 Cloudflare Access 之下)很多時候無法從 n8n pod 直接存取。
因此這裡會只取出 consoleUrl路徑,再改指向內部 service
jenkins-app.jenkins.svc.cluster.local:8080
如此一來可以繞過 Cloudflare,同時因為 host 固定為內部位址,也能降低 SSRF 風險。

3. Claude Analyze(透過 Execute Command 抓日誌 → AI 摘要)

在 n8n pod 內透過 Execute Command 節點一次完成。

wget --header="Authorization: Basic <base64(user:token)>" \
  http://jenkins-app.jenkins.svc.cluster.local:8080/.../consoleText \
  -O - \
  | tail -n 400 | head -c 24000 \
  | claude -p "請用中文簡潔摘要這個 Jenkins 建置失敗的原因,並附上根本原因、對應階段與處理方式"

實際環境中有兩個常見坑:

  • pod 裡沒有 curl → 改用 busybox 的 wget。busybox 的 wget 不支援 --user/--password
    所以 Basic 驗證要改用 --header="Authorization: Basic <base64>" 傳入。
  • 日誌很長 → 先用 tail -n 400 | head -c 24000 只擷取尾端再交給 claude。
    失敗的根本原因通常會出現在尾端附近;如果原因不在尾端,再調整擷取範圍。

Claude CLI 的認證由 pod 的環境變數 CLAUDE_CODE_OAUTH_TOKEN(來自 Vault)提供。
Jenkins 的 API 認證資訊也透過 pod env 傳入,而不是放在 n8n 的 credential store 裡
因此 n8n 端不需要另外新增 credential(只有 Slack 會沿用既有 credential)。

4. Notify Slack(通知)

把 Claude 的摘要發到 Slack 頻道。這裡共用既有的 Slack API credential。
如果 Claude 回傳太長,可能會碰到 Slack 字數上限,因此必要時可在 prompt 端限制輸出長度。

動作確認與踩坑

我實際故意把 CI 弄失敗,完成了 end-to-end 驗證。
對於因 ssh: connect to host github.com port 22: Connection timed out 而失敗的建置,
Claude 成功整理出根本原因、對應階段、處理方式,並且發到 Slack。

導入過程中踩到/需要注意的點如下:

  • 失敗通知要走 best-effort。Webhook 沒送到時,仍然讓 job 以 FAILURE 正常結束。
    最重要的是不要因為通知失敗而把建置後處理弄壞。
  • 不要從 Jenkins 直接送出日誌本體。只傳 metadata + URL,把回收留給 n8n,
    可以讓 payload 保持精簡,責任分界也更清楚。
  • 透過改寫成內部 service 來繞過公開閘道(例如 Cloudflare Access)。
  • 假設 pod 裡沒有 curl,因此使用 wget + Basic 標頭。
  • n8n 的各個節點沒有設定 retryOnFail / onError。如果要面對暫時性故障,可以再補上。

總結

透過「Jenkins 只負責丟出失敗事件,回收、AI 調查、通知都交給 n8n」這個切分方式,
就能把 CI 失敗的初步調查整個自動化。Jenkins 端只要 1 行 Shared Library,
AI 解析邏輯也被收斂在 n8n 內,因此後續迭代調整會很方便。

同樣的架構也可以把 Slack 換成 Discord 或 Teams,也可以把 Claude 換成其他 CLI 解析工具。
如果你也常常在 CI 掛掉時用肉眼追日誌,建議先從「把失敗事件集中到單一入口」開始,效果通常會很明顯。


:sparkles:從零開始也能學會!一起挑戰看看吧:sparkles:

我也有寫 note ↓



原文出處:https://qiita.com/jqit-yukiono/items/61985c6743b89aa6924b


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝13   💬2   ❤️1
571
🥈
我愛JS
📝1   ❤️1
71
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登