PM-327: add the linked PR as a Jira issue web link - #203
Conversation
jira_sync_logic.py and jira_sync_modules.py
jira_sync_logic.py and jira_sync_modules.pyscripts/__pycache__
|
@dani-tweig What is this about? |
|
Something I started working on and dropped in the middle.
This should add the GH PR link as web link to the relevant jira issue (so
it will appear not only in the developer section)
…On Sun, Aug 9, 2026, 07:57 Yaron Kaikov ***@***.***> wrote:
*yaronkaikov* left a comment (scylladb/github-automation#203)
<#203 (comment)>
@dani-tweig <https://github.com/dani-tweig> What is this about?
—
Reply to this email directly, view it on GitHub
<#203?email_source=notifications&email_token=BFF3UPVKRPMVZ2KJFNND6OL5JAADTA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMRSHE4DIOBRGIZ2M4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-5229848123>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/BFF3UPRZZVQZZT2WTZU3CDL5JAADTAVCNFSNUABFKJSXA33TNF2G64TZHM3DCNRZGA2TINBZHNEXG43VMU5TINZWG44TKMJRHE2KC5QC>
.
Triage notifications, keep track of coding agent tasks and review pull
requests on the go with GitHub Mobile for iOS
<https://github.com/notifications/mobile/ios/BFF3UPWP7YDNDMZ5XM4E7FD5JAADTA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMRSHE4DIOBRGIZ2M4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJKTGN5XXIZLSL5UW64Y>
and Android
<https://github.com/notifications/mobile/android/BFF3UPUIJRMPA5XFIIOIJAT5JAADTA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMRSHE4DIOBRGIZ2M4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJLTGN5XXIZLSL5QW4ZDSN5UWI>.
Download it today!
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
4a5fc0a to
88469da
Compare
📝 WalkthroughWalkthroughThe change adds Sequence Diagram(s)sequenceDiagram
participant JiraEvent
participant JiraSync
participant JiraAPI
JiraEvent->>JiraSync: Extract Jira keys and construct PR URL
JiraSync->>JiraAPI: Fetch existing remote links
JiraAPI-->>JiraSync: Return remote-link data
JiraSync->>JiraAPI: Post missing PR links
JiraSync-->>JiraEvent: Continue existing event synchronization
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Comment |
|
Rebased onto the current
Checked after the rebase, since PM-327 and PM-334 touch the same two files:
Worth noting the two changes compose correctly: Not executed end-to-end. |
scripts/__pycache__There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/jira_sync_modules.py`:
- Line 648: Update _jira_get to catch json.JSONDecodeError when parsing Jira
responses and return None, preserving the documented behavior for malformed JSON
while leaving other response handling unchanged.
- Around line 1323-1413: Update the Jira HTTP helpers _jira_get, _jira_post, and
_jira_put so every urlopen call supplies a finite timeout value. Reuse an
existing timeout configuration or define one shared constant, ensuring all Jira
requests—including those used by add_pr_weblink_to_jira—cannot block
indefinitely.
- Around line 1353-1396: Add a stable globalId field to the payload object,
deriving its value deterministically from normalized_pr_url so repeated or
concurrent remotelink requests identify the same Jira link. Keep the existing
object URL and title fields unchanged.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: QUIET
Plan: Pro Plus
Run ID: 590dbef7-254a-46e1-bf64-bcccd94c0374
📒 Files selected for processing (2)
scripts/jira_sync_logic.pyscripts/jira_sync_modules.py
|
|
||
|
|
||
| def _jira_get(url: str, jira_auth: str) -> dict | None: | ||
| def _jira_get(url: str, jira_auth: str) -> dict | list | None: |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- target function and surrounding code ---'
sed -n '620,685p' scripts/jira_sync_modules.py
printf '%s\n' '--- JSON and urlopen usage ---'
rg -n -C 3 'json\.loads|urlopen|_jira_get\(' scripts/jira_sync_modules.pyRepository: scylladb/github-automation
Length of output: 6278
🏁 Script executed:
python3 - <<'PY'
import json
try:
json.loads(b'{"incomplete":')
except Exception as exc:
print(type(exc).__name__)
print(isinstance(exc, json.JSONDecodeError))
print(isinstance(exc, (ConnectionError, json.JSONDecodeError)))
PYRepository: scylladb/github-automation
Length of output: 190
Handle malformed JSON responses.
If Jira returns malformed JSON, json.loads raises json.JSONDecodeError, which the current handler does not catch. Catch it so _jira_get returns None as documented.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/jira_sync_modules.py` at line 648, Update _jira_get to catch
json.JSONDecodeError when parsing Jira responses and return None, preserving the
documented behavior for malformed JSON while leaving other response handling
unchanged.
| 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): | ||
| 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.") | ||
|
|
||
|
|
There was a problem hiding this comment.
🩺 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.
_jira_get, _jira_post, and _jira_put call urlopen without a timeout. A stalled Jira request can block later synchronization steps indefinitely.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/jira_sync_modules.py` around lines 1323 - 1413, Update the Jira HTTP
helpers _jira_get, _jira_post, and _jira_put so every urlopen call supplies a
finite timeout value. Reuse an existing timeout configuration or define one
shared constant, ensuring all Jira requests—including those used by
add_pr_weblink_to_jira—cannot block indefinitely.
| 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): |
There was a problem hiding this comment.
🗄️ 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:
Atlassian Jira Cloud REST API v3 issue remote links globalId POST duplicate update existing remote link
💡 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:
- 1: https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-remote-links/
- 2: https://docs.go-atlassian.io/jira-software-cloud/issues/link/remote
- 3: https://github.com/ilpanich/jirust-cli/blob/4a8d03be23a2b6fab751a2865074681f0a689e63/jira_v3_openapi/docs/IssueRemoteLinksApi.md
- 4: https://mrrefactoring.github.io/jira.js/classes/Version3.IssueRemoteLinks.html
- 5: https://developer.atlassian.com/display/JIRADEV/JIRA+REST+API+for+Remote+Issue+Links
- 6: https://developer.atlassian.com/server/jira/platform/jira-rest-api-for-remote-issue-links/
🌐 Web query:
site:developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-remote-links "globalId" "If a global ID is provided"
💡 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 globalId derived from normalized_pr_url to payload. Jira’s POST /remotelink endpoint updates an existing link with the same globalId, which prevents concurrent flows from creating duplicates.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/jira_sync_modules.py` around lines 1353 - 1396, Add a stable globalId
field to the payload object, deriving its value deterministically from
normalized_pr_url so repeated or concurrent remotelink requests identify the
same Jira link. Keep the existing object URL and title fields unchanged.
Two compiled Python artifacts were accidentally included in the PR under
scripts/__pycache__, adding binary noise to the change set. This PR removes those tracked.pycfiles so the PR contains only source-level changes.Scope
scripts/__pycache__/jira_sync_modules.cpython-312.pycscripts/__pycache__/jira_sync_logic.cpython-312.pycImpact
git rm scripts/__pycache__/jira_sync_modules.cpython-312.pyc \ scripts/__pycache__/jira_sync_logic.cpython-312.pyc