diff --git a/.github/workflows/foundry_release.py b/.github/workflows/foundry_release.py index 1a207a7..5686de1 100644 --- a/.github/workflows/foundry_release.py +++ b/.github/workflows/foundry_release.py @@ -1,27 +1,109 @@ import json import os import re -from http.client import HTTPSConnection from html.parser import HTMLParser -from pprint import pprint, pformat -from time import sleep +from http.client import HTTPSConnection +from pprint import pformat from urllib.parse import urlencode from markdown_it import MarkdownIt -# GitHub Action Secrets +# Secrets FOUNDRY_PACKAGE_RELEASE_TOKEN = os.environ['FOUNDRY_PACKAGE_RELEASE_TOKEN'] FOUNDRY_USERNAME = os.environ['FOUNDRY_USERNAME'] FOUNDRY_PASSWORD = os.environ['FOUNDRY_PASSWORD'] FOUNDRY_AUTHOR = os.environ['FOUNDRY_AUTHOR'] UPDATE_DISCORD_KEY = os.environ['UPDATE_DISCORD_KEY'] +SECRETS = [FOUNDRY_PACKAGE_RELEASE_TOKEN, FOUNDRY_USERNAME, FOUNDRY_PASSWORD, FOUNDRY_AUTHOR, UPDATE_DISCORD_KEY] -# Build Variables +# Environment Variables +GITHUB_URL = os.environ['GITHUB_URL'] +TAG = os.environ['TAG'] CHANGES = os.environ['CHANGES'] FILES_CHANGED = os.environ['FILES_CHANGED'] +def main(): + if all(f.startswith('.github') for f in FILES_CHANGED.split()): + SKIP('SKIPPING DEPLOYMENT. ONLY RELEASE CONFIG MODIFIED') + for f in FILES_CHANGED.split(): + INFO(f) + return + + with open('./module.json', 'r') as file: + module_json = json.load(file) + + update_repo_description(module_json) + push_release(module_json) + post_update_to_discord() + + +def update_repo_description(module_json): + if not any(f in FILES_CHANGED for f in ['README.md', 'module.json', 'foundry_release.py']): + SKIP('SKIPPING REPO DESCRIPTION UPDATE') + return + + INFO('Acquiring CSRF tokens') + conn = HTTPSConnection('foundryvtt.com') + conn.request('GET', '/', headers={}) + response = conn.getresponse() + if response.status != 200: + BAD(response.reason) + csrf_token = response.getheader('Set-Cookie').split('csrftoken=')[1].split(';')[0].strip() + csrf_middleware_token = re.search(r'name="csrfmiddlewaretoken" value="([^"]+)"', response.read().decode()).group(1) + + INFO('Acquiring session id') + conn = HTTPSConnection('foundryvtt.com') + conn.request('POST', '/auth/login/', + headers={ + 'Referer': 'https://foundryvtt.com/', + 'Content-Type': 'application/x-www-form-urlencoded', + 'Cookie': f'csrftoken={csrf_token}; privacy-policy-accepted=accepted' + }, + body=urlencode({ + 'csrfmiddlewaretoken': csrf_middleware_token, + 'username': FOUNDRY_USERNAME, + 'password': FOUNDRY_PASSWORD + })) + response = conn.getresponse() + if response.status == 403: + BAD(response.reason) + session_id = response.getheader('Set-Cookie').split('sessionid=')[1].split(';')[0].strip() + + INFO('Converting README.md to html') + md = MarkdownIt('commonmark', {'html': True}).enable('table') + with open('./README.md', 'r') as readme_file: + readme_contents = readme_file.read() + readme = md.render(readme_contents) + + INFO('Updating Foundry VTT Module Repository Description') + conn = HTTPSConnection('foundryvtt.com') + conn.request('POST', f"/packages/{module_json['id']}/edit", + headers={ + 'Referer': f"https://foundryvtt.com/packages/{module_json['id']}/edit", + 'Content-Type': 'application/x-www-form-urlencoded', + 'Cookie': f'csrftoken={csrf_token}; privacy-policy-accepted=accepted; sessionid={session_id}', + }, + body=urlencode([ + ('username', FOUNDRY_USERNAME), + ('title', module_json['title']), + ('description', readme), + ('url', module_json['url']), + ('csrfmiddlewaretoken', csrf_middleware_token), + ('author', FOUNDRY_AUTHOR), + ('secret-key', FOUNDRY_PACKAGE_RELEASE_TOKEN), + ('requires', 1), + ('tags', 15), + ('tags', 17) + ])) + response = conn.getresponse() + if response.status != 302: + BAD(f'Update Description Failed\n{extract_errorlist_text(response.read().decode())}') + GOOD('REPO DESCRIPTION UPDATED') + + def push_release(module_json: dict) -> None: + INFO('Pushing new release to Foundry VTT Module Repository') conn = HTTPSConnection("api.foundryvtt.com") conn.request( "POST", "/_api/packages/release_version/", @@ -32,56 +114,37 @@ def push_release(module_json: dict) -> None: body=json.dumps({ 'id': module_json['id'], 'release': { - 'version': module_json['version'], - 'manifest': f"{module_json['url']}/releases/download/{module_json['version']}/module.json", - 'notes': f"{module_json['url']}/releases/tag/{module_json['version']}", + 'version': TAG, + 'manifest': f"{GITHUB_URL}/releases/download/{TAG}/module.json", + 'notes': f"{GITHUB_URL}/releases/tag/{TAG}", 'compatibility': module_json['compatibility'] } }) ) response_json = json.loads(conn.getresponse().read().decode()) if response_json['status'] != 'success': - pprint(module_json) - raise Exception(pformat(response_json['errors'])) - print('✅ MODULE POSTED TO REPO') - - -def get_readme_as_html() -> str: - md = MarkdownIt('commonmark', {'html': True}).enable('table') - with open('./README.md', 'r') as readme_file: - readme = readme_file.read() - return md.render(readme) + BAD(pformat(response_json['errors'])) + GOOD('MODULE POSTED TO REPO') -def get_tokens() -> (str, str): - conn = HTTPSConnection('foundryvtt.com') - conn.request('GET', '/', headers={}) +def post_update_to_discord() -> None: + INFO('Notifying Discord of new release') + deduped_changes = '\n'.join(dict.fromkeys(CHANGES.split('\n'))) + conn = HTTPSConnection("api.oronder.com") + conn.request( + "POST", '/update_discord', + headers={ + 'Content-Type': 'application/json', + 'Authorization': UPDATE_DISCORD_KEY + }, + body=json.dumps({'version': TAG, 'changes': deduped_changes}) + ) response = conn.getresponse() if response.status != 200: - raise Exception(response.reason) - csrf_token = response.getheader('Set-Cookie').split('csrftoken=')[1].split(';')[0].strip() - csrf_middleware_token = re.search(r'name="csrfmiddlewaretoken" value="([^"]+)"', response.read().decode()).group(1) - return csrf_token, csrf_middleware_token - - -def get_session_id(csrf_token: str, csrf_middleware_token: str) -> str: - body = urlencode({ - 'csrfmiddlewaretoken': csrf_middleware_token, - 'username': FOUNDRY_USERNAME, - 'password': FOUNDRY_PASSWORD - }) - headers = { - 'Referer': 'https://foundryvtt.com/', - 'Content-Type': 'application/x-www-form-urlencoded', - 'Cookie': f'csrftoken={csrf_token}; privacy-policy-accepted=accepted' - } - conn = HTTPSConnection('foundryvtt.com') - conn.request('POST', '/auth/login/', body, headers) - response = conn.getresponse() - if response.status == 403: - raise Exception(response.reason) - cookies = response.getheader('Set-Cookie') - return cookies.split('sessionid=')[1].split(';')[0].strip() + content = response.read().decode() + headers = response.headers.as_string() + BAD(f'Failed to send Update Message to Discord\n{content=}\n{headers=}') + GOOD('DISCORD NOTIFIED OF NEW RELEASE') def extract_errorlist_text(html_string: str) -> str: @@ -108,79 +171,27 @@ def handle_data(self, data): return parser.errorlist_content -def post_packages_oronder_edit(csrf_token, csrf_middleware_token, session_id, description, module_json) -> None: - conn = HTTPSConnection('foundryvtt.com') - headers = { - 'Referer': f"https://foundryvtt.com/packages/{module_json['id']}/edit", - 'Content-Type': 'application/x-www-form-urlencoded', - 'Cookie': f'csrftoken={csrf_token}; privacy-policy-accepted=accepted; sessionid={session_id}', - } - body = urlencode([ - ('username', FOUNDRY_USERNAME), - ('title', module_json['title']), - ('description', description), - ('url', module_json['url']), - ('csrfmiddlewaretoken', csrf_middleware_token), - ('author', FOUNDRY_AUTHOR), - ('secret-key', FOUNDRY_PACKAGE_RELEASE_TOKEN), - ('requires', 1), - ('tags', 15), - ('tags', 17) - ]) - conn.request('POST', f"/packages/{module_json['id']}/edit", body, headers) - response = conn.getresponse() - if response.status != 302: - content = response.read().decode() - raise Exception(f'Update Description Failed\n{extract_errorlist_text(content)}') +def safe_print(s: str): + for secret in SECRETS: + s = s.replace(secret, '*****') + print(s) -def post_update_to_discord(version) -> None: - deduped_changes = '\n'.join(dict.fromkeys(CHANGES.split('\n'))) - conn = HTTPSConnection("api.oronder.com") - conn.request( - "POST", '/update_discord', - headers={ - 'Content-Type': 'application/json', - 'Authorization': UPDATE_DISCORD_KEY - }, - body=json.dumps({'version': version, 'changes': deduped_changes}) - ) - response = conn.getresponse() - if response.status != 200: - content = response.read().decode() - headers = response.headers.as_string() - raise Exception(f'Failed to send Update Message to Discord\n{content=}\n{headers=}') - print('✅ DISCORD NOTIFIED OF NEW RELEASE') +def INFO(s: str): + safe_print(f' {s}') -def update_repo_description(module_json): - if any(f in FILES_CHANGED for f in ['README.md', 'module.json']): - csrf_token, csrf_middleware_token = get_tokens() - session_id = get_session_id(csrf_token, csrf_middleware_token) - readme = get_readme_as_html() - post_packages_oronder_edit(csrf_token, csrf_middleware_token, session_id, readme, module_json) - print('✅ REPO DESCRIPTION UPDATED') - for i in range(10): - print('💤' * (10 - i)) - sleep(1) - else: - print('🪧 SKIPPING REPO DESCRIPTION UPDATE') +def SKIP(s: str): + safe_print(f'🪧 {s}') -def main(): - if all(f.startswith('.github') for f in FILES_CHANGED.split()): - print('\n'.join([ - f'⛔ SKIPPING DEPLOYMENT. ONLY RELEASE CONFIG MODIFIED', - *[f" {f}" for f in FILES_CHANGED.split()] - ])) - return +def GOOD(s: str): + safe_print(f'✅ {s}') - with open('./module.json', 'r') as file: - module_json = json.load(file) - update_repo_description(module_json) - push_release(module_json) - post_update_to_discord(module_json['version']) +def BAD(s: str): + safe_print(f'❌ {s}') + exit(1) if __name__ == '__main__': diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1c644db..45cba50 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,5 +1,6 @@ name: Create Module Files For GitHub Release env: + GITHUB_URL: "https://github.com/${{ github.repository }}" TAG: ${{ github.event.release.tag_name || github.ref_name }} on: @@ -25,9 +26,7 @@ jobs: uses: cschleiden/replace-tokens@v1.2 with: files: 'module.json' - env: - VERSION: ${{ env.TAG }} - URL: "https://github.com/${{ github.repository }}" + - name: Downloading Dependencies run: npm install --omit=dev diff --git a/module.json b/module.json index 3c6c209..5147926 100644 --- a/module.json +++ b/module.json @@ -2,7 +2,7 @@ "id": "oronder", "title": "Oronder | Discord Sync", "description": "Bidirectional Discord Integration for Foundry VTT", - "version": "#{VERSION}#", + "version": "#{TAG}#", "library": "false", "manifestPlusVersion": "1.2.0", "compatibility": { @@ -47,11 +47,11 @@ ], "socket": true, "url": "https://discord.gg/ZggUCHsQqg", - "bugs": "#{URL}#/issues", - "manifest": "#{URL}#/releases/latest/download/module.json", - "download": "#{URL}#/releases/download/#{VERSION}#/module.zip", - "license": "#{URL}#/releases/download/#{VERSION}#/LICENSE", - "readme": "#{URL}#/releases/download/#{VERSION}#/README.md", + "bugs": "#{GITHUB_URL}#/issues", + "manifest": "#{GITHUB_URL}#/releases/latest/download/module.json", + "download": "#{GITHUB_URL}#/releases/download/#{TAG}#/module.zip", + "license": "#{GITHUB_URL}#/releases/download/#{TAG}#/LICENSE", + "readme": "#{GITHUB_URL}#/releases/download/#{TAG}#/README.md", "media": [ { "type": "icon",