supermodel_build_bot.py: New build bot script for GitHub

This commit is contained in:
Bart Trzynadlowski 2022-06-23 14:06:07 -07:00
parent 35a7e8a60b
commit 46eff8c5eb

View file

@ -1,312 +1,321 @@
# #
# Supermodel # Supermodel
# A Sega Model 3 Arcade Emulator. # A Sega Model 3 Arcade Emulator.
# Copyright 2011-2021 Bart Trzynadlowski, Nik Henson, Ian Curtis, # Copyright 2003-2022 The Supermodel Team
# Harry Tuttle, and Spindizzi #
# # This file is part of Supermodel.
# This file is part of Supermodel. #
# # Supermodel is free software: you can redistribute it and/or modify it under
# Supermodel is free software: you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free
# the terms of the GNU General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option)
# Software Foundation, either version 3 of the License, or (at your option) # any later version.
# any later version. #
# # Supermodel is distributed in the hope that it will be useful, but WITHOUT
# Supermodel is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # more details.
# more details. #
# # You should have received a copy of the GNU General Public License along
# You should have received a copy of the GNU General Public License along # with Supermodel. If not, see <http://www.gnu.org/licenses/>.
# with Supermodel. If not, see <http://www.gnu.org/licenses/>. #
#
#
# # supermodel_build_bot.py
# update_model3emu_snapshot.py #
# # Git snapshot build script. Checks latest Git commit against Supermodel3.com
# SVN snapshot build script. Checks latest SVN revision against Supermodel3.com # download page and builds and uploads a snapshot if needed. Intended to be run
# download page and builds and uploads a snapshot if needed. Intended to be run # at least once daily as part of an automated job.
# at least once daily as part of an automated job. #
# # Dependencies:
# Dependencies: # - MSYS2 installed at c:\msys64
# - MSYS2 installed at c:\msys64 # - mingw64/mingw-w64-x86_64-gcc package
# - mingw64/mingw-w64-x86_64-gcc package # - mingw64/mingw-w64-x86_64-make package
# - mingw64/mingw-w64-x86_64-make package # - mingw64/mingw-w64-x86_64-SDL2 package
# - mingw64/mingw-w64-x86_64-SDL2 package # - mingw64/mingw-w64-x86_64-SDL2_net package
# - mingw64/mingw-w64-x86_64-SDL2_net package # - msys/subversion package
# - msys/subversion package # - msys/zip package
# - msys/zip package # - git
# #
# To perform a test run: # To perform a test run:
# - Download https://supermodel3.com/Download.html # - Download https://supermodel3.com/Download.html to the directory from
# - Run: python update_model3emu_snapshot.py --working-dir=c:\tmp\build --test-run # which the script will be run.
# # - Run: python update_model3emu_snapshot.py --working-dir=c:\tmp\build --test-run
#
import argparse
import os import argparse
import shutil import os
import sys import shutil
import tempfile import sys
import tempfile
class CheckoutError(Exception):
pass class CheckoutError(Exception):
pass
class BuildError(Exception):
pass class BuildError(Exception):
pass
class PackageError(Exception):
pass class PackageError(Exception):
pass
class ParseError(Exception):
pass class ParseError(Exception):
pass
class Bash:
def __init__(self, bash_path): class Bash:
self.bash_path = bash_path def __init__(self, bash_path):
self.bash_path = bash_path
def execute(self, working_dir, command, log=False, print_output=False):
import subprocess def execute(self, working_dir, command, log=False, print_output=False):
working_dir = working_dir.replace("\\", "/") # convert to UNIX-style path import subprocess
invocation = self.bash_path + " -l -c \"" + "cd " + working_dir + " && " + command + "\"" working_dir = working_dir.replace("\\", "/") # convert to UNIX-style path
if log: invocation = self.bash_path + " -l -c '" + "cd " + working_dir + " && " + command + "'"
print("Executing: %s" % invocation) if log:
result = subprocess.run(invocation, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) print("Executing: %s" % invocation)
if print_output: result = subprocess.run(invocation, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
print(result.stdout.decode()) if print_output:
return result print(result.stdout.decode())
return result
def get_web_page(url, test_run):
if test_run: def get_web_page(test_run):
with open("Download.html") as fp: if test_run:
html = fp.read() with open("Download.html") as fp:
else: html = fp.read()
import urllib.request else:
with urllib.request.urlopen("https://supermodel3.com/Download.html") as data: import urllib.request
html = data.read().decode("utf-8") with urllib.request.urlopen("https://supermodel3.com/Download.html") as data:
return html html = data.read().decode("utf-8")
return html
def get_comment(line):
comment_begin = line.find("<!--") def get_comment(line):
comment_end = line.find("-->") comment_begin = line.find("<!--")
if comment_begin >= 0 and comment_end > 0 and comment_end > comment_begin: comment_end = line.find("-->")
return line[comment_begin+4:comment_end].strip() if comment_begin >= 0 and comment_end > 0 and comment_end > comment_begin:
return None return line[comment_begin+4:comment_end].strip()
return None
def add_new_file(html, filename, version, svn_revision):
# Scan for: <!-- BEGIN_SVN_SNAPSHOTS $TEMPLATE --> def add_new_file(html, filename, version, git_sha, date):
lines = html.splitlines() # Scan for: <!-- BEGIN_GIT_SNAPSHOTS $TEMPLATE -->
for i in range(len(lines)): lines = html.splitlines()
line = lines[i] for i in range(len(lines)):
text = get_comment(line) line = lines[i]
if text and text.startswith("BEGIN_SVN_SNAPSHOTS "): text = get_comment(line)
template = text[len("BEGIN_SVN_SNAPSHOTS "):] if text and text.startswith("BEGIN_GIT_SNAPSHOTS "):
new_row = template.replace("$FILENAME", filename).replace("$VERSION", version).replace("$REVISION", svn_revision) template = text[len("BEGIN_GIT_SNAPSHOTS "):]
svn_tag = "<!-- SVN_REVISION=%s -->" % svn_revision new_row = template.replace("$FILENAME", filename).replace("$VERSION", version).replace("$GIT_SHA", git_sha).replace("$DATE", date)
lines.insert(i + 1, new_row + " " + svn_tag) git_tag = "<!-- GIT_SHA=%s -->" % git_sha
html = "\n".join(lines) lines.insert(i + 1, new_row + " " + git_tag)
return html html = "\n".join(lines)
raise ParseError("BEGIN_SVN_SNAPSHOTS not found") return html
raise ParseError("BEGIN_GIT_SNAPSHOTS not found")
def get_uploaded_svn_revisions(html):
# Scan for all lines with: <!-- SVN_REVISION=$REVISION --> def get_uploaded_git_shas(html):
revisions = [] # Scan for all lines with: <!-- GIT_SHA=$SHA -->
for line in html.splitlines(): shas = []
text = get_comment(line) for line in html.splitlines():
if text and text.startswith("SVN_REVISION"): text = get_comment(line)
tokens = text.split("=") if text and text.startswith("GIT_SHA"):
if len(tokens) == 2: tokens = text.split("=")
revision = tokens[1] if len(tokens) == 2:
revisions.append(revision) sha = tokens[1]
else: shas.append(sha)
raise ParseError("Error parsing SVN_REVISION") else:
revisions.reverse() raise ParseError("Error parsing GIT_SHA")
return revisions shas.reverse()
return shas
def create_change_log(bash, repo_dir, file_path, uploaded_revisions, current_revision):
from_revision = uploaded_revisions[0] if len(uploaded_revisions) > 0 else current_revision def create_change_log(bash, repo_dir, file_path, uploaded_shas, current_sha):
result = bash.execute(working_dir=repo_dir, command="svn log -r " + current_revision + ":" + from_revision) # Log from first commit after 0.2a release until now
if result.returncode != 0: result = bash.execute(working_dir=repo_dir, command="git -c color.ui=never log 06594a5..." + current_sha + " --no-decorate --pretty=\"Commit: %H%nAuthor: %an%nDate: %ad%n%n%w(80,4,4)%B\"")
return PackageError("Unable to obtain SVN log") if result.returncode != 0:
change_log = result.stdout.decode().strip() return PackageError("Unable to obtain Git log")
with open(file_path, "w") as fp: change_log = result.stdout.decode().strip()
header = "\n\n" \ with open(file_path, "w", encoding="utf-8") as fp:
" #### ### ###\n" \ header = "\n\n" \
" ## ## ## ##\n" \ " #### ### ###\n" \
" ### ## ## ## ### #### ## ### ## ## #### ## #### ##\n" \ " ## ## ## ##\n" \
" ### ## ## ## ## ## ## ### ## ####### ## ## ##### ## ## ##\n" \ " ### ## ## ## ### #### ## ### ## ## #### ## #### ##\n" \
" ### ## ## ## ## ###### ## ## ####### ## ## ## ## ###### ##\n" \ " ### ## ## ## ## ## ## ### ## ####### ## ## ##### ## ## ##\n" \
" ## ## ## ## ##### ## ## ## # ## ## ## ## ## ## ##\n" \ " ### ## ## ## ## ###### ## ## ####### ## ## ## ## ###### ##\n" \
" #### ### ## ## #### #### ## ## #### ### ## #### ####\n" \ " ## ## ## ## ##### ## ## ## # ## ## ## ## ## ## ##\n" \
" ####\n" \ " #### ### ## ## #### #### ## ## #### ### ## #### ####\n" \
"\n" \ " ####\n" \
" A Sega Model 3 Arcade Emulator.\n" \ "\n" \
"\n" \ " A Sega Model 3 Arcade Emulator.\n" \
" Copyright 2011-2021 Bart Trzynadlowski, Nik Henson, Ian Curtis,\n" \ "\n" \
" Harry Tuttle, and Spindizzi\n" \ " Copyright 2003-2022 The Supermodel Team\n" \
"\n" \ "\n" \
" SVN CHANGE LOG\n" \ " CHANGE LOG\n" \
"\n" \ "\n" \
"\n" "\n"
fp.write(header) fp.write(header)
fp.write(change_log) fp.write(change_log)
def write_html_file(html, file_path): def write_html_file(html, file_path):
with open(file_path, "w") as fp: with open(file_path, "w") as fp:
fp.write(html) fp.write(html)
def upload(html_file, zip_file_path, username, password): def upload(html_file, zip_file_path, username, password):
from ftplib import FTP from ftplib import FTP
ftp = FTP("supermodel3.com") ftp = FTP("supermodel3.com")
ftp.login(username, password) ftp.login(username, password)
ftp.cwd("public_html/Files/SVN_Snapshots") ftp.cwd("public_html/Files/Git_Snapshots")
with open(zip_file_path, "rb") as fp: with open(zip_file_path, "rb") as fp:
ftp.storbinary("STOR " + os.path.basename(zip_file_path), fp) ftp.storbinary("STOR " + os.path.basename(zip_file_path), fp)
ftp.cwd("../../") ftp.cwd("../../")
with open(html_file, "rb") as fp: with open(html_file, "rb") as fp:
ftp.storlines("STOR Download.html", fp) ftp.storlines("STOR Download.html", fp)
ftp.quit() ftp.quit()
def confirm_package_contents(package_dir, package_files): def confirm_package_contents(package_dir, package_files):
all_found = True all_found = True
for file in package_files: for file in package_files:
path = os.path.join(package_dir, file) path = os.path.join(package_dir, file)
if not os.path.exists(path): if not os.path.exists(path):
print("Missing package file: %s" % path) print("Missing package file: %s" % path)
all_found = False all_found = False
if not all_found: if not all_found:
raise PackageError("Failed to generate package files") raise PackageError("Failed to generate package files")
def rmdir(dir): def rmdir(dir):
def delete_readonly(action, name, exc): def delete_readonly(action, name, exc):
import stat # https://stackoverflow.com/questions/2656322/shutil-rmtree-fails-on-windows-with-access-is-denied
os.chmod(name, stat.S_IWRITE) import stat
os.remove(name) if not os.access(name, os.W_OK):
for root, dirs, files in os.walk(dir): # we expect .svn to be write-protected os.chmod(name, stat.S_IWUSR)
if '.svn' in dirs: action(name)
shutil.rmtree(root+'\.svn',onerror=delete_readonly) for root, dirs, files in os.walk(dir): # we expect .git to be write-protected
shutil.rmtree(dir) if '.git' in dirs:
shutil.rmtree(root+'\.git',onerror=delete_readonly)
def update_svn_snapshot(working_dir, username, password, test_run, make): shutil.rmtree(dir)
failed = False
print("Starting %s in working directory: %s" % ("test run" if test_run else "release process", working_dir)) def update_git_snapshot(working_dir, username, password, test_run, make):
failed = False
try: print("Starting %s in working directory: %s" % ("test run" if test_run else "release process", working_dir))
bash = Bash(bash_path="c:\\msys64\\usr\\bin\\bash.exe")
repo_dir = os.path.join(working_dir, "model3emu") try:
bash = Bash(bash_path="c:\\msys64\\usr\\bin\\bash.exe")
# Fetch Supermodel download page and compare most recent uploaded SVN snapshot against current SVN revision repo_dir = os.path.join(working_dir, "model3emu")
print("Fetching Supermodel download page...")
html = get_web_page(url="https://supermodel3.com/Forum/Download.html", test_run=test_run) # Clone Git repo. Unfortunately need to always do this in order to get short SHA
uploaded_revisions = get_uploaded_svn_revisions(html=html) result = bash.execute(working_dir=working_dir, command="git clone https://github.com/trzy/Supermodel.git model3emu")
last_uploaded_revision = uploaded_revisions[-1] if len(uploaded_revisions) > 0 else None if result.returncode != 0:
print("Checking current SVN revision...") raise CheckoutError("Git clone failed")
result = bash.execute(working_dir=working_dir, command="svn info --show-item revision https://svn.code.sf.net/p/model3emu/code/trunk")
if result.returncode != 0: # Fetch Supermodel download page and compare most recent uploaded Git snapshot against current Git sha
raise CheckoutError("SVN revision check failed") print("Fetching Supermodel download page...")
current_revision = result.stdout.decode().strip() html = get_web_page(test_run=test_run)
uploaded_shas = get_uploaded_git_shas(html=html)
# If SVN has a newer version, or if performing a test run, build it and update the web page last_uploaded_sha = uploaded_shas[-1] if len(uploaded_shas) > 0 else None
if current_revision == last_uploaded_revision and (not test_run): print("Checking current Git SHA...")
print("Nothing to do. Revision already uploaded: %s" % current_revision) result = bash.execute(working_dir=repo_dir, command="git rev-parse --short HEAD")
else: if result.returncode != 0:
# Check out raise CheckoutError("Git HEAD SHA check failed")
print("SVN revision is %s but uploaded revision is %s" % (current_revision, last_uploaded_revision)) current_sha = result.stdout.decode().strip()
print("Checking out Supermodel...")
result = bash.execute(working_dir=working_dir, command="svn co https://svn.code.sf.net/p/model3emu/code/trunk model3emu") # Get date of commit
if result.returncode != 0: result = bash.execute(working_dir=repo_dir, command="git show -s --format=%as HEAD")
raise CheckoutError("SVN checkout failed") if result.returncode != 0:
raise CheckoutError("Unable to obtain HEAD commit date")
# Build current_date = result.stdout.decode().strip()
print("Building Supermodel...")
result = bash.execute(working_dir=repo_dir, command=make + " -f Makefiles/Makefile.Win32 version") # If Git has a newer version, or if performing a test run, build it and update the web page
if result.returncode != 0: if current_sha == last_uploaded_sha and (not test_run):
raise BuildError("Failed to obtain version") print("Nothing to do. Current Git SHA already uploaded: %s" % current_sha)
version = result.stdout.decode().strip() else:
result = bash.execute(working_dir=repo_dir, command=make + " -f Makefiles/Makefile.Win32 release NET_BOARD=1") # Check out
if result.returncode != 0: print("Git SHA is %s but last uploaded SHA is %s" % (current_sha, last_uploaded_sha))
raise BuildError("Build failed")
# Build
# Stage the release package files print("Building Supermodel...")
print("Creating release package...") result = bash.execute(working_dir=repo_dir, command=make + " -f Makefiles/Makefile.Win32 version")
pkg_dir = os.path.join(working_dir, "pkg") if result.returncode != 0:
bash.execute(working_dir=working_dir, command="mkdir pkg && mkdir pkg/Config && mkdir pkg/NVRAM && mkdir pkg/Saves && mkdir pkg/ROMs") raise BuildError("Failed to obtain version")
change_log_file_path = os.path.join(pkg_dir, "CHANGES.txt") version = result.stdout.decode().strip()
create_change_log(bash, repo_dir=repo_dir, file_path=change_log_file_path, uploaded_revisions=uploaded_revisions, current_revision=current_revision) result = bash.execute(working_dir=repo_dir, command=make + " -f Makefiles/Makefile.Win32 release NET_BOARD=1", print_output=False)
bash.execute(working_dir=working_dir, command="cp model3emu/Config/Supermodel.ini pkg/Config && cp model3emu/Config/Games.xml pkg/Config") if result.returncode != 0:
bash.execute(working_dir=working_dir, command="echo NVRAM files go here. >pkg/NVRAM/DIR.txt") raise BuildError("Build failed")
bash.execute(working_dir=working_dir, command="echo Save states go here. >pkg/Saves/DIR.txt")
bash.execute(working_dir=working_dir, command="echo Recommended \\(but not mandatory\\) location for ROM sets. >pkg/ROMs/DIR.txt") # Stage the release package files
bash.execute(working_dir=working_dir, command="cp model3emu/Docs/README.txt pkg && cp model3emu/Docs/LICENSE.txt pkg") print("Creating release package...")
bash.execute(working_dir=working_dir, command="cp model3emu/bin64/supermodel.exe pkg/Supermodel.exe") pkg_dir = os.path.join(working_dir, "pkg")
#bash.execute(working_dir=working_dir, command="cp /mingw64/bin/SDL2.dll pkg && cp /mingw64/bin/SDL2_net.dll pkg") bash.execute(working_dir=working_dir, command="mkdir pkg && mkdir pkg/Config && mkdir pkg/NVRAM && mkdir pkg/Saves && mkdir pkg/ROMs")
package_files = [ change_log_file_path = os.path.join(pkg_dir, "CHANGES.txt")
"Supermodel.exe", create_change_log(bash, repo_dir=repo_dir, file_path=change_log_file_path, uploaded_shas=uploaded_shas, current_sha=current_sha)
#"SDL2.dll", bash.execute(working_dir=working_dir, command="cp model3emu/Config/Supermodel.ini pkg/Config && cp model3emu/Config/Games.xml pkg/Config")
#"SDL2_net.dll", bash.execute(working_dir=working_dir, command="echo NVRAM files go here. >pkg/NVRAM/DIR.txt")
"README.txt", bash.execute(working_dir=working_dir, command="echo Save states go here. >pkg/Saves/DIR.txt")
"LICENSE.txt", bash.execute(working_dir=working_dir, command="echo Recommended \\(but not mandatory\\) location for ROM sets. >pkg/ROMs/DIR.txt")
"CHANGES.txt", bash.execute(working_dir=working_dir, command="cp model3emu/Docs/README.txt pkg && cp model3emu/Docs/LICENSE.txt pkg")
"Config/Supermodel.ini", bash.execute(working_dir=working_dir, command="cp model3emu/bin64/supermodel.exe pkg/Supermodel.exe")
"Config/Games.xml", #bash.execute(working_dir=working_dir, command="cp /mingw64/bin/SDL2.dll pkg && cp /mingw64/bin/SDL2_net.dll pkg")
"NVRAM/DIR.txt", package_files = [
"Saves/DIR.txt", "Supermodel.exe",
"ROMs/DIR.txt" #"SDL2.dll",
] #"SDL2_net.dll",
confirm_package_contents(package_dir=pkg_dir, package_files=package_files) "README.txt",
"LICENSE.txt",
# Zip them up "CHANGES.txt",
print("Compressing...") "Config/Supermodel.ini",
zip_file = "Supermodel_" + version + "_Win64.zip" "Config/Games.xml",
zip_path = os.path.join(pkg_dir, zip_file) "NVRAM/DIR.txt",
result = bash.execute(working_dir=pkg_dir, command="zip " + zip_file + " " + " ".join(package_files)) "Saves/DIR.txt",
if result.returncode != 0: "ROMs/DIR.txt"
raise PackageError("Failed to compress package") ]
confirm_package_contents(package_dir=pkg_dir, package_files=package_files)
# Update the web page
print("Updating Download page HTML...") # Zip them up
html = add_new_file(html=html, filename=zip_file, version=version, svn_revision=current_revision) print("Compressing...")
html_file_path = os.path.join(working_dir, "Download.updated.html") zip_file = "Supermodel_" + version + "_Win64.zip"
write_html_file(html=html, file_path=html_file_path) zip_path = os.path.join(pkg_dir, zip_file)
if test_run: result = bash.execute(working_dir=pkg_dir, command="zip " + zip_file + " " + " ".join(package_files))
input("Test run finished. Press Enter to complete clean-up process...") if result.returncode != 0:
else: raise PackageError("Failed to compress package")
print("Uploading Download page...")
upload(html_file=html_file_path, zip_file_path=zip_path, username=username, password=password) # Update the web page
print("Updating Download page HTML...")
except Exception as e: html = add_new_file(html=html, filename=zip_file, version=version, git_sha=current_sha, date=current_date)
print("Error: %s" % str(e)) html_file_path = os.path.join(working_dir, "Download.updated.html")
failed = True write_html_file(html=html, file_path=html_file_path)
except: if test_run:
print("Unknown error: ", sys.exc_info()[0]) input("Test run finished. Press Enter to complete clean-up process...")
failed = True else:
print("Uploading Download page...")
print("Cleaning up...") upload(html_file=html_file_path, zip_file_path=zip_path, username=username, password=password)
rmdir(working_dir)
return failed except Exception as e:
print("Error: %s" % str(e))
if __name__ == "__main__": failed = True
parser = argparse.ArgumentParser() except:
parser.add_argument("--username", metavar="user", type=str, action="store", help="Supermodel3.com FTP username") print("Unknown error: ", sys.exc_info()[0])
parser.add_argument("--password", metavar="pass", type=str, action="store", help="Supermodel3.com FTP password") failed = True
parser.add_argument("--working-dir", metavar="path", type=str, action="store", help="Working directory to use (must not already exist); temporary directory if none specified")
parser.add_argument("--test-run", action="store_true", help="Force a build without uploading and insert a pause") print("Cleaning up...")
parser.add_argument("--make", metavar="command", type=str, default="mingw32-make", action="store", help="Make command to use") rmdir(working_dir)
options = parser.parse_args() return failed
max_attempts = 3 if __name__ == "__main__":
attempt = 1 parser = argparse.ArgumentParser()
parser.add_argument("--username", metavar="user", type=str, action="store", help="Supermodel3.com FTP username")
while attempt <= max_attempts: parser.add_argument("--password", metavar="pass", type=str, action="store", help="Supermodel3.com FTP password")
if options.working_dir: parser.add_argument("--working-dir", metavar="path", type=str, action="store", help="Working directory to use (must not already exist); temporary directory if none specified")
if os.path.exists(options.working_dir): parser.add_argument("--test-run", action="store_true", help="Force a build without uploading and insert a pause")
raise Exception("Specified working directory (%s) already exists. This script requires a non-existent path that can safely be overwritten and then deleted." % options.working_dir) parser.add_argument("--make", metavar="command", type=str, default="mingw32-make", action="store", help="Make command to use")
os.makedirs(options.working_dir) options = parser.parse_args()
failed = update_svn_snapshot(working_dir=options.working_dir, username=options.username, password=options.password, test_run=options.test_run, make=options.make)
else: max_attempts = 3
with tempfile.TemporaryDirectory() as working_dir: attempt = 1
failed = update_svn_snapshot(working_dir=working_dir, username=options.username, password=options.password, test_run=options.test_run, make=options.make)
# Retry until success while attempt <= max_attempts:
if not failed: if options.working_dir:
break if os.path.exists(options.working_dir):
attempt += 1 raise Exception("Specified working directory (%s) already exists. This script requires a non-existent path that can safely be overwritten and then deleted." % options.working_dir)
if attempt <= max_attempts: os.makedirs(options.working_dir)
print("Release failed. Retrying (%d/%d)..." % (attempt, max_attempts)) failed = update_git_snapshot(working_dir=options.working_dir, username=options.username, password=options.password, test_run=options.test_run, make=options.make)
else:
with tempfile.TemporaryDirectory() as working_dir:
failed = update_git_snapshot(working_dir=working_dir, username=options.username, password=options.password, test_run=options.test_run, make=options.make)
# Retry until success
if not failed:
break
attempt += 1
if attempt <= max_attempts:
print("Release failed. Retrying (%d/%d)..." % (attempt, max_attempts))