Skip to content

Commit

Permalink
Add workflow for changelog
Browse files Browse the repository at this point in the history
  • Loading branch information
Vincent056 committed Mar 23, 2023
1 parent e746b53 commit 5f19d9a
Show file tree
Hide file tree
Showing 4 changed files with 340 additions and 0 deletions.
24 changes: 24 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Description

Please include a summary of the changes and the related issue.

Related Issue Link (if applicable)

## Type of change

Only Select one of the change type

- [ ] Enhancements (Adds functionality)
- [ ] Fixes (Fixes an issue, please reference the issue)
- [ ] Internal Changes (Documentation, Tests, etc.)
- [ ] Deprecations (Signals removal of a feature)
- [ ] Removals (Removes a feature)
- [ ] Security (Fixes a vulnerability)
- [ ] No Changelog (Release PR, Changelog PR, this PR will not be added to the changelog)

# How Has This Been Tested?

- [ ] Unit Test
- [ ] E2E Test
- [ ] Manual Test
- [ ] Not Applicable
45 changes: 45 additions & 0 deletions .github/workflows/check-pr-description.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: "check-pr-description"

on:
pull_request:
branches: [ master ]

jobs:
check-description:
name: Check PR description to see if everything has been filled in
runs-on: ubuntu-latest
steps:
- uses: 8BitJonny/[email protected]
id: PR
with:
# Authetication token to access GitHub APIs. (Can be omitted by default.)
github-token: ${{ github.token }}
# Verbose setting SHA when using Pull_Request event trigger to fix #16. (For push even trigger this is not necessary.)
sha: ${{ github.event.pull_request.head.sha }}
# Only return if PR is still open. (By default it returns PRs in any state.)
filterOutClosed: true
# Only return if PR is not in draft state. (By default it returns PRs in any state.)
filterOutDraft: true

- name: Get PR description and save it to a file
if: steps.PR.outputs.pr_found == 'true'
run: |
echo "PR description: ${{ steps.PR.outputs.pr_body }}"
echo "${{ steps.PR.outputs.pr_body }}" > pr_description.txt
- name: Get python script to check PR description
if: steps.PR.outputs.pr_found == 'true'
run: |
curl -o pr_utility.py https://raw.githubusercontent.com/Vincent056/compliance-operator/test-pr/utils/pr_utility.py
chmod +x pr_utility.py
- name: Install Python
if: steps.PR.outputs.pr_found == 'true'
run: |
sudo apt-get update
sudo apt-get install python3-pip
- name: Check PR description to see if everything has been filled in correctly
if: steps.PR.outputs.pr_found == 'true'
run: |
python3 pr_utility.py -i pr_description.txt -p
68 changes: 68 additions & 0 deletions .github/workflows/merge-pr-description.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: "merge-pr-description"

on:
pull_request:
types: [ closed ]

jobs:
generate-changelog:
name: Check PR description to see if everything has been filled in
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0

- uses: 8BitJonny/[email protected]
id: PR
with:
# Authetication token to access GitHub APIs. (Can be omitted by default.)
github-token: ${{ github.token }}
# Verbose setting SHA when using Pull_Request event trigger to fix #16. (For push even trigger this is not necessary.)
sha: ${{ github.event.pull_request.head.sha }}
# Only return if PR is still open. (By default it returns PRs in any state.)
filterOutClosed: false
# Only return if PR is not in draft state. (By default it returns PRs in any state.)
filterOutDraft: true

- name: Get PR description and save it to a file
if: steps.PR.outputs.pr_found == 'true'
run: |
echo "PR description: ${{ steps.PR.outputs.pr_body }}"
echo "${{ steps.PR.outputs.pr_body }}" > pr_description.txt
- name: Get python script to check PR description
if: steps.PR.outputs.pr_found == 'true'
run: |
curl -o pr_utility.py https://raw.githubusercontent.com/Vincent056/compliance-operator/test-pr/utils/pr_utility.py
chmod +x pr_utility.py
- name: Install Python
if: steps.PR.outputs.pr_found == 'true'
run: |
sudo apt-get update
sudo apt-get install python3-pip
- name: Save PR description to a file
if: steps.PR.outputs.pr_found == 'true'
run: |
python3 pr_utility.py -i pr_description.txt -p > pr_output.json
- name: Check if PR is needed to be added to CHANGELOG.md
if: steps.PR.outputs.pr_found == 'true'
id: needs_changelog
run: |
NO_CHANGELOG=$(cat pr_output.json | jq '."change_type"."not_applicable"')
echo "NO_CHANGELOG=$NO_CHANGELOG" >> $GITHUB_OUTPUT
- name: Update Changelog
if: steps.needs_changelog.outputs.NO_CHANGELOG == 'false'
run: |
python3 pr_utility.py -u -c ./CHANGELOG.md -i pr_output.json -pr ${{ github.event.pull_request.number }}
git config user.name github-actions[bot]
git config user.email github-actions[bot]@users.noreply.github.com
git add CHANGELOG.md
git commit -m "Update CHANGELOG.md for PR ${{ github.event.pull_request.number }}"
git push
203 changes: 203 additions & 0 deletions utils/pr_utility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import argparse
import json
import os, fileinput

def parse_description(description_file):
# Read the description file
with open(description_file, 'r') as f:
description = f.read()

# Initialize the JSON object
json_data = {
'description': '',
'change_type': {
'enhancements': False,
'fixes': False,
'internal_changes': False,
'deprecations': False,
'removals': False,
'security': False,
'not_applicable': False
},
'testing': {
'unit_test': False,
'e2e_test': False,
'manual_test': False,
'not_applicable': False
}
}

# Parse the description from the input
for line in description.split('\n'):
if line.startswith('## Description'):
continue
elif line.startswith('## Type of change'):
break
else:
json_data['description'] += line

# Check description is templated text "Please include a summary of the changes and the related issue." or empty
if json_data['description'] == 'Please include a summary of the changes and the related issue.' or json_data['description'] == '':
raise ValueError("Description is empty or templated text.")

# Initialize the change types
change_types = {
'Enhancements (Adds functionality)': 'enhancements',
'Fixes (Fixes an issue, please reference the issue)': 'fixes',
'Internal Changes (Documentation, Tests, etc.)': 'internal_changes',
'Deprecations (Signals removal of a feature)': 'deprecations',
'Removals (Removes a feature)': 'removals',
'Security (Fixes a vulnerability)': 'security',
'No Changelog(Release PR, Changelog PR, this PR will not be added to the changelog)': 'not_applicable'
}

# Parse change types from the input
for line in description.split('\n'):
for key, value in change_types.items():
if line.startswith(f'- [x] {key}'):
json_data['change_type'][value] = True

# Perform input validation for change types
# Only one can be selected
if not any(json_data['change_type'].values()):
raise ValueError("No change type selected.")
elif sum(json_data['change_type'].values()) > 1:
raise ValueError("Only one change type can be selected.")

# Initialize the testing types
testing_types = {
'Unit Test': 'unit_test',
'E2E Test': 'e2e_test',
'Manual Test': 'manual_test',
'Not Applicable': 'not_applicable'
}

# Parse testing types from the input
for line in description.split('\n'):
for key, value in testing_types.items():
if line.startswith(f'- [x] {key}'):
json_data['testing'][value] = True

# Perform input validation for testing types
if not any(json_data['testing'].values()):
raise ValueError("No testing type selected.")
elif json_data['testing']['not_applicable'] and any(json_data['testing'].values()) and sum(json_data['testing'].values()) > 1:
raise ValueError("Not Applicable cannot be selected with other testing types.")

# Output the JSON object
return json.dumps(json_data, indent=4)

def match_pr_type_to_changelog_section(pr_type):
# Initialize the change types
change_types = {
'enhancements': 'Enhancements',
'fixes': 'Fixes',
'internal_changes': 'Internal Changes',
'deprecations': 'Deprecations',
'removals': 'Removals',
'security': 'Security'
}
# Return the matching changelog section
return change_types[pr_type]

def insert_changelog_entry(description_json_file, changelog_file, pr_number):
# Parse the json description from the input file
with open(description_json_file, 'r') as f:
json_data = json.load(f)


# Get the change type
change_type = ''
for key, value in json_data['change_type'].items():
if value:
change_type = key

# Get the description
description = json_data['description']

# Insert the new entry under the section for the given change type
with open(changelog_file, 'r+') as f:
# Read the file
changelog = f.read()

# Find the Unreleased section
unreleased_index = changelog.find(f"\n## Unreleased\n")

if unreleased_index == -1:
raise ValueError("Changelog Unreleased section not found.")

nextrelease_index = changelog.find(f"\n## [")

if nextrelease_index == -1:
nextrelease_index = len(changelog)

changelog_type = match_pr_type_to_changelog_section(change_type)

# Find the section for the given change type
section_index = changelog.find(f"\n### {changelog_type}\n", unreleased_index, nextrelease_index)

if section_index == -1:
raise ValueError("Changelog section not found.")


nextpond_index = changelog.find(f"\n##", section_index+1)

if nextpond_index == -1:
nextpond_index = len(changelog)

new_entry = f"\n- {description} (#{pr_number})\n\n"

next_entry_index = changelog.find(f"\n- ", section_index-1, nextpond_index)

if next_entry_index == -1:
if nextpond_index == nextrelease_index:
secondhalf_index = nextpond_index
else:
secondhalf_index = nextpond_index + 1
else:
secondhalf_index = next_entry_index + 1

first_half = changelog[:section_index + len(f"\n### {changelog_type}\n")]

second_half = changelog[secondhalf_index:]

changelog = first_half + new_entry + second_half

# Write the file
f.seek(0)
f.write(changelog)











if __name__ == '__main__':

parser = argparse.ArgumentParser(description='PR changelog utility')
parser.add_argument('-i', '--input', type=str, help='Description file')
parser.add_argument('-p', '--parse', action='store_true', help='Parse the description from the input file')
parser.add_argument('-u', '--update', action='store_true', help='Update the changelog file with the entry from the input file')
parser.add_argument('-c', '--changelog', type=str, help='Changelog file')
parser.add_argument('-pr', '--number', type=str, help='PR number')


args = parser.parse_args()

if args.parse:
if args.input:
print(parse_description(args.input))
else:
print("Input file not specified.")
elif args.update:
if args.input and args.changelog and args.number:
insert_changelog_entry(args.input, args.changelog, args.number)
else:
print("Input file, changelog file, or PR number not specified.")
else:
parser.print_help()

0 comments on commit 5f19d9a

Please sign in to comment.