Repository: sunpy/sunpy

Issue:
Make a bot to automatically rename changelog files
It's a bit tedious to write some code, commit, open a PR, and then have to go back to rename the changelog entries to match the PR number after the PR has been opened. Instead it would be super nice if we had a bot that automatically renamed newly added changelog entries of the form `xxxx.breaking.rst` etc. to the correct PR number once the PR had been opened.


Discussion:
Two thoughts:

- When you say `xxxx.breaking.rst` as an example, I hope you mean that the filename literally has the string "xxxx" rather than a (guess at the) number.  In rare cases, we need to update a past changelog entry in a newer PR, so I wouldn't want to fight a bot in those cases.
- I'd imagine that the implementation would have to add a commit to the user's branch.  I think less-experienced developers are always thrown a little when their online branch is moved ahead of their local branch without their action.  Granted, in this case, since the developer would need to actively name a file `xxxx.breaking.rst`, perhaps the developer documentation would just need to be crystal clear what to do immediately afterwards to re-sync the local branch.
>   When you say `xxxx.breaking.rst` as an example, I hope you mean that the filename literally has the string "xxxx" rather than a (guess at the) number.

Yes.

To throw another idea out there, to avoid updating user branches perhaps the bot could rename the file after the PR has been merged by adding a commit to the `main` branch.
To be honest I would rather have a bot where you type the changelog entry in a PR comment and it adds the changelog for you. The hard part of either of these workflows is teaching the bot how to commit.

Resolving patch:
diff --git a/tools/write_changelog_entry.py b/tools/write_changelog_entry.py
new file mode 100644
--- /dev/null
+++ b/tools/write_changelog_entry.py
@@ -0,0 +1,81 @@
+import os
+import re
+import glob
+import subprocess
+
+
+def inc(obj, key):
+    """Increment a dictionary[key] value."""
+    try:
+        obj[key] += 1
+    except KeyError:
+        obj[key] = 1
+
+
+# Load PR number, comment and bot name
+try:
+    pr = os.environ['PR_NUMBER']
+    body = os.environ['PR_BODY']
+    bot = os.environ['BOT_AUTHOR']
+except KeyError:
+    raise ValueError('PR_NUMBER, PR_BODY and BOT_AUTHOR env vars must be set.')
+assert str(int(pr)) == pr
+body = body.replace('\\r', '').replace('\\n', '\n')
+
+# Get changelog directory
+# TODO: get directory from pyproject.toml
+path = os.path.join(os.getenv('CHANGELOG_DIR', 'changelog'))
+if not os.path.exists(path):
+    raise ValueError(f"CHANGELOG_DIR path '{path}' must exist.")
+
+# Check if bot has control of the changelogs
+existing = glob.glob(os.path.join(path, f"{pr}.*.rst"))
+for entry in existing:
+    author = subprocess.check_output(["git", "log", "-s", "-n1", "--pretty=%an <%ae>", entry], text=True).strip()
+    if author != bot:
+        print(f"{bot!r} is not modifying any changelogs because")
+        print(f"{entry!r} was last modified by {author!r}.")
+        exit(0)
+
+# Remove existing
+for entry in existing:
+    os.remove(entry)
+    print(f"Deleted existing changelog: {entry}")
+
+# Search for changelog entries
+#     markdown heading of "Changelog [category]"
+#     followed by a message in the next ``` code block.
+body = re.sub("(<!--.*?-->)", "", body, flags=re.DOTALL)
+regex = r"[#]+[ ]*Changelog[ ]*\[([a-zA-Z]+)\]*[^`]*```([^`]*)```"
+results = re.findall(regex, body)
+
+# Count entries per category
+count = {}
+for category, _ in results:
+    inc(count, category)
+
+# Generates (filename, message) pairs
+entries = []  # list of changelogs
+n = {}  # counter for each category
+for category, text in results:
+
+    if count[category] > 1:  # include filename [.COUNTER]
+        inc(n, category)
+        counter = f'.{n[category]}'
+    else:
+        counter = ''
+
+    filename = f"{pr}.{category}{counter}.rst"
+    text = text.strip()
+    if len(text) > 0:  # ignore empty messages
+        entries += [(filename, text)]
+
+if len(entries) == 0:
+    print("No new changelog entries found.")
+    exit(0)
+
+for filename, text in entries:
+    full_path = os.path.join(path, filename)
+    with open(full_path, 'w') as f:
+        f.write(text + '\n')
+    print(f"Wrote new changelog: {full_path}")


Test patch: