We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Remaining issues with automatic changelog PR generation
This is coming along nicely. Still a few hiccups:

* The linter complains the title of the PR itself is not a conventional commit message. Suggestion: Prefix with `chore:`. That passes.
* The mapped dev name come with a prefix `@`. (-> `@Ervin Hegedüs`). This should be removed.
* There is 1 message per dev merging a PR per day. Yesterday we had 2 dev merging 1 PR, this leading to 2 Changelog PRs trying to add something to the same original CHANGES file, obviously resulting in a conflict. Can be resolved by hand, but a single Changelog PR per day would be easier for handling.
* The PRs are now changing the first few lines of the CHANGES file. I suggest to shift this down a bit to get a better looking file without having these new entries sticking out on top. Suggestion: Add the entries following the first line matching the pattern `/^## Version/`.


I have resolved the conflict right in the GUI and I have also rewritten the Changelog message by hand right in the GUI. I think that works smoothly. Then self-approval, then merging.

We do not usually self-approve, but on these administrative updates, we should keep the work to an absolute minimum.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `.github/create-changelog-prs.py`
Content:
```
1 #! /usr/bin/env python
2 
3 import subprocess
4 import json
5 import datetime
6 import tempfile
7 import sys
8 import os
9 import shutil
10 import re
11 
12 DEVELOPERS = dict()
13 
14 def get_pr(repository: str, number: int) -> dict:
15 	command = f"""gh pr view \
16 		--repo "{repository}" \
17 		"{number}" \
18 		--json mergeCommit,mergedBy,title,author,baseRefName,number
19 	"""
20 	proc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
21 	pr_json, errors = proc.communicate()
22 	if proc.returncode != 0:
23 		print(errors)
24 		exit(1)
25 	return json.loads(pr_json)
26 
27 def get_prs(repository: str, day: datetime.date) -> list:
28 	print(f"Fetching PRs for {day}")
29 	command = f"""gh search prs \
30 		--repo "{repository}" \
31 		--merged-at "{day}" \
32 		--json number
33 	"""
34 	proc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
35 	prs_json, errors = proc.communicate()
36 	if proc.returncode != 0:
37 		print(errors)
38 		exit(1)
39 	prs = list()
40 	for result in json.loads(prs_json):
41 		prs.append(get_pr(repository, result["number"]))
42 
43 	return prs
44 
45 def parse_prs(prs: list) -> dict:
46 	pr_map = dict()
47 	for pr in prs:
48 		merged_by = pr["mergedBy"]["login"]
49 		if merged_by not in pr:
50 			pr_list = list()
51 			pr_map[merged_by] = pr_list
52 		else:
53 			pr_list = pr_map[merged_by]
54 		pr_list.append(pr)
55 	return pr_map
56 
57 
58 def create_prs(repository: str, merged_by_prs_map: dict, day: datetime.date):
59 	for author in merged_by_prs_map.keys():
60 		create_pr(repository, author, merged_by_prs_map[author], day)
61 
62 def create_pr(repository: str, merged_by: str, prs: list, day: datetime.date):
63 	if len(prs) == 0:
64 		return
65 	print(f"Creating changelog PR for @{merged_by}")
66 
67 	sample_pr = prs[0]
68 	base_branch = sample_pr["baseRefName"]
69 	pr_branch_name = create_pr_branch(day, merged_by, base_branch)
70 	pr_body, changelog_lines = generate_content(prs, merged_by)
71 	create_commit(changelog_lines)
72 	push_pr_branch(pr_branch_name)
73 
74 	command = f"""gh pr create \
75 		--repo "{repository}" \
76 		--assignee "{merged_by}" \
77 		--base "{base_branch}" \
78 		--label "changelog-pr" \
79 		--title "Changelog updates for {day}, merged by @{merged_by}" \
80 		--body '{pr_body}'
81 	"""
82 
83 	proc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
84 	outs, errors = proc.communicate()
85 	if proc.returncode != 0:
86 		print(errors)
87 		exit(1)
88 	print(f"Created PR: {outs.decode()}")
89 
90 def create_commit(changelog_lines: str):
91 	new_changelog = tempfile.NamedTemporaryFile(delete=False, delete_on_close=False)
92 	new_changelog.write(changelog_lines.encode())
93 	with open('CHANGES.md', 'rt') as changelog:
94 		new_changelog.write(changelog.read().encode())
95 
96 	new_changelog.close()
97 	os.remove('CHANGES.md')
98 	shutil.move(new_changelog.name, 'CHANGES.md')
99 
100 	command = "git commit CHANGES.md -m 'Add pending changelog entries to changelog'"
101 	proc = subprocess.Popen(command, shell=True, stderr=subprocess.PIPE)
102 	_, errors = proc.communicate()
103 	if proc.returncode != 0:
104 		print(errors)
105 		exit(1)
106 
107 def generate_content(prs: list, merged_by: str) -> (str, str):
108 	changelog_lines = f"Entries for PRs merged by {merged_by}:\n"
109 	pr_body = f"This PR was auto-generated to update the changelog with the following entries, merged by @{merged_by}:\n```\n"
110 	pr_links = ""
111 	for pr in prs:
112 		pr_number = pr["number"]
113 		pr_title = pr["title"]
114 		pr_author = get_pr_author_name(pr["author"]["login"])
115 		new_line = f"* {pr_title} (@{pr_author}) [#{pr_number}]\n"
116 		pr_body += new_line
117 		pr_links += f"- #{pr_number}\n"
118 
119 		changelog_lines += new_line
120 	pr_body += "```\n\n" + pr_links
121 	changelog_lines += "\n\n"
122 
123 	return pr_body, changelog_lines
124 
125 def get_pr_author_name(login: str) -> str:
126 	if len(DEVELOPERS) == 0:
127 		parse_contributors()
128 
129 	return DEVELOPERS[login] if login in DEVELOPERS else login
130 
131 def parse_contributors():
132 	regex = re.compile(r'^\s*?-\s*?\[([^]]+)\]\s*?\(http.*/([^/]+)\s*?\)')
133 	with open('CONTRIBUTORS.md', 'rt') as handle:
134 		line = handle.readline()
135 		while not ('##' in line and 'Contributors' in line):
136 			match = regex.match(line)
137 			if match:
138 				DEVELOPERS[match.group(2)] = match.group(1)
139 			line = handle.readline()
140 
141 def create_pr_branch(day: datetime.date, author: str, base_branch: str) -> str:
142 	branch_name = f"changelog-updates-for-{day}-{author} {base_branch}"
143 	command = f"git checkout -b {branch_name}"
144 	proc = subprocess.Popen(command, shell=True, stderr=subprocess.PIPE)
145 	_, errors = proc.communicate()
146 	if proc.returncode != 0:
147 		print(errors)
148 		exit(1)
149 
150 	return branch_name
151 
152 def push_pr_branch(branch_name: str):
153 	command = f"git push origin {branch_name}"
154 	proc = subprocess.Popen(command, shell=True, stderr=subprocess.PIPE)
155 	_, errors = proc.communicate()
156 	if proc.returncode != 0:
157 		print(errors)
158 		exit(1)
159 
160 def run(source_repository: str, target_repository: str, today: datetime.date):
161 	day = today - datetime.timedelta(days=1)
162 	prs = get_prs(source_repository, day)
163 	prs_length = len(prs)
164 	print(f"Found {prs_length} PRs")
165 	if prs_length == 0:
166 		return
167 
168 	merged_by_prs_map = parse_prs(prs)
169 	create_prs(target_repository, merged_by_prs_map, day)
170 
171 if __name__ == "__main__":
172 	# disable pager
173 	os.environ["GH_PAGER"] = ''
174 	# set variables for Git
175 	os.environ["GIT_AUTHOR_NAME"] = "changelog-pr-bot"
176 	os.environ["GIT_AUTHOR_EMAIL"] = "dummy@coreruleset.org"
177 	os.environ["GIT_COMMITTER_NAME"] = "changelog-pr-bot"
178 	os.environ["GIT_COMMITTER_EMAIL"] = "dummy@coreruleset.org"
179 
180 	source_repository = 'coreruleset/coreruleset'
181 	target_repository = source_repository
182 	# the cron schedule for the workflow uses UTC
183 	today = datetime.datetime.now(datetime.timezone.utc).date()
184 
185 	if len(sys.argv) > 1:
186 		source_repository = sys.argv[1]
187 	if len(sys.argv) > 2:
188 		target_repository = sys.argv[2]
189 	if len(sys.argv) > 3:
190 		today = datetime.date.fromisoformat(sys.argv[3])
191 	run(source_repository, target_repository, today)
192
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/.github/create-changelog-prs.py b/.github/create-changelog-prs.py
--- a/.github/create-changelog-prs.py
+++ b/.github/create-changelog-prs.py
@@ -6,7 +6,6 @@
 import tempfile
 import sys
 import os
-import shutil
 import re
 
 DEVELOPERS = dict()
@@ -76,7 +75,7 @@
 		--assignee "{merged_by}" \
 		--base "{base_branch}" \
 		--label "changelog-pr" \
-		--title "Changelog updates for {day}, merged by @{merged_by}" \
+		--title "chore: changelog updates for {day}, merged by @{merged_by}" \
 		--body '{pr_body}'
 	"""
 
@@ -88,16 +87,10 @@
 	print(f"Created PR: {outs.decode()}")
 
 def create_commit(changelog_lines: str):
-	new_changelog = tempfile.NamedTemporaryFile(delete=False, delete_on_close=False)
-	new_changelog.write(changelog_lines.encode())
-	with open('CHANGES.md', 'rt') as changelog:
-		new_changelog.write(changelog.read().encode())
+	with open('.changes-pending.md', 'at') as changelog:
+		changelog.write(changelog_lines.encode())
 
-	new_changelog.close()
-	os.remove('CHANGES.md')
-	shutil.move(new_changelog.name, 'CHANGES.md')
-
-	command = "git commit CHANGES.md -m 'Add pending changelog entries to changelog'"
+	command = "git commit .changes-pending.md -m 'Add pending changelog entries'"
 	proc = subprocess.Popen(command, shell=True, stderr=subprocess.PIPE)
 	_, errors = proc.communicate()
 	if proc.returncode != 0:
@@ -112,7 +105,7 @@
 		pr_number = pr["number"]
 		pr_title = pr["title"]
 		pr_author = get_pr_author_name(pr["author"]["login"])
-		new_line = f"* {pr_title} (@{pr_author}) [#{pr_number}]\n"
+		new_line = f"* {pr_title} ({pr_author}) [#{pr_number}]\n"
 		pr_body += new_line
 		pr_links += f"- #{pr_number}\n"
 
@@ -126,7 +119,7 @@
 	if len(DEVELOPERS) == 0:
 		parse_contributors()
 
-	return DEVELOPERS[login] if login in DEVELOPERS else login
+	return DEVELOPERS[login] if login in DEVELOPERS else f"@{login}"
 
 def parse_contributors():
 	regex = re.compile(r'^\s*?-\s*?\[([^]]+)\]\s*?\(http.*/([^/]+)\s*?\)')

{"golden_diff": "diff --git a/.github/create-changelog-prs.py b/.github/create-changelog-prs.py\n--- a/.github/create-changelog-prs.py\n+++ b/.github/create-changelog-prs.py\n@@ -6,7 +6,6 @@\n import tempfile\n import sys\n import os\n-import shutil\n import re\n \n DEVELOPERS = dict()\n@@ -76,7 +75,7 @@\n \t\t--assignee \"{merged_by}\" \\\n \t\t--base \"{base_branch}\" \\\n \t\t--label \"changelog-pr\" \\\n-\t\t--title \"Changelog updates for {day}, merged by @{merged_by}\" \\\n+\t\t--title \"chore: changelog updates for {day}, merged by @{merged_by}\" \\\n \t\t--body '{pr_body}'\n \t\"\"\"\n \n@@ -88,16 +87,10 @@\n \tprint(f\"Created PR: {outs.decode()}\")\n \n def create_commit(changelog_lines: str):\n-\tnew_changelog = tempfile.NamedTemporaryFile(delete=False, delete_on_close=False)\n-\tnew_changelog.write(changelog_lines.encode())\n-\twith open('CHANGES.md', 'rt') as changelog:\n-\t\tnew_changelog.write(changelog.read().encode())\n+\twith open('.changes-pending.md', 'at') as changelog:\n+\t\tchangelog.write(changelog_lines.encode())\n \n-\tnew_changelog.close()\n-\tos.remove('CHANGES.md')\n-\tshutil.move(new_changelog.name, 'CHANGES.md')\n-\n-\tcommand = \"git commit CHANGES.md -m 'Add pending changelog entries to changelog'\"\n+\tcommand = \"git commit .changes-pending.md -m 'Add pending changelog entries'\"\n \tproc = subprocess.Popen(command, shell=True, stderr=subprocess.PIPE)\n \t_, errors = proc.communicate()\n \tif proc.returncode != 0:\n@@ -112,7 +105,7 @@\n \t\tpr_number = pr[\"number\"]\n \t\tpr_title = pr[\"title\"]\n \t\tpr_author = get_pr_author_name(pr[\"author\"][\"login\"])\n-\t\tnew_line = f\"* {pr_title} (@{pr_author}) [#{pr_number}]\\n\"\n+\t\tnew_line = f\"* {pr_title} ({pr_author}) [#{pr_number}]\\n\"\n \t\tpr_body += new_line\n \t\tpr_links += f\"- #{pr_number}\\n\"\n \n@@ -126,7 +119,7 @@\n \tif len(DEVELOPERS) == 0:\n \t\tparse_contributors()\n \n-\treturn DEVELOPERS[login] if login in DEVELOPERS else login\n+\treturn DEVELOPERS[login] if login in DEVELOPERS else f\"@{login}\"\n \n def parse_contributors():\n \tregex = re.compile(r'^\\s*?-\\s*?\\[([^]]+)\\]\\s*?\\(http.*/([^/]+)\\s*?\\)')\n", "issue": "Remaining issues with automatic changelog PR generation\nThis is coming along nicely. Still a few hiccups:\r\n\r\n* The linter complains the title of the PR itself is not a conventional commit message. Suggestion: Prefix with `chore:`. That passes.\r\n* The mapped dev name come with a prefix `@`. (-> `@Ervin Heged\u00fcs`). This should be removed.\r\n* There is 1 message per dev merging a PR per day. Yesterday we had 2 dev merging 1 PR, this leading to 2 Changelog PRs trying to add something to the same original CHANGES file, obviously resulting in a conflict. Can be resolved by hand, but a single Changelog PR per day would be easier for handling.\r\n* The PRs are now changing the first few lines of the CHANGES file. I suggest to shift this down a bit to get a better looking file without having these new entries sticking out on top. Suggestion: Add the entries following the first line matching the pattern `/^## Version/`.\r\n\r\n\r\nI have resolved the conflict right in the GUI and I have also rewritten the Changelog message by hand right in the GUI. I think that works smoothly. Then self-approval, then merging.\r\n\r\nWe do not usually self-approve, but on these administrative updates, we should keep the work to an absolute minimum.\n", "before_files": [{"content": "#! /usr/bin/env python\n\nimport subprocess\nimport json\nimport datetime\nimport tempfile\nimport sys\nimport os\nimport shutil\nimport re\n\nDEVELOPERS = dict()\n\ndef get_pr(repository: str, number: int) -> dict:\n\tcommand = f\"\"\"gh pr view \\\n\t\t--repo \"{repository}\" \\\n\t\t\"{number}\" \\\n\t\t--json mergeCommit,mergedBy,title,author,baseRefName,number\n\t\"\"\"\n\tproc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n\tpr_json, errors = proc.communicate()\n\tif proc.returncode != 0:\n\t\tprint(errors)\n\t\texit(1)\n\treturn json.loads(pr_json)\n\ndef get_prs(repository: str, day: datetime.date) -> list:\n\tprint(f\"Fetching PRs for {day}\")\n\tcommand = f\"\"\"gh search prs \\\n\t\t--repo \"{repository}\" \\\n\t\t--merged-at \"{day}\" \\\n\t\t--json number\n\t\"\"\"\n\tproc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n\tprs_json, errors = proc.communicate()\n\tif proc.returncode != 0:\n\t\tprint(errors)\n\t\texit(1)\n\tprs = list()\n\tfor result in json.loads(prs_json):\n\t\tprs.append(get_pr(repository, result[\"number\"]))\n\n\treturn prs\n\ndef parse_prs(prs: list) -> dict:\n\tpr_map = dict()\n\tfor pr in prs:\n\t\tmerged_by = pr[\"mergedBy\"][\"login\"]\n\t\tif merged_by not in pr:\n\t\t\tpr_list = list()\n\t\t\tpr_map[merged_by] = pr_list\n\t\telse:\n\t\t\tpr_list = pr_map[merged_by]\n\t\tpr_list.append(pr)\n\treturn pr_map\n\n\ndef create_prs(repository: str, merged_by_prs_map: dict, day: datetime.date):\n\tfor author in merged_by_prs_map.keys():\n\t\tcreate_pr(repository, author, merged_by_prs_map[author], day)\n\ndef create_pr(repository: str, merged_by: str, prs: list, day: datetime.date):\n\tif len(prs) == 0:\n\t\treturn\n\tprint(f\"Creating changelog PR for @{merged_by}\")\n\n\tsample_pr = prs[0]\n\tbase_branch = sample_pr[\"baseRefName\"]\n\tpr_branch_name = create_pr_branch(day, merged_by, base_branch)\n\tpr_body, changelog_lines = generate_content(prs, merged_by)\n\tcreate_commit(changelog_lines)\n\tpush_pr_branch(pr_branch_name)\n\n\tcommand = f\"\"\"gh pr create \\\n\t\t--repo \"{repository}\" \\\n\t\t--assignee \"{merged_by}\" \\\n\t\t--base \"{base_branch}\" \\\n\t\t--label \"changelog-pr\" \\\n\t\t--title \"Changelog updates for {day}, merged by @{merged_by}\" \\\n\t\t--body '{pr_body}'\n\t\"\"\"\n\n\tproc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n\touts, errors = proc.communicate()\n\tif proc.returncode != 0:\n\t\tprint(errors)\n\t\texit(1)\n\tprint(f\"Created PR: {outs.decode()}\")\n\ndef create_commit(changelog_lines: str):\n\tnew_changelog = tempfile.NamedTemporaryFile(delete=False, delete_on_close=False)\n\tnew_changelog.write(changelog_lines.encode())\n\twith open('CHANGES.md', 'rt') as changelog:\n\t\tnew_changelog.write(changelog.read().encode())\n\n\tnew_changelog.close()\n\tos.remove('CHANGES.md')\n\tshutil.move(new_changelog.name, 'CHANGES.md')\n\n\tcommand = \"git commit CHANGES.md -m 'Add pending changelog entries to changelog'\"\n\tproc = subprocess.Popen(command, shell=True, stderr=subprocess.PIPE)\n\t_, errors = proc.communicate()\n\tif proc.returncode != 0:\n\t\tprint(errors)\n\t\texit(1)\n\ndef generate_content(prs: list, merged_by: str) -> (str, str):\n\tchangelog_lines = f\"Entries for PRs merged by {merged_by}:\\n\"\n\tpr_body = f\"This PR was auto-generated to update the changelog with the following entries, merged by @{merged_by}:\\n```\\n\"\n\tpr_links = \"\"\n\tfor pr in prs:\n\t\tpr_number = pr[\"number\"]\n\t\tpr_title = pr[\"title\"]\n\t\tpr_author = get_pr_author_name(pr[\"author\"][\"login\"])\n\t\tnew_line = f\"* {pr_title} (@{pr_author}) [#{pr_number}]\\n\"\n\t\tpr_body += new_line\n\t\tpr_links += f\"- #{pr_number}\\n\"\n\n\t\tchangelog_lines += new_line\n\tpr_body += \"```\\n\\n\" + pr_links\n\tchangelog_lines += \"\\n\\n\"\n\n\treturn pr_body, changelog_lines\n\ndef get_pr_author_name(login: str) -> str:\n\tif len(DEVELOPERS) == 0:\n\t\tparse_contributors()\n\n\treturn DEVELOPERS[login] if login in DEVELOPERS else login\n\ndef parse_contributors():\n\tregex = re.compile(r'^\\s*?-\\s*?\\[([^]]+)\\]\\s*?\\(http.*/([^/]+)\\s*?\\)')\n\twith open('CONTRIBUTORS.md', 'rt') as handle:\n\t\tline = handle.readline()\n\t\twhile not ('##' in line and 'Contributors' in line):\n\t\t\tmatch = regex.match(line)\n\t\t\tif match:\n\t\t\t\tDEVELOPERS[match.group(2)] = match.group(1)\n\t\t\tline = handle.readline()\n\ndef create_pr_branch(day: datetime.date, author: str, base_branch: str) -> str:\n\tbranch_name = f\"changelog-updates-for-{day}-{author} {base_branch}\"\n\tcommand = f\"git checkout -b {branch_name}\"\n\tproc = subprocess.Popen(command, shell=True, stderr=subprocess.PIPE)\n\t_, errors = proc.communicate()\n\tif proc.returncode != 0:\n\t\tprint(errors)\n\t\texit(1)\n\n\treturn branch_name\n\ndef push_pr_branch(branch_name: str):\n\tcommand = f\"git push origin {branch_name}\"\n\tproc = subprocess.Popen(command, shell=True, stderr=subprocess.PIPE)\n\t_, errors = proc.communicate()\n\tif proc.returncode != 0:\n\t\tprint(errors)\n\t\texit(1)\n\ndef run(source_repository: str, target_repository: str, today: datetime.date):\n\tday = today - datetime.timedelta(days=1)\n\tprs = get_prs(source_repository, day)\n\tprs_length = len(prs)\n\tprint(f\"Found {prs_length} PRs\")\n\tif prs_length == 0:\n\t\treturn\n\n\tmerged_by_prs_map = parse_prs(prs)\n\tcreate_prs(target_repository, merged_by_prs_map, day)\n\nif __name__ == \"__main__\":\n\t# disable pager\n\tos.environ[\"GH_PAGER\"] = ''\n\t# set variables for Git\n\tos.environ[\"GIT_AUTHOR_NAME\"] = \"changelog-pr-bot\"\n\tos.environ[\"GIT_AUTHOR_EMAIL\"] = \"dummy@coreruleset.org\"\n\tos.environ[\"GIT_COMMITTER_NAME\"] = \"changelog-pr-bot\"\n\tos.environ[\"GIT_COMMITTER_EMAIL\"] = \"dummy@coreruleset.org\"\n\n\tsource_repository = 'coreruleset/coreruleset'\n\ttarget_repository = source_repository\n\t# the cron schedule for the workflow uses UTC\n\ttoday = datetime.datetime.now(datetime.timezone.utc).date()\n\n\tif len(sys.argv) > 1:\n\t\tsource_repository = sys.argv[1]\n\tif len(sys.argv) > 2:\n\t\ttarget_repository = sys.argv[2]\n\tif len(sys.argv) > 3:\n\t\ttoday = datetime.date.fromisoformat(sys.argv[3])\n\trun(source_repository, target_repository, today)\n", "path": ".github/create-changelog-prs.py"}], "after_files": [{"content": "#! /usr/bin/env python\n\nimport subprocess\nimport json\nimport datetime\nimport tempfile\nimport sys\nimport os\nimport re\n\nDEVELOPERS = dict()\n\ndef get_pr(repository: str, number: int) -> dict:\n\tcommand = f\"\"\"gh pr view \\\n\t\t--repo \"{repository}\" \\\n\t\t\"{number}\" \\\n\t\t--json mergeCommit,mergedBy,title,author,baseRefName,number\n\t\"\"\"\n\tproc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n\tpr_json, errors = proc.communicate()\n\tif proc.returncode != 0:\n\t\tprint(errors)\n\t\texit(1)\n\treturn json.loads(pr_json)\n\ndef get_prs(repository: str, day: datetime.date) -> list:\n\tprint(f\"Fetching PRs for {day}\")\n\tcommand = f\"\"\"gh search prs \\\n\t\t--repo \"{repository}\" \\\n\t\t--merged-at \"{day}\" \\\n\t\t--json number\n\t\"\"\"\n\tproc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n\tprs_json, errors = proc.communicate()\n\tif proc.returncode != 0:\n\t\tprint(errors)\n\t\texit(1)\n\tprs = list()\n\tfor result in json.loads(prs_json):\n\t\tprs.append(get_pr(repository, result[\"number\"]))\n\n\treturn prs\n\ndef parse_prs(prs: list) -> dict:\n\tpr_map = dict()\n\tfor pr in prs:\n\t\tmerged_by = pr[\"mergedBy\"][\"login\"]\n\t\tif merged_by not in pr:\n\t\t\tpr_list = list()\n\t\t\tpr_map[merged_by] = pr_list\n\t\telse:\n\t\t\tpr_list = pr_map[merged_by]\n\t\tpr_list.append(pr)\n\treturn pr_map\n\n\ndef create_prs(repository: str, merged_by_prs_map: dict, day: datetime.date):\n\tfor author in merged_by_prs_map.keys():\n\t\tcreate_pr(repository, author, merged_by_prs_map[author], day)\n\ndef create_pr(repository: str, merged_by: str, prs: list, day: datetime.date):\n\tif len(prs) == 0:\n\t\treturn\n\tprint(f\"Creating changelog PR for @{merged_by}\")\n\n\tsample_pr = prs[0]\n\tbase_branch = sample_pr[\"baseRefName\"]\n\tpr_branch_name = create_pr_branch(day, merged_by, base_branch)\n\tpr_body, changelog_lines = generate_content(prs, merged_by)\n\tcreate_commit(changelog_lines)\n\tpush_pr_branch(pr_branch_name)\n\n\tcommand = f\"\"\"gh pr create \\\n\t\t--repo \"{repository}\" \\\n\t\t--assignee \"{merged_by}\" \\\n\t\t--base \"{base_branch}\" \\\n\t\t--label \"changelog-pr\" \\\n\t\t--title \"chore: changelog updates for {day}, merged by @{merged_by}\" \\\n\t\t--body '{pr_body}'\n\t\"\"\"\n\n\tproc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n\touts, errors = proc.communicate()\n\tif proc.returncode != 0:\n\t\tprint(errors)\n\t\texit(1)\n\tprint(f\"Created PR: {outs.decode()}\")\n\ndef create_commit(changelog_lines: str):\n\twith open('.changes-pending.md', 'at') as changelog:\n\t\tchangelog.write(changelog_lines.encode())\n\n\tcommand = \"git commit .changes-pending.md -m 'Add pending changelog entries'\"\n\tproc = subprocess.Popen(command, shell=True, stderr=subprocess.PIPE)\n\t_, errors = proc.communicate()\n\tif proc.returncode != 0:\n\t\tprint(errors)\n\t\texit(1)\n\ndef generate_content(prs: list, merged_by: str) -> (str, str):\n\tchangelog_lines = f\"Entries for PRs merged by {merged_by}:\\n\"\n\tpr_body = f\"This PR was auto-generated to update the changelog with the following entries, merged by @{merged_by}:\\n```\\n\"\n\tpr_links = \"\"\n\tfor pr in prs:\n\t\tpr_number = pr[\"number\"]\n\t\tpr_title = pr[\"title\"]\n\t\tpr_author = get_pr_author_name(pr[\"author\"][\"login\"])\n\t\tnew_line = f\"* {pr_title} ({pr_author}) [#{pr_number}]\\n\"\n\t\tpr_body += new_line\n\t\tpr_links += f\"- #{pr_number}\\n\"\n\n\t\tchangelog_lines += new_line\n\tpr_body += \"```\\n\\n\" + pr_links\n\tchangelog_lines += \"\\n\\n\"\n\n\treturn pr_body, changelog_lines\n\ndef get_pr_author_name(login: str) -> str:\n\tif len(DEVELOPERS) == 0:\n\t\tparse_contributors()\n\n\treturn DEVELOPERS[login] if login in DEVELOPERS else f\"@{login}\"\n\ndef parse_contributors():\n\tregex = re.compile(r'^\\s*?-\\s*?\\[([^]]+)\\]\\s*?\\(http.*/([^/]+)\\s*?\\)')\n\twith open('CONTRIBUTORS.md', 'rt') as handle:\n\t\tline = handle.readline()\n\t\twhile not ('##' in line and 'Contributors' in line):\n\t\t\tmatch = regex.match(line)\n\t\t\tif match:\n\t\t\t\tDEVELOPERS[match.group(2)] = match.group(1)\n\t\t\tline = handle.readline()\n\ndef create_pr_branch(day: datetime.date, author: str, base_branch: str) -> str:\n\tbranch_name = f\"changelog-updates-for-{day}-{author} {base_branch}\"\n\tcommand = f\"git checkout -b {branch_name}\"\n\tproc = subprocess.Popen(command, shell=True, stderr=subprocess.PIPE)\n\t_, errors = proc.communicate()\n\tif proc.returncode != 0:\n\t\tprint(errors)\n\t\texit(1)\n\n\treturn branch_name\n\ndef push_pr_branch(branch_name: str):\n\tcommand = f\"git push origin {branch_name}\"\n\tproc = subprocess.Popen(command, shell=True, stderr=subprocess.PIPE)\n\t_, errors = proc.communicate()\n\tif proc.returncode != 0:\n\t\tprint(errors)\n\t\texit(1)\n\ndef run(source_repository: str, target_repository: str, today: datetime.date):\n\tday = today - datetime.timedelta(days=1)\n\tprs = get_prs(source_repository, day)\n\tprs_length = len(prs)\n\tprint(f\"Found {prs_length} PRs\")\n\tif prs_length == 0:\n\t\treturn\n\n\tmerged_by_prs_map = parse_prs(prs)\n\tcreate_prs(target_repository, merged_by_prs_map, day)\n\nif __name__ == \"__main__\":\n\t# disable pager\n\tos.environ[\"GH_PAGER\"] = ''\n\t# set variables for Git\n\tos.environ[\"GIT_AUTHOR_NAME\"] = \"changelog-pr-bot\"\n\tos.environ[\"GIT_AUTHOR_EMAIL\"] = \"dummy@coreruleset.org\"\n\tos.environ[\"GIT_COMMITTER_NAME\"] = \"changelog-pr-bot\"\n\tos.environ[\"GIT_COMMITTER_EMAIL\"] = \"dummy@coreruleset.org\"\n\n\tsource_repository = 'coreruleset/coreruleset'\n\ttarget_repository = source_repository\n\t# the cron schedule for the workflow uses UTC\n\ttoday = datetime.datetime.now(datetime.timezone.utc).date()\n\n\tif len(sys.argv) > 1:\n\t\tsource_repository = sys.argv[1]\n\tif len(sys.argv) > 2:\n\t\ttarget_repository = sys.argv[2]\n\tif len(sys.argv) > 3:\n\t\ttoday = datetime.date.fromisoformat(sys.argv[3])\n\trun(source_repository, target_repository, today)\n", "path": ".github/create-changelog-prs.py"}]}