mirror of
https://github.com/frappe/bench.git
synced 2025-01-10 09:02:10 +00:00
Merge branch 'staging' into v5.x
This commit is contained in:
commit
93eedbd876
9
.github/workflows/easy-install.yml
vendored
9
.github/workflows/easy-install.yml
vendored
@ -21,12 +21,17 @@ jobs:
|
|||||||
name: Easy Install Test
|
name: Easy Install Test
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.8'
|
||||||
|
|
||||||
- name: Perform production easy install
|
- name: Perform production easy install
|
||||||
run: |
|
run: |
|
||||||
python3 ${GITHUB_WORKSPACE}/easy-install.py -p -n actions_test --email test@frappe.io
|
python3 ${GITHUB_WORKSPACE}/easy-install.py -p -n actions_test --email test@frappe.io
|
||||||
docker compose -p actions_test exec backend bench version --format json
|
docker compose -p actions_test exec backend bench version --format json
|
||||||
docker compose -p actions_test exec backend bench --site site1.local list-apps --format json
|
docker compose -p actions_test exec backend bench --site site1.localhost list-apps --format json
|
||||||
result=$(curl -sk https://127.0.0.1/api/method/ping | jq -r ."message")
|
result=$(curl -H "Host: site1.localhost" -sk https://127.0.0.1/api/method/ping | jq -r ."message")
|
||||||
if [[ "$result" == "pong" ]]; then echo "New instance works fine"; else exit 1; fi
|
if [[ "$result" == "pong" ]]; then echo "New instance works fine"; else exit 1; fi
|
||||||
docker compose -p actions_test down
|
docker compose -p actions_test down
|
||||||
docker volume prune -f
|
docker volume prune -f
|
||||||
|
18
bench/app.py
18
bench/app.py
@ -198,7 +198,7 @@ class App(AppMeta):
|
|||||||
|
|
||||||
@step(title="Archiving App {repo}", success="App {repo} Archived")
|
@step(title="Archiving App {repo}", success="App {repo} Archived")
|
||||||
def remove(self, no_backup: bool = False):
|
def remove(self, no_backup: bool = False):
|
||||||
active_app_path = os.path.join("apps", self.repo)
|
active_app_path = os.path.join("apps", self.app_name)
|
||||||
|
|
||||||
if no_backup:
|
if no_backup:
|
||||||
if not os.path.islink(active_app_path):
|
if not os.path.islink(active_app_path):
|
||||||
@ -209,7 +209,7 @@ class App(AppMeta):
|
|||||||
else:
|
else:
|
||||||
archived_path = os.path.join("archived", "apps")
|
archived_path = os.path.join("archived", "apps")
|
||||||
archived_name = get_available_folder_name(
|
archived_name = get_available_folder_name(
|
||||||
f"{self.repo}-{date.today()}", archived_path
|
f"{self.app_name}-{date.today()}", archived_path
|
||||||
)
|
)
|
||||||
archived_app_path = os.path.join(archived_path, archived_name)
|
archived_app_path = os.path.join(archived_path, archived_name)
|
||||||
|
|
||||||
@ -233,7 +233,7 @@ class App(AppMeta):
|
|||||||
|
|
||||||
verbose = bench.cli.verbose or verbose
|
verbose = bench.cli.verbose or verbose
|
||||||
app_name = get_app_name(self.bench.name, self.app_name)
|
app_name = get_app_name(self.bench.name, self.app_name)
|
||||||
if not resolved and self.repo != "frappe" and not ignore_resolution:
|
if not resolved and self.app_name != "frappe" and not ignore_resolution:
|
||||||
click.secho(
|
click.secho(
|
||||||
f"Ignoring dependencies of {self.name}. To install dependencies use --resolve-deps",
|
f"Ignoring dependencies of {self.name}. To install dependencies use --resolve-deps",
|
||||||
fg="yellow",
|
fg="yellow",
|
||||||
@ -262,7 +262,7 @@ class App(AppMeta):
|
|||||||
from bench.utils.app import get_required_deps, required_apps_from_hooks
|
from bench.utils.app import get_required_deps, required_apps_from_hooks
|
||||||
|
|
||||||
if self.on_disk:
|
if self.on_disk:
|
||||||
required_deps = os.path.join(self.mount_path, self.repo, "hooks.py")
|
required_deps = os.path.join(self.mount_path, self.app_name, "hooks.py")
|
||||||
try:
|
try:
|
||||||
return required_apps_from_hooks(required_deps, local=True)
|
return required_apps_from_hooks(required_deps, local=True)
|
||||||
except IndexError:
|
except IndexError:
|
||||||
@ -290,16 +290,16 @@ def make_resolution_plan(app: App, bench: "Bench"):
|
|||||||
decide what apps and versions to install and in what order
|
decide what apps and versions to install and in what order
|
||||||
"""
|
"""
|
||||||
resolution = OrderedDict()
|
resolution = OrderedDict()
|
||||||
resolution[app.repo] = app
|
resolution[app.app_name] = app
|
||||||
|
|
||||||
for app_name in app._get_dependencies():
|
for app_name in app._get_dependencies():
|
||||||
dep_app = App(app_name, bench=bench)
|
dep_app = App(app_name, bench=bench)
|
||||||
is_valid_frappe_branch(dep_app.url, dep_app.branch)
|
is_valid_frappe_branch(dep_app.url, dep_app.branch)
|
||||||
dep_app.required_by = app.name
|
dep_app.required_by = app.name
|
||||||
if dep_app.repo in resolution:
|
if dep_app.app_name in resolution:
|
||||||
click.secho(f"{dep_app.repo} is already resolved skipping", fg="yellow")
|
click.secho(f"{dep_app.app_name} is already resolved skipping", fg="yellow")
|
||||||
continue
|
continue
|
||||||
resolution[dep_app.repo] = dep_app
|
resolution[dep_app.app_name] = dep_app
|
||||||
resolution.update(make_resolution_plan(dep_app, bench))
|
resolution.update(make_resolution_plan(dep_app, bench))
|
||||||
app.local_resolution = [repo_name for repo_name, _ in reversed(resolution.items())]
|
app.local_resolution = [repo_name for repo_name, _ in reversed(resolution.items())]
|
||||||
return resolution
|
return resolution
|
||||||
@ -315,7 +315,7 @@ def get_excluded_apps(bench_path="."):
|
|||||||
|
|
||||||
def add_to_excluded_apps_txt(app, bench_path="."):
|
def add_to_excluded_apps_txt(app, bench_path="."):
|
||||||
if app == "frappe":
|
if app == "frappe":
|
||||||
raise ValueError("Frappe app cannot be excludeed from update")
|
raise ValueError("Frappe app cannot be excluded from update")
|
||||||
if app not in os.listdir("apps"):
|
if app not in os.listdir("apps"):
|
||||||
raise ValueError(f"The app {app} does not exist")
|
raise ValueError(f"The app {app} does not exist")
|
||||||
apps = get_excluded_apps(bench_path=bench_path)
|
apps = get_excluded_apps(bench_path=bench_path)
|
||||||
|
@ -131,6 +131,7 @@ class Bench(Base, Validator):
|
|||||||
except InvalidRemoteException:
|
except InvalidRemoteException:
|
||||||
if not force:
|
if not force:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
self.apps.sync()
|
self.apps.sync()
|
||||||
# self.build() - removed because it seems unnecessary
|
# self.build() - removed because it seems unnecessary
|
||||||
self.reload(_raise=False)
|
self.reload(_raise=False)
|
||||||
@ -309,13 +310,13 @@ class BenchApps(MutableSequence):
|
|||||||
def add(self, app: "App"):
|
def add(self, app: "App"):
|
||||||
app.get()
|
app.get()
|
||||||
app.install()
|
app.install()
|
||||||
super().append(app.repo)
|
super().append(app.app_name)
|
||||||
self.apps.sort()
|
self.apps.sort()
|
||||||
|
|
||||||
def remove(self, app: "App", no_backup: bool = False):
|
def remove(self, app: "App", no_backup: bool = False):
|
||||||
app.uninstall()
|
app.uninstall()
|
||||||
app.remove(no_backup=no_backup)
|
app.remove(no_backup=no_backup)
|
||||||
super().remove(app.repo)
|
super().remove(app.app_name)
|
||||||
|
|
||||||
def append(self, app: "App"):
|
def append(self, app: "App"):
|
||||||
return self.add(app)
|
return self.add(app)
|
||||||
|
28
bench/cli.py
28
bench/cli.py
@ -28,6 +28,8 @@ from bench.utils import (
|
|||||||
get_cmd_from_sysargv,
|
get_cmd_from_sysargv,
|
||||||
)
|
)
|
||||||
from bench.utils.bench import get_env_cmd
|
from bench.utils.bench import get_env_cmd
|
||||||
|
from importlib.util import find_spec
|
||||||
|
|
||||||
|
|
||||||
# these variables are used to show dynamic outputs on the terminal
|
# these variables are used to show dynamic outputs on the terminal
|
||||||
dynamic_feed = False
|
dynamic_feed = False
|
||||||
@ -38,6 +40,7 @@ bench.LOG_BUFFER = []
|
|||||||
|
|
||||||
change_uid_msg = "You should not run this command as root"
|
change_uid_msg = "You should not run this command as root"
|
||||||
src = os.path.dirname(__file__)
|
src = os.path.dirname(__file__)
|
||||||
|
SKIP_MODULE_TRACEBACK = ("click",)
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
@ -118,6 +121,8 @@ def cli():
|
|||||||
_opts = [x.opts + x.secondary_opts for x in bench_command.params]
|
_opts = [x.opts + x.secondary_opts for x in bench_command.params]
|
||||||
opts = {item for sublist in _opts for item in sublist}
|
opts = {item for sublist in _opts for item in sublist}
|
||||||
|
|
||||||
|
setup_exception_handler()
|
||||||
|
|
||||||
# handle usages like `--use-feature='feat-x'` and `--use-feature 'feat-x'`
|
# handle usages like `--use-feature='feat-x'` and `--use-feature 'feat-x'`
|
||||||
if cmd_from_sys and cmd_from_sys.split("=", 1)[0].strip() in opts:
|
if cmd_from_sys and cmd_from_sys.split("=", 1)[0].strip() in opts:
|
||||||
bench_command()
|
bench_command()
|
||||||
@ -240,3 +245,26 @@ def setup_clear_cache():
|
|||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
os.chdir = _chdir
|
os.chdir = _chdir
|
||||||
|
|
||||||
|
|
||||||
|
def setup_exception_handler():
|
||||||
|
from traceback import format_exception
|
||||||
|
from bench.exceptions import CommandFailedError
|
||||||
|
|
||||||
|
def handle_exception(exc_type, exc_info, tb):
|
||||||
|
if exc_type == CommandFailedError:
|
||||||
|
print("".join(generate_exc(exc_type, exc_info, tb)))
|
||||||
|
else:
|
||||||
|
sys.__excepthook__(exc_type, exc_info, tb)
|
||||||
|
|
||||||
|
def generate_exc(exc_type, exc_info, tb):
|
||||||
|
TB_SKIP = [
|
||||||
|
os.path.dirname(find_spec(module).origin) for module in SKIP_MODULE_TRACEBACK
|
||||||
|
]
|
||||||
|
|
||||||
|
for tb_line in format_exception(exc_type, exc_info, tb):
|
||||||
|
for skip_module in TB_SKIP:
|
||||||
|
if skip_module not in tb_line:
|
||||||
|
yield tb_line
|
||||||
|
|
||||||
|
sys.excepthook = handle_exception
|
||||||
|
@ -182,12 +182,12 @@ programs={{ bench_name }}-frappe-web {%- if node -%} ,{{ bench_name }}-node-sock
|
|||||||
{% if use_rq %}
|
{% if use_rq %}
|
||||||
|
|
||||||
[group:{{ bench_name }}-workers]
|
[group:{{ bench_name }}-workers]
|
||||||
programs={{ bench_name }}-frappe-schedule,{{ bench_name }}-frappe-default-worker,{{ bench_name }}-frappe-short-worker,{{ bench_name }}-frappe-long-worker
|
programs={{ bench_name }}-frappe-schedule,{{ bench_name }}-frappe-default-worker,{{ bench_name }}-frappe-short-worker,{{ bench_name }}-frappe-long-worker{%- for worker_name in workers -%},{{ bench_name }}-frappe-{{ worker_name }}-worker{%- endfor %}
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
[group:{{ bench_name }}-workers]
|
[group:{{ bench_name }}-workers]
|
||||||
programs={{ bench_name }}-frappe-workerbeat,{{ bench_name }}-frappe-worker,{{ bench_name }}-frappe-longjob-worker,{{ bench_name }}-frappe-async-worker
|
programs={{ bench_name }}-frappe-workerbeat,{{ bench_name }}-frappe-worker,{{ bench_name }}-frappe-longjob-worker,{{ bench_name }}-frappe-async-worker{%- for worker_name in workers -%},{{ bench_name }}-frappe-{{ worker_name }}-worker{%- endfor %}
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -28,8 +28,8 @@ class TestBenchInit(TestBenchBase):
|
|||||||
self.init_bench(bench_name, **kwargs)
|
self.init_bench(bench_name, **kwargs)
|
||||||
app = App("file:///tmp/frappe")
|
app = App("file:///tmp/frappe")
|
||||||
self.assertTupleEqual(
|
self.assertTupleEqual(
|
||||||
(app.mount_path, app.url, app.repo, app.org),
|
(app.mount_path, app.url, app.repo, app.app_name, app.org),
|
||||||
("/tmp/frappe", "file:///tmp/frappe", "frappe", "frappe"),
|
("/tmp/frappe", "file:///tmp/frappe", "frappe", "frappe", "frappe"),
|
||||||
)
|
)
|
||||||
self.assert_folders(bench_name)
|
self.assert_folders(bench_name)
|
||||||
self.assert_virtual_env(bench_name)
|
self.assert_virtual_env(bench_name)
|
||||||
|
@ -101,4 +101,6 @@ class TestUtils(unittest.TestCase):
|
|||||||
|
|
||||||
def test_ssh_ports(self):
|
def test_ssh_ports(self):
|
||||||
app = App("git@github.com:22:frappe/frappe")
|
app = App("git@github.com:22:frappe/frappe")
|
||||||
self.assertEqual((app.use_ssh, app.org, app.repo), (True, "frappe", "frappe"))
|
self.assertEqual(
|
||||||
|
(app.use_ssh, app.org, app.repo, app.app_name), (True, "frappe", "frappe", "frappe")
|
||||||
|
)
|
||||||
|
@ -155,7 +155,7 @@ def exec_cmd(cmd, cwd=".", env=None, _raise=True):
|
|||||||
if return_code:
|
if return_code:
|
||||||
logger.warning(f"{cmd_log} executed with exit code {return_code}")
|
logger.warning(f"{cmd_log} executed with exit code {return_code}")
|
||||||
if _raise:
|
if _raise:
|
||||||
raise CommandFailedError from subprocess.CalledProcessError(return_code, cmd)
|
raise CommandFailedError(cmd) from subprocess.CalledProcessError(return_code, cmd)
|
||||||
return return_code
|
return return_code
|
||||||
|
|
||||||
|
|
||||||
|
@ -155,7 +155,7 @@ def update_npm_packages(bench_path=".", apps=None):
|
|||||||
else:
|
else:
|
||||||
package_json[key] = value
|
package_json[key] = value
|
||||||
|
|
||||||
if package_json is {}:
|
if package_json == {}:
|
||||||
with open(os.path.join(os.path.dirname(__file__), "package.json")) as f:
|
with open(os.path.join(os.path.dirname(__file__), "package.json")) as f:
|
||||||
package_json = json.loads(f.read())
|
package_json = json.loads(f.read())
|
||||||
|
|
||||||
|
@ -73,13 +73,13 @@ def get_from_env(dir, file) -> Dict:
|
|||||||
|
|
||||||
def write_to_env(
|
def write_to_env(
|
||||||
wd: str,
|
wd: str,
|
||||||
site: str,
|
sites,
|
||||||
db_pass: str,
|
db_pass: str,
|
||||||
admin_pass: str,
|
admin_pass: str,
|
||||||
email: str,
|
email: str,
|
||||||
erpnext_version: str = None,
|
erpnext_version: str = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
site_name = site or ""
|
quoted_sites = ",".join([f"`{site}`" for site in sites]).strip(",")
|
||||||
example_env = get_from_env(wd, "example.env")
|
example_env = get_from_env(wd, "example.env")
|
||||||
erpnext_version = erpnext_version or example_env["ERPNEXT_VERSION"]
|
erpnext_version = erpnext_version or example_env["ERPNEXT_VERSION"]
|
||||||
with open(os.path.join(wd, ".env"), "w") as f:
|
with open(os.path.join(wd, ".env"), "w") as f:
|
||||||
@ -93,8 +93,8 @@ def write_to_env(
|
|||||||
"REDIS_QUEUE=redis-queue:6379\n",
|
"REDIS_QUEUE=redis-queue:6379\n",
|
||||||
"REDIS_SOCKETIO=redis-socketio:6379\n",
|
"REDIS_SOCKETIO=redis-socketio:6379\n",
|
||||||
f"LETSENCRYPT_EMAIL={email}\n",
|
f"LETSENCRYPT_EMAIL={email}\n",
|
||||||
f"FRAPPE_SITE_NAME_HEADER={site_name}\n",
|
f"SITE_ADMIN_PASS={admin_pass}\n",
|
||||||
f"SITE_ADMIN_PASS={admin_pass}",
|
f"SITES={quoted_sites}\n",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -114,7 +114,7 @@ def check_repo_exists() -> bool:
|
|||||||
return os.path.exists(os.path.join(os.getcwd(), "frappe_docker"))
|
return os.path.exists(os.path.join(os.getcwd(), "frappe_docker"))
|
||||||
|
|
||||||
|
|
||||||
def setup_prod(project: str, sitename: str, email: str, version: str = None) -> None:
|
def setup_prod(project: str, sites, email: str, version: str = None) -> None:
|
||||||
if check_repo_exists():
|
if check_repo_exists():
|
||||||
compose_file_name = os.path.join(os.path.expanduser("~"), f"{project}-compose.yml")
|
compose_file_name = os.path.join(os.path.expanduser("~"), f"{project}-compose.yml")
|
||||||
docker_repo_path = os.path.join(os.getcwd(), "frappe_docker")
|
docker_repo_path = os.path.join(os.getcwd(), "frappe_docker")
|
||||||
@ -129,7 +129,7 @@ def setup_prod(project: str, sitename: str, email: str, version: str = None) ->
|
|||||||
if not os.path.exists(os.path.join(docker_repo_path, ".env")):
|
if not os.path.exists(os.path.join(docker_repo_path, ".env")):
|
||||||
admin_pass = generate_pass()
|
admin_pass = generate_pass()
|
||||||
db_pass = generate_pass(9)
|
db_pass = generate_pass(9)
|
||||||
write_to_env(docker_repo_path, sitename, db_pass, admin_pass, email, version)
|
write_to_env(docker_repo_path, sites, db_pass, admin_pass, email, version)
|
||||||
cprint(
|
cprint(
|
||||||
"\nA .env file is generated with basic configs. Please edit it to fit to your needs \n",
|
"\nA .env file is generated with basic configs. Please edit it to fit to your needs \n",
|
||||||
level=3,
|
level=3,
|
||||||
@ -193,40 +193,13 @@ def setup_prod(project: str, sitename: str, email: str, version: str = None) ->
|
|||||||
cprint(" Docker Compose failed, please check the container logs\n", e)
|
cprint(" Docker Compose failed, please check the container logs\n", e)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
cprint(f"\nCreating site: {sitename} \n", level=3)
|
for sitename in sites:
|
||||||
|
create_site(sitename, project, db_pass, admin_pass)
|
||||||
|
|
||||||
try:
|
|
||||||
subprocess.run(
|
|
||||||
[
|
|
||||||
which("docker"),
|
|
||||||
"compose",
|
|
||||||
"-p",
|
|
||||||
project,
|
|
||||||
"exec",
|
|
||||||
"backend",
|
|
||||||
"bench",
|
|
||||||
"new-site",
|
|
||||||
sitename,
|
|
||||||
"--no-mariadb-socket",
|
|
||||||
"--db-root-password",
|
|
||||||
db_pass,
|
|
||||||
"--admin-password",
|
|
||||||
admin_pass,
|
|
||||||
"--install-app",
|
|
||||||
"erpnext",
|
|
||||||
"--set-default",
|
|
||||||
],
|
|
||||||
check=True,
|
|
||||||
)
|
|
||||||
logging.info("New site creation completed")
|
|
||||||
except Exception as e:
|
|
||||||
logging.error("Bench site creation failed", exc_info=True)
|
|
||||||
cprint("Bench Site creation failed\n", e)
|
|
||||||
sys.exit(1)
|
|
||||||
else:
|
else:
|
||||||
install_docker()
|
install_docker()
|
||||||
clone_frappe_docker_repo()
|
clone_frappe_docker_repo()
|
||||||
setup_prod(project, sitename, email, version) # Recursive
|
setup_prod(project, sites, email, version) # Recursive
|
||||||
|
|
||||||
|
|
||||||
def setup_dev_instance(project: str):
|
def setup_dev_instance(project: str):
|
||||||
@ -294,6 +267,43 @@ def install_docker():
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def create_site(
|
||||||
|
sitename: str,
|
||||||
|
project: str,
|
||||||
|
db_pass: str,
|
||||||
|
admin_pass: str,
|
||||||
|
):
|
||||||
|
cprint(f"\nCreating site: {sitename} \n", level=3)
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
which("docker"),
|
||||||
|
"compose",
|
||||||
|
"-p",
|
||||||
|
project,
|
||||||
|
"exec",
|
||||||
|
"backend",
|
||||||
|
"bench",
|
||||||
|
"new-site",
|
||||||
|
sitename,
|
||||||
|
"--no-mariadb-socket",
|
||||||
|
"--db-root-password",
|
||||||
|
db_pass,
|
||||||
|
"--admin-password",
|
||||||
|
admin_pass,
|
||||||
|
"--install-app",
|
||||||
|
"erpnext",
|
||||||
|
"--set-default",
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
logging.info("New site creation completed")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Bench site creation failed for {sitename}", exc_info=True)
|
||||||
|
cprint(f"Bench Site creation failed for {sitename}\n", e)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(description="Install Frappe with Docker")
|
parser = argparse.ArgumentParser(description="Install Frappe with Docker")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@ -305,8 +315,10 @@ if __name__ == "__main__":
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-s",
|
"-s",
|
||||||
"--sitename",
|
"--sitename",
|
||||||
help="The Site Name for your production site",
|
help="Site Name(s) for your production bench",
|
||||||
default="site1.local",
|
default=["site1.localhost"],
|
||||||
|
action="append",
|
||||||
|
dest="sites",
|
||||||
)
|
)
|
||||||
parser.add_argument("-n", "--project", help="Project Name", default="frappe")
|
parser.add_argument("-n", "--project", help="Project Name", default="frappe")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@ -326,6 +338,6 @@ if __name__ == "__main__":
|
|||||||
if "example.com" in args.email:
|
if "example.com" in args.email:
|
||||||
cprint("Emails with example.com not acceptable", level=1)
|
cprint("Emails with example.com not acceptable", level=1)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
setup_prod(args.project, args.sitename, args.email, args.version)
|
setup_prod(args.project, args.sites, args.email, args.version)
|
||||||
else:
|
else:
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
|
Loading…
Reference in New Issue
Block a user