Skip to content

Commit ce4a943

Browse files
committed
docs(jira): add an end-to-end Testing guide to the README
A step-by-step pass a human can follow against a real account: create the credentials, verify the client standalone, deploy the task the receiver launches, run it directly, deploy the app, wire the provider up, trigger a real event, and confirm idempotency. Ends with a troubleshooting table mapping each failure mode to its cause. Ordered so each step fails in isolation: the client is exercised before the platform, and the launched task is deployed before the app that looks it up. Also adds the `triage_issue` task the webhook example launches. It was looked up by name but defined nowhere, so the example could not work as written. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VKZrTNjjWVzTZUxDFbn4Nk Signed-off-by: Niels Bantilan <niels.bantilan@gmail.com>
1 parent 88b8e9b commit ce4a943

2 files changed

Lines changed: 131 additions & 0 deletions

File tree

plugins/jira/README.md

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,121 @@ agent = Agent(
183183
path, timeouts, and retries. The module exports `default_config`; pass a custom
184184
`Config` to `JiraClient`, `build_mcp_server`, or the app environment when you
185185
need it.
186+
187+
## Testing
188+
189+
An end-to-end pass against a real Jira Cloud site. Use a scratch project —
190+
step 4 creates and comments on real issues.
191+
192+
**1. Create the credentials.** An API token from
193+
<https://id.atlassian.com/manage-profile/security/api-tokens>. Jira uses basic
194+
auth over your account email plus the token, so all three values are secrets:
195+
196+
```bash
197+
flyte create secret JIRA_BASE_URL --value https://<your-site>.atlassian.net
198+
flyte create secret JIRA_EMAIL --value you@example.com
199+
flyte create secret JIRA_API_TOKEN --value <api-token>
200+
flyte create secret JIRA_WEBHOOK_TOKEN --value <random-string>
201+
```
202+
203+
`JIRA_WEBHOOK_TOKEN` is a value you invent. Jira webhooks are **not signed**,
204+
so the receiver authenticates them with this shared token instead — see step 6.
205+
206+
**2. Check the client works before involving the platform:**
207+
208+
```bash
209+
export JIRA_BASE_URL=https://<your-site>.atlassian.net
210+
export JIRA_EMAIL=you@example.com
211+
export JIRA_API_TOKEN=<api-token>
212+
python -c "
213+
from flyteplugins.jira import JiraClient
214+
with JiraClient() as c:
215+
print(c.get_myself()['displayName'])
216+
print([p['key'] for p in c.list_projects()])
217+
"
218+
```
219+
220+
A 401 here is almost always the email/token pair rather than the token alone.
221+
222+
**3. Deploy the task the webhook will launch.**
223+
224+
```bash
225+
flyte deploy plugins/jira/examples/manage_ticket.py env
226+
```
227+
228+
`react_to_jira_events.py` looks this task up by name (`triage_issue`), so it
229+
has to exist before the app can launch it.
230+
231+
**4. Run a task directly**, to confirm writes land before any webhook is
232+
involved:
233+
234+
```bash
235+
flyte run plugins/jira/examples/manage_ticket.py open_ticket \
236+
--project_key <PROJ> --summary "Flyte test issue" --description "created by the plugin test"
237+
```
238+
239+
**5. Deploy the webhook app.**
240+
241+
```bash
242+
python plugins/jira/examples/react_to_jira_events.py
243+
```
244+
245+
It prints the app URL. Open it: the dashboard should show all four secrets
246+
mounted, and *Verify Jira credentials* should return your display name.
247+
248+
**6. Point Jira at the app.** Jira Settings → System → Webhooks → *Create a
249+
Webhook*:
250+
251+
- URL: `<app-url>/webhook`
252+
- Events: *Issue created* and *Issue updated*
253+
254+
Jira cannot add custom headers, and it does not sign its webhooks — so the
255+
`X-Webhook-Token` header the receiver requires has to be injected by whatever
256+
sits in front of the app (an API gateway, a reverse proxy, or a small
257+
forwarder). For a first local test, deploy the app with
258+
`require_webhook_token=False` and protect it at the network level instead,
259+
then send a delivery by hand to confirm the path works:
260+
261+
```bash
262+
curl -X POST <app-url>/webhook \
263+
-H 'Content-Type: application/json' \
264+
-H 'X-Webhook-Token: <the value you chose>' \
265+
-d '{"webhookEvent":"jira:issue_created","issue":{"key":"PROJ-1","fields":{"summary":"hand-made","project":{"key":"PROJ"}}}}'
266+
```
267+
268+
**7. Trigger a real event.** Create an issue in the test project. Then check,
269+
in order:
270+
271+
- `<app-url>/api/events` — the normalized event, `qualified_type` of
272+
`jira:issue_created`.
273+
- `flyte get runs` — a run whose `dedupe` label matches.
274+
- The issue — the triage task's comment.
275+
276+
**8. Confirm idempotency.** The dedupe key folds in Jira's event timestamp, so
277+
a redelivered event dedupes while a later update to the same issue gets its own
278+
run.
279+
280+
**9. Optional — the allowlist.** Redeploy with `project_keys=["<PROJ>"]` and
281+
create an issue in another project. The receiver should answer 200 with a
282+
`skipped` message. The allowlist fails closed, so an event carrying no project
283+
key is skipped too.
284+
285+
**10. Optional — the MCP server.**
286+
287+
```bash
288+
python plugins/jira/examples/jira_mcp_server.py
289+
claude mcp add --transport http jira-mcp <app-url>/mcp/mcp
290+
```
291+
292+
Ask an agent to summarize open bugs in the project. The default surface is
293+
read-only.
294+
295+
### Troubleshooting
296+
297+
| Symptom | Cause |
298+
| --- | --- |
299+
| Delivery returns 401 | The `X-Webhook-Token` header is missing or does not match `JIRA_WEBHOOK_TOKEN`. Jira alone cannot send it — see step 6. |
300+
| Delivery returns 503 | `JIRA_WEBHOOK_TOKEN` is not mounted; check `/api/status`. |
301+
| Client raises 401 | Basic auth needs the *email + token* pair, not the token alone. |
302+
| A description shows as `[object Object]` | Jira v3 stores rich text as ADF; the client converts plain strings for you, so pass a plain string. |
303+
| 200 but no run | No handler matched, or the allowlist skipped it — the response body says which. |

plugins/jira/examples/manage_ticket.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,19 @@ async def summarize_open_bugs(project_key: str) -> str:
6161
return "\n".join(f"{i['key']} [{i['priority']}] {i['summary']}" for i in issues[:20])
6262

6363

64+
@env.task
65+
async def triage_issue(issue_key: str) -> str:
66+
"""Comment on a newly created issue.
67+
68+
This is the task `react_to_jira_events.py` launches for every
69+
`jira:issue_created` event.
70+
"""
71+
async with JiraClient() as client:
72+
issue = await client.get_issue.aio(issue_key)
73+
await client.add_comment.aio(issue_key, f"Flyte triaged this issue (status: {issue.get('status')}).")
74+
return f"triaged {issue_key}"
75+
76+
6477
if __name__ == "__main__":
6578
# Replace with a project key you can access.
6679
flyte.run(summarize_open_bugs, project_key="PROJ")

0 commit comments

Comments
 (0)