Skip to content

Commit

Permalink
Changelog Validation & Merge Upstream Workflow (#992)
Browse files Browse the repository at this point in the history
## Что этот PR делает
Портирует с пары валидацию чейнджлога и мерге апстрим.
Предложения по эмодзи и контексту перевода приветствуются.

В боте думаю оставить только отправку в дис без сохранения в БД. ТГ
чейнджлог в файле скорее всего может быть использован для игры.

rscadd="✨"
bugfix="🩹"
rscdel="🗑️"
qol="🌿"
sound="🎶"
image="🖼️"
map="🗺️"
spellcheck="📝"
balance="⚖️"
code_imp="🔨"
refactor="🛠️"
config="⚙️"
admin="🪄"
server="🛡️"

## Тестирование


![image](https://github.com/user-attachments/assets/4c9c67a1-cfb9-4d57-a225-1e32c955a943)

Тестовый мерге:
m-dzianishchyts#6

Актион:

https://github.com/m-dzianishchyts/BandaStation/actions/runs/12772336604/job/35601875054

Публикация в дисе:

https://discord.com/channels/1097181193939730453/1097880873992454164/1328770007169237122

## Summary by Sourcery

Update the changelog validation process and implement a workflow for
merging upstream changes.

New Features:
- Add automatic changelog translation using OpenAI API.

Enhancements:
- Improve changelog format and validation rules.
- Standardize changelog tags and their meanings.

Build:
- Add scripts for merging upstream changes and validating changelogs.

CI:
- Add a continuous integration workflow to automatically merge upstream
changes and validate changelogs.
  • Loading branch information
m-dzianishchyts authored Jan 15, 2025
1 parent e045a4c commit 56cbc9d
Show file tree
Hide file tree
Showing 9 changed files with 855 additions and 11 deletions.
23 changes: 13 additions & 10 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,20 @@
## Changelog

:cl:
add: Что-то добавил
del: Что-то удалил
tweak: Поменял что-то по мелочи
add: Изменил геймплей или добавил новую механику
fix: Что-то починил
wip: Какие-либо наработки в процессе
soundadd: Добавил новый звук
sounddel: Удалил старый звук
imageadd: Добавил новую картинку
imagedel: Удалил старую картинку
spellcheck: Исправил опечатку
experiment: Добавил экспериментальную функцию
del: Что-то удалил
qol: Сделал что-то удобнее
sound: Добавил, изменил или удалил звук
image: Добавил, изменил или удалил картинку
map: Добавил, изменил или удалил что-то на карте
typo: Исправил опечатку
code_imp: Незначительно улучшил качество кода
refactor: Значительно улучшил качество кода
balance: Сделал правки в балансе
config: Изменил что-то в конфиге
admin: Поменял кнопки админам
server: Изменил что-то серверное, о чем должен знать хост
/:cl:

<!-- Оба :cl:'а должны быть на месте, что-бы чейнджлог работал! Вы можете написать свой ник справа от первого :cl:, если хотите. Иначе будет использован ваш ник на ГитХабе. -->
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/auto_changelog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ permissions:
jobs:
auto_changelog:
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true
if: github.event.pull_request.merged == true && github.head_ref != 'merge-upstream'
steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down
45 changes: 45 additions & 0 deletions .github/workflows/check_changelog.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Changelog validation

permissions:
contents: read
pull-requests: write
issues: write

on:
pull_request_target:
types: [opened, reopened, edited, labeled, unlabeled, ready_for_review]

jobs:
CheckCL:
runs-on: ubuntu-latest
if: github.base_ref == 'master' && github.event.pull_request.draft == false

steps:
- id: create_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}

- run: echo "GH_TOKEN=${{ steps.create_token.outputs.token }}" >> "$GITHUB_ENV"

- name: Downloading scripts
run: |
wget https://raw.githubusercontent.com/${{ github.repository }}/${{ github.base_ref }}/tools/changelog/changelog_utils.py
wget https://raw.githubusercontent.com/${{ github.repository }}/${{ github.base_ref }}/tools/changelog/check_changelog.py
wget https://raw.githubusercontent.com/${{ github.repository }}/${{ github.base_ref }}/tools/changelog/tags.yml
- name: Installing Python
uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1
with:
python-version: '3.x'

- name: Installing deps
run: |
python -m pip install --upgrade pip
pip install ruamel.yaml PyGithub
- name: Changelog validation
env:
GITHUB_TOKEN: ${{ env.GH_TOKEN }}
run: python check_changelog.py
50 changes: 50 additions & 0 deletions .github/workflows/merge_upstream.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Merge Upstream

on:
workflow_dispatch:

jobs:
merge-upstream:
runs-on: ubuntu-latest

steps:
- id: create_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}

- run: echo "GH_TOKEN=${{ steps.create_token.outputs.token }}" >> "$GITHUB_ENV"

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.x

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install PyGithub openai
- name: Download the script
run: |
wget https://raw.githubusercontent.com/${{ github.repository }}/${{ github.ref_name }}/tools/changelog/changelog_utils.py
wget https://raw.githubusercontent.com/${{ github.repository }}/${{ github.ref_name }}/tools/merge-upstream/merge_upstream.py
wget https://raw.githubusercontent.com/${{ github.repository }}/${{ github.ref_name }}/tools/merge-upstream/translation_context.txt
- name: Run the script
env:
GITHUB_TOKEN: ${{ env.GH_TOKEN }}
TARGET_REPO: 'ss220club/BandaStation'
TARGET_BRANCH: 'master'
UPSTREAM_REPO: 'tgstation/tgstation'
UPSTREAM_BRANCH: 'master'
MERGE_BRANCH: 'merge-upstream'
CHANGELOG_AUTHOR: 'tgstation'
TRANSLATE_CHANGES: 'true'
OPENAI_API_KEY: ${{ secrets.ORG_EMPTY_TOKEN }}
LOG_LEVEL: ${{ runner.debug && 'DEBUG' || 'INFO' }}
run: |
git config --global user.email "[email protected]"
git config --global user.name "Upstream Sync"
python3 -u merge_upstream.py
99 changes: 99 additions & 0 deletions tools/changelog/changelog_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import re
import copy

CL_INVALID = ":scroll: CL невалиден"
CL_VALID = ":scroll: CL валиден"
CL_NOT_NEEDED = ":scroll: CL не требуется"

CL_BODY = re.compile(r"(:cl:|🆑)[ \t]*(?P<author>.+?)?\s*\n(?P<content>(.|\n)*?)\n/(:cl:|🆑)", re.MULTILINE)
CL_SPLIT = re.compile(r"\s*(?:(?P<tag>\w+)\s*:)?\s*(?P<message>.*)")

DISCORD_TAG_EMOJI = {
"rscadd": ":sparkles:",
"bugfix": ":adhesive_bandage:",
"rscdel": ":wastebasket:",
"qol": ":herb:",
"sound": ":notes:",
"image": ":frame_photo:",
"map": ":map:",
"spellcheck": ":pencil:",
"balance": ":scales:",
"code_imp": ":hammer:",
"refactor": ":tools:",
"config": ":gear:",
"admin": ":magic_wand:",
"server": ":shield:"
}


def build_changelog(pr: dict, tags_config: dict) -> dict:
changelog = parse_changelog(pr.body, tags_config)
if changelog is None:
raise Exception("Failed to parse the changelog. Check changelog format.")
changelog["author"] = changelog["author"] or pr.user.login
return changelog


def emojify_changelog(changelog: dict):
changelog_copy = copy.deepcopy(changelog)
for change in changelog_copy["changes"]:
if change["tag"] in DISCORD_TAG_EMOJI:
change["tag"] = DISCORD_TAG_EMOJI[change["tag"]]
else:
raise Exception(f"Invalid tag for emoji: {change}")
return changelog_copy


def validate_changelog(changelog: dict):
if not changelog:
raise Exception("No changelog.")
if not changelog["author"]:
raise Exception("The changelog has no author.")
if len(changelog["changes"]) == 0:
raise Exception("No changes found in the changelog. Use special label if changelog is not expected.")


def parse_changelog(pr_body: str, tags_config: dict | None = None) -> dict | None:
clean_pr_body = re.sub(r"<!--.*?-->", "", pr_body, flags=re.DOTALL)
cl_parse_result = CL_BODY.search(clean_pr_body)
if cl_parse_result is None:
return None

cl_changes = []
for cl_line in cl_parse_result.group("content").splitlines():
if not cl_line:
continue
change_parse_result = CL_SPLIT.search(cl_line)
if not change_parse_result:
raise Exception(f"Invalid change: '{cl_line}'")
tag = change_parse_result["tag"]
message = change_parse_result["message"]

if tags_config and tag and tag not in tags_config['tags'].keys():
raise Exception(f"Invalid tag: '{cl_line}'. Valid tags: {', '.join(tags_config['tags'].keys())}")
if not message:
raise Exception(f"No message for change: '{cl_line}'")

message = message.strip()

if tags_config and message in list(tags_config['defaults'].values()): # Check to see if the tags are associated with something that isn't the default text
raise Exception(f"Don't use default message for change: '{cl_line}'")
if tag:
cl_changes.append({
"tag": tags_config['tags'][tag] if tags_config else tag,
"message": message
})
# Append line without tag to the previous change
else:
if len(cl_changes):
prev_change = cl_changes[-1]
prev_change["message"] += f" {message}"
else:
raise Exception(f"Change with no tag: {cl_line}")

if len(cl_changes) == 0:
raise Exception("No changes found in the changelog. Use special label if changelog is not expected.")
return {
"author": str.strip(cl_parse_result.group("author") or "") or None, # I want this to be None, not empty
"changes": cl_changes
}
85 changes: 85 additions & 0 deletions tools/changelog/check_changelog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""
DO NOT MANUALLY RUN THIS SCRIPT.
---------------------------------
Expected environmental variables:
-----------------------------------
GITHUB_REPOSITORY: Github action variable representing the active repo (Action provided)
GITHUB_TOKEN: A repository account token, this will allow the action to push the changes (Action provided)
GITHUB_EVENT_PATH: path to JSON file containing the event info (Action provided)
"""
import os
from pathlib import Path
from ruamel.yaml import YAML
from github import Github
import json

import changelog_utils

# Blessed is the GoOnStAtIoN birb ZeWaKa for thinking of this first
repo = os.getenv("GITHUB_REPOSITORY")
token = os.getenv("GITHUB_TOKEN")
event_path = os.getenv("GITHUB_EVENT_PATH")

with open(event_path, 'r') as f:
event_data = json.load(f)

git = Github(token)
repo = git.get_repo(repo)
pr = repo.get_pull(event_data['number'])

pr_body = pr.body or ""
pr_author = pr.user.login
pr_labels = pr.labels

pr_is_mirror = pr.title.startswith("[MIRROR]")

has_valid_label = False
has_invalid_label = False
cl_required = True
for label in pr_labels:
print("Found label: ", label.name)
if label.name == changelog_utils.CL_NOT_NEEDED:
print("No CL needed!")
cl_required = False
if label.name == changelog_utils.CL_VALID:
has_valid_label = True
if label.name == changelog_utils.CL_INVALID:
has_invalid_label = True

if pr_is_mirror:
cl_required = False

if not cl_required:
# remove invalid, remove valid
if has_invalid_label:
pr.remove_from_labels(changelog_utils.CL_INVALID)
if has_valid_label:
pr.remove_from_labels(changelog_utils.CL_VALID)
exit(0)

try:
with open(Path.cwd().joinpath("tags.yml")) as file:
yaml = YAML(typ = 'safe', pure = True)
tags_config = yaml.load(file)
cl = changelog_utils.build_changelog(pr, tags_config)
cl_emoji = changelog_utils.emojify_changelog(cl)
cl_emoji["author"] = cl_emoji["author"] or pr_author
changelog_utils.validate_changelog(cl_emoji)
except Exception as e:
print("Changelog parsing error:")
print(e)

# add invalid, remove valid
if not has_invalid_label:
pr.add_to_labels(changelog_utils.CL_INVALID)
if has_valid_label:
pr.remove_from_labels(changelog_utils.CL_VALID)
exit(1)

# remove invalid, add valid
if has_invalid_label:
pr.remove_from_labels(changelog_utils.CL_INVALID)
if not has_valid_label:
pr.add_to_labels(changelog_utils.CL_VALID)
print("Changelog is valid.")
47 changes: 47 additions & 0 deletions tools/changelog/tags.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
tags:
rscadd: 'rscadd'
add: 'rscadd'
adds: 'rscadd'
bugfix: 'bugfix'
fix: 'bugfix'
fixes: 'bugfix'
rscdel: 'rscdel'
del: 'rscdel'
dels: 'rscdel'
qol: 'qol'
sound: 'sound'
image: 'image'
map: 'map'
spellcheck: 'spellcheck'
typo: 'spellcheck'
balance: 'balance'
code_imp: 'code_imp'
code: 'code_imp'
refactor: 'refactor'
config: 'config'
admin: 'admin'
server: 'server'

defaults:
rscadd: 'Изменил геймплей или добавил новую механику'
add: 'Изменил геймплей или добавил новую механику'
adds: 'Изменил геймплей или добавил новую механику'
bugfix: 'Что-то починил'
fix: 'Что-то починил'
fixes: 'Что-то починил'
rscdel: 'Что-то удалил'
del: 'Что-то удалил'
dels: 'Что-то удалил'
qol: 'Сделал что-то удобнее'
sound: 'Добавил, изменил или удалил звук'
image: 'Добавил, изменил или удалил картинку'
map: 'Добавил, изменил или удалил что-то на карте'
spellcheck: 'Исправил опечатку'
typo: 'Исправил опечатку'
code_imp: 'Незначительно улучшил качество кода'
code: 'Незначительно улучшил качество кода'
balance: 'Сделал правки в балансе'
refactor: 'Значительно улучшил качество кода'
config: 'Изменил что-то в конфиге'
admin: 'Поменял кнопки админам'
server: 'Изменил что-то серверное, о чем должен знать хост'
Loading

0 comments on commit 56cbc9d

Please sign in to comment.