# uploads artifacts to nightly releases # NOTE: this is just a quick fix, feel free to handle releases more cleanly import urllib.request, json, argparse, glob, subprocess, os # to handle nightly releases: # - create repo (could be in main repo but needs a tag, that is associated with some commit) # - create release w/ tag, this is were uploads go # - upload assets manually or via API below, needs access token # - assets have a fixed link based on tag = good # # To generate access tokens: # - go to user settings > developer settings # # * there are github actions that automate this that could be used, this is based from manual tests # * also "import github" to use Github(token) that comes with many helpers # gh = Github(token) # gh_repo = gh.get_repo(repo) # gh_release = gh_repo.get_release(tag) # assets = gh_release.get_assets() # gh_release.update_release(...) # gh_release.upload_asset(...) # # API: https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28 # https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28 # # Github's API has some limits but hopefully not reached by current token RELEASE_TAG = 'nightly' # gives info about release (public) URL_RELEASE = 'https://api.github.com/repos/vgmstream/vgmstream-releases/releases/tags/nightly' # allows deleting a single asset URL_DELETE = 'https://api.github.com/repos/vgmstream/vgmstream-releases/releases/assets/%s' # allows uploading a single asset URL_UPLOAD = 'https://uploads.github.com/repos/vgmstream/vgmstream-releases/releases/%s/assets?name=%s' # change release info URL_UPDATE = 'https://api.github.com/repos/vgmstream/vgmstream-releases/releases/%s' # gives info about last vgmstream tag URL_VGMSTREAM = 'https://api.github.com/repos/vgmstream/vgmstream/releases?per_page=1' #------------------------------------------------------------------------------ def get_release(): contents = urllib.request.urlopen(URL_RELEASE).read() data = json.loads(contents) return data def get_vgmstream_tag(): # TODO could use local git tag contents = urllib.request.urlopen(URL_VGMSTREAM).read() data = json.loads(contents) return data[0]['tag_name'] def update_release(release, token, debug, body): release_id = release['id'] args = [ 'curl', '-X', 'PATCH', '-H', 'Accept: application/vnd.github+json', '-H', 'Authorization: Bearer %s' % (token), '-H', 'X-GitHub-Api-Version: 2022-11-28', URL_UPDATE % (release_id), '-d', '{"body":"%s"}' % (body), ] #-d '{"tag_name":"v1.0.0","target_commitish":"master","name":"v1.0.0","body":"...","draft":false,"prerelease":false}' print("* updating release text") if debug: print(' '.join(args)) else: subprocess.run(args, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) def delete_asset(release, token, debug, file): basename = os.path.basename(file) asset_id = None for asset in release['assets']: if asset['name'].lower() == basename.lower(): asset_id = asset['id'] break if not asset_id: print("asset id not found") return args = [ 'curl', '-X', 'DELETE', '-H', 'Accept: application/vnd.github+json', '-H', 'Authorization: Bearer %s' % (token), '-H', 'X-GitHub-Api-Version: 2022-11-28', URL_DELETE % (asset_id), ] print("* deleting old asset %s (%s)" % (file, asset_id)) if debug: print(' '.join(args)) else: subprocess.run(args, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) def upload_asset(release, token, debug, file): basename = os.path.basename(file) release_id = release['id'] args = [ 'curl', '-X', 'POST', '-H', 'Accept: application/vnd.github+json', '-H', 'Authorization: Bearer %s' % (token), '-H', 'X-GitHub-Api-Version: 2022-11-28', '-H', 'Content-Type: application/octet-stream', URL_UPLOAD % (release_id, basename), '--data-binary', '@%s' % (file) ] print("* uploading asset %s" % (file)) if debug: print(' '.join(args)) else: subprocess.run(args, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) #------------------------------------------------------------------------------ def generate_changelog(release, token, debug): print("* generating changelog") try: import changelog # writes in work dir and gets lines lines = changelog.main() current_tag = get_vgmstream_tag() body = [ 'Automated releases ([full diffs here](https://github.com/vgmstream/vgmstream/compare/%s...master)).' % (current_tag), '', '', '<details>' '<summary>Recent changes</summary>', '', #important, collapsable doesn't work otherwise ] body.extend(lines) body.extend([ '</details>' ]) body_text = '\\n'.join(body) body_text = body_text.replace('"', '\"') file = "changelog.txt" update_release(release, token, debug, body_text) delete_asset(release, token, debug, file) upload_asset(release, token, debug, file) #with open("body.json",'w') as f: # f.write('{"body":"%s"}' % (body_text)) #with open("changelog.txt",'rb') as f: # print(f.read()) except Exception as e: print("couldn't generate changelog", e) def main(args): print("starting asset uploader") files = [] for file_glob in args.files: files += glob.glob(file_glob) # allow for changelog only #if not files: # raise ValueError("no files found") # shouldn't happen (points to non-existing files) if args.files and not files: raise ValueError("no files found, expected: %s" % (args.files)) # this token usually only exists in env on merges, but allow passing for tests token = args.token if not token: token = os.environ.get('UPLOADER_GITHUB_TOKEN') if not token: print("token not defined") raise ValueError("token not defined") print("handling %s files" % (len(files))) try: release = get_release() for file in files: delete_asset(release, token, args.debug, file) upload_asset(release, token, args.debug, file) except Exception as e: print("error during process: %s" % (e)) raise ValueError("could't upload") # this should be invoked separately so release doesn't change per artifact if args.changelog: generate_changelog(release, token, args.debug) print("done") def parse_args(): description = ( "uploads artifacts to releases" ) epilog = None ap = argparse.ArgumentParser(description=description, epilog=epilog, formatter_class=argparse.RawTextHelpFormatter) ap.add_argument("files", help="files to upload", nargs='*') ap.add_argument("-t","--token", help="security token") ap.add_argument("-c","--changelog", help="update changelog as well", action="store_true") ap.add_argument("-x","--debug", help="no actions", action="store_true") args = ap.parse_args() return args if __name__ == "__main__": args = parse_args() main(args)