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 2496e73 commit 7eae803
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 1 deletion.
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:
push:
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 }}
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
# Global tasks.
# =============================================================================
PYTHON_VERSION := python3.11
LINTER_CHECKED_DIRS := config itou tests
LINTER_CHECKED_DIRS := config itou scripts tests
PGDATABASE ?= itou
REQUIREMENTS_PATH ?= requirements/dev.txt

Expand Down
142 changes: 142 additions & 0 deletions scripts/mkchangelog
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#!/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):
gh("pr", "create", "--fill", "--label", "no-changelog", "--base", os.environ["GITHUB_BASE_REF"], "--head", branch)


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):
if pull_request["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}"
subprocess.run(["git", "switch", "--create", branch], check=True)
subprocess.run(
[
"git",
"-c",
"user.name=Changelog Bot",
"-c",
"[email protected]",
"commit",
"--all",
"--message",
f"changelog: Sprint {sprint_number}",
],
check=True,
)
subprocess.run(["git", "push", "--set-upstream", "origin", "changelog/2024-04-22"], check=True)
open_pull_request(branch)


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 7eae803

Please sign in to comment.