-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Automatically populate the changelog from GitHub PRs
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
1 parent
05bb941
commit edf5547
Showing
2 changed files
with
164 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.") |