atlassian-cli はコミュニティによる独立したオープンソースプロジェクトです。Atlassian と提携・関連しておらず、Atlassian による承認、推奨、後援のいずれも受けておらず、Atlassian が提供する公式 CLI(acli)でもありません。製品名は互換性を示す目的でのみ使用しています。
Jira の Webhook とは
Jira の Webhook は外向きの HTTP コールバックです。Jira プロジェクトで何かが起きると(課題の作成、遷移、コメント、削除など)、Jira Cloud は登録した URL に JSON ボディを含む HTTP POST を送ります。スクリプトでこれに反応するには3つのものが必要です。自分が管理する URL を指す Webhook、その URL で動く小さなリスナー、そしてペイロードが届いたときに実行するコマンドです。この記事ではその3つをつなぎ、「何かをする」役として atlassian-cli を使います。Jira でのステータス変更をきっかけに、対象の課題を自動で割り当て、遷移させ、更新できます。
考え方はシンプルです。Jira がイベントの発生源です。リスナーは、生の JSON を判断に変える翻訳役です。atlassian-cli は、その判断を Jira(あるいは Confluence、Bitbucket、JSM)へ反映するアクチュエーターです。アクチュエーターがただのコマンドラインバイナリなので、反応全体が数行の Bash か Python に収まります。SDK もフレームワークもビルド手順も不要です。
Atlassian 自身のツールとの違い: atlassian-cli はコミュニティによる独立したオープンソースプロジェクトです。Atlassian と提携・関連しておらず、Atlassian による承認、推奨、後援、保守のいずれも受けていません。Atlassian が提供する公式 CLI(acli)でもありません。ベンダーによる一次サポートが必要であれば公式の acli を使ってください。Jira、Confluence、Bitbucket、JSM をカバーし、Webhook ハンドラーにそのまま組み込める単一の無料 Rust バイナリが欲しい場合は atlassian-cli を選んでください。
ペイロードの構造
何かに反応する前に、Jira が何を送ってくるかを知る必要があります。課題の Webhook のペイロードは JSON オブジェクトです。ほぼすべてのイベントで読むことになるフィールドは2つ、webhookEvent(何が起きたか)と issue.key(どの課題か)です。更新イベントには changelog が加わり、どのフィールドが何から何へ変わったかを正確に伝えます。
// jira:issue_updated payload (trimmed to the useful parts)
{
"timestamp": 1752105600000,
"webhookEvent": "jira:issue_updated",
"issue_event_type_name": "issue_generic",
"user": {
"accountId": "5f8a1b2c3d4e",
"displayName": "Dana Ops"
},
"issue": {
"key": "DEV-482",
"fields": {
"summary": "Payment retry fails on timeout",
"status": { "name": "Done" },
"priority": { "name": "High" },
"assignee": null
}
},
"changelog": {
"items": [
{ "field": "status", "fromString": "In Review", "toString": "Done" }
]
}
}
よくあるイベント名は覚えておく価値があります。リスナーはこれで分岐するからです。
| webhookEvent | 発生タイミング | changelog の有無 |
|---|---|---|
jira:issue_created |
課題が新しく作成されたとき | なし |
jira:issue_updated |
フィールドが変更されたとき(ステータス、担当者、優先度など) | あり |
jira:issue_deleted |
課題が削除されたとき | なし |
comment_created |
課題にコメントが追加されたとき | なし |
ペイロードはただの JSON なので、シェルスクリプトなら jq で、他の言語なら標準ライブラリで解析できます。読み取りに Jira 固有の要素はありません。
登録済みの Webhook を確認する
Webhook 自体は Jira の管理設定(System → WebHooks)で登録し、そこで送信先の URL と発火させるイベントを設定します。登録したあとは、atlassian-cli で登録済みの Webhook を一覧できるため、管理 UI をクリックして回らずにターミナルから URL とイベントの範囲を確認できます。
# List webhooks registered on this Jira instance
atlassian-cli jira webhooks list
# Machine-readable output for auditing or diffing
atlassian-cli jira webhooks list --format json | jq '.[] | {name, url, events}'
これはレビューで役立ちます。本番でリスナーを信頼する前に、Webhook の一覧を JSON に出力し、URL、イベント、JQL フィルターが想定どおりかを確認してください。Jira の自動化ルールも使っているなら、atlassian-cli jira automation list でそれらを併せて確認でき、atlassian-cli jira audit list --from 2025-01-01 --limit 100 でリスナーが行った変更が実際に反映されたかを突き合わせられます。フラグの詳細はコマンドリファレンスをご覧ください。
最小構成のリスナー
Webhook を受け取るのに Web フレームワークは要りません。Python の標準ライブラリには、エンドポイント1つなら十分すぎる HTTP サーバーが含まれています。以下のリスナーは POST のボディを読んで解析し、イベントと課題キーを取り出して、実際の判断はハンドラー関数に委ねます。
#!/usr/bin/env python3
# jira_hook.py -- a minimal Jira webhook listener (stdlib only)
import json, os, subprocess, hmac
from http.server import BaseHTTPRequestHandler, HTTPServer
SECRET = os.environ["HOOK_SECRET"] # shared token set on the webhook URL
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
# 1. Reject anything without the correct shared token
token = self.path.split("token=")[-1]
if not hmac.compare_digest(token, SECRET):
self.send_response(403); self.end_headers(); return
# 2. Read and parse the JSON body
length = int(self.headers.get("Content-Length", 0))
payload = json.loads(self.rfile.read(length))
# 3. Acknowledge fast, then act
self.send_response(200); self.end_headers()
handle_event(payload)
def handle_event(payload):
event = payload.get("webhookEvent")
key = payload.get("issue", {}).get("key")
if not key:
return
if event == "jira:issue_created":
on_created(key, payload)
elif event == "jira:issue_updated":
on_updated(key, payload)
if __name__ == "__main__":
HTTPServer(("0.0.0.0", 8000), Handler).serve_forever()
ここでは設計上の選択が2つ重要です。1つめは、リスナーが作業を始める前にリクエストを受理する点です。200 応答を返してからハンドラーを呼びます。Jira は素早い応答を期待しており、遅いとリトライすることがあります。コマンド数回より重い処理なら、ペイロードをキューに積んですぐ返すべきです。2つめは、トークンの検証に定数時間比較の hmac.compare_digest を使っている点です。これはセキュリティの節でまた取り上げます。
反応として atlassian-cli を呼び出す
ここからが本題です。イベントを Jira の操作に変えるハンドラー関数を見ていきます。これらは atlassian-cli を呼び出します。以下のコマンドはいずれも実在のサブコマンドなので、そのままハンドラーにコピーできます。
優先度の高い新規課題を自動でトリアージする。 High または Highest の優先度で課題が作成されたら、担当者不在にならないようトリアージ担当のリードに割り当てます。
def on_created(key, payload):
priority = (payload["issue"]["fields"]
.get("priority") or {}).get("name", "")
if priority in ("Highest", "High"):
subprocess.run([
"atlassian-cli", "jira", "issue", "assign", key,
"--assignee", "triage-lead@example.com",
], check=True)
レビューが終わったら課題をクローズする。「Done」へのステータス遷移が届いたら、その課題を自動で「Closed」に進めます。
def on_updated(key, payload):
changes = payload.get("changelog", {}).get("items", [])
moved_to_done = any(
c["field"] == "status" and c["toString"] == "Done"
for c in changes
)
if moved_to_done:
subprocess.run([
"atlassian-cli", "jira", "issue", "transition", key,
"--transition", "Closed", "--profile", "prod",
], check=True)
シェルだけで済ませたい場合は、jq で標準入力からペイロードを読めば同じ反応を実現できます。次が Bash 版リスナーの本体そのものです。
# react.sh -- reads a webhook payload on stdin and acts
payload="$(cat)"
event="$(echo "$payload" | jq -r '.webhookEvent')"
key="$(echo "$payload" | jq -r '.issue.key')"
if [ "$event" = "jira:issue_updated" ]; then
status="$(echo "$payload" | jq -r '.issue.fields.status.name')"
if [ "$status" = "Done" ]; then
atlassian-cli jira issue transition "$key" --transition "Closed"
fi
fi
対象が Jira に限られない点にも注目してください。同じバイナリが複数の製品にまたがるため、Jira のイベントが製品をまたいだ反応を起こせます。リリースされた課題をきっかけに atlassian-cli confluence page create でリリースノートを作ったり、マージのイベントを Jira に戻したりできます。あらゆる API を話すアクチュエーターの魅力はここにあります。ルールベースのフローについては Jira 自動化ガイドをご覧ください。
ループを防ぐ
Webhook の反応でもっともよくある失敗はフィードバックループです。リスナーが jira:issue_updated に反応して atlassian-cli で同じ課題に書き戻すと、その書き込み自体が変更なので Jira はまた jira:issue_updated を発火し、リスナーがふたたび反応します。何も対策しないと、人間による1回の編集が果てしない API 呼び出しの連鎖に膨れ上がります。
次の2つのガードを併用すれば、これを確実に止められます。
- 自分のアカウントを無視する。 atlassian-cli の認証に使うトークンは、固有の
accountIdを持つアカウントに属します。どのペイロードにも、イベントを起こした人物のuser.accountIdが含まれます。その ID が自動化用アカウントなら、早期に return して何もしません。ボットが自分自身に反応することはなくなります。 - 操作を冪等にする。 書き込む前に現在の状態を確認します。「Closed」への遷移を、そのイベントで実際にステータスが「Done」へ変わったとき(
changelogから読み取ります)だけ実行すれば、古い重複配信を再処理しても何も変わりません。
BOT_ACCOUNT = "5f8a1b2c3d4e" # the account atlassian-cli authenticates as
def handle_event(payload):
# Guard 1: never react to changes our own bot made
if payload.get("user", {}).get("accountId") == BOT_ACCOUNT:
return
...
Jira は同じイベントを2回以上配信することもあるため、ループがなくても冪等性は必須です。現在のステータスに反応するのではなく、changelog を読んでそのイベントで特定の遷移が起きたことを確認すれば、ループ対策と重複対策を1回のチェックで両立できます。
エンドポイントを保護する
リスナーはコマンドを実行する公開 URL です。それにふさわしく扱ってください。管理者が登録した Jira Cloud の Webhook は、他のシステムのようにリクエストを HMAC で署名しません。そのため現実的な方法は、Webhook の URL にクエリパラメーターとして共有シークレットを埋め込むことです。たとえば https://hooks.example.com/jira?token=LONG_RANDOM_STRING のようにします。リスナーはトークンが一致しないリクエストを拒否し、比較には定数時間の関数を使うため、処理時間から推測される心配がありません。
その上に次の対策を重ねてください。
- TLS のみ。 エンドポイントかリバースプロキシで HTTPS を終端し、トークンが平文で流れないようにします。
- 最小権限のトークン。 atlassian-cli の認証には専用のアカウントと API トークンを使い、必要なプロジェクトだけに触れられるようにします。名前付きプロファイルとして一度設定し、
--profileで参照します。 - 実行前に検証する。 外部コマンドを呼ぶ前に、ペイロードに想定どおりのフィールドがあるかを確認します。壊れたボディはクラッシュではなく、何もしないで終わるべきです。
- ネットワークの許可リスト。 多層防御として、ファイアウォールやプロキシで受信トラフィックを Atlassian が公開している送信元レンジに限定します。
URL 内のシークレットは、TLS やネットワーク制御の代わりにはなりません。「このリクエストは本当に自分の Jira から来たのか」に答える層であり、他の対策は何かがすり抜けたときの影響範囲を抑えます。リスナーを CI やコンテナーで動かす場合も、同じコマンドのパターンが使えます。その環境でのトークンの扱いは、姉妹記事の GitHub Actions で Jira CLI を動かすで解説しています。
よくある質問
Jira の課題が変更されたときにスクリプトを実行するには?
自分が管理する URL を指す Jira の Webhook を登録し、その URL で小さな HTTP リスナーを動かします。Jira Cloud は、条件に一致するイベントごとに JSON のペイロードをエンドポイントへ POST します。リスナーはペイロードを解析し、課題キーとイベントタイプを取り出して、atlassian-cli を呼び出して処理します。たとえば atlassian-cli jira issue transition や atlassian-cli jira issue assign です。
Jira の Webhook のペイロードはどのような形ですか?
Jira の課題の Webhook のペイロードは JSON オブジェクトで、webhookEvent フィールド(jira:issue_created や jira:issue_updated など)、イベントを起こした人物の user オブジェクト、キーとフィールドを含む issue オブジェクトを持ちます。更新イベントには、どのフィールドが変わったかを fromString と toString の値とともに列挙する changelog オブジェクトも含まれます。
Jira の Webhook が無限ループを起こすのを防ぐには?
ループは、リスナーが更新イベントに反応して同じ課題に書き戻し、それがまた更新イベントを発火させることで起きます。防ぎ方は2つあります。ペイロードの user.accountId を確認して自動化用アカウントが起こしたイベントを無視すること、そして同じ状態を再処理しても何も変わらないよう操作を冪等にすることです。両方あわせれば、反応が自分自身を呼び続けることはなくなります。
HMAC 署名なしで Jira の Webhook を保護できますか?
管理者が登録した Jira Cloud の Webhook はリクエストを HMAC で署名しないため、現実的なガードは Webhook の URL にクエリパラメーターとして埋め込んだ共有シークレットです。リスナーは、トークンが一致しないリクエストを定数時間比較で拒否します。エンドポイントで TLS を終端し、多層防御として受信トラフィックを Atlassian の送信元レンジに限定するのもよいでしょう。