From 4c30071bcbf1b40ce168dee2257ae1a4372af703 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 13 Oct 2016 12:58:07 +0530 Subject: [PATCH 01/31] [fix] readme --- README.md | 17 +++++++++++------ vm/ansible/vm.yml | 1 + 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ac611727..aab80dda 100755 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Easy Setup - This is an opinionated setup with logging and SE Linux. So, it is best to setup on a blank server. - Works on Ubuntu 14.04 to 16.04, CentOS 7+, Debian 7 to 8 and MacOS X. -- You may have to install Python 2.7 (eg on Ubuntu 16.04+) by running `apt-get install python-minimal` +- You may have to install Python 2.7 (eg on Ubuntu 16.04+) by running `apt-get install python-minimal` - This script will install the pre-requisites, install bench and setup an ERPNext site - Passwords for Frappe Administrator and MariaDB (root) will be asked - You can then login as **Administrator** with the Administrator password @@ -31,18 +31,23 @@ Steps Open your Terminal and enter: - # Linux: +#### Linux: wget https://raw.githubusercontent.com/frappe/bench/master/playbooks/install.py - # Mac OSX: +#### MacOS: + +Install X Code (from App store) and HomeBrew (http://brew.sh/) - # install X Code (from App store) - # install HomeBrew (http://brew.sh/) brew install python brew install git + +Download the Script + curl "https://raw.githubusercontent.com/frappe/bench/master/playbooks/install.py" -o install.py +#### Run the Script + # For development sudo python install.py --develop @@ -50,7 +55,7 @@ Open your Terminal and enter: sudo python install.py --production # If you're logged in as root, use --user flag to create a user and install using that user - sudo python install.py --develop --user frappe + python install.py --develop --user frappe For development, you have to explicitly start services by running `bench start`. This script requires Python2.7+ installed on your machine. You will have to manually create a new site (`bench new-site`) and get apps that you need (`bench get-app`, `bench install-app`). diff --git a/vm/ansible/vm.yml b/vm/ansible/vm.yml index 42b638d7..18357509 100644 --- a/vm/ansible/vm.yml +++ b/vm/ansible/vm.yml @@ -7,6 +7,7 @@ - git: repo=https://github.com/frappe/bench dest=/home/frappe/bench-repo + depth=no tags: - bench_install From c1af9ca4e952672edf6b5df0c308d1179dfaf442 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 18 Apr 2017 14:52:14 +0530 Subject: [PATCH 02/31] Removed swtich-to-v4 and switch-to-v5 commands --- bench/app.py | 6 ------ bench/commands/__init__.py | 5 +---- bench/commands/update.py | 26 ++------------------------ bench/utils.py | 7 +++++++ 4 files changed, 10 insertions(+), 34 deletions(-) diff --git a/bench/app.py b/bench/app.py index 9b4481cc..0d341b49 100755 --- a/bench/app.py +++ b/bench/app.py @@ -279,12 +279,6 @@ def switch_to_master(apps=None, bench_path='.', upgrade=False): def switch_to_develop(apps=None, bench_path='.', upgrade=False): switch_branch('develop', apps=apps, bench_path=bench_path, upgrade=upgrade) -def switch_to_v4(apps=None, bench_path='.', upgrade=False): - switch_branch('v4.x.x', apps=apps, bench_path=bench_path, upgrade=upgrade) - -def switch_to_v5(apps=None, bench_path='.', upgrade=False): - switch_branch('v5.x.x', apps=apps, bench_path=bench_path, upgrade=upgrade) - def get_version_from_string(contents, field='__version__'): match = re.search(r"^(\s*%s\s*=\s*['\\\"])(.+?)(['\"])(?sm)" % field, contents) diff --git a/bench/commands/__init__.py b/bench/commands/__init__.py index 1da94c30..1bf61806 100755 --- a/bench/commands/__init__.py +++ b/bench/commands/__init__.py @@ -29,15 +29,12 @@ bench_command.add_command(remove_app) bench_command.add_command(new_site) -from bench.commands.update import update, retry_upgrade, switch_to_branch, switch_to_master, switch_to_develop, switch_to_v4, switch_to_v5 +from bench.commands.update import update, retry_upgrade, switch_to_branch, switch_to_master, switch_to_develop bench_command.add_command(update) bench_command.add_command(retry_upgrade) bench_command.add_command(switch_to_branch) bench_command.add_command(switch_to_master) bench_command.add_command(switch_to_develop) -bench_command.add_command(switch_to_v4) -bench_command.add_command(switch_to_v5) - from bench.commands.utils import (start, restart, set_nginx_port, set_ssl_certificate, set_ssl_certificate_key, set_url_root, set_mariadb_host, set_default_site, download_translations, shell, backup_site, backup_all_sites, release, renew_lets_encrypt, diff --git a/bench/commands/update.py b/bench/commands/update.py index 7afc0765..afd1362f 100755 --- a/bench/commands/update.py +++ b/bench/commands/update.py @@ -54,7 +54,7 @@ def update(pull=False, patch=False, build=False, bench=False, auto=False, restar print "This update will cause a major version change in Frappe/ERPNext from {0} to {1}.".format(*version_upgrade[1:]) print "This would take significant time to migrate and might break custom apps. Please run `bench update --upgrade` to confirm." print - print "You can stay on the latest stable release by running `bench switch-to-master` or pin your bench to {0} by running `bench switch-to-v{0}`".format(version_upgrade[1]) + print "You can stay on the latest stable release by running `bench switch-to-master` or pin your bench to {0}".format(version_upgrade[1]) sys.exit(1) _update(pull, patch, build, bench, auto, restart_supervisor, requirements, no_backup, upgrade, force=force) @@ -145,26 +145,4 @@ def switch_to_develop(upgrade=False): switch_to_develop(upgrade=upgrade, apps=['frappe', 'erpnext']) print print 'Switched to develop' - print 'Please run `bench update --patch` to be safe from any differences in database schema' - - -@click.command('switch-to-v4') -@click.option('--upgrade',is_flag=True) -def switch_to_v4(upgrade=False): - "Switch frappe and erpnext to v4 branch" - from bench.app import switch_to_v4 - switch_to_v4(upgrade=upgrade) - print - print 'Switched to v4' - print 'Please run `bench update --patch` to be safe from any differences in database schema' - - -@click.command('switch-to-v5') -@click.option('--upgrade',is_flag=True) -def switch_to_v5(upgrade=False): - "Switch frappe and erpnext to v5 branch" - from bench.app import switch_to_v5 - switch_to_v5(upgrade=upgrade) - print - print 'Switched to v5' - print 'Please run `bench update --patch` to be safe from any differences in database schema' + print 'Please run `bench update --patch` to be safe from any differences in database schema' \ No newline at end of file diff --git a/bench/utils.py b/bench/utils.py index 5fed8246..ac282cec 100755 --- a/bench/utils.py +++ b/bench/utils.py @@ -593,6 +593,13 @@ def update_translations(app, lang): f.flush() print 'downloaded for', app, lang + +def download_chart_of_accounts(): + charts_dir = os.path.join('apps', "erpnext", "erpnext", 'accounts', 'chart_of_accounts', "submitted") + csv_file = os.path.join(translations_dir, lang + '.csv') + url = "https://translate.erpnext.com/files/{}-{}.csv".format(app, lang) + r = requests.get(url, stream=True) + r.raise_for_status() def print_output(p): while p.poll() is None: From a04f45b0d979c9f051c079867136c9562266826e Mon Sep 17 00:00:00 2001 From: Daniel Weipert Date: Mon, 15 May 2017 17:43:37 +0200 Subject: [PATCH 03/31] Add --overwrite argument --- playbooks/install.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/playbooks/install.py b/playbooks/install.py index 657cf465..e61dd298 100755 --- a/playbooks/install.py +++ b/playbooks/install.py @@ -335,6 +335,9 @@ def parse_commandline_args(): parser.add_argument('--without-bench-setup', dest='without_bench_setup', action='store_true', default=False, help=argparse.SUPPRESS) + # whether to overwrite an existing bench + parser.add_argument('--overwrite', dest='overwrite', action='store_true', default=False) + args = parser.parse_args() return args From c21554519f8766bdb12a6f658fd2f1a147769de5 Mon Sep 17 00:00:00 2001 From: Daniel Weipert Date: Mon, 15 May 2017 17:44:47 +0200 Subject: [PATCH 04/31] Set directory permissions recursively --- playbooks/develop/create_user.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/playbooks/develop/create_user.yml b/playbooks/develop/create_user.yml index 306dabc1..2980ed4e 100755 --- a/playbooks/develop/create_user.yml +++ b/playbooks/develop/create_user.yml @@ -10,14 +10,24 @@ file: path: '/home/{{ frappe_user }}' mode: 'o+rx' + owner: '{{ frappe_user }}' + group: '{{ frappe_user }}' + recurse: yes when: ansible_distribution == 'Ubuntu' or ansible_distribution == 'CentOS' or ansible_distribution == 'Debian' - name: Set home folder perms file: path: '/Users/{{ frappe_user }}' mode: 'o+rx' + owner: '{{ frappe_user }}' + group: '{{ frappe_user }}' + recurse: yes when: ansible_distribution == 'MacOSX' - name: Set /tmp/.bench folder perms - command: 'chown -R {{ frappe_user }}:{{ frappe_user }} {{ repo_path }}' + file: + path: '{{ repo_path }}' + owner: '{{ frappe_user }}' + group: '{{ frappe_user }}' + recurse: yes when: ansible_distribution == 'Ubuntu' or ansible_distribution == 'CentOS' or ansible_distribution == 'Debian' \ No newline at end of file From b38ba7f7b5add5935513f5ed42832efb84417ab4 Mon Sep 17 00:00:00 2001 From: Daniel Weipert Date: Mon, 15 May 2017 17:45:26 +0200 Subject: [PATCH 05/31] Remove bench dir for new install --- playbooks/develop/includes/setup_bench.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/playbooks/develop/includes/setup_bench.yml b/playbooks/develop/includes/setup_bench.yml index f25cf9f0..7b768c4c 100644 --- a/playbooks/develop/includes/setup_bench.yml +++ b/playbooks/develop/includes/setup_bench.yml @@ -18,6 +18,12 @@ become: yes become_user: root + - name: Overwrite bench if required + file: + state: absent + path: "{{ bench_path }}" + when: overwrite + - name: Check whether bench exists stat: path="{{ bench_path }}" register: bench_stat From 6d2afdd9ec4684595bc40bfa84de8e7190117607 Mon Sep 17 00:00:00 2001 From: Daniel Weipert Date: Mon, 15 May 2017 21:39:35 +0200 Subject: [PATCH 06/31] Add overwrite help message --- playbooks/install.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/playbooks/install.py b/playbooks/install.py index e61dd298..65e6f35d 100755 --- a/playbooks/install.py +++ b/playbooks/install.py @@ -336,7 +336,8 @@ def parse_commandline_args(): help=argparse.SUPPRESS) # whether to overwrite an existing bench - parser.add_argument('--overwrite', dest='overwrite', action='store_true', default=False) + parser.add_argument('--overwrite', dest='overwrite', action='store_true', default=False, + help='Whether to overwrite an existing bench') args = parser.parse_args() From 485d10089ad954c18317503216bbd505f1268f94 Mon Sep 17 00:00:00 2001 From: DRogue Date: Fri, 19 May 2017 12:56:52 +0200 Subject: [PATCH 07/31] consider passwords.txt (#408) --- bench/utils.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/bench/utils.py b/bench/utils.py index 1714f451..1ab43537 100755 --- a/bench/utils.py +++ b/bench/utils.py @@ -139,20 +139,36 @@ def setup_env(bench_path='.'): def setup_socketio(bench_path='.'): exec_cmd("npm install socket.io redis express superagent cookie", cwd=bench_path) + def new_site(site, mariadb_root_password=None, admin_password=None, bench_path='.'): + """ + Creates a new site in the specified bench, default is current bench + """ + logger.info('creating new site {}'.format(site)) - mariadb_root_password_fragment = '--root_password {}'.format(mariadb_root_password) if mariadb_root_password else '' + + # consider an existing passwords.txt file + passwords_file_path = os.path.join(os.path.expanduser('~'), 'passwords.txt') + if os.path.isfile(passwords_file_path): + with open(passwords_file_path, 'r') as f: + passwords = json.load(f) + mariadb_root_password, admin_password = passwords['mysql_root_password'], passwords['admin_password'] + + mysql_root_password_fragment = '--root_password {}'.format(mariadb_root_password) if mariadb_root_password else '' admin_password_fragment = '--admin_password {}'.format(admin_password) if admin_password else '' - exec_cmd("{frappe} {site} --install {db_name} {mariadb_root_password_fragment} {admin_password_fragment}".format( + + exec_cmd("{frappe} {site} --install {db_name} {mysql_root_password_fragment} {admin_password_fragment}".format( frappe=get_frappe(bench_path=bench_path), site=site, - db_name = hashlib.sha1(site).hexdigest()[:10], - mariadb_root_password_fragment=mariadb_root_password_fragment, + db_name=hashlib.sha1(site).hexdigest()[:10], + mysql_root_password_fragment=mysql_root_password_fragment, admin_password_fragment=admin_password_fragment ), cwd=os.path.join(bench_path, 'sites')) + if len(get_sites(bench_path=bench_path)) == 1: exec_cmd("{frappe} --use {site}".format(frappe=get_frappe(bench_path=bench_path), site=site), cwd=os.path.join(bench_path, 'sites')) + def patch_sites(bench_path='.'): bench.set_frappe_version(bench_path=bench_path) From de0d88c0fa5ae190951e5ebda1db12892fb09993 Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Mon, 22 May 2017 15:07:21 +0530 Subject: [PATCH 08/31] Fixes bench release github username error (#406) * ran 2to3 script * Correct urllib.parse import * Backward compatible urllib import * removed test_setup_production_v6 * Fix bug global hashlib is not defined (#399) * Don't need $query_string (#390) * removed semicolon, click.prompt adds it --- bench/config/templates/nginx.conf | 2 +- bench/release.py | 2 +- bench/utils.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bench/config/templates/nginx.conf b/bench/config/templates/nginx.conf index bab700d4..589db7e3 100644 --- a/bench/config/templates/nginx.conf +++ b/bench/config/templates/nginx.conf @@ -128,7 +128,7 @@ server { {% endfor -%} ; - return 301 https://$host$request_uri?$query_string; + return 301 https://$host$request_uri; } {% endif %} diff --git a/bench/release.py b/bench/release.py index caea605e..a82a0826 100755 --- a/bench/release.py +++ b/bench/release.py @@ -38,7 +38,7 @@ def validate(bench_path): github_password = config.get('github_password') if not github_username: - github_username = input('Username: ') + github_username = click.prompt('Username', type=str) if not github_password: github_password = getpass.getpass() diff --git a/bench/utils.py b/bench/utils.py index 1ab43537..6cc26a1f 100755 --- a/bench/utils.py +++ b/bench/utils.py @@ -1,4 +1,4 @@ -import os, sys, shutil, subprocess, logging, itertools, requests, json, platform, select, pwd, grp, multiprocessing +import os, sys, shutil, subprocess, logging, itertools, requests, json, platform, select, pwd, grp, multiprocessing, hashlib from distutils.spawn import find_executable import bench from bench import env From d53f2b07fdcdc9fdf2c59b92123092245bfe16f4 Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Mon, 22 May 2017 15:07:40 +0530 Subject: [PATCH 09/31] Letsencrypt fix develop (#407) * [Fix] Letsencrypt error - Please specify user or filename to write. * Fix bug global hashlib is not defined (#399) * Don't need $query_string (#390) * [Fix] Issue #404 - write to system cron * letsencrypt.py - renamed user_crontab to system_crontab --- bench/config/lets_encrypt.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bench/config/lets_encrypt.py b/bench/config/lets_encrypt.py index d21ec580..f09a75bf 100755 --- a/bench/config/lets_encrypt.py +++ b/bench/config/lets_encrypt.py @@ -81,12 +81,12 @@ def run_certbot_and_setup_ssl(site, custom_domain, bench_path): def setup_crontab(): job_command = 'sudo service nginx stop && /opt/certbot-auto renew && sudo service nginx start' - user_crontab = CronTab() - if job_command not in str(user_crontab): - job = user_crontab.new(command=job_command, comment="Renew lets-encrypt every month") + system_crontab = CronTab(tabfile='/etc/crontab', user=True) + if job_command not in str(system_crontab): + job = system_crontab.new(command=job_command, comment="Renew lets-encrypt every month") job.every().month() job.enable() - user_crontab.write() + system_crontab.write() def create_dir_if_missing(path): From b1e598f52835dba5194e85df38198ce1d577a360 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Tue, 23 May 2017 11:12:18 +0530 Subject: [PATCH 10/31] Npm dependency management using package.json (#409) * Fix bug global hashlib is not defined (#399) * Don't need $query_string (#390) * Develop (#400) * [fix] readme * Removed swtich-to-v4 and switch-to-v5 commands * Add "bench setup babel" command * Add "less" package * Npm dependency management using package.json * Add default package.json --- bench/app.py | 3 +- bench/commands/setup.py | 7 ++++ bench/commands/update.py | 7 ++-- bench/package.json | 18 ++++++++++ bench/utils.py | 35 +++++++++++++++++++- playbooks/develop/includes/setup_dev_env.yml | 6 ---- 6 files changed, 65 insertions(+), 11 deletions(-) create mode 100644 bench/package.json diff --git a/bench/app.py b/bench/app.py index 281a0f8f..f5fd8fa4 100755 --- a/bench/app.py +++ b/bench/app.py @@ -253,7 +253,7 @@ def get_repo_dir(app, bench_path='.'): return os.path.join(bench_path, 'apps', app) def switch_branch(branch, apps=None, bench_path='.', upgrade=False, check_upgrade=True): - from .utils import update_requirements, backup_all_sites, patch_sites, build_assets, pre_upgrade, post_upgrade + from .utils import update_requirements, update_npm_packages, backup_all_sites, patch_sites, build_assets, pre_upgrade, post_upgrade from . import utils apps_dir = os.path.join(bench_path, 'apps') version_upgrade = (False,) @@ -293,6 +293,7 @@ def switch_branch(branch, apps=None, bench_path='.', upgrade=False, check_upgrad if version_upgrade[0] and upgrade: update_requirements() + update_npm_packages() pre_upgrade(version_upgrade[1], version_upgrade[2]) reload(utils) backup_all_sites() diff --git a/bench/commands/setup.py b/bench/commands/setup.py index 76921023..e8975eb0 100755 --- a/bench/commands/setup.py +++ b/bench/commands/setup.py @@ -117,6 +117,12 @@ def setup_socketio(): from bench.utils import setup_socketio setup_socketio() +@click.command('requirements') +def setup_requirements(): + "Setup python and node requirements" + from bench.utils import update_requirements, update_npm_packages + update_requirements() + update_npm_packages() @click.command('config') def setup_config(): @@ -185,6 +191,7 @@ setup.add_command(setup_backups) setup.add_command(setup_env) setup.add_command(setup_procfile) setup.add_command(setup_socketio) +setup.add_command(setup_requirements) setup.add_command(setup_config) setup.add_command(setup_fonts) setup.add_command(add_domain) diff --git a/bench/commands/update.py b/bench/commands/update.py index b3bcc383..d38e82e0 100755 --- a/bench/commands/update.py +++ b/bench/commands/update.py @@ -3,7 +3,7 @@ import sys, os from bench.config.common_site_config import get_config from bench.app import pull_all_apps, is_version_upgrade from bench.utils import (update_bench, validate_upgrade, pre_upgrade, post_upgrade, before_update, - update_requirements, backup_all_sites, patch_sites, build_assets, restart_supervisor_processes) + update_requirements, update_npm_packages, backup_all_sites, patch_sites, build_assets, restart_supervisor_processes) from bench import patches #TODO: Not DRY @@ -62,7 +62,8 @@ def update(pull=False, patch=False, build=False, bench=False, auto=False, restar _update(pull, patch, build, bench, auto, restart_supervisor, requirements, no_backup, upgrade, force=force, reset=reset) -def _update(pull=False, patch=False, build=False, update_bench=False, auto=False, restart_supervisor=False, requirements=False, no_backup=False, upgrade=False, bench_path='.', force=False, reset=False): +def _update(pull=False, patch=False, build=False, update_bench=False, auto=False, restart_supervisor=False, + requirements=False, no_backup=False, upgrade=False, bench_path='.', force=False, reset=False): conf = get_config(bench_path=bench_path) version_upgrade = is_version_upgrade(bench_path=bench_path) @@ -78,8 +79,8 @@ def _update(pull=False, patch=False, build=False, update_bench=False, auto=False pull_all_apps(bench_path=bench_path, reset=reset) if requirements: - print('Updating Python libraries...') update_requirements(bench_path=bench_path) + update_npm_packages(bench_path=bench_path) if upgrade and (version_upgrade[0] or (not version_upgrade[0] and force)): pre_upgrade(version_upgrade[1], version_upgrade[2], bench_path=bench_path) diff --git a/bench/package.json b/bench/package.json new file mode 100644 index 00000000..31121283 --- /dev/null +++ b/bench/package.json @@ -0,0 +1,18 @@ +{ + "name": "frappe", + "description": "Default package.json for frappe apps", + "dependencies": { + "babel-core": "^6.24.1", + "babel-preset-babili": "0.0.12", + "babel-preset-es2015": "^6.24.1", + "babel-preset-es2016": "^6.24.1", + "babel-preset-es2017": "^6.24.1", + "chokidar": "^1.7.0", + "cookie": "^0.3.1", + "express": "^4.15.3", + "less": "^2.7.2", + "redis": "^2.7.1", + "socket.io": "^2.0.1", + "superagent": "^3.5.2" + } +} diff --git a/bench/utils.py b/bench/utils.py index 6cc26a1f..50040f33 100755 --- a/bench/utils.py +++ b/bench/utils.py @@ -61,7 +61,7 @@ def init(path, apps_path=None, no_procfile=False, no_backups=False, bench.set_frappe_version(bench_path=path) if bench.FRAPPE_VERSION > 5: - setup_socketio(bench_path=path) + update_npm_packages(bench_path=path) set_all_patches_executed(bench_path=path) build_assets(bench_path=path) @@ -382,6 +382,7 @@ def set_default_site(site, bench_path='.'): cwd=os.path.join(bench_path, 'sites')) def update_requirements(bench_path='.'): + print('Updating Python libraries...') pip = os.path.join(bench_path, 'env', 'bin', 'pip') # upgrade pip to latest @@ -397,6 +398,38 @@ def update_requirements(bench_path='.'): req_file = os.path.join(apps_dir, app, 'requirements.txt') install_requirements(pip, req_file) +def update_npm_packages(bench_path='.'): + print('Updating node libraries...') + apps_dir = os.path.join(bench_path, 'apps') + package_json = {} + + for app in os.listdir(apps_dir): + package_json_path = os.path.join(apps_dir, app, 'package.json') + + if os.path.exists(package_json_path): + with open(package_json_path, "r") as f: + app_package_json = json.loads(f.read()) + # package.json is usually a dict in a dict + for key, value in app_package_json.iteritems(): + if not key in package_json: + package_json[key] = value + else: + if isinstance(value, dict): + package_json[key].update(value) + elif isinstance(value, list): + package_json[key].extend(value) + else: + package_json[key] = value + + if package_json is {}: + with open(os.path.join(os.path.dirname(__file__), 'package.json'), 'r') as f: + package_json = json.loads(f.read()) + + with open(os.path.join(bench_path, 'package.json'), 'w') as f: + f.write(json.dumps(package_json, indent=1, sort_keys=True)) + + exec_cmd('npm install', cwd=bench_path) + def install_requirements(pip, req_file): if os.path.exists(req_file): exec_cmd("{pip} install -q -r {req_file}".format(pip=pip, req_file=req_file)) diff --git a/playbooks/develop/includes/setup_dev_env.yml b/playbooks/develop/includes/setup_dev_env.yml index 3b54c2af..4e07360e 100644 --- a/playbooks/develop/includes/setup_dev_env.yml +++ b/playbooks/develop/includes/setup_dev_env.yml @@ -1,10 +1,4 @@ --- - # Setup Socketio - - name: setup procfile - command: bench setup socketio - args: - creates: "{{ bench_path }}/node_modules" - chdir: "{{ bench_path }}" # Setup Procfile - name: setup procfile From b5a029033d58337c9833c0fb4aafc4b71b5c710d Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 23 May 2017 11:14:16 +0530 Subject: [PATCH 11/31] Develop (#411) * [fix] readme * Removed swtich-to-v4 and switch-to-v5 commands * consider passwords.txt (#408) * Fixes bench release github username error (#406) * ran 2to3 script * Correct urllib.parse import * Backward compatible urllib import * removed test_setup_production_v6 * Fix bug global hashlib is not defined (#399) * Don't need $query_string (#390) * removed semicolon, click.prompt adds it * Letsencrypt fix develop (#407) * [Fix] Letsencrypt error - Please specify user or filename to write. * Fix bug global hashlib is not defined (#399) * Don't need $query_string (#390) * [Fix] Issue #404 - write to system cron * letsencrypt.py - renamed user_crontab to system_crontab * Npm dependency management using package.json (#409) * Fix bug global hashlib is not defined (#399) * Don't need $query_string (#390) * Develop (#400) * [fix] readme * Removed swtich-to-v4 and switch-to-v5 commands * Add "bench setup babel" command * Add "less" package * Npm dependency management using package.json * Add default package.json --- bench/app.py | 3 +- bench/commands/setup.py | 7 +++ bench/commands/update.py | 7 ++- bench/config/lets_encrypt.py | 8 +-- bench/package.json | 18 ++++++ bench/release.py | 2 +- bench/utils.py | 59 ++++++++++++++++++-- playbooks/develop/includes/setup_dev_env.yml | 6 -- 8 files changed, 90 insertions(+), 20 deletions(-) create mode 100644 bench/package.json diff --git a/bench/app.py b/bench/app.py index 281a0f8f..f5fd8fa4 100755 --- a/bench/app.py +++ b/bench/app.py @@ -253,7 +253,7 @@ def get_repo_dir(app, bench_path='.'): return os.path.join(bench_path, 'apps', app) def switch_branch(branch, apps=None, bench_path='.', upgrade=False, check_upgrade=True): - from .utils import update_requirements, backup_all_sites, patch_sites, build_assets, pre_upgrade, post_upgrade + from .utils import update_requirements, update_npm_packages, backup_all_sites, patch_sites, build_assets, pre_upgrade, post_upgrade from . import utils apps_dir = os.path.join(bench_path, 'apps') version_upgrade = (False,) @@ -293,6 +293,7 @@ def switch_branch(branch, apps=None, bench_path='.', upgrade=False, check_upgrad if version_upgrade[0] and upgrade: update_requirements() + update_npm_packages() pre_upgrade(version_upgrade[1], version_upgrade[2]) reload(utils) backup_all_sites() diff --git a/bench/commands/setup.py b/bench/commands/setup.py index 76921023..e8975eb0 100755 --- a/bench/commands/setup.py +++ b/bench/commands/setup.py @@ -117,6 +117,12 @@ def setup_socketio(): from bench.utils import setup_socketio setup_socketio() +@click.command('requirements') +def setup_requirements(): + "Setup python and node requirements" + from bench.utils import update_requirements, update_npm_packages + update_requirements() + update_npm_packages() @click.command('config') def setup_config(): @@ -185,6 +191,7 @@ setup.add_command(setup_backups) setup.add_command(setup_env) setup.add_command(setup_procfile) setup.add_command(setup_socketio) +setup.add_command(setup_requirements) setup.add_command(setup_config) setup.add_command(setup_fonts) setup.add_command(add_domain) diff --git a/bench/commands/update.py b/bench/commands/update.py index b3bcc383..d38e82e0 100755 --- a/bench/commands/update.py +++ b/bench/commands/update.py @@ -3,7 +3,7 @@ import sys, os from bench.config.common_site_config import get_config from bench.app import pull_all_apps, is_version_upgrade from bench.utils import (update_bench, validate_upgrade, pre_upgrade, post_upgrade, before_update, - update_requirements, backup_all_sites, patch_sites, build_assets, restart_supervisor_processes) + update_requirements, update_npm_packages, backup_all_sites, patch_sites, build_assets, restart_supervisor_processes) from bench import patches #TODO: Not DRY @@ -62,7 +62,8 @@ def update(pull=False, patch=False, build=False, bench=False, auto=False, restar _update(pull, patch, build, bench, auto, restart_supervisor, requirements, no_backup, upgrade, force=force, reset=reset) -def _update(pull=False, patch=False, build=False, update_bench=False, auto=False, restart_supervisor=False, requirements=False, no_backup=False, upgrade=False, bench_path='.', force=False, reset=False): +def _update(pull=False, patch=False, build=False, update_bench=False, auto=False, restart_supervisor=False, + requirements=False, no_backup=False, upgrade=False, bench_path='.', force=False, reset=False): conf = get_config(bench_path=bench_path) version_upgrade = is_version_upgrade(bench_path=bench_path) @@ -78,8 +79,8 @@ def _update(pull=False, patch=False, build=False, update_bench=False, auto=False pull_all_apps(bench_path=bench_path, reset=reset) if requirements: - print('Updating Python libraries...') update_requirements(bench_path=bench_path) + update_npm_packages(bench_path=bench_path) if upgrade and (version_upgrade[0] or (not version_upgrade[0] and force)): pre_upgrade(version_upgrade[1], version_upgrade[2], bench_path=bench_path) diff --git a/bench/config/lets_encrypt.py b/bench/config/lets_encrypt.py index d21ec580..f09a75bf 100755 --- a/bench/config/lets_encrypt.py +++ b/bench/config/lets_encrypt.py @@ -81,12 +81,12 @@ def run_certbot_and_setup_ssl(site, custom_domain, bench_path): def setup_crontab(): job_command = 'sudo service nginx stop && /opt/certbot-auto renew && sudo service nginx start' - user_crontab = CronTab() - if job_command not in str(user_crontab): - job = user_crontab.new(command=job_command, comment="Renew lets-encrypt every month") + system_crontab = CronTab(tabfile='/etc/crontab', user=True) + if job_command not in str(system_crontab): + job = system_crontab.new(command=job_command, comment="Renew lets-encrypt every month") job.every().month() job.enable() - user_crontab.write() + system_crontab.write() def create_dir_if_missing(path): diff --git a/bench/package.json b/bench/package.json new file mode 100644 index 00000000..31121283 --- /dev/null +++ b/bench/package.json @@ -0,0 +1,18 @@ +{ + "name": "frappe", + "description": "Default package.json for frappe apps", + "dependencies": { + "babel-core": "^6.24.1", + "babel-preset-babili": "0.0.12", + "babel-preset-es2015": "^6.24.1", + "babel-preset-es2016": "^6.24.1", + "babel-preset-es2017": "^6.24.1", + "chokidar": "^1.7.0", + "cookie": "^0.3.1", + "express": "^4.15.3", + "less": "^2.7.2", + "redis": "^2.7.1", + "socket.io": "^2.0.1", + "superagent": "^3.5.2" + } +} diff --git a/bench/release.py b/bench/release.py index caea605e..a82a0826 100755 --- a/bench/release.py +++ b/bench/release.py @@ -38,7 +38,7 @@ def validate(bench_path): github_password = config.get('github_password') if not github_username: - github_username = input('Username: ') + github_username = click.prompt('Username', type=str) if not github_password: github_password = getpass.getpass() diff --git a/bench/utils.py b/bench/utils.py index 8ef8b70a..50040f33 100755 --- a/bench/utils.py +++ b/bench/utils.py @@ -61,7 +61,7 @@ def init(path, apps_path=None, no_procfile=False, no_backups=False, bench.set_frappe_version(bench_path=path) if bench.FRAPPE_VERSION > 5: - setup_socketio(bench_path=path) + update_npm_packages(bench_path=path) set_all_patches_executed(bench_path=path) build_assets(bench_path=path) @@ -139,20 +139,36 @@ def setup_env(bench_path='.'): def setup_socketio(bench_path='.'): exec_cmd("npm install socket.io redis express superagent cookie", cwd=bench_path) + def new_site(site, mariadb_root_password=None, admin_password=None, bench_path='.'): + """ + Creates a new site in the specified bench, default is current bench + """ + logger.info('creating new site {}'.format(site)) - mariadb_root_password_fragment = '--root_password {}'.format(mariadb_root_password) if mariadb_root_password else '' + + # consider an existing passwords.txt file + passwords_file_path = os.path.join(os.path.expanduser('~'), 'passwords.txt') + if os.path.isfile(passwords_file_path): + with open(passwords_file_path, 'r') as f: + passwords = json.load(f) + mariadb_root_password, admin_password = passwords['mysql_root_password'], passwords['admin_password'] + + mysql_root_password_fragment = '--root_password {}'.format(mariadb_root_password) if mariadb_root_password else '' admin_password_fragment = '--admin_password {}'.format(admin_password) if admin_password else '' - exec_cmd("{frappe} {site} --install {db_name} {mariadb_root_password_fragment} {admin_password_fragment}".format( + + exec_cmd("{frappe} {site} --install {db_name} {mysql_root_password_fragment} {admin_password_fragment}".format( frappe=get_frappe(bench_path=bench_path), site=site, - db_name = hashlib.sha1(site).hexdigest()[:10], - mariadb_root_password_fragment=mariadb_root_password_fragment, + db_name=hashlib.sha1(site).hexdigest()[:10], + mysql_root_password_fragment=mysql_root_password_fragment, admin_password_fragment=admin_password_fragment ), cwd=os.path.join(bench_path, 'sites')) + if len(get_sites(bench_path=bench_path)) == 1: exec_cmd("{frappe} --use {site}".format(frappe=get_frappe(bench_path=bench_path), site=site), cwd=os.path.join(bench_path, 'sites')) + def patch_sites(bench_path='.'): bench.set_frappe_version(bench_path=bench_path) @@ -366,6 +382,7 @@ def set_default_site(site, bench_path='.'): cwd=os.path.join(bench_path, 'sites')) def update_requirements(bench_path='.'): + print('Updating Python libraries...') pip = os.path.join(bench_path, 'env', 'bin', 'pip') # upgrade pip to latest @@ -381,6 +398,38 @@ def update_requirements(bench_path='.'): req_file = os.path.join(apps_dir, app, 'requirements.txt') install_requirements(pip, req_file) +def update_npm_packages(bench_path='.'): + print('Updating node libraries...') + apps_dir = os.path.join(bench_path, 'apps') + package_json = {} + + for app in os.listdir(apps_dir): + package_json_path = os.path.join(apps_dir, app, 'package.json') + + if os.path.exists(package_json_path): + with open(package_json_path, "r") as f: + app_package_json = json.loads(f.read()) + # package.json is usually a dict in a dict + for key, value in app_package_json.iteritems(): + if not key in package_json: + package_json[key] = value + else: + if isinstance(value, dict): + package_json[key].update(value) + elif isinstance(value, list): + package_json[key].extend(value) + else: + package_json[key] = value + + if package_json is {}: + with open(os.path.join(os.path.dirname(__file__), 'package.json'), 'r') as f: + package_json = json.loads(f.read()) + + with open(os.path.join(bench_path, 'package.json'), 'w') as f: + f.write(json.dumps(package_json, indent=1, sort_keys=True)) + + exec_cmd('npm install', cwd=bench_path) + def install_requirements(pip, req_file): if os.path.exists(req_file): exec_cmd("{pip} install -q -r {req_file}".format(pip=pip, req_file=req_file)) diff --git a/playbooks/develop/includes/setup_dev_env.yml b/playbooks/develop/includes/setup_dev_env.yml index 3b54c2af..4e07360e 100644 --- a/playbooks/develop/includes/setup_dev_env.yml +++ b/playbooks/develop/includes/setup_dev_env.yml @@ -1,10 +1,4 @@ --- - # Setup Socketio - - name: setup procfile - command: bench setup socketio - args: - creates: "{{ bench_path }}/node_modules" - chdir: "{{ bench_path }}" # Setup Procfile - name: setup procfile From fad1cbed50b1340a642a721032eb302b1dd1008c Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Tue, 23 May 2017 12:39:35 +0530 Subject: [PATCH 12/31] Fix travis check for mysql --- bench/tests/test_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bench/tests/test_init.py b/bench/tests/test_init.py index d1045a6f..b1ccda98 100755 --- a/bench/tests/test_init.py +++ b/bench/tests/test_init.py @@ -210,7 +210,7 @@ class TestBenchInit(unittest.TestCase): self.assert_exists(python_path, "site-packages", "pip") site_packages = os.listdir(os.path.join(python_path, "site-packages")) - self.assertTrue(any(package.startswith("MySQL_python-1.2.5") for package in site_packages)) + self.assertTrue(any(package.startswith("mysqlclient-1.3.8") for package in site_packages)) def assert_config(self, bench_name): for config, search_key in ( From 3a4da6b47eeda8fca954de55b021d835eb6b8516 Mon Sep 17 00:00:00 2001 From: jevonearth Date: Wed, 24 May 2017 10:47:15 -0700 Subject: [PATCH 13/31] Use downloads.wkhtmltopdf.org instead of gna.org gna.org is supposedly shutting down, and was unavailable for most of today. --- playbooks/develop/includes/wkhtmltopdf.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playbooks/develop/includes/wkhtmltopdf.yml b/playbooks/develop/includes/wkhtmltopdf.yml index bdd7e419..7929d3c0 100644 --- a/playbooks/develop/includes/wkhtmltopdf.yml +++ b/playbooks/develop/includes/wkhtmltopdf.yml @@ -1,6 +1,6 @@ --- - name: download wkthmltox linux - get_url: url=http://download.gna.org/wkhtmltopdf/0.12/0.12.3/wkhtmltox-0.12.3_linux-generic-{{ "amd64" if ansible_architecture == "x86_64" else "i386"}}.tar.xz dest=/tmp/wkhtmltox.tar.xz + get_url: url=http://downloads.wkhtmltopdf.org/0.12/0.12.3/wkhtmltox-0.12.3_linux-generic-{{ "amd64" if ansible_architecture == "x86_64" else "i386"}}.tar.xz dest=/tmp/wkhtmltox.tar.xz - name: Creates directory file: path=/tmp/wkhtmltox state=directory From 726a8a551783648924167dd399327cbb5cd1149b Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Wed, 24 May 2017 11:54:21 +0530 Subject: [PATCH 14/31] Add file_watcher_port to common_site_config.json --- bench/config/common_site_config.py | 3 ++- bench/tests/test_init.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/bench/config/common_site_config.py b/bench/config/common_site_config.py index 02edfc3b..f99b07cb 100644 --- a/bench/config/common_site_config.py +++ b/bench/config/common_site_config.py @@ -62,7 +62,7 @@ def update_config_for_frappe(config, bench_path): if key not in config: config[key] = "redis://localhost:{0}".format(ports[key]) - for key in ('webserver_port', 'socketio_port'): + for key in ('webserver_port', 'socketio_port', 'file_watcher_port'): if key not in config: config[key] = ports[key] @@ -75,6 +75,7 @@ def make_ports(bench_path): default_ports = { "webserver_port": 8000, "socketio_port": 9000, + "file_watcher_port": 6787, "redis_queue": 11000, "redis_socketio": 12000, "redis_cache": 13000 diff --git a/bench/tests/test_init.py b/bench/tests/test_init.py index b1ccda98..1b8d63d2 100755 --- a/bench/tests/test_init.py +++ b/bench/tests/test_init.py @@ -40,6 +40,7 @@ class TestBenchInit(unittest.TestCase): self.assert_common_site_config("test-bench-1", { "webserver_port": 8000, "socketio_port": 9000, + "file_watcher_port": 6787, "redis_queue": "redis://localhost:11000", "redis_socketio": "redis://localhost:12000", "redis_cache": "redis://localhost:13000" @@ -51,6 +52,7 @@ class TestBenchInit(unittest.TestCase): self.assert_common_site_config("test-bench-2", { "webserver_port": 8001, "socketio_port": 9001, + "file_watcher_port": 6788, "redis_queue": "redis://localhost:11001", "redis_socketio": "redis://localhost:12001", "redis_cache": "redis://localhost:13001" From e968df3e39cbf832ec564f0ea6780613f1561603 Mon Sep 17 00:00:00 2001 From: Makarand Bauskar Date: Fri, 9 Jun 2017 15:28:04 +0530 Subject: [PATCH 15/31] [hotfix] get wkhtmltopdf from frappe/wkhtmltopdf repo, setup npm dependencies in bench init (#428) --- bench/utils.py | 4 ++-- playbooks/develop/includes/wkhtmltopdf.yml | 4 ++-- vm/ansible/roles/wkhtmltopdf/tasks/main.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bench/utils.py b/bench/utils.py index 50040f33..cd51a47e 100755 --- a/bench/utils.py +++ b/bench/utils.py @@ -137,8 +137,8 @@ def setup_env(bench_path='.'): exec_cmd('./env/bin/pip -q install -e git+https://github.com/frappe/python-pdfkit.git#egg=pdfkit', cwd=bench_path) def setup_socketio(bench_path='.'): - exec_cmd("npm install socket.io redis express superagent cookie", cwd=bench_path) - + exec_cmd("npm install socket.io redis express superagent cookie babel-core less chokidar \ + babel-cli babel-preset-es2015 babel-preset-es2016 babel-preset-es2017 babel-preset-babili", cwd=bench_path) def new_site(site, mariadb_root_password=None, admin_password=None, bench_path='.'): """ diff --git a/playbooks/develop/includes/wkhtmltopdf.yml b/playbooks/develop/includes/wkhtmltopdf.yml index 7929d3c0..ea4c0c28 100644 --- a/playbooks/develop/includes/wkhtmltopdf.yml +++ b/playbooks/develop/includes/wkhtmltopdf.yml @@ -1,7 +1,7 @@ --- - name: download wkthmltox linux - get_url: url=http://downloads.wkhtmltopdf.org/0.12/0.12.3/wkhtmltox-0.12.3_linux-generic-{{ "amd64" if ansible_architecture == "x86_64" else "i386"}}.tar.xz dest=/tmp/wkhtmltox.tar.xz - + get_url: url=https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-{{ "amd64" if ansible_architecture == "x86_64" else "i386"}}.tar.xz dest=/tmp/wkhtmltox.tar.xz + - name: Creates directory file: path=/tmp/wkhtmltox state=directory diff --git a/vm/ansible/roles/wkhtmltopdf/tasks/main.yml b/vm/ansible/roles/wkhtmltopdf/tasks/main.yml index fbd0c775..52c95925 100644 --- a/vm/ansible/roles/wkhtmltopdf/tasks/main.yml +++ b/vm/ansible/roles/wkhtmltopdf/tasks/main.yml @@ -18,7 +18,7 @@ when: ansible_os_family == 'Debian' - name: download wkthmltox linux - get_url: url=http://download.gna.org/wkhtmltopdf/0.12/0.12.3/wkhtmltox-0.12.3_linux-generic-{{ "amd64" if ansible_architecture == "x86_64" else "i386"}}.tar.xz dest=/tmp/wkhtmltox.tar.xz + get_url: url=https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-{{ "amd64" if ansible_architecture == "x86_64" else "i386"}}.tar.xz dest=/tmp/wkhtmltox.tar.xz - name: unarchive wkhtmltopdf unarchive: src=/tmp/wkhtmltox.tar.xz dest=/tmp/wkhtmltox From b9b38d4c35cd8475dc5923bc9a88ab73d6701257 Mon Sep 17 00:00:00 2001 From: Javier Wong Date: Fri, 16 Jun 2017 08:46:56 +0800 Subject: [PATCH 16/31] Installation fails for PDFKit - no module named six Get the following error when installing: `./env/bin/pip install -e git+https://github.com/frappe/python-pdfkit.git#egg=pdfkit Obtaining pdfkit from git+https://github.com/frappe/python-pdfkit.git#egg=pdfkit Updating ./env/src/pdfkit clone Installing collected packages: pdfkit Running setup.py develop for pdfkit Complete output from command /home/strella/frappe-bench/env/bin/python -c "import setuptools, tokenize;__file__='/home/strella/frappe-bench/env/src/pdfkit/setup.py';f=getattr(tokenize, 'open', open)(__file__);code=f.read().replace('\r\n', '\n');f.close();exec(compile(code, __file__, 'exec'))" develop --no-deps: Traceback (most recent call last): File "", line 1, in File "/home/strella/frappe-bench/env/src/pdfkit/setup.py", line 51, in 'Topic :: Utilities' File "/usr/lib/python2.7/distutils/core.py", line 137, in setup ok = dist.parse_command_line() File "/home/strella/frappe-bench/env/local/lib/python2.7/site-packages/setuptools/dist.py", line 363, in parse_command_line result = _Distribution.parse_command_line(self) File "/usr/lib/python2.7/distutils/dist.py", line 467, in parse_command_line args = self._parse_command_opts(parser, args) File "/home/strella/frappe-bench/env/local/lib/python2.7/site-packages/setuptools/dist.py", line 674, in _parse_command_opts nargs = _Distribution._parse_command_opts(self, parser, args) File "/usr/lib/python2.7/distutils/dist.py", line 523, in _parse_command_opts cmd_class = self.get_command_class(command) File "/home/strella/frappe-bench/env/local/lib/python2.7/site-packages/setuptools/dist.py", line 495, in get_command_class self.cmdclass[command] = cmdclass = ep.load() File "/home/strella/frappe-bench/env/local/lib/python2.7/site-packages/pkg_resources/__init__.py", line 2303, in load return self.resolve() File "/home/strella/frappe-bench/env/local/lib/python2.7/site-packages/pkg_resources/__init__.py", line 2309, in resolve module = __import__(self.module_name, fromlist=['__name__'], level=0) File "/home/strella/frappe-bench/env/local/lib/python2.7/site-packages/setuptools/command/develop.py", line 11, in from setuptools.command.easy_install import easy_install File "/home/strella/frappe-bench/env/local/lib/python2.7/site-packages/setuptools/command/easy_install.py", line 49, in from setuptools.py27compat import rmtree_safe File "/home/strella/frappe-bench/env/local/lib/python2.7/site-packages/setuptools/py27compat.py", line 7, in import six ImportError: No module named six` --- bench/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bench/utils.py b/bench/utils.py index cd51a47e..34b97cb2 100755 --- a/bench/utils.py +++ b/bench/utils.py @@ -134,6 +134,7 @@ def setup_env(bench_path='.'): exec_cmd('./env/bin/pip -q install --upgrade pip', cwd=bench_path) exec_cmd('./env/bin/pip -q install wheel', cwd=bench_path) # exec_cmd('./env/bin/pip -q install https://github.com/frappe/MySQLdb1/archive/MySQLdb-1.2.5-patched.tar.gz', cwd=bench_path) + exec_cmd('./env/bin/pip -q install six', cwd=bench_path) exec_cmd('./env/bin/pip -q install -e git+https://github.com/frappe/python-pdfkit.git#egg=pdfkit', cwd=bench_path) def setup_socketio(bench_path='.'): From 7ae4f993d4e2729de57e4de18135177480595929 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Fri, 16 Jun 2017 17:21:54 +0530 Subject: [PATCH 17/31] [fix] accept domains as tuple argument --- bench/commands/setup.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bench/commands/setup.py b/bench/commands/setup.py index e8975eb0..703e20fa 100755 --- a/bench/commands/setup.py +++ b/bench/commands/setup.py @@ -160,17 +160,18 @@ def remove_domain(domain, site=None): remove_domain(site, domain, bench_path='.') @click.command('sync-domains') -@click.argument('domains') +@click.option('--domain', multiple=True) @click.option('--site', prompt=True) -def sync_domains(domains, site=None): +def sync_domains(domain=None, site=None): from bench.config.site_config import sync_domains if not site: print("Please specify site") sys.exit(1) - domains = json.loads(domains) - if not isinstance(domains, list): + try: + domains = list(map(str,domain)) + except Exception: print("Domains should be a json list of strings or dictionaries") sys.exit(1) From 48b82043c1ba76b18e2283046e3209e575c250d1 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Thu, 22 Jun 2017 16:20:41 +0530 Subject: [PATCH 18/31] use deprecated trusty build for now --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 89dbafb2..6ae5eba9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: python dist: trusty +group: deprecated-2017Q2 sudo: required python: From 8faf03988c643da7809b1047bc00fe51315b76ee Mon Sep 17 00:00:00 2001 From: Saurabh Date: Wed, 19 Jul 2017 18:02:05 +0530 Subject: [PATCH 19/31] [docs] documentation for release strategy and contribution guidelines --- bench/docs/contribution_guidelines.md | 46 +++++++++++++++++++++++++++ bench/docs/release_policy.md | 46 +++++++++++++++++++++++++++ bench/docs/releasing_frappe_erpext.md | 31 ++++++++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 bench/docs/contribution_guidelines.md create mode 100644 bench/docs/release_policy.md create mode 100644 bench/docs/releasing_frappe_erpext.md diff --git a/bench/docs/contribution_guidelines.md b/bench/docs/contribution_guidelines.md new file mode 100644 index 00000000..9d6ba368 --- /dev/null +++ b/bench/docs/contribution_guidelines.md @@ -0,0 +1,46 @@ +# Contribution Guidelines + +### Introduction (for first timers) + +Thank you for your interesting in contributing to an open source project! Our world works on people taking initiative to contribute to the "commons" and contributing to open source means you are contributing to make things better for not only yourself, but everyone else too! So thank you for taking this initiative. + +Great projects also work because of great quality. Open source or not, the user really cares that things should work as they are advertised, and consistently. New features should follow the same pattern and so that users don't have to learn things again and again. + +Developers who maintain open source also expect that you follow certain guidelines. These guidelines ensure that developers are able quickly give feedback on your contribution and how to make it better. Most probably you might have to go back and change a few things, but it will be in th interest of making this process better for everyone. So do be prepared for some back and forth. + +Happy contributing! + +### Feedback Policy + +We will strive for a "Zero Pull Request Pending" policy, inspired by "Zero Inbox". This means, that if the pull request is good, it will be merged within a day and if it does not meet the requirements, it will be closed. + +### Design Guides + +Please read the following design guidelines carefully when contributing: + +1. [Form Design Guidelines](https://github.com/frappe/erpnext/wiki/Form-Design-Guidelines) +1. [How to break large contributions into smaller ones](https://github.com/frappe/erpnext/wiki/Cascading-Pull-Requests) + +### Pull Request Requirements + +1. **Test Cases:** Important to add test cases, even if its a very simple one that just calls the function. For UI, till we don't have Selenium testing setup, we need to see a screenshot / animated GIF. +1. **UX:** If your change involves user experience, add a screenshot / narration / animated GIF. +1. **Documentation:** Test Case must involve updating necessary documentation +1. **Explanation:** Include explanation if there is a design change, explain the use case and why this suggested change is better. If you are including a new library or replacing one, please give sufficient reference of why the suggested library is better. +1. **Demo:** Remember to update the demo script so that data related your feature is included in the demo. +1. **Failing Tests:** This is simple, you must make sure all automated tests are passing. +1. **Very Large Contribution:** It is very hard to accept and merge very large contributions, because there are too many lines of code to check and its implications can be large and unexpected. They way to contribute big features is to build them part by part. We can understand there are exceptions, but in most cases try and keep your pull-request to **30 lines of code** excluding tests and config files. **Use [Cascading Pull Requests](https://github.com/frappe/erpnext/wiki/Cascading-Pull-Requests)** for large features. +1. **Incomplete Contributions must be hidden:** If the contribution is WIP or incomplete - which will most likely be the case, you can send small PRs as long as the user is not exposed to unfinished functionality. This will ensure that your code does not have build or other collateral issues. But these features must remain completely hidden to the user. +1. **Incorrect Patches:** If your design involves schema change and you must include patches that update the data as per your new schema. +1. **Incorrect Naming:** The naming of variables, models, fields etc must be consistent as per the existing design and semantics used in the system. +1. **Translated Strings:** All user facing strings / text must be wrapped in the `__("")` function in javascript and `_("")` function in Python, so that it is shown as translated to the user. +1. **Deprecated API:** The API used in the pull request must be the latest recommended methods and usage of globals like `cur_frm` must be avoided. +1. **Whitespace and indentation:** The ERPNext and Frappe Project uses tabs (I know and we are sorry, but its too much effort to change it now and we don't want to lose the history). The indentation must be consistent whether you are writing Javascript or Python. Multi-line strings or expressions must also be consistently indented, not hanging like a bee hive at the end of the line. We just think the code looks a lot more stable that way. + +#### What if my Pull Request is closed? + +Don't worry, fix the problem and re-open it! + +#### Why do we follow this policy? + +This is because ERPNext is at a stage where it is being used by thousands of companies and introducing breaking changes can be harmful for everyone. Also we do not want to stop the speed of contributions and the best way to encourage contributors is to give fast feedback. \ No newline at end of file diff --git a/bench/docs/release_policy.md b/bench/docs/release_policy.md new file mode 100644 index 00000000..73cb4462 --- /dev/null +++ b/bench/docs/release_policy.md @@ -0,0 +1,46 @@ +# Release Policy + +We maintain a **staging** branch for erpnext and frappe. On every Wednesday, release team will create a **staging** branch from **develop** (Internal Release). Same on every Tuesday,we will release from **staging** branch to **master** (Community Release). We also maintain **hotfix** branch, to fix bugs from release. Hot fixes release will take place as per the priority and severity. + + +**I. Branch Description** + +1. develop: All new feature developments will go in develop branch +2. staging: This branch serves as a release candidate. Before a week, release team will pull feature from develop branch to staging branch.
EG: if feature is in 25 July's milestone then it should go in staging on 19th July. +3. master: Community release. +4. hotfix: mainly define for support issues. This will include bugs or any high priority task like security patches. + + +**II. Where to send PR?** + +1. If you are working on a new feature, then PR should point to develop branch +2. If you are working on support issue / bug / error report, then PR should point to hotfix brach +3. While performing testing on Staging branch, if any fix needed then only send that fix PR to staging. +4. Direct push to master strictly prohibited. + + +**III. Versioning** + +1. develop to staging: No release number, cherry-pick or hard push from develop. +2. staging to master: + Patch: Small fixes + Minor: For new features updates. + Major: If any API changes +3. hotfix to master: Patch version release + + +**IV. Release Impact on branches:** + +1. Releasing from staging: + staging -> master -> develop -> hotfix +2. Releasing from hotfix: + hotfix -> master -> develop -> staging + + +**V. Servers/Sites and Branches:** + +1. Frappe Cloud: master branch (Every Tuesday) +2. Frappe.io and Central: staging Branch (Every Wednesday) +3. demo.erpnext.com: master +4. beta.erpnext.com: staging + diff --git a/bench/docs/releasing_frappe_erpext.md b/bench/docs/releasing_frappe_erpext.md new file mode 100644 index 00000000..d9e134d3 --- /dev/null +++ b/bench/docs/releasing_frappe_erpext.md @@ -0,0 +1,31 @@ +# Releasing Frappe ERPNext + +* Make a new bench dedicated for releasing +``` +bench init release-bench --frappe-path git@github.com:frappe/frappe.git +``` + +* Get ERPNext in the release bench +``` +bench get-app erpnext git@github.com:frappe/erpnext.git +``` + +* Configure as release bench. Add this to the common_site_config.json +``` +"release_bench": true, +``` + +* Use the release commands to release +``` +Usage: bench release [OPTIONS] APP BUMP_TYPE +``` + +* Arguments : + * _APP_ App name e.g [frappe|erpnext|yourapp] + * _BUMP_TYPE_ [major|minor|patch|stable|prerelease] +* Options: + * --develop git develop branch, default is develop + * --master git master branch, default is master + * --remote git remote, default is upstream + * --owner git owner, default is frappe + * --repo-name git repo name if different from app name \ No newline at end of file From 85e32346c34df45ed171e4691fba0d2e53699ec7 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Fri, 21 Jul 2017 19:05:31 +0530 Subject: [PATCH 20/31] [update] release documentation --- bench/docs/release_policy.md | 66 +++++++++++++++------------ bench/docs/releasing_frappe_erpext.md | 12 ++++- 2 files changed, 48 insertions(+), 30 deletions(-) diff --git a/bench/docs/release_policy.md b/bench/docs/release_policy.md index 73cb4462..7b328342 100644 --- a/bench/docs/release_policy.md +++ b/bench/docs/release_policy.md @@ -1,46 +1,56 @@ # Release Policy -We maintain a **staging** branch for erpnext and frappe. On every Wednesday, release team will create a **staging** branch from **develop** (Internal Release). Same on every Tuesday,we will release from **staging** branch to **master** (Community Release). We also maintain **hotfix** branch, to fix bugs from release. Hot fixes release will take place as per the priority and severity. +#### Create staging branch +- On Wednesday morning, we will create a `staging` branch from develop branch. `staging` branch is a release candidate. All new features will first go from `develop` to `staging` and then `staging` to `master`. -**I. Branch Description** +- Use the prepare-staging command to create staging branch +```usage: bench prepare-staging APP``` -1. develop: All new feature developments will go in develop branch -2. staging: This branch serves as a release candidate. Before a week, release team will pull feature from develop branch to staging branch.
EG: if feature is in 25 July's milestone then it should go in staging on 19th July. -3. master: Community release. -4. hotfix: mainly define for support issues. This will include bugs or any high priority task like security patches. +- Impact on branches ? + - merge all commits from develop branch to staging + - push merge commit back to develop +- QA will use staging for testing. -**II. Where to send PR?** +- Deploy staging branch on frappe.io -1. If you are working on a new feature, then PR should point to develop branch -2. If you are working on support issue / bug / error report, then PR should point to hotfix brach -3. While performing testing on Staging branch, if any fix needed then only send that fix PR to staging. -4. Direct push to master strictly prohibited. +- Only regression and security fixes can be cherry-picked into staging +- Create a discuss post on what all new features or fixes going in next version. -**III. Versioning** +--- -1. develop to staging: No release number, cherry-pick or hard push from develop. -2. staging to master: - Patch: Small fixes - Minor: For new features updates. - Major: If any API changes -3. hotfix to master: Patch version release +#### Create release from staging +- On Tuesday, we will release from staging to master. +- Versioning: + - patch: Small fixes + - minor: For new features updates. + - major: If any API changes -**IV. Release Impact on branches:** +- Impact on branches: + - merge staging branch to master + - push merge commit back to staging branch + - push merge commit to develop branch + - push merge commit to hotfix branch -1. Releasing from staging: - staging -> master -> develop -> hotfix -2. Releasing from hotfix: - hotfix -> master -> develop -> staging +- Use release command to create release, +``` usage: bench release APP patch|minor|major --from-branch staging ``` +--- -**V. Servers/Sites and Branches:** +#### Create release from hotfix +- Depending on priority, hotfix release will take place. -1. Frappe Cloud: master branch (Every Tuesday) -2. Frappe.io and Central: staging Branch (Every Wednesday) -3. demo.erpnext.com: master -4. beta.erpnext.com: staging +- Versioning: + - patch: Small fixes +- Impact on branches: + - merge hotfix branch to master + - push merge commit back to staging branch + - push merge commit to develop branch + - push merge commit to staging branch + +- Use release command to create release, +``` usage: bench release APP patch --from-branch hotfix ``` diff --git a/bench/docs/releasing_frappe_erpext.md b/bench/docs/releasing_frappe_erpext.md index d9e134d3..606c4c4d 100644 --- a/bench/docs/releasing_frappe_erpext.md +++ b/bench/docs/releasing_frappe_erpext.md @@ -15,6 +15,14 @@ bench get-app erpnext git@github.com:frappe/erpnext.git "release_bench": true, ``` +* Add branches to update in common_site_config.json +``` +"branches_to_update": { + "staging": ["develop", "hotfix"], + "hotfix": ["develop", "staging"] +} +``` + * Use the release commands to release ``` Usage: bench release [OPTIONS] APP BUMP_TYPE @@ -24,8 +32,8 @@ Usage: bench release [OPTIONS] APP BUMP_TYPE * _APP_ App name e.g [frappe|erpnext|yourapp] * _BUMP_TYPE_ [major|minor|patch|stable|prerelease] * Options: - * --develop git develop branch, default is develop - * --master git master branch, default is master + * --from-branch git develop branch, default is develop + * --to-branch git master branch, default is master * --remote git remote, default is upstream * --owner git owner, default is frappe * --repo-name git repo name if different from app name \ No newline at end of file From c1767f8a438094dbea93f080b825105ed066d73d Mon Sep 17 00:00:00 2001 From: Saurabh Date: Mon, 24 Jul 2017 12:40:48 +0530 Subject: [PATCH 21/31] [fix] branching details --- bench/docs/branch_details.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 bench/docs/branch_details.md diff --git a/bench/docs/branch_details.md b/bench/docs/branch_details.md new file mode 100644 index 00000000..46e920e6 --- /dev/null +++ b/bench/docs/branch_details.md @@ -0,0 +1,13 @@ +### ERPNext/Frappe Branching + +#### Branch Description + - `develop` Branch: All new feature developments will go in develop branch + - `staging` Branch: This branch serves as a release candidate. Before a week, release team will pull the feature from develop branch to staging branch. + EG: if the feature is in 25 July's milestone then it should go in staging on 19th July. + - `master` Branch: Community release. + - `hotFix` Branch: mainly define for support issues. This will include bugs or any high priority task like security patches. + +#### Where to send PR? + - If you are working on a new feature, then PR should point to develop branch + - If you are working on support issue / bug / error report, then PR should point to hotfix brach + - While performing testing on Staging branch, if any fix needed then only send that fix PR to staging. \ No newline at end of file From 12fbd8aea0c1728b05912184046893e60fdb69f4 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Mon, 24 Jul 2017 12:46:18 +0530 Subject: [PATCH 22/31] [fix] typo fix --- bench/docs/branch_details.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bench/docs/branch_details.md b/bench/docs/branch_details.md index 46e920e6..413d939a 100644 --- a/bench/docs/branch_details.md +++ b/bench/docs/branch_details.md @@ -5,7 +5,7 @@ - `staging` Branch: This branch serves as a release candidate. Before a week, release team will pull the feature from develop branch to staging branch. EG: if the feature is in 25 July's milestone then it should go in staging on 19th July. - `master` Branch: Community release. - - `hotFix` Branch: mainly define for support issues. This will include bugs or any high priority task like security patches. + - `hotfix` Branch: mainly define for support issues. This will include bugs or any high priority task like security patches. #### Where to send PR? - If you are working on a new feature, then PR should point to develop branch From f9e3dedf3b2b6627e7c0ac2add980b3bf7148f6e Mon Sep 17 00:00:00 2001 From: Saurabh Date: Mon, 24 Jul 2017 15:26:19 +0530 Subject: [PATCH 23/31] [fix] detailed release policy --- {bench/docs => docs}/branch_details.md | 0 .../docs => docs}/contribution_guidelines.md | 0 {bench/docs => docs}/release_policy.md | 57 +++++++++++-------- .../docs => docs}/releasing_frappe_erpext.md | 0 4 files changed, 32 insertions(+), 25 deletions(-) rename {bench/docs => docs}/branch_details.md (100%) rename {bench/docs => docs}/contribution_guidelines.md (100%) rename {bench/docs => docs}/release_policy.md (50%) rename {bench/docs => docs}/releasing_frappe_erpext.md (100%) diff --git a/bench/docs/branch_details.md b/docs/branch_details.md similarity index 100% rename from bench/docs/branch_details.md rename to docs/branch_details.md diff --git a/bench/docs/contribution_guidelines.md b/docs/contribution_guidelines.md similarity index 100% rename from bench/docs/contribution_guidelines.md rename to docs/contribution_guidelines.md diff --git a/bench/docs/release_policy.md b/docs/release_policy.md similarity index 50% rename from bench/docs/release_policy.md rename to docs/release_policy.md index 7b328342..c105ed56 100644 --- a/bench/docs/release_policy.md +++ b/docs/release_policy.md @@ -1,33 +1,19 @@ # Release Policy -#### Create staging branch - -- On Wednesday morning, we will create a `staging` branch from develop branch. `staging` branch is a release candidate. All new features will first go from `develop` to `staging` and then `staging` to `master`. - -- Use the prepare-staging command to create staging branch -```usage: bench prepare-staging APP``` - -- Impact on branches ? - - merge all commits from develop branch to staging - - push merge commit back to develop - -- QA will use staging for testing. - -- Deploy staging branch on frappe.io - -- Only regression and security fixes can be cherry-picked into staging - -- Create a discuss post on what all new features or fixes going in next version. - ---- +#### Definitions: + - `develop` Branch: All new feature developments will go in develop branch + - `staging` Branch: This branch serves as a release candidate. Before a week, release team will pull the feature from develop branch to staging branch. + EG: if the feature is in 25 July's milestone then it should go in staging on 19th July. + - `master` Branch: `master` branch serves as a stable branch. This will use as production deployment. + - `hotfix` Branch: mainly define for support issues. This will include bugs or any high priority task like security patches. #### Create release from staging - On Tuesday, we will release from staging to master. -- Versioning: - - patch: Small fixes - - minor: For new features updates. - - major: If any API changes +- Versioning: Given a version number MAJOR.MINOR.PATCH, increment the: + - MAJOR version when you make incompatible API changes, + - MINOR version when you add functionality in a backwards-compatible manner, and + - PATCH version when you make backwards-compatible bug fixes. - Impact on branches: - merge staging branch to master @@ -40,11 +26,32 @@ --- +#### Create staging branch + +- On Wednesday morning, `develop` will be merge into `staging`. `staging` branch is a release candidate. All new features will first go from `develop` to `staging` and then `staging` to `master`. + +- Use the prepare-staging command to create staging branch +```usage: bench prepare-staging APP``` + +- Impact on branches? + - merge all commits from develop branch to staging + - push merge commit back to develop + +- QA will use staging for testing. + +- Deploy staging branch on frappe.io, erpnext.org, frappe.erpnext.com. + +- Only regression and security fixes can be cherry-picked into staging + +- Create a discuss post on what all new features or fixes going in next version. + +--- + #### Create release from hotfix - Depending on priority, hotfix release will take place. - Versioning: - - patch: Small fixes + - PATCH version when you make backwards-compatible bug fixes. - Impact on branches: - merge hotfix branch to master diff --git a/bench/docs/releasing_frappe_erpext.md b/docs/releasing_frappe_erpext.md similarity index 100% rename from bench/docs/releasing_frappe_erpext.md rename to docs/releasing_frappe_erpext.md From 756d4b08177c625851b39881e5018e5fb4c7b34d Mon Sep 17 00:00:00 2001 From: Saurabh Date: Tue, 25 Jul 2017 12:32:07 +0530 Subject: [PATCH 24/31] [test-case][fix] for mysqlclient version check --- bench/tests/test_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bench/tests/test_init.py b/bench/tests/test_init.py index 1b8d63d2..b4c16b47 100755 --- a/bench/tests/test_init.py +++ b/bench/tests/test_init.py @@ -212,7 +212,7 @@ class TestBenchInit(unittest.TestCase): self.assert_exists(python_path, "site-packages", "pip") site_packages = os.listdir(os.path.join(python_path, "site-packages")) - self.assertTrue(any(package.startswith("mysqlclient-1.3.8") for package in site_packages)) + self.assertTrue(any(package.startswith("mysqlclient-1.3.10") for package in site_packages)) def assert_config(self, bench_name): for config, search_key in ( From 86328172682dc98f725fd2771abc0f9af14b3af0 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Thu, 20 Jul 2017 19:01:09 +0530 Subject: [PATCH 25/31] [fix] provision to handle release from staging branch --- bench/release.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/bench/release.py b/bench/release.py index a82a0826..e382f8e9 100755 --- a/bench/release.py +++ b/bench/release.py @@ -4,10 +4,8 @@ import os import sys import semantic_version import git -import json import requests import getpass -import argparse import re from requests.auth import HTTPBasicAuth import requests.exceptions @@ -18,6 +16,12 @@ import click github_username = None github_password = None +branches_to_update = { + 'develop': [], + 'staging': ['develop', 'hotfix'], + 'hotfix': ['develop', 'staging'] +} + def release(bench_path, app, bump_type, develop='develop', master='master', remote='upstream', owner='frappe', repo_name=None): @@ -74,8 +78,9 @@ def update_branches_and_check_for_changelog(repo_path, develop='develop', master update_branch(repo_path, master, remote=remote) update_branch(repo_path, develop, remote=remote) - if develop != 'develop': - update_branch(repo_path, 'develop', remote=remote) + + for branch in branches_to_update[develop]: + update_branch(repo_path, branch, remote=remote) git.Repo(repo_path).git.checkout(develop) check_for_unmerged_changelog(repo_path) @@ -218,13 +223,13 @@ def create_release(repo_path, new_version, develop='develop', master='master'): except git.exc.GitCommandError as e: handle_merge_error(e, source=master, target=develop) - if develop != 'develop': - print('merging master into develop') - g.checkout('develop') + for branch in branches_to_update[develop]: + print('merging master into', branch) + g.checkout(branch) try: g.merge(master) except git.exc.GitCommandError as e: - handle_merge_error(e, source=master, target='develop') + handle_merge_error(e, source=master, target=branch) return tag_name @@ -245,9 +250,9 @@ def push_release(repo_path, develop='develop', master='master', remote='upstream '{develop}:{develop}'.format(develop=develop) ] - if develop != 'develop': - print('pushing develop branch of', repo_path) - args.append('develop:develop') + for branch in branches_to_update[develop]: + print('pushing {0} branch of'.format(branch), repo_path) + args.append('{branch}:{branch}'.format(branch=branch)) args.append('--tags') From 25b96886ad6198efa7d5cc4243881d3b086878cf Mon Sep 17 00:00:00 2001 From: Saurabh Date: Fri, 21 Jul 2017 16:01:34 +0530 Subject: [PATCH 26/31] [fix] keyword fixes, use from-branch for develop and to-branch for master. Configure branches to update via common_site_config --- bench/commands/utils.py | 8 ++-- bench/release.py | 91 +++++++++++++++++++++-------------------- 2 files changed, 51 insertions(+), 48 deletions(-) diff --git a/bench/commands/utils.py b/bench/commands/utils.py index f8ed4a15..ef23bfd6 100644 --- a/bench/commands/utils.py +++ b/bench/commands/utils.py @@ -119,15 +119,15 @@ def backup_all_sites(): @click.command('release') @click.argument('app') @click.argument('bump-type', type=click.Choice(['major', 'minor', 'patch', 'stable', 'prerelease'])) -@click.option('--develop', default='develop') -@click.option('--master', default='master') +@click.option('--from-branch', default='develop') +@click.option('--to-branch', default='master') @click.option('--remote', default='upstream') @click.option('--owner', default='frappe') @click.option('--repo-name') -def release(app, bump_type, develop, master, owner, repo_name, remote): +def release(app, bump_type, from_branch, to_branch, owner, repo_name, remote): "Release app (internal to the Frappe team)" from bench.release import release - release(bench_path='.', app=app, bump_type=bump_type, develop=develop, master=master, + release(bench_path='.', app=app, bump_type=bump_type, from_branch=from_branch, to_branch=to_branch, remote=remote, owner=owner, repo_name=repo_name) diff --git a/bench/release.py b/bench/release.py index e382f8e9..c58a446a 100755 --- a/bench/release.py +++ b/bench/release.py @@ -13,29 +13,32 @@ from time import sleep from .config.common_site_config import get_config import click +branches_to_update = { + 'develop': [], + 'hotfix': ['develop'] +} + github_username = None github_password = None -branches_to_update = { - 'develop': [], - 'staging': ['develop', 'hotfix'], - 'hotfix': ['develop', 'staging'] -} - -def release(bench_path, app, bump_type, develop='develop', master='master', +def release(bench_path, app, bump_type, from_branch='develop', to_branch='master', remote='upstream', owner='frappe', repo_name=None): - validate(bench_path) - - bump(bench_path, app, bump_type, develop=develop, master=master, owner=owner, - repo_name=repo_name, remote=remote) - -def validate(bench_path): config = get_config(bench_path) + if not config.get('release_bench'): print('bench not configured to release') sys.exit(1) + if config.get('branches_to_update'): + branches_to_update.update(config.get('branches_to_update')) + + validate(bench_path, config) + + bump(bench_path, app, bump_type, from_branch=from_branch, to_branch=to_branch, owner=owner, + repo_name=repo_name, remote=remote) + +def validate(bench_path, config): global github_username, github_password github_username = config.get('github_username') @@ -50,12 +53,12 @@ def validate(bench_path): r = requests.get('https://api.github.com/user', auth=HTTPBasicAuth(github_username, github_password)) r.raise_for_status() -def bump(bench_path, app, bump_type, develop, master, remote, owner, repo_name=None): +def bump(bench_path, app, bump_type, from_branch, to_branch, remote, owner, repo_name=None): assert bump_type in ['minor', 'major', 'patch', 'stable', 'prerelease'] repo_path = os.path.join(bench_path, 'apps', app) - update_branches_and_check_for_changelog(repo_path, develop, master, remote=remote) - message = get_release_message(repo_path, develop=develop, master=master, remote=remote) + update_branches_and_check_for_changelog(repo_path, from_branch, to_branch, remote=remote) + message = get_release_message(repo_path, from_branch=from_branch, to_branch=to_branch, remote=remote) if not message: print('No commits to release') @@ -67,22 +70,22 @@ def bump(bench_path, app, bump_type, develop, master, remote, owner, repo_name=N click.confirm('Do you want to continue?', abort=True) - new_version = bump_repo(repo_path, bump_type, develop=develop, master=master) + new_version = bump_repo(repo_path, bump_type, from_branch=from_branch, to_branch=to_branch) commit_changes(repo_path, new_version) - tag_name = create_release(repo_path, new_version, develop=develop, master=master) - push_release(repo_path, develop=develop, master=master, remote=remote) + tag_name = create_release(repo_path, new_version, from_branch=from_branch, to_branch=to_branch) + push_release(repo_path, from_branch=from_branch, to_branch=to_branch, remote=remote) create_github_release(repo_path, tag_name, message, remote=remote, owner=owner, repo_name=repo_name) print('Released {tag} for {repo_path}'.format(tag=tag_name, repo_path=repo_path)) -def update_branches_and_check_for_changelog(repo_path, develop='develop', master='master', remote='upstream'): +def update_branches_and_check_for_changelog(repo_path, from_branch='develop', to_branch='master', remote='upstream'): - update_branch(repo_path, master, remote=remote) - update_branch(repo_path, develop, remote=remote) + update_branch(repo_path, to_branch, remote=remote) + update_branch(repo_path, from_branch, remote=remote) - for branch in branches_to_update[develop]: + for branch in branches_to_update[from_branch]: update_branch(repo_path, branch, remote=remote) - git.Repo(repo_path).git.checkout(develop) + git.Repo(repo_path).git.checkout(from_branch) check_for_unmerged_changelog(repo_path) def update_branch(repo_path, branch, remote): @@ -99,18 +102,18 @@ def check_for_unmerged_changelog(repo_path): if os.path.exists(current) and [f for f in os.listdir(current) if f != "readme.md"]: raise Exception("Unmerged change log! in " + repo_path) -def get_release_message(repo_path, develop='develop', master='master', remote='upstream'): - print('getting release message for', repo_path, 'comparing', master, '...', develop) +def get_release_message(repo_path, from_branch='develop', to_branch='master', remote='upstream'): + print('getting release message for', repo_path, 'comparing', to_branch, '...', from_branch) repo = git.Repo(repo_path) g = repo.git - log = g.log('{remote}/{master}..{remote}/{develop}'.format( - remote=remote, master=master, develop=develop), '--format=format:%s', '--no-merges') + log = g.log('{remote}/{to_branch}..{remote}/{from_branch}'.format( + remote=remote, to_branch=to_branch, from_branch=from_branch), '--format=format:%s', '--no-merges') if log: return "* " + log.replace('\n', '\n* ') -def bump_repo(repo_path, bump_type, develop='develop', master='master'): +def bump_repo(repo_path, bump_type, from_branch='develop', to_branch='master'): current_version = get_current_version(repo_path) new_version = get_bumped_version(current_version, bump_type) @@ -204,32 +207,32 @@ def commit_changes(repo_path, new_version): repo.index.add([os.path.join(app_name, '__init__.py')]) repo.index.commit('bumped to version {}'.format(new_version)) -def create_release(repo_path, new_version, develop='develop', master='master'): +def create_release(repo_path, new_version, from_branch='develop', to_branch='master'): print('creating release for version', new_version) repo = git.Repo(repo_path) g = repo.git - g.checkout(master) + g.checkout(to_branch) try: - g.merge(develop, '--no-ff') + g.merge(from_branch, '--no-ff') except git.exc.GitCommandError as e: - handle_merge_error(e, source=develop, target=master) + handle_merge_error(e, source=from_branch, target=to_branch) tag_name = 'v' + new_version repo.create_tag(tag_name, message='Release {}'.format(new_version)) - g.checkout(develop) + g.checkout(from_branch) try: - g.merge(master) + g.merge(to_branch) except git.exc.GitCommandError as e: - handle_merge_error(e, source=master, target=develop) + handle_merge_error(e, source=to_branch, target=from_branch) - for branch in branches_to_update[develop]: + for branch in branches_to_update[from_branch]: print('merging master into', branch) g.checkout(branch) try: - g.merge(master) + g.merge(to_branch) except git.exc.GitCommandError as e: - handle_merge_error(e, source=master, target=branch) + handle_merge_error(e, source=to_branch, target=branch) return tag_name @@ -241,16 +244,16 @@ def handle_merge_error(e, source, target): print('-'*80) click.confirm('Have you manually resolved the error?', abort=True) -def push_release(repo_path, develop='develop', master='master', remote='upstream'): - print('pushing branches', master, develop, 'of', repo_path) +def push_release(repo_path, from_branch='develop', to_branch='master', remote='upstream'): + print('pushing branches', to_branch, from_branch, 'of', repo_path) repo = git.Repo(repo_path) g = repo.git args = [ - '{master}:{master}'.format(master=master), - '{develop}:{develop}'.format(develop=develop) + '{to_branch}:{to_branch}'.format(to_branch=to_branch), + '{from_branch}:{from_branch}'.format(from_branch=from_branch) ] - for branch in branches_to_update[develop]: + for branch in branches_to_update[from_branch]: print('pushing {0} branch of'.format(branch), repo_path) args.append('{branch}:{branch}'.format(branch=branch)) From 9b99b1d34fe88ac6e22533a8888b5139ed6ebe0e Mon Sep 17 00:00:00 2001 From: Saurabh Date: Thu, 20 Jul 2017 16:11:02 +0530 Subject: [PATCH 27/31] command to staging branch from develop brach --- bench/commands/__init__.py | 3 +- bench/commands/utils.py | 7 ++++ bench/prepare_staging.py | 70 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100755 bench/prepare_staging.py diff --git a/bench/commands/__init__.py b/bench/commands/__init__.py index 1bf61806..683df339 100755 --- a/bench/commands/__init__.py +++ b/bench/commands/__init__.py @@ -38,7 +38,7 @@ bench_command.add_command(switch_to_develop) from bench.commands.utils import (start, restart, set_nginx_port, set_ssl_certificate, set_ssl_certificate_key, set_url_root, set_mariadb_host, set_default_site, download_translations, shell, backup_site, backup_all_sites, release, renew_lets_encrypt, - disable_production, bench_src) + disable_production, bench_src, prepare_staging) bench_command.add_command(start) bench_command.add_command(restart) bench_command.add_command(set_nginx_port) @@ -52,6 +52,7 @@ bench_command.add_command(shell) bench_command.add_command(backup_site) bench_command.add_command(backup_all_sites) bench_command.add_command(release) +bench_command.add_command(prepare_staging) bench_command.add_command(renew_lets_encrypt) bench_command.add_command(disable_production) bench_command.add_command(bench_src) diff --git a/bench/commands/utils.py b/bench/commands/utils.py index ef23bfd6..3a3202c4 100644 --- a/bench/commands/utils.py +++ b/bench/commands/utils.py @@ -130,6 +130,13 @@ def release(app, bump_type, from_branch, to_branch, owner, repo_name, remote): release(bench_path='.', app=app, bump_type=bump_type, from_branch=from_branch, to_branch=to_branch, remote=remote, owner=owner, repo_name=repo_name) +@click.command('prepare-staging') +@click.argument('app') +def prepare_staging(app): + """Prepare staging branch from develop branch""" + from bench.prepare_staging import prepare_staging + prepare_staging(bench_path='.', app=app) + @click.command('disable-production') def disable_production(): diff --git a/bench/prepare_staging.py b/bench/prepare_staging.py new file mode 100755 index 00000000..9b55eff0 --- /dev/null +++ b/bench/prepare_staging.py @@ -0,0 +1,70 @@ +#! env python +import os +import git +import click + +github_username = None +github_password = None + +def prepare_staging(bench_path, app, remote='upstream'): + from .release import get_release_message + validate(bench_path) + + repo_path = os.path.join(bench_path, 'apps', app) + update_branches(repo_path, remote) + message = get_release_message(repo_path, develop='develop', master='staging', remote=remote) + + if not message: + print('No commits to release') + return + + print() + print(message) + print() + + click.confirm('Do you want to continue?', abort=True) + + create_staging(repo_path) + push_commits(repo_path) + +def validate(bench_path): + from .release import validate + validate(bench_path) + +def update_branches(repo_path, remote): + from .release import update_branch + update_branch(repo_path, 'staging', remote) + update_branch(repo_path, 'develop', remote) + + git.Repo(repo_path).git.checkout('develop') + +def create_staging(repo_path, develop='develop'): + from .release import handle_merge_error + + print('creating staging from', develop) + repo = git.Repo(repo_path) + g = repo.git + g.checkout('staging') + try: + g.merge(develop, '--no-ff') + except git.exc.GitCommandError as e: + handle_merge_error(e, source=develop, target='staging') + + g.checkout(develop) + try: + g.merge('staging') + except git.exc.GitCommandError as e: + handle_merge_error(e, source='staging', target=develop) + +def push_commits(repo_path, remote='upstream'): + print('pushing staging branch of', repo_path) + + repo = git.Repo(repo_path) + g = repo.git + + args = [ + 'develop:develop', + 'staging:staging' + ] + + print(g.push(remote, *args)) From ce4a02b224e33921f37dfb9aca3dd9d36db1b825 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Tue, 25 Jul 2017 18:00:54 +0530 Subject: [PATCH 28/31] [fix] to make compatible with new release functionality --- bench/prepare_staging.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/bench/prepare_staging.py b/bench/prepare_staging.py index 9b55eff0..baa2a7c3 100755 --- a/bench/prepare_staging.py +++ b/bench/prepare_staging.py @@ -2,6 +2,7 @@ import os import git import click +from .config.common_site_config import get_config github_username = None github_password = None @@ -12,7 +13,7 @@ def prepare_staging(bench_path, app, remote='upstream'): repo_path = os.path.join(bench_path, 'apps', app) update_branches(repo_path, remote) - message = get_release_message(repo_path, develop='develop', master='staging', remote=remote) + message = get_release_message(repo_path, from_branch='develop', to_branch='staging', remote=remote) if not message: print('No commits to release') @@ -29,7 +30,9 @@ def prepare_staging(bench_path, app, remote='upstream'): def validate(bench_path): from .release import validate - validate(bench_path) + + config = get_config(bench_path) + validate(bench_path, config) def update_branches(repo_path, remote): from .release import update_branch @@ -38,23 +41,23 @@ def update_branches(repo_path, remote): git.Repo(repo_path).git.checkout('develop') -def create_staging(repo_path, develop='develop'): +def create_staging(repo_path, from_branch='develop'): from .release import handle_merge_error - print('creating staging from', develop) + print('creating staging from', from_branch) repo = git.Repo(repo_path) g = repo.git g.checkout('staging') try: - g.merge(develop, '--no-ff') + g.merge(from_branch, '--no-ff') except git.exc.GitCommandError as e: - handle_merge_error(e, source=develop, target='staging') + handle_merge_error(e, source=from_branch, target='staging') - g.checkout(develop) + g.checkout(from_branch) try: g.merge('staging') except git.exc.GitCommandError as e: - handle_merge_error(e, source='staging', target=develop) + handle_merge_error(e, source='staging', target=from_branch) def push_commits(repo_path, remote='upstream'): print('pushing staging branch of', repo_path) From aea6bf5e4bffffabf5fdf754622477cafe946d2c Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 3 Aug 2017 16:33:57 +0530 Subject: [PATCH 29/31] Develop (#463) * Add arguments for mysql_root_password and admin_password Consider existing passwords.txt * Remove duplicated --overwrite after rebase * python 3 compatible changes for `update_npm_packages` (#434) * Fix travis (#446) * python 3 compatible changes for `update_npm_packages` * install urllib3 pyOpenSSL ndg-httpsclient pyasn1 to fix wkhtmltopdf download failure on Travis * use latest version of ansible * test should look for mysqlclient-1.3.10 not mysqlclient-1.3.8 * `exec_cmd` causes TypeError on Python 3 (#451) (#452) * python 3 compatible changes for `update_npm_packages` * add universal_newlines=True to Popen * Added docker specific changes to bench (#460) * Added skip_bench_mkdir flag * added skip_bench_mkdir and skip_redis_config_generation * Removed the brackets around not in the utils.py/redis.generate_config() * Replace lines 56-61 in utils.py since the tests failed. * Reinstated lines 56-61 * printing the dirname * Removed the rogue break * Update utils.py --- .travis.yml | 2 ++ bench/commands/make.py | 8 ++++---- bench/utils.py | 34 ++++++++++++++++++++++------------ playbooks/install.py | 38 ++++++++++++++++++++++++++++++++------ 4 files changed, 60 insertions(+), 22 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6ae5eba9..9c42c989 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,8 @@ python: - "2.7" install: + - sudo rm /etc/apt/sources.list.d/docker.list + - sudo pip install urllib3 pyOpenSSL ndg-httpsclient pyasn1 - sudo apt-get purge -y mysql-common mysql-server mysql-client - sudo apt-get install --only-upgrade -y git - mkdir -p ~/.bench diff --git a/bench/commands/make.py b/bench/commands/make.py index 8059f62f..18a06c92 100755 --- a/bench/commands/make.py +++ b/bench/commands/make.py @@ -10,13 +10,15 @@ import click @click.option('--no-backups',is_flag=True, help="Run migrations for all sites in the bench") @click.option('--no-auto-update',is_flag=True, help="Build JS and CSS artifacts for the bench") @click.option('--verbose',is_flag=True, help="Verbose output during install") +@click.option('--skip-bench-mkdir', is_flag=True, help="Skip mkdir frappe-bench") +@click.option('--skip-redis-config-generation', is_flag=True, help="Skip redis config generation if already specifying the common-site-config file") def init(path, apps_path, frappe_path, frappe_branch, no_procfile, no_backups, - no_auto_update, clone_from, verbose): + no_auto_update, clone_from, verbose, skip_bench_mkdir, skip_redis_config_generation): "Create a new bench" from bench.utils import init init(path, apps_path=apps_path, no_procfile=no_procfile, no_backups=no_backups, no_auto_update=no_auto_update, frappe_path=frappe_path, frappe_branch=frappe_branch, - verbose=verbose, clone_from=clone_from) + verbose=verbose, clone_from=clone_from, skip_bench_mkdir=skip_bench_mkdir, skip_redis_config_generation=skip_redis_config_generation) click.echo('Bench {} initialized'.format(path)) @@ -54,5 +56,3 @@ def new_site(site, mariadb_root_password=None, admin_password=None): "Create a new site in the bench" from bench.utils import new_site new_site(site, mariadb_root_password=mariadb_root_password, admin_password=admin_password) - - diff --git a/bench/utils.py b/bench/utils.py index 34b97cb2..896fc0c0 100755 --- a/bench/utils.py +++ b/bench/utils.py @@ -2,6 +2,8 @@ import os, sys, shutil, subprocess, logging, itertools, requests, json, platform from distutils.spawn import find_executable import bench from bench import env +from six import iteritems + class PatchError(Exception): pass @@ -25,21 +27,27 @@ def get_env_cmd(cmd, bench_path='.'): def init(path, apps_path=None, no_procfile=False, no_backups=False, no_auto_update=False, frappe_path=None, frappe_branch=None, wheel_cache_dir=None, - verbose=False, clone_from=None): + verbose=False, clone_from=None, skip_bench_mkdir=False, skip_redis_config_generation=False): from .app import get_app, install_apps_from_path from .config.common_site_config import make_config from .config import redis from .config.procfile import setup_procfile from bench.patches import set_all_patches_executed - if os.path.exists(path): - print('Directory {} already exists!'.format(path)) - raise Exception("Site directory already exists") - # sys.exit(1) + if(skip_bench_mkdir): + pass + else: + if os.path.exists(path): + print('Directory {} already exists!'.format(path)) + raise Exception("Site directory already exists") + os.makedirs(path) - os.makedirs(path) for dirname in folders_in_bench: - os.mkdir(os.path.join(path, dirname)) + try: + os.makedirs(os.path.join(path, dirname)) + except OSError, e: + if e.errno != os.errno.EEXIST: + pass setup_logging() @@ -65,7 +73,9 @@ def init(path, apps_path=None, no_procfile=False, no_backups=False, set_all_patches_executed(bench_path=path) build_assets(bench_path=path) - redis.generate_config(path) + + if not skip_redis_config_generation: + redis.generate_config(path) if not no_procfile: setup_procfile(path) @@ -119,7 +129,7 @@ def exec_cmd(cmd, cwd='.'): logger.info(cmd) - p = subprocess.Popen(cmd, cwd=cwd, shell=True, stdout=stdout, stderr=stderr) + p = subprocess.Popen(cmd, cwd=cwd, shell=True, stdout=stdout, stderr=stderr, universal_newlines=True) if async: return_code = print_output(p) @@ -411,7 +421,7 @@ def update_npm_packages(bench_path='.'): with open(package_json_path, "r") as f: app_package_json = json.loads(f.read()) # package.json is usually a dict in a dict - for key, value in app_package_json.iteritems(): + for key, value in iteritems(app_package_json): if not key in package_json: package_json[key] = value else: @@ -421,7 +431,7 @@ def update_npm_packages(bench_path='.'): package_json[key].extend(value) else: package_json[key] = value - + if package_json is {}: with open(os.path.join(os.path.dirname(__file__), 'package.json'), 'r') as f: package_json = json.loads(f.read()) @@ -651,7 +661,7 @@ def update_translations(app, lang): f.flush() print('downloaded for', app, lang) - + def download_chart_of_accounts(): charts_dir = os.path.join('apps', "erpnext", "erpnext", 'accounts', 'chart_of_accounts', "submitted") csv_file = os.path.join(translations_dir, lang + '.csv') diff --git a/playbooks/install.py b/playbooks/install.py index 65e6f35d..1c6f7406 100755 --- a/playbooks/install.py +++ b/playbooks/install.py @@ -61,7 +61,7 @@ def install_bench(args): # Restricting ansible version due to following bug in ansible 2.1 # https://github.com/ansible/ansible-modules-core/issues/3752 success = run_os_command({ - 'pip': "sudo pip install 'ansible==2.0.2.0'" + 'pip': "sudo pip install ansible" }) if not success: @@ -97,7 +97,7 @@ def install_bench(args): extra_vars.update(repo_path=repo_path) run_playbook('develop/create_user.yml', extra_vars=extra_vars) - extra_vars.update(get_passwords(args.run_travis or args.without_bench_setup)) + extra_vars.update(get_passwords(args)) if args.production: extra_vars.update(max_worker_connections=multiprocessing.cpu_count() * 1024) @@ -229,9 +229,31 @@ def could_not_install(package): def is_sudo_user(): return os.geteuid() == 0 -def get_passwords(ignore_prompt=False): + +def get_passwords(args): + """ + Returns a dict of passwords for further use + and creates passwords.txt in the bench user's home directory + """ + + ignore_prompt = args.run_travis or args.without_bench_setup + mysql_root_password, admin_password = '', '' + passwords_file_path = os.path.join(os.path.expanduser('~' + args.user), 'passwords.txt') + if not ignore_prompt: - mysql_root_password, admin_password = '', '' + # set passwords from existing passwords.txt + if os.path.isfile(passwords_file_path): + with open(passwords_file_path, 'r') as f: + passwords = json.load(f) + mysql_root_password, admin_password = passwords['mysql_root_password'], passwords['admin_password'] + + # set passwords from cli args + if args.mysql_root_password: + mysql_root_password = args.mysql_root_password + if args.admin_password: + admin_password = args.admin_password + + # prompt for passwords pass_set = True while pass_set: # mysql root password @@ -262,7 +284,6 @@ def get_passwords(ignore_prompt=False): } if not ignore_prompt: - passwords_file_path = os.path.join(os.path.expanduser('~'), 'passwords.txt') with open(passwords_file_path, 'w') as f: json.dump(passwords, f, indent=1) @@ -270,6 +291,7 @@ def get_passwords(ignore_prompt=False): return passwords + def get_extra_vars_json(extra_args): # We need to pass production as extra_vars to the playbook to execute conditionals in the # playbook. Extra variables can passed as json or key=value pair. Here, we will use JSON. @@ -334,11 +356,15 @@ def parse_commandline_args(): parser.add_argument('--without-bench-setup', dest='without_bench_setup', action='store_true', default=False, help=argparse.SUPPRESS) - + # whether to overwrite an existing bench parser.add_argument('--overwrite', dest='overwrite', action='store_true', default=False, help='Whether to overwrite an existing bench') + # set passwords + parser.add_argument('--mysql-root-password', dest='mysql_root_password', help='Set mysql root password') + parser.add_argument('--admin-password', dest='admin_password', help='Set admin password') + args = parser.parse_args() return args From 6b50f4c220fe7757fdfe252e291c25409eaa0a98 Mon Sep 17 00:00:00 2001 From: Agus Syahputra Date: Thu, 3 Aug 2017 18:04:18 +0700 Subject: [PATCH 30/31] Update README.md (#462) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 0ea8c84c..17578fda 100755 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ Note: Please do not remove the bench directory the above commands will create - This is an opinionated setup so it is best to setup on a blank server. - Works on Ubuntu 14.04 to 16.04, CentOS 7+, Debian 7 to 8 and MacOS X. - You may have to install Python 2.7 (eg on Ubuntu 16.04+) by running `apt-get install python-minimal` +- You may also have to install build-essential and python-setuptools by running `apt-get install build-essential python-setuptools` - This script will install the pre-requisites, install bench and setup an ERPNext site - Passwords for Frappe Administrator and MariaDB (root) will be asked - You can then login as **Administrator** with the Administrator password From 753c739d8b153135e629f686e882c518d3b263b4 Mon Sep 17 00:00:00 2001 From: vishalseshagiri Date: Thu, 17 Aug 2017 15:35:00 +0530 Subject: [PATCH 31/31] Added docker repo link to README.md (#471) * Added docker repo link to README.md * Update README.md * Update README.md --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 17578fda..487c3946 100755 --- a/README.md +++ b/README.md @@ -135,6 +135,13 @@ For production: --- +## Docker Install - For Developers (beta) + +1. For developer setup, you can also use the official [Frappé Docker](https://github.com/frappe/frappe_docker/). +2. The app, mariadb and redis run on individual containers +3. This setup supports multi-tenancy and exposes the frappe-bench volume as a external storage. +4. For more details, [read the instructions on the Frappé Docker README](https://github.com/frappe/frappe_docker/) + Help ====