-
Notifications
You must be signed in to change notification settings - Fork 7
PM-327: add the linked PR as a Jira issue web link #203
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d8c0a1a
4019a2d
339fab0
020fdf3
67b5b44
c481f31
a360350
4edc3d3
a6758fd
72ed316
064f831
15971d5
08da9ce
fb6b6f6
88469da
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,7 @@ | |
| apply_jira_labels_to_pr | ||
| jira_status_transition | ||
| add_comment_to_jira | ||
| add_pr_weblink_to_jira | ||
|
|
||
| The orchestrator functions and CLI dispatcher live in jira_sync_logic.py | ||
| which imports from this module. | ||
|
|
@@ -644,7 +645,7 @@ def remove_label_from_jira_issue(jira_keys_json: str, label: str, jira_auth: str | |
| _DETAIL_DELIM = ";" | ||
|
|
||
|
|
||
| def _jira_get(url: str, jira_auth: str) -> dict | None: | ||
| def _jira_get(url: str, jira_auth: str) -> dict | list | None: | ||
| """GET JSON from a Jira REST endpoint. Returns parsed JSON or None on failure.""" | ||
| encoded_auth = base64.b64encode(jira_auth.encode()).decode() | ||
|
|
||
|
|
@@ -1319,6 +1320,97 @@ def add_comment_to_jira( | |
| print(f"WARNING: {failed} comment(s) failed. Continuing.") | ||
|
|
||
|
|
||
| def _normalize_url(url: str) -> str: | ||
| """Normalize URL for stable comparisons.""" | ||
| if not url: | ||
| return "" | ||
| return url.strip().rstrip("/") | ||
|
|
||
|
|
||
| def add_pr_weblink_to_jira( | ||
| jira_keys_json: str, | ||
| pr_title: str, | ||
| pr_url: str, | ||
| jira_auth: str, | ||
| ) -> None: | ||
| """Ensure each Jira issue has a remote link to the PR URL (idempotent).""" | ||
| if not jira_auth: | ||
| print("Error: jira_auth is not set or empty.") | ||
| sys.exit(1) | ||
|
|
||
| keys = _parse_jira_keys_json(jira_keys_json) | ||
| if not keys: | ||
| print("No Jira keys to sync web links for.") | ||
| return | ||
|
|
||
| normalized_pr_url = _normalize_url(pr_url) | ||
| if not normalized_pr_url: | ||
| print("PR URL is empty; skipping Jira web-link sync.") | ||
| return | ||
|
|
||
| stripped_title = pr_title.strip() if pr_title else "" | ||
| link_title = stripped_title or normalized_pr_url | ||
| payload = { | ||
| "object": { | ||
| "url": normalized_pr_url, | ||
| "title": link_title, | ||
| } | ||
| } | ||
|
|
||
| ok = 0 | ||
| skipped = 0 | ||
| failed = 0 | ||
|
|
||
| for key in keys: | ||
| get_url = f"{JIRA_BASE_URL}/rest/api/3/issue/{key}/remotelink" | ||
| print(f"Fetching existing remote links for {key} ...") | ||
| existing = _jira_get(get_url, jira_auth) | ||
| if existing is None: | ||
| print(f"SKIP {key}: failed to fetch existing remote links.") | ||
| skipped += 1 | ||
| continue | ||
| if not isinstance(existing, list): | ||
| print(f"SKIP {key}: unexpected remotelink response format.") | ||
| skipped += 1 | ||
| continue | ||
|
|
||
| has_pr_link = False | ||
| for item in existing: | ||
| if not isinstance(item, dict): | ||
| continue | ||
| obj = item.get("object") | ||
| if not isinstance(obj, dict): | ||
| continue | ||
| existing_url = _normalize_url(obj.get("url", "")) | ||
| if existing_url and existing_url == normalized_pr_url: | ||
| has_pr_link = True | ||
| break | ||
|
|
||
| if has_pr_link: | ||
| print(f"SKIP {key}: PR web link already exists.") | ||
| skipped += 1 | ||
| continue | ||
|
|
||
| post_url = f"{JIRA_BASE_URL}/rest/api/3/issue/{key}/remotelink" | ||
| code, body_text = _jira_post(post_url, payload, jira_auth) | ||
| if code in (200, 201): | ||
|
Comment on lines
+1353
to
+1396
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- target context ---'
sed -n '1300,1425p' scripts/jira_sync_modules.py
printf '%s\n' '--- remote-link helpers and call sites ---'
rg -n -C 3 '_jira_(get|post)|globalId|remotelink|remote link' scripts/jira_sync_modules.py
printf '%s\n' '--- repository status ---'
git status --shortRepository: scylladb/github-automation Length of output: 7259 🏁 Script executed: #!/bin/bash
set -e
sed -n '1300,1425p' scripts/jira_sync_modules.py
rg -n -C 3 '_jira_(get|post)|globalId|remotelink|remote link' scripts/jira_sync_modules.py
git status --shortRepository: scylladb/github-automation Length of output: 7167 🌐 Web query:
💡 Result: In the Jira Cloud REST API v3, using the POST method on the /rest/api/3/issue/{issueIdOrKey}/remotelink endpoint provides an "upsert" (create or update) behavior based on the globalId [1][2]. When you send a request to create a remote link: 1. If a globalId is provided in the request body, Jira checks if a remote link with that same globalId already exists for the specified issue [1][3]. 2. If the globalId is found, the existing remote link is updated with the values provided in the request [1][4]. 3. If the globalId is not found, a new remote link is created [1][3]. Important considerations for this update behavior: - Field Overwriting: When an update occurs, the API replaces the existing fields with the new values provided in the request [1][5]. Crucially, any fields omitted from your request body will be set to null [1][3][4]. Therefore, ensure your request includes the full object structure you wish to persist to avoid accidental data loss [5][6]. - URL Encoding: If your globalId contains reserved URL characters, you must ensure it is correctly encoded for the request [1][2]. - Feature Requirement: This operation requires the "Issue Linking" feature to be active in your Jira instance [1][3]. If you wish to update a remote link without potentially triggering a "create" action or if you have the internal Jira link ID (rather than the globalId), you can alternatively use the PUT /rest/api/3/issue/{issueIdOrKey}/remotelink/{linkId} endpoint, which specifically performs an update on a known link [3][4]. Citations:
🌐 Web query:
💡 Result: In software systems and APIs, a global ID (or globalId) is a unique identifier used to reference an entity across different systems or datasets, distinguishing it from local IDs that are only unique within a single database or application instance [1]. When a documentation or API specification states that "if a global ID is provided," it typically describes conditional logic for how the system handles a request [1]. A common pattern, as seen in the Jira Cloud platform REST API, involves the following behaviors [1]: 1. Retrieval: If a global ID is provided in a request, the system retrieves the specific record associated with that identifier; if omitted, the system may return a collection of all available records instead [1]. 2. Upsert (Update or Create): In some API operations, providing a global ID serves as a signal to update an existing record if it exists, or to create a new one if it does not [1]. 3. Handling/Escaping: Because global IDs often contain complex strings (such as URLs or system-specific paths), technical documentation frequently notes that these values must be properly escaped if they include reserved characters before being sent in a request URL [1]. In essence, the global ID acts as a primary, system-agnostic key that allows external services to perform deterministic operations on entities without needing to know the internal database ID assigned by the host system [1]. Citations: 🏁 Script executed: #!/bin/bash
set -e
python3 - <<'PY'
from urllib.request import urlopen
from re import finditer
urls = [
"https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-remote-links/",
"https://dac-static.atlassian.com/cloud/jira/platform/swagger-v3.v3.json",
]
for url in urls:
print(f"--- {url} ---")
try:
text = urlopen(url, timeout=20).read().decode("utf-8", "replace")
except Exception as exc:
print(type(exc).__name__, exc)
continue
for needle in ("globalId", "Create or update remote issue link", "If a global ID"):
positions = [m.start() for m in finditer(needle, text, flags=0)]
print(f"{needle}: {len(positions)} matches")
for pos in positions[:3]:
print(text[max(0, pos-500):pos+1000].replace("\n", " ")[:1600])
PYRepository: scylladb/github-automation Length of output: 630 🏁 Script executed: #!/bin/bash
set -e
for url in \
"https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-remote-links/" \
"https://dac-static.atlassian.com/cloud/jira/platform/swagger-v3.v3.json"
do
echo "--- $url ---"
curl -k -L --max-time 30 -sS "$url" |
grep -oEi '.{0,500}(globalId|create or update remote issue link|if a global ID).{0,1200}' |
head -c 12000 || true
echo
doneRepository: scylladb/github-automation Length of output: 24444 Add a stable 🤖 Prompt for AI Agents |
||
| print(f"OK {key} ({code}) web link added.") | ||
| ok += 1 | ||
| elif code == 404: | ||
| print(f"SKIP {key} ({code}) issue not found or no permission. Continuing.") | ||
| skipped += 1 | ||
| else: | ||
| print(f"FAIL {key} ({code}) First 400 chars:") | ||
| print(body_text[:400]) | ||
| failed += 1 | ||
|
|
||
| time.sleep(0.2) | ||
|
|
||
| print(f"Web-link sync summary: ok={ok} skipped={skipped} failed={failed}") | ||
| if failed > 0: | ||
| print(f"WARNING: {failed} web-link update(s) failed. Continuing.") | ||
|
|
||
|
|
||
|
Comment on lines
+1323
to
+1413
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- _jira_get definition and callers ---'
rg -n -A35 -B10 'def _jira_get|_jira_get\(' scripts/jira_sync_modules.py
printf '%s\n' '--- urlopen usage in the module ---'
rg -n -A8 -B5 'urlopen|Request\(' scripts/jira_sync_modules.py
printf '%s\n' '--- synchronization entry points and sequencing ---'
rg -n -A25 -B15 'add_pr_weblink_to_jira|sync|event|flow|main\(' scripts/jira_sync_modules.pyRepository: scylladb/github-automation Length of output: 32525 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- candidate orchestration files ---'
fd -t f -i 'jira_sync_logic|jira_sync_modules|jira.*yml|jira.*yaml' .
printf '%s\n' '--- orchestration symbols and relevant call order ---'
rg -n -A18 -B12 'extract_jira_issue_details|add_pr_weblink_to_jira|_filter_out_excluded_issue_types|jira_status_transition|add_comment_to_jira|apply_jira_labels_to_pr' .
printf '%s\n' '--- helper source ranges ---'
cat -n scripts/jira_sync_modules.py | sed -n '95,160p;640,665p;1018,1045p'Repository: scylladb/github-automation Length of output: 50382 🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
import ast
import socket
from pathlib import Path
path = Path("scripts/jira_sync_modules.py")
tree = ast.parse(path.read_text())
def calls_named(node, name):
return [
n for n in ast.walk(node)
if isinstance(n, ast.Call)
and ((isinstance(n.func, ast.Name) and n.func.id == name)
or (isinstance(n.func, ast.Attribute) and n.func.attr == name))
]
jira_get = next(n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name == "_jira_get")
calls = calls_named(jira_get, "urlopen")
print("jira_get_urlopen_calls:", len(calls))
for call in calls:
print("line", call.lineno, "timeout_keyword:",
next((kw.value.value for kw in call.keywords
if kw.arg == "timeout" and isinstance(kw.value, ast.Constant)), "<absent>"))
print("socket_default_timeout:", socket.getdefaulttimeout())
print("module_sets_global_socket_timeout:",
any(isinstance(n, ast.Call)
and isinstance(n.func, ast.Attribute)
and isinstance(n.func.value, ast.Name)
and n.func.value.id == "socket"
and n.func.attr == "setdefaulttimeout"
for n in ast.walk(tree)))
for name in ("_jira_get", "_jira_post", "_jira_put"):
fn = next(n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name == name)
calls = calls_named(fn, "urlopen")
print(name, "urlopen_timeout_keywords:",
[kw.arg for call in calls for kw in call.keywords if kw.arg == "timeout"])
PYRepository: scylladb/github-automation Length of output: 413 Set a finite timeout on Jira HTTP requests.
🤖 Prompt for AI Agents |
||
| def get_done_issue_keys(details_csv: str) -> set[str]: | ||
| """Parse the details CSV and return issue keys whose status is in a closed/done state. | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: scylladb/github-automation
Length of output: 6278
🏁 Script executed:
Repository: scylladb/github-automation
Length of output: 190
Handle malformed JSON responses.
If Jira returns malformed JSON,
json.loadsraisesjson.JSONDecodeError, which the current handler does not catch. Catch it so_jira_getreturnsNoneas documented.🤖 Prompt for AI Agents