import base64, json, os, sys, urllib.parse, urllib.request, urllib.error import re from datetime import datetime, timezone def _cfg(): return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) def _is_cloud(): return bool(str(_cfg().get("cloud_id") or "").strip()) def _api(): return "3" if _is_cloud() else "2" def _base(): cfg = _cfg() root = str(cfg.get("server_url") or "https://api.atlassian.com/ex/jira").rstrip("/") cloud_id = str(cfg.get("cloud_id") or "").strip() return root + "/" + cloud_id if cloud_id else root def _auth(): cfg = _cfg() pat = str(cfg.get("pat") or "") if pat: return "Bearer " + pat cred = (str(cfg.get("username") or "") + ":" + str(cfg.get("api_key") or "")).encode("utf-8") return "Basic " + base64.b64encode(cred).decode("ascii") def request(method, path, params=None, body=None, full_url=None, raw=False): url = full_url or (_base() + path) q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} if q: url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) data = json.dumps(body).encode("utf-8") if body is not None else None headers = {"Accept": "application/json", "Authorization": _auth()} if data is not None: headers["Content-Type"] = "application/json" req = urllib.request.Request(url, data=data, headers=headers, method=method) with urllib.request.urlopen(req, timeout=90) as r: content = r.read() if raw: return content return json.loads(content) if content else {} def issue_ref(inputs): ref = str(inputs.get("issue_id") or "") or str(inputs.get("issue_key") or "") if not ref: raise Exception("issue_id or issue_key is required") return urllib.parse.quote(ref, safe="") def adf(text): # Jira Cloud (API v3) expects Atlassian Document Format for rich text; On-Prem (v2) takes plain text. if not _is_cloud(): return text return {"type": "doc", "version": 1, "content": [{"type": "paragraph", "content": [{"type": "text", "text": text}]}]} def flatten_issue(issue): f = issue.get("fields") or {} rendered = issue.get("renderedFields") or {} def user(u): u = u or {} return u.get("displayName") or u.get("name") or u.get("emailAddress") or "" desc = rendered.get("description") or "" if desc: desc = re.sub(r"<[^>]+>", "", desc) else: d = f.get("description") desc = d if isinstance(d, str) else "" return { "id": issue.get("id") or "", "key": issue.get("key") or "", "summary": f.get("summary") or "", "description": desc, "status": (f.get("status") or {}).get("name") or "", "resolution": (f.get("resolution") or {}).get("name") or "", "priority": (f.get("priority") or {}).get("name") or "", "project": (f.get("project") or {}).get("name") or "", "issue_type": (f.get("issuetype") or {}).get("name") or "", "created": f.get("created") or "", "updated": f.get("updated") or "", "due_date": f.get("duedate") or "", "labels": f.get("labels") or [], "assignee": user(f.get("assignee")), "reporter": user(f.get("reporter")), "creator": user(f.get("creator")), "attachments": [{"id": a.get("id"), "filename": a.get("filename"), "size": a.get("size")} for a in (f.get("attachment") or [])], "url": issue.get("self") or "", } def search_issues(jql, max_results, next_page_token=None, start_at=None): params = {"jql": jql, "maxResults": max_results, "fields": "*all", "expand": "renderedFields,names"} if _is_cloud() and not start_at: if next_page_token: params["nextPageToken"] = next_page_token return request("GET", "/rest/api/3/search/jql", params=params) if start_at: params["startAt"] = start_at return request("GET", "/rest/api/" + _api() + "/search", params=params) def to_jira_time(v): s = str(v or "").strip() if not s: return None if s.isdigit(): ts = int(s) if ts > 10 ** 12: ts = ts // 1000 return datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%Y-%m-%d %H:%M") try: dt = datetime.fromisoformat(s.replace("Z", "+00:00")) if dt.tzinfo is not None: dt = dt.astimezone(timezone.utc) return dt.strftime("%Y-%m-%d %H:%M") except Exception: return s def main(): inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) query = str(inputs.get("query") or "").strip() max_results = int(inputs.get("max") or 50) watermark = to_jira_time(inputs.get("after")) jql = query if watermark: clause = 'created >= "' + watermark + '"' jql = (jql + " AND " + clause) if jql else clause jql = jql + " ORDER BY created ASC" res = search_issues(jql, max_results) print(json.dumps({"result": [flatten_issue(i) for i in res.get("issues", [])]})) try: main() except urllib.error.HTTPError as e: print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) sys.exit(1) except Exception as e: print(json.dumps({"error": str(e)})) sys.exit(1)