Jira
Receive Jira Cloud webhooks in Flyte and turn them into runs.
Jira Cloud doesn’t sign its webhooks. This plugin authenticates deliveries with a shared token instead, which requires a proxy or a Jira Automation rule to add the token. Read Authentication before you expose the route.
Installation
pip install "flyteplugins-jira[app]"Requires Python 3.10 or later. The app extra adds fastapi and uvicorn for serving the receiver.
Authentication
The provider checks for a shared token in the X-Webhook-Token header, using a constant-time comparison. JiraProvider reports signed=False, and the setup dashboard shows that the route isn’t signature-verified.
A shared token is weaker than a signature:
- The same token is sent with every request. Anyone who obtains it can forge deliveries.
- It doesn’t cover the body. Anything between Jira and the app can change the payload undetected.
Jira webhooks can’t send custom headers, so you need one of these to add X-Webhook-Token:
- An API gateway or ingress rule in front of the app that adds the header to requests from Jira’s IP ranges.
- A Jira Automation rule that uses the Send web request action, which can set custom headers. This is the simpler option if your events are available as Automation triggers.
Without the header, the app rejects every delivery, because require_signature defaults to True.
Setting require_signature=False makes the route launch runs for any request. Use it only for local development.
Set a narrow scopes allowlist as well.
The receiver
import flyte
from flyte.extras.webhooks import WebhookAppEnvironment, WebhookEvent, run_once
from flyteplugins.jira import JiraProvider, events
# JIRA_WEBHOOK_TOKEN is mounted automatically. It's a shared token, sent with
# every request: anyone who has it can post a delivery. Restrict `scopes` to
# the projects the app should act on.
app_env = WebhookAppEnvironment(
name="jira-webhooks",
providers=[JiraProvider()],
# Jira project keys.
scopes=["PROJ"],
image=flyte.Image.from_debian_base().with_pip_packages("flyteplugins-jira[app]"),
resources=flyte.Resources(cpu=1, memory="512Mi"),
)
The provider reads the token from JIRA_WEBHOOK_TOKEN, which the app mounts automatically. Jira doesn’t issue this token, so generate one yourself:
flyte create secret jira-webhook-token --value "$(openssl rand -hex 32)"@app_env.on_event(events.Issue.CREATED)
async def on_issue_created(event: WebhookEvent) -> dict:
"""Launch triage once per new issue.
`event.resource_id` is the issue key, such as `PROJ-1`.
"""
import flyte.remote as remote
task = remote.Task.get(name="jira-ops.triage_issue", auto_version="latest")
result = await run_once.aio(
task,
key=event.dedupe_key(),
issue_key=event.resource_id,
)
return {"run": result.run.name, "created": result.created}
Set up the webhook in Jira
Go to Settings → System → Webhooks → Create a WebHook and set:
- URL: the
/webhook/jiraURL from the app’s dashboard. - Events: the events your handlers match. Optionally, add a JQL filter.
Then configure your proxy or Automation rule to add X-Webhook-Token.
Events
Constants live in flyteplugins.jira.events. Jira prefixes some event names (jira:issue_created) but not others (comment_created); the constants handle both.
| Class | Members |
|---|---|
Issue |
CREATED, UPDATED, DELETED |
Comment |
CREATED, UPDATED, DELETED |
Worklog |
CREATED, UPDATED, DELETED |
Project |
CREATED, UPDATED, DELETED |
Version |
CREATED, UPDATED, RELEASED, UNRELEASED, DELETED |
Sprint |
CREATED, UPDATED, STARTED, CLOSED, DELETED |
Scope and deduplication
scope is the project key, such as PROJ. resource_id is the issue key, such as PROJ-1. occurred_at is Jira’s timestamp field.
The task it launches
env = flyte.TaskEnvironment(
name="jira-ops",
image=flyte.Image.from_debian_base().with_pip_packages("jira"),
secrets=[
flyte.Secret(key="jira-url", as_env_var="JIRA_URL"),
flyte.Secret(key="jira-email", as_env_var="JIRA_EMAIL"),
flyte.Secret(key="jira-api-token", as_env_var="JIRA_API_TOKEN"),
],
resources=flyte.Resources(cpu=1, memory="512Mi"),
)
@env.task
async def triage_issue(issue_key: str) -> str:
"""Comment on an issue and move it to In Progress.
Transition IDs differ between workflows, so this looks up the transition
by name instead of hard-coding an ID.
"""
import asyncio
import os
from jira import JIRA
def _work() -> str:
client = JIRA(
server=os.environ["JIRA_URL"],
basic_auth=(os.environ["JIRA_EMAIL"], os.environ["JIRA_API_TOKEN"]),
)
issue = client.issue(issue_key)
client.add_comment(issue, "Triaged by Flyte.")
for transition in client.transitions(issue):
if transition["name"].lower() == "in progress":
client.transition_issue(issue, transition["id"])
return transition["name"]
return "no matching transition"
# The `jira` client is synchronous; run it off the event loop.
return await asyncio.to_thread(_work)
The example looks up transitions by name rather than by ID. Transition IDs differ between workflows, so a hard-coded ID breaks when someone edits the project’s workflow.
The jira client is synchronous, so the example calls it through asyncio.to_thread to avoid blocking the event loop.
Authenticate with an Atlassian account email and an API token, using HTTP basic auth.
Test without a Jira site
# A separate environment with no secrets, so the replay runs before any
# secret is created.
replay_env = flyte.TaskEnvironment(
name="jira-replay",
image=flyte.Image.from_debian_base().with_pip_packages("flyteplugins-jira"),
resources=flyte.Resources(cpu=1, memory="512Mi"),
)
@replay_env.task
async def replay_sample_delivery() -> dict[str, str]:
"""Verify and parse the sample delivery bundled with the plugin.
Jira doesn't sign deliveries, so `verify` compares a shared token in the
`X-Webhook-Token` header. The sample's sign function only sets that header.
"""
import flyteplugins.jira as plugin
secret = "a-shared-webhook-token"
sign, body = plugin.SAMPLE_DELIVERY
headers = sign(body, secret)
assert plugin.verify(body, headers, secret), "the right token must verify"
assert not plugin.verify(body, headers, "wrong-token"), "a wrong token must not"
event = plugin.parse(headers, body)
return {
# For example, `jira:issue_created`. Some Jira event names have the
# `jira:` prefix and some don't.
"qualified_type": event.qualified_type,
"scope": event.scope or "",
"resource_id": event.resource_id or "",
"title": event.title or "",
"dedupe_key": event.dedupe_key(),
# False for Jira. The setup dashboard shows this too.
"provider_signs_deliveries": str(plugin.JiraProvider().signed),
}
flyte run --local jira_tasks.py replay_sample_deliveryIn this replay, verify compares the shared token; there’s no signature to check. The output’s provider_signs_deliveries is False.
Examples
Both files are in v2/integrations/flyte-plugins/jira:
jira_webhooks.py: the receiver and an issue-created handler.jira_tasks.py: commenting, transitioning, and the offline replay.
See also
- Software development tools for the event model,
run_once, and scopes. - Event-driven automation for the pattern across Linear, Jira, and ClickUp.
- Jira API reference.