flyteplugins-jira 2.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flyteplugins/jira/__init__.py +31 -0
- flyteplugins/jira/_provider.py +83 -0
- flyteplugins/jira/events.py +63 -0
- flyteplugins_jira-2.7.0.dist-info/METADATA +91 -0
- flyteplugins_jira-2.7.0.dist-info/RECORD +7 -0
- flyteplugins_jira-2.7.0.dist-info/WHEEL +5 -0
- flyteplugins_jira-2.7.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Jira webhooks for Flyte.
|
|
2
|
+
|
|
3
|
+
Hand a `JiraProvider()` to a `WebhookAppEnvironment` and register handlers with the
|
|
4
|
+
typed constants in `events`. Calling the Jira API is not this plugin's job — use
|
|
5
|
+
the `jira` package from your tasks. See `examples/external_saas_integrations`.
|
|
6
|
+
|
|
7
|
+
Note Jira does not sign its webhooks; see `_provider` for what this plugin does
|
|
8
|
+
instead.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from . import events
|
|
12
|
+
from ._provider import JiraProvider, parse, verify
|
|
13
|
+
|
|
14
|
+
__all__ = ["SAMPLE_DELIVERY", "JiraProvider", "events", "parse", "verify"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _sample_headers(body: bytes, secret: str) -> dict[str, str]:
|
|
18
|
+
# No signature to compute: Jira sends a static shared token.
|
|
19
|
+
return {"X-Webhook-Token": secret}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
#: A real `jira:issue_created` delivery, trimmed to the fields the parser reads.
|
|
23
|
+
SAMPLE_DELIVERY = (
|
|
24
|
+
_sample_headers,
|
|
25
|
+
(
|
|
26
|
+
b'{"webhookEvent": "jira:issue_created", "timestamp": 1700000000000,'
|
|
27
|
+
b' "user": {"displayName": "Bob"},'
|
|
28
|
+
b' "issue": {"key": "PROJ-1", "id": "10001",'
|
|
29
|
+
b' "fields": {"summary": "A bug", "project": {"key": "PROJ"}}}}'
|
|
30
|
+
),
|
|
31
|
+
)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Jira webhook verification and payload normalization.
|
|
2
|
+
|
|
3
|
+
Jira Cloud does **not** sign its webhooks. There is no HMAC to check, so this
|
|
4
|
+
plugin authenticates with a shared token in `X-Webhook-Token` — which something
|
|
5
|
+
in front of the app has to inject, because Jira itself cannot send custom
|
|
6
|
+
headers. `JiraProvider` reports `signed=False` so the dashboard says so plainly rather than
|
|
7
|
+
implying a guarantee that is not there.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import ClassVar, Mapping
|
|
13
|
+
|
|
14
|
+
from flyte.extras.webhooks import (
|
|
15
|
+
Provider,
|
|
16
|
+
WebhookEvent,
|
|
17
|
+
constant_time_equals,
|
|
18
|
+
json_body,
|
|
19
|
+
lower_headers,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def verify(body: bytes, headers: Mapping[str, str], secret: str) -> bool:
|
|
24
|
+
"""Compare the `X-Webhook-Token` header against the shared token."""
|
|
25
|
+
token = lower_headers(headers).get("x-webhook-token")
|
|
26
|
+
if not token:
|
|
27
|
+
return False
|
|
28
|
+
return constant_time_equals(token.strip(), secret)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def parse(headers: Mapping[str, str], body: bytes) -> WebhookEvent:
|
|
32
|
+
"""Normalize a Jira delivery into a `WebhookEvent`."""
|
|
33
|
+
payload = json_body(body)
|
|
34
|
+
issue = payload.get("issue") or {}
|
|
35
|
+
fields = issue.get("fields") or {}
|
|
36
|
+
user = payload.get("user") or {}
|
|
37
|
+
return WebhookEvent(
|
|
38
|
+
provider="jira",
|
|
39
|
+
event_type=payload.get("webhookEvent", "unknown"),
|
|
40
|
+
delivery_id=str(payload.get("timestamp") or ""),
|
|
41
|
+
resource_id=issue.get("key"),
|
|
42
|
+
occurred_at=str(payload.get("timestamp")) if payload.get("timestamp") is not None else None,
|
|
43
|
+
scope=(fields.get("project") or {}).get("key"),
|
|
44
|
+
title=fields.get("summary"),
|
|
45
|
+
actor=user.get("displayName") or user.get("name"),
|
|
46
|
+
payload=payload,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class JiraProvider(Provider):
|
|
51
|
+
"""Jira's webhook provider, with its defaults pre-wired.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from flyte.extras.webhooks import WebhookAppEnvironment
|
|
55
|
+
from flyteplugins.jira import JiraProvider
|
|
56
|
+
|
|
57
|
+
app_env = WebhookAppEnvironment(name="webhooks", providers=[JiraProvider()])
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Jira does not sign its webhooks, so this provider authenticates with a
|
|
61
|
+
shared token instead and reports `signed=False` — which is what makes the
|
|
62
|
+
dashboard say so rather than implying a guarantee that is absent.
|
|
63
|
+
|
|
64
|
+
`WebhookAppEnvironment` mounts `default_secret_env` for you, so it does not
|
|
65
|
+
need naming again in `secrets=`.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
secret_env: Environment variable holding the secret. Pass one only to
|
|
69
|
+
point this provider at a secret stored under a different name;
|
|
70
|
+
otherwise `default_secret_env` applies.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
default_secret_env: ClassVar[str] = "JIRA_WEBHOOK_TOKEN"
|
|
74
|
+
|
|
75
|
+
def __init__(self, *, secret_env: str | None = None) -> None:
|
|
76
|
+
super().__init__(
|
|
77
|
+
name="jira",
|
|
78
|
+
secret_env=secret_env or self.default_secret_env,
|
|
79
|
+
verify=verify,
|
|
80
|
+
parse=parse,
|
|
81
|
+
signed=False,
|
|
82
|
+
setup_hint="Jira Settings -> System -> Webhooks (needs a proxy to inject X-Webhook-Token)",
|
|
83
|
+
)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Jira webhook events, from the payload's `webhookEvent` field.
|
|
2
|
+
|
|
3
|
+
Some names take a `jira:` prefix and some do not — that inconsistency is Jira's.
|
|
4
|
+
These constants carry the exact wire values so you need not remember which.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from flyte.extras.webhooks import EventType
|
|
10
|
+
|
|
11
|
+
__all__ = ["Comment", "Issue", "Project", "Sprint", "Version", "Worklog"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Issue(EventType):
|
|
15
|
+
"""Issue events. Note the `jira:` prefix, which comment events lack."""
|
|
16
|
+
|
|
17
|
+
CREATED = "jira:issue_created"
|
|
18
|
+
UPDATED = "jira:issue_updated"
|
|
19
|
+
DELETED = "jira:issue_deleted"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Comment(EventType):
|
|
23
|
+
"""Comment events. These carry no `jira:` prefix."""
|
|
24
|
+
|
|
25
|
+
CREATED = "comment_created"
|
|
26
|
+
UPDATED = "comment_updated"
|
|
27
|
+
DELETED = "comment_deleted"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Worklog(EventType):
|
|
31
|
+
"""Worklog events."""
|
|
32
|
+
|
|
33
|
+
CREATED = "worklog_created"
|
|
34
|
+
UPDATED = "worklog_updated"
|
|
35
|
+
DELETED = "worklog_deleted"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Project(EventType):
|
|
39
|
+
"""Project events."""
|
|
40
|
+
|
|
41
|
+
CREATED = "project_created"
|
|
42
|
+
UPDATED = "project_updated"
|
|
43
|
+
DELETED = "project_deleted"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Version(EventType):
|
|
47
|
+
"""Version (release) events."""
|
|
48
|
+
|
|
49
|
+
CREATED = "jira:version_created"
|
|
50
|
+
UPDATED = "jira:version_updated"
|
|
51
|
+
RELEASED = "jira:version_released"
|
|
52
|
+
UNRELEASED = "jira:version_unreleased"
|
|
53
|
+
DELETED = "jira:version_deleted"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Sprint(EventType):
|
|
57
|
+
"""Sprint events (Jira Software)."""
|
|
58
|
+
|
|
59
|
+
CREATED = "sprint_created"
|
|
60
|
+
UPDATED = "sprint_updated"
|
|
61
|
+
STARTED = "sprint_started"
|
|
62
|
+
CLOSED = "sprint_closed"
|
|
63
|
+
DELETED = "sprint_deleted"
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flyteplugins-jira
|
|
3
|
+
Version: 2.7.0
|
|
4
|
+
Summary: Receive Jira webhooks in Flyte.
|
|
5
|
+
Author: Flyte Contributors
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: flyte
|
|
9
|
+
Provides-Extra: app
|
|
10
|
+
Requires-Dist: fastapi>=0.115; extra == "app"
|
|
11
|
+
Requires-Dist: uvicorn>=0.30; extra == "app"
|
|
12
|
+
|
|
13
|
+
# flyteplugins-jira
|
|
14
|
+
|
|
15
|
+
Receive Jira webhooks in Flyte.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install "flyteplugins-jira[app]"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Using it
|
|
22
|
+
|
|
23
|
+
Hand a `JiraProvider()` to a `WebhookAppEnvironment` and register handlers with the
|
|
24
|
+
typed constants in `events`:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import flyte
|
|
28
|
+
from flyte.extras.webhooks import WebhookAppEnvironment, run_once
|
|
29
|
+
from flyteplugins.jira import JiraProvider, events
|
|
30
|
+
|
|
31
|
+
# JiraProvider.default_secret_env is mounted for you.
|
|
32
|
+
app_env = WebhookAppEnvironment(name="jira-webhooks", providers=[JiraProvider()])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app_env.on_event(events.Issue.CREATED)
|
|
36
|
+
async def handle(event):
|
|
37
|
+
import flyte.remote as remote
|
|
38
|
+
|
|
39
|
+
task = remote.Task.get(name="my-env.my_task", auto_version="latest")
|
|
40
|
+
result = await run_once.aio(task, key=event.dedupe_key(), resource=event.resource_id)
|
|
41
|
+
if not result.created:
|
|
42
|
+
return {"skipped": result.run.name, "url": result.run.url}
|
|
43
|
+
return {"run": result.run.name}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
flyte.serve(app_env)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Handlers must `await run_once.aio(...)`. The blocking form stalls the
|
|
50
|
+
app's event loop, and Jira times deliveries out in seconds.
|
|
51
|
+
|
|
52
|
+
One app can serve several products at once — hand it one provider per product.
|
|
53
|
+
|
|
54
|
+
## Try it
|
|
55
|
+
|
|
56
|
+
`examples/jira_webhooks.py` runs two ways. The first needs no Jira account:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
python examples/jira_webhooks.py --local # replay a real sample delivery in-process
|
|
60
|
+
python examples/jira_webhooks.py # deploy the receiver to Flyte
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`--local` posts this plugin's `SAMPLE_DELIVERY` through the app with FastAPI's
|
|
64
|
+
test client, so you see a delivery verified, normalized, and dispatched — plus
|
|
65
|
+
an unsigned one refused with a 401, and the same delivery replayed to show the
|
|
66
|
+
dedupe key is stable.
|
|
67
|
+
|
|
68
|
+
## Setup
|
|
69
|
+
|
|
70
|
+
1. Store the secret and mount it on the app:
|
|
71
|
+
```bash
|
|
72
|
+
flyte create secret JIRA_WEBHOOK_TOKEN --value <secret>
|
|
73
|
+
```
|
|
74
|
+
2. Point Jira at `<app-url>/webhook/jira`, from
|
|
75
|
+
Jira Settings → System → Webhooks.
|
|
76
|
+
|
|
77
|
+
**Verification:** **None.** Jira Cloud does not sign its webhooks.
|
|
78
|
+
|
|
79
|
+
Because there is no signature, this plugin authenticates with a shared token in `X-Webhook-Token` — which something in front of the app has to inject, since Jira cannot send custom headers. `JiraProvider` reports `signed=False`, so the dashboard says the product does not sign rather than implying a guarantee that is absent. A shared token also cannot detect body tampering, only that the sender knew the token.
|
|
80
|
+
|
|
81
|
+
## Event constants
|
|
82
|
+
|
|
83
|
+
`events` spells every event this plugin can dispatch, as `str` enums grouped by
|
|
84
|
+
event type, so a typo fails at import rather than by silently never matching.
|
|
85
|
+
Raw strings still work, for events the constants do not cover yet.
|
|
86
|
+
|
|
87
|
+
## What this plugin does not do
|
|
88
|
+
|
|
89
|
+
Call the Jira API. Use the `jira` package directly from your tasks — see
|
|
90
|
+
`examples/external_saas_integrations`. This plugin owns only the part that is
|
|
91
|
+
Flyte's: authenticating an inbound delivery and turning it into a run.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
flyteplugins/jira/__init__.py,sha256=1BB7HbHLPOFNSOK3GcOe3V2XOQ4GRaYTRj_-lkjMBXo,1077
|
|
2
|
+
flyteplugins/jira/_provider.py,sha256=3sLCzIZDrIP7Xfts1IKp7PwKVN7V3D3x-Ut5B_2TKmk,3002
|
|
3
|
+
flyteplugins/jira/events.py,sha256=t5pJMq_0tVahp71i7qLK-CjHVNAWqM5NGhBJGqSoBzo,1557
|
|
4
|
+
flyteplugins_jira-2.7.0.dist-info/METADATA,sha256=_SuYYTzQ34v2lG8gaUOXlx8cNF7ck1VmeYRhNGM6LwQ,3203
|
|
5
|
+
flyteplugins_jira-2.7.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
6
|
+
flyteplugins_jira-2.7.0.dist-info/top_level.txt,sha256=cgd779rPu9EsvdtuYgUxNHHgElaQvPn74KhB5XSeMBE,13
|
|
7
|
+
flyteplugins_jira-2.7.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
flyteplugins
|