diff --git a/.github/workflows/experimental.yml b/.github/workflows/experimental.yml index e1f735122..e1098b92b 100644 --- a/.github/workflows/experimental.yml +++ b/.github/workflows/experimental.yml @@ -15,6 +15,10 @@ on: permissions: contents: read +concurrency: + group: experimental-release + cancel-in-progress: true + jobs: make-release: permissions: write-all @@ -31,7 +35,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v4 with: - python-version: '3.x' + python-version: '3.9' - name: Setup MSVC uses: ilammy/msvc-dev-cmd@v1 @@ -57,7 +61,7 @@ jobs: xmake build - name: Package - run: python tools/buildscripts/build.py package -e + run: python tools/buildscripts/release.py package -e - name: Release uses: softprops/action-gh-release@v1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 52b74a758..615204f1a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,10 @@ on: workflow_dispatch permissions: contents: read +concurrency: + group: stable-release + cancel-in-progress: true + jobs: make-release: permissions: write-all @@ -20,7 +24,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v4 with: - python-version: '3.x' + python-version: '3.9' - name: Setup MSVC uses: ilammy/msvc-dev-cmd@v1 @@ -32,7 +36,7 @@ jobs: - name: Release Commit id: release_commit - run: python build.py release_commit ${{ github.actor }} + run: python release.py release_commit ${{ github.actor }} working-directory: ./tools/buildscripts - name: Cache @@ -51,7 +55,8 @@ jobs: xmake build - name: Package - run: python tools/buildscripts/build.py package + id: package + run: python tools/buildscripts/release.py package - name: Push changes uses: ad-m/github-push-action@master diff --git a/assets/Mods/mods.json b/assets/Mods/mods.json new file mode 100644 index 000000000..d79717fa9 --- /dev/null +++ b/assets/Mods/mods.json @@ -0,0 +1,42 @@ +[ + { + "mod_name": "CheatManagerEnablerMod", + "mod_enabled": true + }, + { + "mod_name": "ActorDumperMod", + "mod_enabled": false + }, + { + "mod_name": "ConsoleCommandsMod", + "mod_enabled": true + }, + { + "mod_name": "ConsoleEnablerMod", + "mod_enabled": true + }, + { + "mod_name": "SplitScreenMod", + "mod_enabled": false + }, + { + "mod_name": "LineTraceMod", + "mod_enabled": true + }, + { + "mod_name": "BPML_GenericFunctions", + "mod_enabled": true + }, + { + "mod_name": "BPModLoaderMod", + "mod_enabled": true + }, + { + "mod_name": "jsbLuaProfilerMod", + "mod_enabled": false + }, + { + "mod_name": "Keybinds", + "mod_enabled": true + } +] \ No newline at end of file diff --git a/assets/UE4SS-settings.ini b/assets/UE4SS-settings.ini index 20e8657e1..5280f7944 100644 --- a/assets/UE4SS-settings.ini +++ b/assets/UE4SS-settings.ini @@ -59,7 +59,7 @@ LoadAllAssetsBeforeGeneratingCXXHeaders = 0 IgnoreAllCoreEngineModules = 0 ; Whether to skip generating the "Engine" and "CoreUObject" packages -; Default: 1 +; Default: 0 IgnoreEngineAndCoreUObject = 0 ; Whether to force all UFUNCTION macros to have "BlueprintCallable" diff --git a/tools/buildscripts/build.py b/tools/buildscripts/build.py deleted file mode 100755 index 3b193dc5e..000000000 --- a/tools/buildscripts/build.py +++ /dev/null @@ -1,296 +0,0 @@ -#!/usr/bin/env python3 - -import re -import os -import shutil -import subprocess -import argparse -from datetime import datetime - -# Change dir to repo root -os.chdir(os.path.join(os.path.dirname(__file__), '..', '..')) - -# Outputs to GitHub env if present -def github_output(name, value): - if 'GITHUB_OUTPUT' in os.environ: - with open(os.environ['GITHUB_OUTPUT'], 'a') as env: - env.write(f'{name}={value}\n') - -changelog_path = 'assets/Changelog.md' - -def parse_changelog(): - with open(changelog_path, 'r') as file: - lines = file.readlines() - delimeters = [index - 1 for index, value in enumerate(lines) if value == '==============\n'] - delimeters.append(len(lines) + 1) - return [{ - 'tag': lines[index[0]].strip(), - 'date': lines[index[0] + 2].strip(), - 'notes': ''.join(lines[index[0] + 3:index[1]]).strip(), - } for index in zip(delimeters, delimeters[1:])] - -def get_release_notes(args): - changelog = parse_changelog() - print(changelog[0]['notes']) - -def release_commit(args): - # TODO perhaps check if index is dirty to avoid clobbering anything - - with open(changelog_path, mode='r') as file: - lines = file.readlines() - version = lines[0].strip() - if lines[2] != 'TBD\n': - raise Exception('date is not "TBD"') - lines[2] = datetime.today().strftime('%Y-%m-%d') + '\n' - - with open(changelog_path, mode='w') as file: - file.writelines(lines) - - message = f'Release {version}' - subprocess.run(['git', 'add', changelog_path], check=True) - if args.username: - subprocess.run(['git', '-c', f'user.name="{args.username}"', '-c', f'user.email="{args.username}@users.noreply.github.com"', 'commit', '-m', message], check=True) - else: - subprocess.run(['git', 'commit', '-m', message], check=True) - subprocess.run(['git', 'tag', version], check=True) - - github_output('release_tag', version) - -def package(args): - is_experimental = args.e - - release_output = 'release' - shutil.rmtree(release_output, ignore_errors=True) - os.mkdir(release_output) - - staging_dev = os.path.join(release_output, 'StagingDev') - staging_release = os.path.join(release_output, 'StagingRelease') - - # List CPP Mods with flags indicating if they need a config folder and if they should be included in release builds - CPPMods = { - 'KismetDebuggerMod': {'create_config': True, 'include_in_release': False}, - } - - def copy_assets_without_mods(src, dst): - # Copy entire directory, excluding Mods folder - for item in os.listdir(src): - s = os.path.join(src, item) - d = os.path.join(dst, item) - if os.path.isdir(s) and item == 'Mods': - continue - if os.path.isdir(s): - shutil.copytree(s, d) - else: - shutil.copy2(s, d) - - def make_staging_dirs(is_dev_release: bool): - # Builds a release version of /assets by copying the directory and then - # removing and disabling dev-only settings and files - exclude_files = [ - 'Mods/shared/Types.lua', - 'UE4SS_Signatures', - 'VTableLayoutTemplates', - 'MemberVarLayoutTemplates', - 'CustomGameConfigs', - 'MapGenBP', - ] - - settings_to_modify_in_release = { - 'GuiConsoleVisible': 0, - 'ConsoleEnabled': 0, - 'EnableHotReloadSystem': 0, - 'IgnoreEngineAndCoreUObject': 1, - 'MaxMemoryUsageDuringAssetLoading': 80, - 'GUIUFunctionCaller': 0, - } - - change_modstxt = { - 'LineTraceMod': 0, - } - - # Disable all dev-only CPP mods by adding to table - for mod_name, mod_info in CPPMods.items(): - if not mod_info['include_in_release']: - change_modstxt[mod_name] = 0 - - staging_dir = staging_dev if is_dev_release else staging_release - - # Copy whole directory excluding Mods - copy_assets_without_mods('assets', staging_dir) - - # Include repo README - shutil.copy('README.md', os.path.join(staging_dir, 'README.md')) - - # Remove files - for file in exclude_files: - path = os.path.join(staging_dir, file) - try: - os.remove(path) - except: - shutil.rmtree(path) - - # Change UE4SS-settings.ini - config_path = os.path.join(staging_dir, 'UE4SS-settings.ini') - - if not is_dev_release: - with open(config_path, mode='r', encoding='utf-8-sig') as file: - content = file.read() - - for key, value in settings_to_modify_in_release.items(): - pattern = rf'(^{key}\s*=).*?$' - content = re.sub(pattern, rf'\1 {value}', content, flags=re.MULTILINE) - - with open(config_path, mode='w', encoding='utf-8-sig') as file: - file.write(content) - - # Change Mods/mods.txt - mods_path = os.path.join(staging_dir, 'Mods/mods.txt') - - with open(mods_path, mode='r', encoding='utf-8-sig') as file: - content = file.read() - - # Add all CPP mods to mods.txt for both release and dev in case someone adds dev mods later - cpp_mods_entries = '\n'.join([f'{mod} : 1' for mod in CPPMods]) - content += '\n' + cpp_mods_entries - - for key, value in change_modstxt.items(): - pattern = rf'(^{key}\s*:).*?$' - content = re.sub(pattern, rf'\1 {value}', content, flags=re.MULTILINE) - - with open(mods_path, mode='w', encoding='utf-8-sig') as file: - file.write(content) - - # Create folders for CPP mods in assets - for mod_name, mod_info in CPPMods.items(): - if is_dev_release or mod_info['include_in_release']: - os.makedirs(os.path.join(staging_dir, 'Mods', mod_name, 'dlls'), exist_ok=True) - if mod_info['create_config']: - os.makedirs(os.path.join(staging_dir, 'Mods', mod_name, 'config'), exist_ok=True) - - def package_release(is_dev_release: bool): - version = subprocess.check_output(['git', 'describe', '--tags']).decode('utf-8').strip() - if is_dev_release: - main_zip_name = f'zDEV-UE4SS_{version}' - staging_dir = staging_dev - else: - main_zip_name = f'UE4SS_{version}' - staging_dir = staging_release - - ue4ss_dll_path = '' - ue4ss_pdb_path = '' - dwmapi_dll_path = '' - - # CPP mods paths - cpp_mods_paths = {mod: '' for mod in CPPMods if is_dev_release or CPPMods[mod]['include_in_release']} - - scan_start_dir = '.' - if str(args.d) != 'None': - scan_start_dir = str(args.d) - - for root, dirs, files in os.walk(scan_start_dir): - for file in files: - if file.lower() == "ue4ss.dll": - ue4ss_dll_path = os.path.join(root, file) - if file.lower() == "ue4ss.pdb": - ue4ss_pdb_path = os.path.join(root, file) - if file.lower() == "dwmapi.dll": - dwmapi_dll_path = os.path.join(root, file) - # Find CPP Mod DLLs - for mod_name in cpp_mods_paths: - if file.lower() == mod_name.lower() + '.dll': - cpp_mods_paths[mod_name] = os.path.join(root, file) - - # Create the ue4ss folder in staging_dir - ue4ss_dir = os.path.join(staging_dir, 'ue4ss') - os.makedirs(ue4ss_dir, exist_ok=True) - - # Move all files from assets folder to the ue4ss folder except dwmapi.dll and Mods folder - for root, _, files in os.walk('assets'): - for file in files: - if file.lower() != 'dwmapi.dll' and not os.path.join(root, file).startswith(os.path.join('assets', 'Mods')): - src_path = os.path.join(root, file) - dst_path = os.path.join(ue4ss_dir, os.path.relpath(src_path, 'assets')) - os.makedirs(os.path.dirname(dst_path), exist_ok=True) - shutil.copy(src_path, dst_path) - - # Copy the Mods folder separately to avoid nesting - mods_src = os.path.join('assets', 'Mods') - mods_dst = os.path.join(ue4ss_dir, 'Mods') - shutil.copytree(mods_src, mods_dst, dirs_exist_ok=True) - - # Main dll and pdb - shutil.copy(ue4ss_dll_path, ue4ss_dir) - - # CPP mods - for mod_name, dll_path in cpp_mods_paths.items(): - mod_dir = os.path.join(ue4ss_dir, 'Mods', mod_name, 'dlls') - os.makedirs(mod_dir, exist_ok=True) - shutil.copy(dll_path, os.path.join(mod_dir, 'main.dll')) - - # Create config folder if needed - if is_dev_release or CPPMods[mod_name]['include_in_release']: - if CPPMods[mod_name]['create_config']: - os.makedirs(os.path.join(ue4ss_dir, 'Mods', mod_name, 'config'), exist_ok=True) - - # Proxy - shutil.copy(dwmapi_dll_path, staging_dir) - - if is_dev_release: - shutil.copy(ue4ss_pdb_path, ue4ss_dir) - if os.path.exists(os.path.join(scan_start_dir, 'docs')): - shutil.copytree('docs', os.path.join(ue4ss_dir, 'Docs')) - - # Move remaining files to the ue4ss dir - dont_move = ['dwmapi.dll', 'docs', 'ue4ss'] - for file in os.listdir(staging_dir): - if file.lower() not in dont_move: - shutil.move(os.path.join(staging_dir, file), os.path.join(ue4ss_dir, file)) - - output = os.path.join(release_output, main_zip_name) - shutil.make_archive(output, 'zip', staging_dir) - print(f'Created package {output}.zip') - - # Clean up staging dir - shutil.rmtree(staging_dir) - - make_staging_dirs(is_dev_release=True) # Create staging directories for dev build - make_staging_dirs(is_dev_release=False) # Create staging directories for release build - - # Package UE4SS Standard - package_release(is_dev_release=False) - package_release(is_dev_release=True) - - # CustomGameConfigs - shutil.make_archive(os.path.join(release_output, 'zCustomGameConfigs'), 'zip', 'assets/CustomGameConfigs') - - # MapGenBP - shutil.make_archive(os.path.join(release_output, 'zMapGenBP'), 'zip', 'assets/MapGenBP') - - changelog = parse_changelog() - with open(os.path.join(release_output, 'release_notes.md'), 'w') as file: - file.write(changelog[0]['notes']) - - print('Done') - -commands = {f.__name__: f for f in [ - get_release_notes, - package, - release_commit, -]} - -def main(): - parser = argparse.ArgumentParser() - subparsers = parser.add_subparsers(dest='command', required=True) - - package_parser = subparsers.add_parser('package') - package_parser.add_argument('-e', action='store_true') - package_parser.add_argument('-d', action='store') - - release_commit_parser = subparsers.add_parser('release_commit') - release_commit_parser.add_argument('username', nargs='?') - - args = parser.parse_args() - commands[args.command](args) - -if __name__ == "__main__": - main() diff --git a/tools/buildscripts/release.py b/tools/buildscripts/release.py new file mode 100644 index 000000000..494da6de7 --- /dev/null +++ b/tools/buildscripts/release.py @@ -0,0 +1,342 @@ +#!/usr/bin/env python3 + +import re +import os +import shutil +import subprocess +import argparse +from datetime import datetime +import sys +import json + +class ReleaseHandler: + def __init__(self, is_dev_release, is_experimental, release_output='release'): + self.is_dev_release = is_dev_release + self.is_experimental = is_experimental + self.release_output = release_output + self.staging_dir = os.path.join(self.release_output, 'StagingDev') if self.is_dev_release else os.path.join(self.release_output, 'StagingRelease') + self.ue4ss_dir = os.path.join(self.staging_dir, 'ue4ss') + + # TODO: Move all these hardcoded values into a release config file or similar to pass into the script + # List of CPP Mods with flags indicating if they need a config folder and if they should be included in release builds + self.cpp_mods = { + 'KismetDebuggerMod': {'create_config': True, 'include_in_release': False}, + } + + # Disable any dev-only mods + self.mods_to_disable_in_release = { + 'LineTraceMod': 0, + } + + # Files to exclude from the non-dev/release version of the zip + self.files_to_exclude_from_release = [ + 'Mods/shared/Types.lua', + 'UE4SS_Signatures', + 'VTableLayoutTemplates', + 'MemberVarLayoutTemplates', + 'CustomGameConfigs', + 'MapGenBP', + ] + + # Settings to change in the release. The default settings in assets/UE4SS-settings.ini are for dev + self.settings_to_modify_in_release = { + 'GuiConsoleVisible': 0, + 'ConsoleEnabled': 0, + 'EnableHotReloadSystem': 0, + 'MaxMemoryUsageDuringAssetLoading': 80, + 'GUIUFunctionCaller': 0, + } + + def make_staging_dirs(self): + shutil.copytree('assets', self.ue4ss_dir) + shutil.copy('README.md', os.path.join(self.ue4ss_dir, 'README.md')) + + if not self.is_dev_release: + for file in self.files_to_exclude_from_release: + path = os.path.join(self.ue4ss_dir, file) + if os.path.exists(path): + if os.path.isfile(path): + os.remove(path) + elif os.path.isdir(path): + shutil.rmtree(path) + self.modify_settings(self.settings_to_modify_in_release) + + ue4ss_dll_path, ue4ss_pdb_path, dwmapi_dll_path, cpp_mods_paths = self.scan_directories() + + self.copy_cpp_mods(cpp_mods_paths) + self.modify_mods_txt() # can only run this after copy mods just in case we are missing mod dlls + self.modify_mods_json() + self.copy_executables(dwmapi_dll_path, ue4ss_dll_path, ue4ss_pdb_path) + self.copy_docs() + + # needs to be run inside staging as we don't want to modify the original + # only run this if we are not doing an experimental release, as we don't want the date to be set in experimental releases + if not self.is_experimental: self.modify_changelog() + + def modify_settings(self, settings_to_modify): + config_path = os.path.join(self.ue4ss_dir, 'UE4SS-settings.ini') + with open(config_path, mode='r', encoding='utf-8-sig') as file: + content = file.read() + + for key, value in settings_to_modify.items(): + pattern = rf'(^{key}\s*=).*?$' + content = re.sub(pattern, rf'\1 {value}', content, flags=re.MULTILINE) + + with open(config_path, mode='w', encoding='utf-8-sig') as file: + file.write(content) + + def scan_directories(self): + ue4ss_dll_path = '' + ue4ss_pdb_path = '' + dwmapi_dll_path = '' + cpp_mods_paths = {mod: '' for mod in self.cpp_mods if self.is_dev_release or self.cpp_mods[mod]['include_in_release']} + scan_start_dir = '.' + + for root, _, files in os.walk(scan_start_dir): + for file in files: + if file.lower() == "ue4ss.dll": + ue4ss_dll_path = os.path.join(root, file) + if file.lower() == "ue4ss.pdb": + ue4ss_pdb_path = os.path.join(root, file) + if file.lower() == "dwmapi.dll": + dwmapi_dll_path = os.path.join(root, file) + for mod_name in cpp_mods_paths: + if file.lower() == mod_name.lower() + '.dll': + cpp_mods_paths[mod_name] = os.path.join(root, file) + + return ue4ss_dll_path, ue4ss_pdb_path, dwmapi_dll_path, cpp_mods_paths + + def copy_cpp_mods(self, cpp_mods_paths): + for mod_name, dll_path in cpp_mods_paths.items(): + if dll_path: + mod_dir = os.path.join(self.ue4ss_dir, 'Mods', mod_name, 'dlls') + os.makedirs(mod_dir, exist_ok=True) + shutil.copy(dll_path, os.path.join(mod_dir, 'main.dll')) + if self.is_dev_release: + pdb_path = dll_path.replace('.dll', '.pdb') + if os.path.exists(pdb_path): + shutil.copy(pdb_path, os.path.join(mod_dir, 'main.pdb')) + else: + print(f'Error: {mod_name}.dll not found, build has failed.') + sys.exit(1) + + for mod_name, mod_info in self.cpp_mods.items(): + if self.is_dev_release or mod_info['include_in_release']: + if mod_info['create_config']: + os.makedirs(os.path.join(self.ue4ss_dir, 'Mods', mod_name, 'config'), exist_ok=True) + + def modify_mods_txt(self): + mods_to_disable_in_release = self.mods_to_disable_in_release.copy() + for mod_name, mod_info in self.cpp_mods.items(): + if not mod_info['include_in_release']: + mods_to_disable_in_release[mod_name] = 0 + + mods_path = os.path.join(self.ue4ss_dir, 'Mods', 'mods.txt') + with open(mods_path, mode='r', encoding='utf-8-sig') as file: + content = file.read() + + if self.cpp_mods: + content = '\n'.join([f'{mod} : 1' for mod in self.cpp_mods]) + '\n' + content + + if not self.is_dev_release: + for key, value in mods_to_disable_in_release.items(): + pattern = rf'(^{key}\s*:).*?$' + content = re.sub(pattern, rf'\1 {value}', content, flags=re.MULTILINE) + + with open(mods_path, mode='w', encoding='utf-8-sig') as file: + file.write(content) + + def modify_mods_json(self): + mods_path = os.path.join(self.ue4ss_dir, 'Mods', 'mods.json') + with open(mods_path, mode='r', encoding='utf-8-sig') as file: + content = json.load(file) + + if self.cpp_mods: + for mod_name, mod_info in self.cpp_mods.items(): + if mod_name not in [mod['mod_name'] for mod in content]: + if not self.is_dev_release: + content.append({'mod_name': mod_name, 'mod_enabled': mod_info['include_in_release']}) + else: + content.append({'mod_name': mod_name, 'mod_enabled': True}) + + for mod in content: + if mod['mod_name'] in self.mods_to_disable_in_release: + mod['mod_enabled'] = bool(self.mods_to_disable_in_release[mod['mod_name']]) + + with open(mods_path, mode='w', encoding='utf-8-sig') as file: + json.dump(content, file, indent=4) + + def copy_executables(self, dwmapi_dll_path, ue4ss_dll_path, ue4ss_pdb_path): + shutil.copy(dwmapi_dll_path, self.staging_dir) + shutil.copy(ue4ss_dll_path, self.ue4ss_dir) + if self.is_dev_release: shutil.copy(ue4ss_pdb_path, self.ue4ss_dir) + + def copy_docs(self): + if self.is_dev_release and os.path.exists('docs'): + shutil.copytree('docs', os.path.join(self.ue4ss_dir, 'Docs')) + + def modify_changelog(self): + changelog_path = os.path.join(self.ue4ss_dir, 'Changelog.md') + if os.path.exists(changelog_path): ready_changelog_for_release(changelog_path) + + def package_release(self): + try: + version = subprocess.check_output(['git', 'describe', '--tags']).decode('utf-8').strip() + except subprocess.CalledProcessError: + print('Error: git describe failed. Make sure the release has been tagged.') + sys.exit(1) + main_zip_name = f'zDEV-UE4SS_{version}' if self.is_dev_release else f'UE4SS_{version}' + output = os.path.join(self.release_output, main_zip_name) + shutil.make_archive(output, 'zip', self.staging_dir) + print(f'Created package {output}.zip') + + def cleanup(self): + shutil.rmtree(self.staging_dir) + +class Packager: + """ + "Release" refers to the zip that is distributed to the end users. It should be in the format UE4SS_.zip and contains only the necessary files. + "Dev" refers to the zip that is used for development. It should be in the format zDEV-UE4SS_.zip and contains all the files, pdbs and docs. + """ + + def __init__(self, args): + self.args = args + self.release_output = 'release' + self.setup() + + def setup(self): + if os.path.exists(self.release_output): shutil.rmtree(self.release_output) + os.mkdir(self.release_output) + + def run(self): + dev_handler = ReleaseHandler(is_dev_release=True, is_experimental=self.args.e, release_output=self.release_output) + release_handler = ReleaseHandler(is_dev_release=False, is_experimental=self.args.e, release_output=self.release_output) + + dev_handler.make_staging_dirs() + dev_handler.package_release() + dev_handler.cleanup() + + release_handler.make_staging_dirs() + release_handler.package_release() + release_handler.cleanup() + + shutil.make_archive(os.path.join(self.release_output, 'zCustomGameConfigs'), 'zip', 'assets/CustomGameConfigs') + shutil.make_archive(os.path.join(self.release_output, 'zMapGenBP'), 'zip', 'assets/MapGenBP') + + changelog = parse_changelog(self.args.changelog_path) + with open(os.path.join(self.release_output, 'release_notes.md'), 'w') as file: + file.write(changelog[0]['notes']) + + print('Done') + +def ready_changelog_for_release(changelog_path): + with open(changelog_path, 'r') as file: + lines = file.readlines() + version = lines[0].strip() + if lines[2] != 'TBD\n': + raise Exception('date is not "TBD"') + lines[2] = datetime.today().strftime('%Y-%m-%d') + '\n' + with open(changelog_path, 'w') as file: + file.writelines(lines) + return version + +def parse_changelog(changelog_path): + with open(changelog_path, 'r') as file: + lines = file.readlines() + delimeters = [index - 1 for index, value in enumerate(lines) if value == '==============\n'] + delimeters.append(len(lines) + 1) + return [{ + 'tag': lines[index[0]].strip(), + 'date': lines[index[0] + 2].strip(), + 'notes': ''.join(lines[index[0] + 3:index[1]]).strip(), + } for index in zip(delimeters, delimeters[1:])] + +def package(args): + packager = Packager(args) + packager.run() + +def release_commit(args): + version = ready_changelog_for_release(args.changelog_path) + message = f'Release {version}' + subprocess.run(['git', 'add', args.changelog_path], check=True) + if args.username: + subprocess.run(['git', '-c', f'user.name="{args.username}"', '-c', f'user.email="{args.username}@users.noreply.github.com"', 'commit', '-m', message], check=True) + else: + subprocess.run(['git', 'commit', '-m', message], check=True) + subprocess.run(['git', 'tag', version], check=True) + + # Outputs to GitHub env if present + def github_output(name, value): + if 'GITHUB_OUTPUT' in os.environ: + with open(os.environ['GITHUB_OUTPUT'], 'a') as env: + env.write(f'{name}={value}\n') + + github_output('release_tag', version) + +if __name__ == "__main__": + # Change dir to repo root + os.chdir(os.path.join(os.path.dirname(__file__), '..', '..')) + + parser = argparse.ArgumentParser() + parser.add_argument('--changelog_path', default='assets/Changelog.md', required=False) + + subparsers = parser.add_subparsers(dest='command', required=True) + + package_parser = subparsers.add_parser('package') + package_parser.add_argument('-e', action='store_true') + + release_commit_parser = subparsers.add_parser('release_commit') + release_commit_parser.add_argument('username', nargs='?') + + args = parser.parse_args() + commands = {f.__name__: f for f in [ + package, + release_commit, + ]} + commands[args.command](args) + +""" +How to run this script: +1. Running the 'package' command: + Usage: python release.py package + + --changelog_path : Optional argument to specify the path to the changelog file. Default is 'assets/Changelog.md' + -e : Argument used when running the script for experimental release + + Examples: + - python release.py package + - python release.py package -e --changelog_path custom/Changelog.md + +2. Running the 'release_commit' command: + Usage: python release.py release_commit [username] + + username : Optional argument to specify a github username + --changelog_path : Optional argument to specify the path to the changelog file. Default is 'assets/Changelog.md' + + Examples: + - python release.py release_commit + - python release.py release_commit ${{ github.actor }} + - python release.py release_commit ${{ github.actor }} custom/Changelog.md +""" + +""" +To locally debug this script in vscode, make launch.json in .vscode and add the following configurations: + +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Debug Release Script", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/tools/buildscripts/release.py", + "console": "integratedTerminal", + "args": [ + "package", + "-e" + ], + "justMyCode": true + } + ] +} +""" \ No newline at end of file