Skip to content

Commit

Permalink
Automatically populate the changelog from GitHub PRs
Browse files Browse the repository at this point in the history
Use the `gh` tool, as it’s available by default on GitHub actions.

Crawl the merged PRs, find those in the merge window (based on the
previous sprint number and date), list their title. When screenshots are
available in the PR, make a link to the pull request to facilitate
the demo.
  • Loading branch information
changelog bot authored and francoisfreitag committed Apr 18, 2024
1 parent 05bb941 commit edf5547
Show file tree
Hide file tree
Showing 2 changed files with 164 additions and 0 deletions.
20 changes: 20 additions & 0 deletions .github/workflows/mkchangelog.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: 📃 Changelog

on:
pull_request:
schedule:
- cron: 7 0 * * 0

jobs:
generate-changelog:
runs-on: ubuntu-latest
steps:
- uses: actions/[email protected]
- name: 💂 Install Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: 📥 Generate changelog
run: scripts/mkchangelog
env:
GH_TOKEN: ${{ secrets.ITOU_TECH_GH_TOKEN }}
144 changes: 144 additions & 0 deletions scripts/mkchangelog
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#!/usr/bin/env python3

import datetime
import json
import logging
import os
import pathlib
import re
import subprocess
import tempfile


logger = logging.getLogger(__name__)


def gh(*args, **kwargs):
return subprocess.run(
["gh", *args],
check=True,
env={
"GH_TOKEN": os.getenv("GH_TOKEN", ""),
"PATH": os.environ["PATH"],
},
**kwargs,
)


def list_merged_pull_requests():
logger.info("Reading pull requests from repository.")
result = gh(
"api",
"/repos/gip-inclusion/les-emplois/pulls?state=closed&sort=updated&direction=desc",
"--header",
"Accept: application/vnd.github+json",
"--header",
"X-GitHub-Api-Version: 2022-11-28",
"--paginate",
capture_output=True,
)
return result.stdout


def open_pull_request(branch, title):
gh("pr", "create", "--label", "no-changelog", "--title", title, "--body", "")


def main():
changelog_path = pathlib.Path("CHANGELOG.md")
with (
open(changelog_path, "r+") as changelog,
tempfile.NamedTemporaryFile(dir=os.getcwd(), mode="w+", suffix="~", delete=False) as new_changelog,
):
try:
for line in changelog:
if line.strip() in ["# Journal des modifications", ""]:
new_changelog.write(line)
continue
break
last_entry_header = line

m = re.match(r"## \[(?P<sprint>[0-9]+)\] - (?P<date>[0-9]+-[0-9]+-[0-9]+)", last_entry_header)
sprint_number = int(m.group("sprint")) + 1
sprint_start = datetime.date.fromisoformat(m.group("date"))
sprint_end = sprint_start + datetime.timedelta(days=14)

merged_pull_requests = list_merged_pull_requests()
added, changed, removed = [], [], []
for pull_request in json.loads(merged_pull_requests):
updated_at = datetime.datetime.fromisoformat(pull_request["updated_at"])
if updated_at.date() < sprint_start:
break
merged_at = pull_request["merged_at"]
if merged_at is None:
# Closed pull request.
continue
merged_at = datetime.datetime.fromisoformat(merged_at)
labels = pull_request["labels"]
if sprint_start <= merged_at.date() < sprint_end and not any(
label["name"] in ["no-changelog", "dependencies"] for label in labels
):
title = re.sub(r"\[(GEN|ITOU)-[0-9]+\]", "", pull_request["title"]).strip()
if re.search(r"\!\[[^]]+]", pull_request["body"]):
title = f"[{title}]({pull_request['html_url']})"
if any(label["name"] == "ajouté" for label in labels):
added.append(title)
elif any(label["name"] == "supprimé" for label in labels):
removed.append(title)
else:
changed.append(title)

if added or changed or removed:
new_changelog.write(f"## [{sprint_number}] - {sprint_end}\n\n")
if added:
new_changelog.write("### Ajouté\n\n")
for title in sorted(added):
new_changelog.write(f"- {title}\n")
new_changelog.write("\n")
if changed:
new_changelog.write("### Modifié\n\n")
for title in sorted(changed):
new_changelog.write(f"- {title}\n")
new_changelog.write("\n")
if removed:
new_changelog.write("### Supprimé\n\n")
for title in sorted(removed):
new_changelog.write(f"- {title}\n")
new_changelog.write("\n")

# Don’t forget the previous header.
new_changelog.write(last_entry_header)
for line in changelog:
new_changelog.write(line)

# Atomically replace file.
pathlib.Path(new_changelog.name).rename(changelog_path)
finally:
pathlib.Path(new_changelog.name).unlink(missing_ok=True)

branch = f"changelog/{sprint_end}"
title = f"changelog: Sprint {sprint_number}"
subprocess.run(["git", "switch", "--create", branch], check=True)
subprocess.run(
[
"git",
"-c",
"user.name=Changelog Bot",
"-c",
"[email protected]",
"commit",
"--all",
"--message",
title,
],
check=True,
)
subprocess.run(["git", "push", "--set-upstream", "origin", "changelog/2024-04-22"], check=True)
open_pull_request(branch, title)


if __name__ == "__main__":
if datetime.date.today().isocalendar().week % 2 == 0:
main()
else:
logger.info("Sprint in progress, nothing to do.")

0 comments on commit edf5547

Please sign in to comment.