From 8ac701b3c92cf412266de0d5f041d0dc039daf56 Mon Sep 17 00:00:00 2001 From: R-Palazzo Date: Thu, 9 Jan 2025 12:40:50 +0100 Subject: [PATCH 1/6] define workflow --- .github/workflows/prepare_release.yml | 60 +++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 .github/workflows/prepare_release.yml diff --git a/.github/workflows/prepare_release.yml b/.github/workflows/prepare_release.yml new file mode 100644 index 00000000..115b8ebf --- /dev/null +++ b/.github/workflows/prepare_release.yml @@ -0,0 +1,60 @@ +name: Release Prep + +on: + push: + branches: + - issue-364-prepare-release + workflow_dispatch: + inputs: + branch: + description: 'Branch to merge release notes and code analysis into.' + required: true + default: 'main' + version: + description: + 'Version to use for the release. Must be in format: X.Y.Z.' + date: + description: + 'Date of the release. Must be in format YYYY-MM-DD.' + +jobs: + preparerelease: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install requests==2.31.0 + python -m pip install bandit==1.7.7 + python -m pip install .[test] + + - name: Generate release notes + env: + GH_ACCESS_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }} + run: > + python scripts/release_notes_generator.py + -v ${{ inputs.version }} + -d ${{ inputs.date }} + + - name: Save static code analysis + run: bandit -r . -x ./tests,./scripts,./build -f txt -o static_code_analysis.txt --exit-zero + + - name: Create pull request + id: cpr + uses: peter-evans/create-pull-request@v4 + with: + token: ${{ secrets.GH_ACCESS_TOKEN }} + commit-message: Prepare release for v${{ inputs.version }} + author: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>" + committer: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>" + title: v${{ inputs.version }} Release Preparation + body: "This is an auto-generated PR to prepare the release." + branch: prepared-release + branch-suffix: short-commit-hash + base: ${{ inputs.branch }} From bcb257e6dd91cfe4e8f63a8956fb8cde9cd74bc1 Mon Sep 17 00:00:00 2001 From: R-Palazzo Date: Thu, 9 Jan 2025 12:43:17 +0100 Subject: [PATCH 2/6] delete static_code_analysis --- .github/workflows/static_code_analysis.yml | 35 ---------------------- 1 file changed, 35 deletions(-) delete mode 100644 .github/workflows/static_code_analysis.yml diff --git a/.github/workflows/static_code_analysis.yml b/.github/workflows/static_code_analysis.yml deleted file mode 100644 index 6bb8de34..00000000 --- a/.github/workflows/static_code_analysis.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Static Code Analysis - -on: - release: - types: [published] - workflow_dispatch: - -jobs: - code-analysis: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set up Python 3.10 - uses: actions/setup-python@v5 - with: - python-version: '3.10' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install bandit==1.7.7 - - name: Save code analysis - run: bandit -r . -x ./tests -f txt -o static_code_analysis.txt --exit-zero - - name: Create pull request - id: cpr - uses: peter-evans/create-pull-request@v4 - with: - token: ${{ secrets.GH_ACCESS_TOKEN }} - commit-message: Update static code analysis - author: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>" - committer: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>" - title: Latest Code Analysis - body: "This is an auto-generated PR with the **latest** code analysis results." - branch: static-code-analysis - branch-suffix: short-commit-hash - base: main From 021fa1ceddbd063b15c9228ad12f0599a137527a Mon Sep 17 00:00:00 2001 From: R-Palazzo Date: Thu, 9 Jan 2025 12:44:33 +0100 Subject: [PATCH 3/6] create release_note_generator --- scripts/release_notes_generator.py | 128 +++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 scripts/release_notes_generator.py diff --git a/scripts/release_notes_generator.py b/scripts/release_notes_generator.py new file mode 100644 index 00000000..7347f2c7 --- /dev/null +++ b/scripts/release_notes_generator.py @@ -0,0 +1,128 @@ +"""Script to generate release notes.""" + +import argparse +import os +from collections import defaultdict + +import requests + +LABEL_TO_HEADER = { + 'feature request': 'New Features', + 'bug': 'Bugs Fixed', + 'internal': 'Internal', + 'maintenance': 'Maintenance', + 'customer success': 'Customer Success', + 'documentation': 'Documentation', + 'misc': 'Miscellaneous', +} +ISSUE_LABELS = [ + 'documentation', + 'maintenance', + 'internal', + 'bug', + 'feature request', + 'customer success', +] +NEW_LINE = '\n' +GITHUB_URL = 'https://api.github.com/repos/sdv-dev/sdmetrics' +GITHUB_TOKEN = os.getenv('GH_ACCESS_TOKEN') + + +def _get_milestone_number(milestone_title): + url = f'{GITHUB_URL}/milestones' + headers = {'Authorization': f'Bearer {GITHUB_TOKEN}'} + query_params = {'milestone': milestone_title, 'state': 'all', 'per_page': 100} + response = requests.get(url, headers=headers, params=query_params) + body = response.json() + if response.status_code != 200: + raise Exception(str(body)) + milestones = body + for milestone in milestones: + if milestone.get('title') == milestone_title: + return milestone.get('number') + raise ValueError(f'Milestone {milestone_title} not found in past 100 milestones.') + + +def _get_issues_by_milestone(milestone): + headers = {'Authorization': f'Bearer {GITHUB_TOKEN}'} + # get milestone number + milestone_number = _get_milestone_number(milestone) + url = f'{GITHUB_URL}/issues' + page = 1 + query_params = {'milestone': milestone_number, 'state': 'all'} + issues = [] + while True: + query_params['page'] = page + response = requests.get(url, headers=headers, params=query_params) + body = response.json() + if response.status_code != 200: + raise Exception(str(body)) + issues_on_page = body + if not issues_on_page: + break + issues.extend(issues_on_page) + page += 1 + return issues + + +def _get_issues_by_category(release_issues): + category_to_issues = defaultdict(list) + for issue in release_issues: + issue_title = issue['title'] + issue_number = issue['number'] + issue_url = issue['html_url'] + line = f'* {issue_title} - Issue [#{issue_number}]({issue_url})' + assignee = issue.get('assignee') + if assignee: + login = assignee['login'] + line += f' by @{login}' + # Check if any known label is marked on the issue + labels = [label['name'] for label in issue['labels']] + found_category = False + for category in ISSUE_LABELS: + if category in labels: + category_to_issues[category].append(line) + found_category = True + break + if not found_category: + category_to_issues['misc'].append(line) + return category_to_issues + + +def _create_release_notes(issues_by_category, version, date): + title = f'## v{version} - {date}' + release_notes = f'{title}{NEW_LINE}{NEW_LINE}' + for category in ISSUE_LABELS + ['misc']: + issues = issues_by_category.get(category) + if issues: + section_text = ( + f'### {LABEL_TO_HEADER[category]}{NEW_LINE}{NEW_LINE}' + f'{NEW_LINE.join(issues)}{NEW_LINE}{NEW_LINE}' + ) + release_notes += section_text + return release_notes + + +def update_release_notes(release_notes): + """Add the release notes for the new release to the ``HISTORY.md``.""" + file_path = 'HISTORY.md' + with open(file_path, 'r') as history_file: + history = history_file.read() + token = '# HISTORY\n\n' + split_index = history.find(token) + len(token) + 1 + header = history[:split_index] + new_notes = f'{header}{release_notes}{history[split_index:]}' + with open(file_path, 'w') as new_history_file: + new_history_file.write(new_notes) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('-v', '--version', type=str, help='Release version number (ie. v1.0.1)') + parser.add_argument('-d', '--date', type=str, help='Date of release in format YYYY-MM-DD') + args = parser.parse_args() + release_number = args.version + release_issues = _get_issues_by_milestone(release_number) + issues_by_category = _get_issues_by_category(release_issues) + release_notes = _create_release_notes(issues_by_category, release_number, args.date) + update_release_notes(release_notes) From 3ca54cdda0ba8e05a79c02850db23e374df86ec7 Mon Sep 17 00:00:00 2001 From: R-Palazzo Date: Thu, 9 Jan 2025 12:45:06 +0100 Subject: [PATCH 4/6] remove trigger on pushes --- .github/workflows/prepare_release.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/prepare_release.yml b/.github/workflows/prepare_release.yml index 115b8ebf..a755683b 100644 --- a/.github/workflows/prepare_release.yml +++ b/.github/workflows/prepare_release.yml @@ -1,9 +1,6 @@ name: Release Prep on: - push: - branches: - - issue-364-prepare-release workflow_dispatch: inputs: branch: From 5839aab9cb7787a0aaf2fc6e465c20a577ccfd3f Mon Sep 17 00:00:00 2001 From: R-Palazzo Date: Thu, 9 Jan 2025 13:02:05 +0100 Subject: [PATCH 5/6] update release notes generator --- scripts/release_notes_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/release_notes_generator.py b/scripts/release_notes_generator.py index 7347f2c7..3e95a107 100644 --- a/scripts/release_notes_generator.py +++ b/scripts/release_notes_generator.py @@ -24,7 +24,7 @@ 'customer success', ] NEW_LINE = '\n' -GITHUB_URL = 'https://api.github.com/repos/sdv-dev/sdmetrics' +GITHUB_URL = 'https://api.github.com/repos/sdv-dev/sdgym' GITHUB_TOKEN = os.getenv('GH_ACCESS_TOKEN') From fa0eca8ea03b9a35ff1934cbf2db4aec545f7d6f Mon Sep 17 00:00:00 2001 From: R-Palazzo Date: Thu, 9 Jan 2025 18:13:53 +0100 Subject: [PATCH 6/6] use enterprise list --- scripts/release_notes_generator.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/scripts/release_notes_generator.py b/scripts/release_notes_generator.py index 3e95a107..18a8c58f 100644 --- a/scripts/release_notes_generator.py +++ b/scripts/release_notes_generator.py @@ -23,6 +23,14 @@ 'feature request', 'customer success', ] +ISSUE_LABELS_ORDERED_BY_IMPORTANCE = [ + 'feature request', + 'customer success', + 'bug', + 'documentation', + 'internal', + 'maintenance', +] NEW_LINE = '\n' GITHUB_URL = 'https://api.github.com/repos/sdv-dev/sdgym' GITHUB_TOKEN = os.getenv('GH_ACCESS_TOKEN') @@ -36,10 +44,12 @@ def _get_milestone_number(milestone_title): body = response.json() if response.status_code != 200: raise Exception(str(body)) + milestones = body for milestone in milestones: if milestone.get('title') == milestone_title: return milestone.get('number') + raise ValueError(f'Milestone {milestone_title} not found in past 100 milestones.') @@ -57,16 +67,22 @@ def _get_issues_by_milestone(milestone): body = response.json() if response.status_code != 200: raise Exception(str(body)) + issues_on_page = body if not issues_on_page: break + + # Filter our PRs + issues_on_page = [issue for issue in issues_on_page if issue.get('pull_request') is None] issues.extend(issues_on_page) page += 1 + return issues def _get_issues_by_category(release_issues): category_to_issues = defaultdict(list) + for issue in release_issues: issue_title = issue['title'] issue_number = issue['number'] @@ -76,6 +92,7 @@ def _get_issues_by_category(release_issues): if assignee: login = assignee['login'] line += f' by @{login}' + # Check if any known label is marked on the issue labels = [label['name'] for label in issue['labels']] found_category = False @@ -84,22 +101,27 @@ def _get_issues_by_category(release_issues): category_to_issues[category].append(line) found_category = True break + if not found_category: category_to_issues['misc'].append(line) + return category_to_issues def _create_release_notes(issues_by_category, version, date): title = f'## v{version} - {date}' release_notes = f'{title}{NEW_LINE}{NEW_LINE}' - for category in ISSUE_LABELS + ['misc']: + + for category in ISSUE_LABELS_ORDERED_BY_IMPORTANCE + ['misc']: issues = issues_by_category.get(category) if issues: section_text = ( f'### {LABEL_TO_HEADER[category]}{NEW_LINE}{NEW_LINE}' f'{NEW_LINE.join(issues)}{NEW_LINE}{NEW_LINE}' ) + release_notes += section_text + return release_notes @@ -108,10 +130,12 @@ def update_release_notes(release_notes): file_path = 'HISTORY.md' with open(file_path, 'r') as history_file: history = history_file.read() + token = '# HISTORY\n\n' split_index = history.find(token) + len(token) + 1 header = history[:split_index] new_notes = f'{header}{release_notes}{history[split_index:]}' + with open(file_path, 'w') as new_history_file: new_history_file.write(new_notes)