Skip to content

PM-327: add the linked PR as a Jira issue web link - #203

Merged
dani-tweig merged 15 commits into
automation-staging-branchfrom
copilot/automation-staging-branch
Aug 9, 2026
Merged

PM-327: add the linked PR as a Jira issue web link#203
dani-tweig merged 15 commits into
automation-staging-branchfrom
copilot/automation-staging-branch

Conversation

Copilot AI commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

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 .pyc files so the PR contains only source-level changes.

  • Scope

    • Removes:
      • scripts/__pycache__/jira_sync_modules.cpython-312.pyc
      • scripts/__pycache__/jira_sync_logic.cpython-312.pyc
  • Impact

    • No runtime or workflow behavior changes.
    • PR diff is reduced to relevant, reviewable source changes only.
git rm scripts/__pycache__/jira_sync_modules.cpython-312.pyc \
       scripts/__pycache__/jira_sync_logic.cpython-312.pyc

Copilot AI changed the title [WIP] Add webhook to Jira for linked PRs PM-327: Add Jira issue web-link sync for linked PRs Jun 29, 2026
Copilot AI requested a review from dani-tweig June 29, 2026 12:11
Copilot AI changed the title PM-327: Add Jira issue web-link sync for linked PRs PM-327: Sync linked PR URL to Jira issue remote links across PR events Jun 29, 2026
Copilot AI changed the title PM-327: Sync linked PR URL to Jira issue remote links across PR events Limit PR scope to Python Jira sync scripts only Jun 29, 2026
Copilot AI changed the title Limit PR scope to Python Jira sync scripts only Narrow PR scope to jira_sync_logic.py and jira_sync_modules.py only Jun 29, 2026
Copilot AI changed the title Narrow PR scope to jira_sync_logic.py and jira_sync_modules.py only Narrow PR scope to jira_sync_logic.py and jira_sync_modules.py Jun 29, 2026
Copilot AI changed the title Narrow PR scope to jira_sync_logic.py and jira_sync_modules.py Remove tracked Python bytecode artifacts from scripts/__pycache__ Jun 29, 2026
@yaronkaikov

Copy link
Copy Markdown
Collaborator

@dani-tweig What is this about?

@dani-tweig

dani-tweig commented Aug 9, 2026 via email

Copy link
Copy Markdown
Contributor

@dani-tweig
dani-tweig force-pushed the copilot/automation-staging-branch branch from 4a5fc0a to 88469da Compare August 9, 2026 14:11
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds add_pr_weblink_to_jira to synchronize GitHub pull-request URLs with Jira issues. The action validates inputs, normalizes URLs, checks existing remote links, posts missing links, and handles failures. Five Jira event orchestrators call the action after extracting Jira keys and before their existing synchronization steps.

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
Loading

Suggested reviewers: dani-tweig

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: adding the linked pull request as a Jira issue web link.
Description check ✅ Passed The description refers to tracked Python artifacts and scope reduction, which aligns with the stated PR objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@dani-tweig

Copy link
Copy Markdown
Contributor

Rebased onto the current automation-staging-branch, which has since picked up #210 (PM-334 statuses + Epic exclusion) and #211 (MERGED_TRANSITION_ID fix).

  • Previous head: 4a5fc0a -> new head: 88469da (force-pushed, so 4a5fc0a is the recovery point if anything looks wrong).
  • All 15 commits replayed with no conflicts; the net diff is unchanged at +129/-1 across the same two files.
  • mergeable_state went from unstable to clean.

Checked after the rebase, since PM-327 and PM-334 touch the same two files:

  • PM-334 survived intact - MERGED_TRANSITION_ID = "7", _EXCLUDED_ISSUE_TYPES = {"epic"}, _WORKING_STATES / _CLOSED_STATES, and the renumbered Step 6a / 6b in manage_labeled_gh_event. No Ready for Merge logic came back.
  • PM-327 landed intact - add_pr_weblink_to_jira plus its five call sites.
  • python -m py_compile passes on both files.

Worth noting the two changes compose correctly: Step 1b runs after the __NO_KEYS_FOUND__ early return, so an Epic-only PR still exits before any Jira write and gets no web link either.

Not executed end-to-end.

@dani-tweig dani-tweig changed the title Remove tracked Python bytecode artifacts from scripts/__pycache__ PM-327: add the linked PR as a Jira issue web link Aug 9, 2026
@dani-tweig
dani-tweig marked this pull request as ready for review August 9, 2026 14:15

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between e56c6a0 and 88469da.

📒 Files selected for processing (2)
  • scripts/jira_sync_logic.py
  • scripts/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:

Copy link
Copy Markdown

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:

#!/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.py

Repository: 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)))
PY

Repository: 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.

Comment on lines +1323 to +1413
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.")


Copy link
Copy Markdown

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:

#!/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.py

Repository: 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"])
PY

Repository: 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.

Comment on lines +1353 to +1396
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):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 --short

Repository: 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 --short

Repository: 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:


🌐 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])
PY

Repository: 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
done

Repository: 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.

@dani-tweig
dani-tweig merged commit 12689f1 into automation-staging-branch Aug 9, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants