Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
38171db
Process Release Notes
vvolkgang Mar 3, 2025
674db9f
Comment linked issues and PRs
vvolkgang Nov 17, 2025
eb7bfb1
Uncomment jira ticket pattern removal
vvolkgang Nov 17, 2025
a9f407e
Fix imports
vvolkgang Nov 17, 2025
e4f5dd9
Implement methods to extract urls and fetch labels
vvolkgang Nov 17, 2025
466a572
Remove consecutive spaces after removing patterns
vvolkgang Nov 17, 2025
244e040
Skip processing if line starts with #
vvolkgang Nov 17, 2025
2d31de4
Fetch labels and implement should_skip_app
vvolkgang Nov 18, 2025
cbd4154
Implement argparse and output debug file
vvolkgang Nov 27, 2025
3fab831
Precache PR labels to speed up the process
vvolkgang Nov 27, 2025
c771eb6
Fix broken links in processed notes
vvolkgang Nov 27, 2025
dcee6b1
Add fixtures
vvolkgang Nov 27, 2025
df88fae
rename folder
vvolkgang Feb 16, 2026
a7dbf2b
Move / rename files
vvolkgang Feb 17, 2026
e08b86c
cleanup
vvolkgang Feb 17, 2026
51ad657
Implement update issue script and workflow
vvolkgang Feb 17, 2026
6fc06f5
refactor comments and help copy
vvolkgang Feb 17, 2026
199c4e4
Add comment
vvolkgang Feb 17, 2026
3fc581f
move script file
vvolkgang Feb 17, 2026
1779767
Remove gh-release-cleanup
vvolkgang Feb 17, 2026
0b27bbd
revert gitignore changes
vvolkgang Feb 17, 2026
344e2ba
Raise graphql error and print error message with github workflow command
vvolkgang Feb 17, 2026
49fea59
Update workflow name / run-name
vvolkgang Feb 19, 2026
7a21619
rename workflow file
vvolkgang Feb 19, 2026
394c2f7
Align workflow name with script
vvolkgang Feb 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions .github/scripts/gh_release_update_issues.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
#!/usr/bin/env python3
# Requires Python 3.9+
"""
Comment GitHub issues linked to Pull Requests mentioned in a given release.

Usage:
python gh_release_update_issues.py <release_url> [--dry-run]

Arguments:
release-url: The URL of the release to comment on
--dry-run: Run without actually updating issues

Examples:
python gh_release_update_issues.py https://github.com/owner/repo/releases/tag/v1.0.0
python gh_release_update_issues.py https://github.com/owner/repo/releases/tag/v1.0.0 --dry-run
"""

import re
import subprocess
import json
import argparse
from collections import defaultdict
from typing import List, Tuple, Dict

def parse_release_url(release_url: str) -> Tuple[str, str, str]:
"""Extract owner, repo name, and tag from a GitHub release URL.

Returns:
Tuple of (owner, repo_name, release_tag)
"""
match = re.search(r'github\.com/([\w-]+)/([\w.-]+)/releases/tag/(.+)$', release_url)
if not match:
raise ValueError(f"Cannot parse release URL: {release_url}")
return match.group(1), match.group(2), match.group(3)

def extract_pr_numbers(release_notes: str) -> List[int]:
return [int(n) for n in re.findall(r'/pull/(\d+)', release_notes)]

def build_issue_comment(repo: str, release_name: str, release_link: str, pr_numbers: List[int]) -> str:
if len(pr_numbers) == 0:
return ""

pr_links = [f"* https://github.com/{repo}/pull/{pr_number}" for pr_number in pr_numbers]

return f":shipit: Pull Request(s) linked to this issue released in [{release_name}]({release_link}):\n\n"+ "\n".join(pr_links)

def gh_fetch_release(repo: str, release_tag: str) -> Tuple[str, str]:
result = subprocess.run(
['gh', 'release', 'view', release_tag, '--repo', repo, '--json', 'name,body'],
capture_output=True, text=True, check=True
)
data = json.loads(result.stdout)
return data['name'], data['body']

def gh_comment_issue(repo: str, issue_number: int, comment: str) -> None:
"""Use GitHub CLI to comment on an issue.
"""
subprocess.run([
'gh', 'issue', 'comment', str(issue_number), '--body', comment, '--repo', repo
], check=True)

def gh_fetch_linked_issues_batched(owner: str, repo_name: str, pr_numbers: List[int]) -> Dict[int, List[int]]:
"""Batch-fetch linked issues for all PRs in a single GraphQL call.

Returns:
Dict mapping each PR number to its list of linked issue numbers.
"""
if not pr_numbers:
return {}

tmpl = 'pr_%d: pullRequest(number: %d) { closingIssuesReferences(first: 100) { nodes { number } } }'
pr_fragments = "\n".join(tmpl % (pr, pr) for pr in pr_numbers)
query = """
query ($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
%s
}
}
""" % pr_fragments

try:
result = subprocess.run(
[
'gh', 'api', 'graphql',
'-F', f'owner={owner}',
'-F', f'repo={repo_name}',
'-f', f'query={query}',
],
capture_output=True, text=True, check=True,
)
data = json.loads(result.stdout)
repo_data = data['data']['repository']

pr_issues_map: Dict[int, List[int]] = {}
for pr_number in pr_numbers:
nodes = repo_data.get(f'pr_{pr_number}', {}).get('closingIssuesReferences', {}).get('nodes', [])
pr_issues = [node['number'] for node in nodes]
pr_issues_map[pr_number] = pr_issues
return pr_issues_map

except subprocess.CalledProcessError as e:
print(f"::error::Error batch-fetching linked issues: {e.stderr}")
raise

def map_issues_to_prs(pr_issues_map: Dict[int, List[int]]) -> Dict[int, List[int]]:
"""Invert a PR->issues map into an issue->PRs map."""
issue_pr_map: Dict[int, List[int]] = defaultdict(list)
for pr_number, issue_numbers in pr_issues_map.items():
for issue_number in issue_numbers:
issue_pr_map[issue_number].append(pr_number)
return dict(issue_pr_map)

def comment_issues(repo: str, issue_pr_map: Dict[int, List[int]], release_name: str, release_url: str, dry_run: bool) -> None:
for issue_number, linked_prs in issue_pr_map.items():
comment = build_issue_comment(repo, release_name, release_url, linked_prs)
print(f"{'Dry run - ' if dry_run else ''}Commenting on issue {issue_number}:\n{comment}\n")
if not dry_run and comment:
gh_comment_issue(repo, issue_number, comment)

def parse_args():
parser = argparse.ArgumentParser(
description='Comment GitHub issues linked to Pull Requests mentioned in a given release.'
)
parser.add_argument(
'release_url',
help='Release URL (e.g. https://github.com/owner/repo/releases/tag/v1.0.0)'
)
parser.add_argument(
'--dry-run',
action='store_true',
help='Run without actually commenting issues'
)
return parser.parse_args()

if __name__ == '__main__':
args = parse_args()

owner, repo_name, release_tag = parse_release_url(args.release_url)
repo = f"{owner}/{repo_name}"
print(f"πŸ“‹ Release URL: {args.release_url}")

release_name, release_notes = gh_fetch_release(repo, release_tag)
print(f"πŸ“‹ Release Name: {release_name}")

pr_numbers = extract_pr_numbers(release_notes)
print(f"πŸ“‹ PR Numbers parsed from release notes: {pr_numbers}")
pr_issues_map = gh_fetch_linked_issues_batched(owner, repo_name, pr_numbers)
print(f"πŸ“‹ PRs with linked issues: {[pr for pr, issues in pr_issues_map.items() if issues]}\n")
issue_pr_map = map_issues_to_prs(pr_issues_map)
comment_issues(repo, issue_pr_map, release_name, args.release_url, args.dry_run)
37 changes: 37 additions & 0 deletions .github/workflows/sdlc-gh-release-update-issue.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: SDLC / Update Linked Issues on Release
run-name: ${{ inputs.dry-run && '(Dry Run) ' || '' }}Update Linked Issues on Release - ${{ github.event.release.name || inputs.release_url }}

on:
release:
types: [published]
workflow_dispatch:
inputs:
release_url:
description: 'Release URL (e.g. https://github.com/owner/repo/releases/tag/v1.0.0)'
required: true
dry-run:
description: 'Dry run'
type: boolean
default: false

permissions:
contents: read
issues: write

jobs:
update-linked-issues:
name: Update Linked Issues
runs-on: ubuntu-24.04
steps:
- name: Check out repo
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false

- name: Update Linked Issues
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
_RELEASE_URL: ${{ github.event.release.html_url || inputs.release_url }}
_DRY_RUN: ${{ inputs.dry-run && '--dry-run' || '' }}
run: |
python3 .github/scripts/gh_release_update_issues.py "$_RELEASE_URL" $_DRY_RUN
Loading