Commits (36)
...@@ -6,6 +6,19 @@ This project adheres to http://semver.org/[Semantic Versioning]. ...@@ -6,6 +6,19 @@ This project adheres to http://semver.org/[Semantic Versioning].
== [Unreleased] == [Unreleased]
* Add a "tags" argument to the "test" command in order to limit which scenarios are run by behat.
=== Fixed
* Site name can now have whitespaces without breaking the shell execution string
=== Added
* Add a Patternlab command to build the static style guide: `fab patternlab.build`.
* Add a Git command to verify repositories states and warn user if changes might be lost.
== 2.0.0 - 2016/05/09
=== Added === Added
* Implements Aegir and standard deployments as Fabric tasks * Implements Aegir and standard deployments as Fabric tasks
......
...@@ -31,8 +31,9 @@ Those are global project parameters mandatory to Drupalizer. ...@@ -31,8 +31,9 @@ Those are global project parameters mandatory to Drupalizer.
|_workspace_ |_workspace_
|Project root directory. |Project root directory.
|_interactive_mode_ |_always_use_pty_
|If false, no prompt. Useful for Jenkins. |This env var is defined by Fabric itself, and it's defaut value is True.
|You can easily pass it to False, addinf --no-pty to your fab command line. Useful in Jenkins jobs.
|_locale_ |_locale_
|If True, install the site in French. |If True, install the site in French.
...@@ -88,6 +89,16 @@ Those are global project parameters mandatory to Drupalizer. ...@@ -88,6 +89,16 @@ Those are global project parameters mandatory to Drupalizer.
|=== |===
=== Patternlab settings
|===
|Parameters |Description
|_patternlab_dir_
|The Patternlab directory. Default: _empty_
|===
=== Docker-related settings === Docker-related settings
|=== |===
...@@ -194,3 +205,7 @@ CAUTION: This command will wipe all the modifications made in the working direct ...@@ -194,3 +205,7 @@ CAUTION: This command will wipe all the modifications made in the working direct
* _Archive_ the full codebase and the database using drush archive_dump: * _Archive_ the full codebase and the database using drush archive_dump:
$ fab drush.archive_dump $ fab drush.archive_dump
* _Generate_ the guide style:
$ fab patternlab.build
...@@ -2,11 +2,13 @@ import docker ...@@ -2,11 +2,13 @@ import docker
from deploy import * from deploy import *
import drush import drush
import behat import behat
import patternlab
from .environments import e from .environments import e
from fabric.api import task, env, execute from fabric.api import task, env, execute
import helpers as h from fabric.colors import red
from fabric.contrib.console import confirm
@task @task
def init(): def init():
...@@ -24,16 +26,19 @@ def init(): ...@@ -24,16 +26,19 @@ def init():
@task @task
def test(): def test(tags=''):
""" """
Setup Behat and run the complete tests suite. Default output formatters: pretty and JUnit. Setup Behat and run the complete tests suite. Default output formatters: pretty and JUnit.
The JUnit report file is specified in the Behat configuration file. Default: tests/behat/out/behat.junit.xml. The JUnit report file is specified in the Behat configuration file. Default: tests/behat/out/behat.junit.xml.
:param tag Specific Behat tests tags to run. :param tags Specific Behat tests tags to run.
""" """
execute(behat.init) execute(behat.init)
execute(behat.run) if not tags:
execute(behat.run)
else:
execute(behat.run, tags='{}'.format(tags))
......
...@@ -51,7 +51,7 @@ def install(): ...@@ -51,7 +51,7 @@ def install():
@task @task
@roles('docker') @roles('docker')
def run(): def run(tags='~@wip&&~@disabled&&~@test'):
""" """
Execute the complete Behat tests suite. Execute the complete Behat tests suite.
:param role Default 'role' where to run the task :param role Default 'role' where to run the task
...@@ -68,8 +68,5 @@ def run(): ...@@ -68,8 +68,5 @@ def run():
if not h.fab_exists(role, '{}/tests/behat/behat.yml'.format(workspace)): if not h.fab_exists(role, '{}/tests/behat/behat.yml'.format(workspace)):
init() init()
with h.fab_cd(role, '{}/tests/behat'.format(workspace)): with h.fab_cd(role, '{}/tests/behat'.format(workspace)):
h.fab_run(role, 'behat --format junit --format pretty --tags "~@wip&&~@disabled&&~@test" --colors') h.fab_run(role, 'behat --format junit --format pretty --tags "{}" --colors'.format(tags))
# To run behat with only one test for example, comment previous line
# and uncomment next one
# fab_run(role, 'behat --format pretty --tags "~@wip&&~@disabled&&@yourTest" --colors')
...@@ -18,6 +18,7 @@ env.site_environment = 'local' ...@@ -18,6 +18,7 @@ env.site_environment = 'local'
env.site_profile = '' env.site_profile = ''
env.site_profile_repo = '' env.site_profile_repo = ''
env.site_profile_makefile = '' env.site_profile_makefile = ''
env.site_profile_branch= '7.x'
env.site_db_user = 'dev' env.site_db_user = 'dev'
env.site_db_pass = 'dev' env.site_db_pass = 'dev'
env.site_db_host = 'localhost' env.site_db_host = 'localhost'
...@@ -26,10 +27,18 @@ env.site_hostname = '' ...@@ -26,10 +27,18 @@ env.site_hostname = ''
env.site_admin_user = 'admin' env.site_admin_user = 'admin'
env.site_admin_pass = 'admin' env.site_admin_pass = 'admin'
env.site_subdir = 'default' env.site_subdir = 'default'
env.site_languages = 'fr'
# PatternLab
# Specify the PatternLab dir is you want the style guide to be generated
env.patternlab_dir = ''
# Database dump # Database dump
# To enable it, replace the boolean value with the absolute path of a gzipped SQL dump file. # To enable it, replace the boolean value with the absolute path of a gzipped SQL dump file.
env.db_dump = False env.db_dump = False
# Docker # Docker
......
...@@ -141,7 +141,7 @@ def container_start(role='local'): ...@@ -141,7 +141,7 @@ def container_start(role='local'):
# If container was successful build, get the IP address and show it to the user. # If container was successful build, get the IP address and show it to the user.
env.container_ip = h.fab_run(role, 'docker inspect -f "{{{{.NetworkSettings.IPAddress}}}}" ' env.container_ip = h.fab_run(role, 'docker inspect -f "{{{{.NetworkSettings.IPAddress}}}}" '
'{}_container'.format(env.project_name), capture=True) '{}_container'.format(env.project_name), capture=True)
if env.get('interactive_mode', True) != 'false': if env.get('always_use_pty', True):
h.fab_update_hosts(env.container_ip, env.site_hostname) h.fab_update_hosts(env.container_ip, env.site_hostname)
print(green('Docker container {}_container was build successful. ' print(green('Docker container {}_container was build successful. '
...@@ -165,7 +165,7 @@ def container_stop(role='local'): ...@@ -165,7 +165,7 @@ def container_stop(role='local'):
""" """
with h.fab_cd(role, env.workspace): with h.fab_cd(role, env.workspace):
if '{}_container'.format(env.project_name) in docker_ps(): if '{}_container'.format(env.project_name) in docker_ps():
if env.get('interactive_mode', True) != 'false': if env.get('always_use_pty', True):
h.fab_remove_from_hosts(env.site_hostname) h.fab_remove_from_hosts(env.site_hostname)
h.fab_run(role, 'docker stop {}_container'.format(env.project_name)) h.fab_run(role, 'docker stop {}_container'.format(env.project_name))
print(green('Docker container {}_container was successful stopped'.format(env.project_name))) print(green('Docker container {}_container was successful stopped'.format(env.project_name)))
...@@ -183,7 +183,7 @@ def container_remove(role='local'): ...@@ -183,7 +183,7 @@ def container_remove(role='local'):
with h.fab_cd(role, env.workspace): with h.fab_cd(role, env.workspace):
if '{}_container'.format(env.project_name) in docker_ps(): if '{}_container'.format(env.project_name) in docker_ps():
if env.get('interactive_mode', True) != 'false': if env.get('always_use_pty', True):
h.fab_remove_from_hosts(env.site_hostname) h.fab_remove_from_hosts(env.site_hostname)
h.fab_run(role, 'docker rm -f {}_container'.format(env.project_name)) h.fab_run(role, 'docker rm -f {}_container'.format(env.project_name))
......
...@@ -20,12 +20,15 @@ from __future__ import unicode_literals ...@@ -20,12 +20,15 @@ from __future__ import unicode_literals
from fabric.api import task, roles, env from fabric.api import task, roles, env
from fabric.contrib.console import confirm from fabric.contrib.console import confirm
from fabric.colors import red, green from fabric.colors import red, green
from fabric.utils import abort
from datetime import datetime from datetime import datetime
import helpers as h import helpers as h
import core as c import core as c
from git import isGitDirty
@task(alias='make') @task(alias='make')
@roles('local') @roles('local')
...@@ -34,6 +37,12 @@ def make(action='install'): ...@@ -34,6 +37,12 @@ def make(action='install'):
Build the platform by running the Makefile specified in the local_vars.py configuration file. Build the platform by running the Makefile specified in the local_vars.py configuration file.
""" """
if env.get('always_use_pty', True):
if (isGitDirty()):
if (not confirm(red('There are warnings on status of your repositories. '
'Do you want to continue and reset all changes to remote repositories'' states?'), default=False)):
abort('Aborting "drush {}" since there might be a risk of loosing local data.'.format(action))
drush_opts = "--prepare-install " if action != 'update' else '' drush_opts = "--prepare-install " if action != 'update' else ''
# Update profile codebase # Update profile codebase
...@@ -41,20 +50,19 @@ def make(action='install'): ...@@ -41,20 +50,19 @@ def make(action='install'):
drush_opts += "--contrib-destination=profiles/{} ".format(env.site_profile) drush_opts += "--contrib-destination=profiles/{} ".format(env.site_profile)
h.update_profile() h.update_profile()
if env.get('interactive_mode', True) == 'false': if not env.get('always_use_pty', True):
drush_opts += "--translations=fr " drush_opts += "--translations=" + env.site_languages + " "
elif confirm(red('Say [Y] to {} the site at {} with the French translation, if you say [n] ' elif confirm(red('Say [Y] to {} the site at {} with the specified translation(s): {}. If you say [n] '
'the site will be installed in English only'.format(action, env.site_root))): 'the site will be installed in English only'.format(action, env.site_root, env.site_languages))):
drush_opts += "--translations=fr " drush_opts += "--translations=" + env.site_languages + " "
if env.get('interactive_mode', True) != 'false': if env.get('always_use_pty', True):
drush_opts += " --working-copy --no-gitinfofile" drush_opts += " --working-copy --no-gitinfofile"
if not h.fab_exists('local', env.site_root): if not h.fab_exists('local', env.site_root):
h.fab_run('local', "mkdir {}".format(env.site_root)) h.fab_run('local', "mkdir {}".format(env.site_root))
with h.fab_cd('local', env.site_root): with h.fab_cd('local', env.site_root):
h.fab_run('local', 'drush make {} {} -y'.format(drush_opts, env.makefile)) h.fab_run('local', 'drush make {} {} -y'.format(drush_opts, env.makefile))
@task @task
@roles('local') @roles('local')
def aliases(): def aliases():
...@@ -117,7 +125,7 @@ def site_install(): ...@@ -117,7 +125,7 @@ def site_install():
with h.fab_cd(role, site_root): with h.fab_cd(role, site_root):
locale = '--locale="fr"' if env.locale else '' locale = '--locale="fr"' if env.locale else ''
h.fab_run(role, 'sudo -u {} drush site-install {} {} --db-url=mysql://{}:{}@{}/{} --site-name={} ' h.fab_run(role, 'sudo -u {} drush site-install {} {} --db-url=mysql://{}:{}@{}/{} --site-name="{}" '
'--account-name={} --account-pass={} --sites-subdir={} -y'.format(apache, profile, locale, '--account-name={} --account-pass={} --sites-subdir={} -y'.format(apache, profile, locale,
db_user, db_pass, db_user, db_pass,
db_host, db_name, site_name, db_host, db_name, site_name,
......
from os import path
from fabric.api import task, env, local
from fabric.colors import red, green, yellow
from fabric.api import task, env, execute
from fabric.contrib.console import confirm
from .environments import e
import helpers as h
import time
import re
@task(alias='is_dirty')
def check_status():
"""
Check workspace's git repositories status.
"""
if (isGitDirty()):
print red('Your workspace is not clean.')
else:
print green('Your workspace is clean.')
def isGitDirty():
repos = local('find ' + path.normpath(env.workspace) + ' -name ".git"', capture=True).splitlines()
nbWarnings = 0
for repo in repos:
repoLocalPath = path.normpath(path.join(repo, '..'))
nbWarnings += _checkRepo(repoLocalPath)
return (nbWarnings > 0)
# STEP 2
# plutot que s'arreter ou continuer betement, on est intelligent et on demande quoi faire:
# - s'il y a du code non-stage, en faire un commit
# - si la Branch n'est pas trackee, la pusher
# - s'il y a des commits non pushes, les pusher
# - attention aux conflits, faire un pull d'abord et valider la fusion automatique
def _checkRepo(repoLocalPath):
nbWarnings = 0
with h.fab_cd('local', repoLocalPath):
print green('---')
print green('Verify repo in ' + repoLocalPath)
remoteName = local('git remote', capture=True)
filesStatusRawInfo = _getFilesStatusInformation()
print green('Verify local files status against current HEAD commit...')
nbWarnings += _checkFilesStatusVsHeadCommit(filesStatusRawInfo)
localBranchesRawInfo = _getLocalBranchesInformation()
print green('Verify local branches exist on remote "' + remoteName + '"...');
nbWarnings += _checkLocalBranchesExistOnRemote(localBranchesRawInfo, remoteName)
print green('Verify branches status against remote...');
nbWarnings += _checkLocalBranchesStatusVsRemote(localBranchesRawInfo, remoteName)
return nbWarnings
def _checkFilesStatusVsHeadCommit(filesStatusRawInfo):
nbWarnings = 0
addableFiles = []
if (len(filesStatusRawInfo) > 0):
for fileStatus in filesStatusRawInfo:
fileStatusData = fileStatus.split()
# Break loop if filename is "fabfile"
if fileStatusData[1] == 'fabfile':
break
nbWarnings += 1
addableFiles.append(fileStatusData[1])
print yellow('File "' + fileStatusData[1] + '" ' + {
'M': 'has un-commited modifications.',
'D': 'has been deleted.',
'??': 'is not indexed.',
}.get(fileStatusData[0], 'is in an unknown state (' + fileStatusData[0] + ')'))
return nbWarnings
def _checkLocalBranchesExistOnRemote(localBranchesRawInfo, remoteName):
nbWarnings = 0
pushableBranches = []
for localBranchRawInfo in localBranchesRawInfo:
localBranchName = _getBranchName(localBranchRawInfo)
if ((localBranchName is not None) and (not _remoteBranchExists(localBranchName))):
nbWarnings += 1
pushableBranches.append(localBranchName)
print yellow('Local branch "' + localBranchName + '" is not present on "' + remoteName + '" remote.')
# On suggere de pusher la(les) branche(s) qui sont seulement sur le local
if (nbWarnings > 0):
if (confirm(red('There are many local branches not present on remote. Do you want to sync theses?'), default=False)):
for branchName in pushableBranches:
local('git push --set-upstream ' + remoteName + ' ' + branchName)
# Do not alert with diff as local branches are now pushed
nbWarnings = 0
return nbWarnings
def _checkLocalBranchesStatusVsRemote(localBranchesRawInfo, remoteName):
nbWarnings = 0
pushableBranches = []
# on gere un git FR ou EN, a voir ce qu'on peut faire
# pour une internationalisation plus cool..
pattern = re.compile('.*\[.* (ahead|en avance de) .*\].*')
for localBranchRawInfo in localBranchesRawInfo:
if (pattern.match(localBranchRawInfo)):
localBranchName = _getBranchName(localBranchRawInfo)
nbWarnings += 1
pushableBranches.append(localBranchName)
print yellow('Local branch "' + localBranchName + '" is ahead of remote branch.');
# On suggere de pusher la(les) branche(s) qui sont seulement sur le local
if (nbWarnings > 0):
if (confirm(red('There are many local branches which are ahead of remote branch. Do you want to sync theses?'), default=False)):
for branchName in pushableBranches:
local('git push ' + remoteName + ' ' + branchName)
# Do not alert with diff as local branches are now pushed
nbWarnings = 0
# TO DO : PUSHER LA BRANCHE SUR LE REPO
return nbWarnings
def _getLocalBranchesInformation():
return local('git branch --list -vv', capture=True).splitlines()
def _getBranchName(branchRawData):
branchName = branchRawData.replace('*', '').strip()
if (_isBranchDetached(branchName)):
return None
else:
return branchName.split()[0]
def _isBranchDetached(branchName):
pattern = re.compile('\(.*\)')
return pattern.match(branchName)
def _remoteBranchExists(branchName):
return (len(local('git branch --list --remote "*' + branchName + '"', capture=True).splitlines()) > 0)
def _getFilesStatusInformation():
return local('git status -s', capture=True).splitlines()
...@@ -169,7 +169,7 @@ def update_profile(role='local'): ...@@ -169,7 +169,7 @@ def update_profile(role='local'):
print green('{} installation profile updated in {}/{}'.format(env.site_profile, env.builddir, env.site_profile)) print green('{} installation profile updated in {}/{}'.format(env.site_profile, env.builddir, env.site_profile))
else: else:
with fab_cd(role, env.builddir): with fab_cd(role, env.builddir):
fab_run(role, 'git clone {} {}'.format(env.site_profile_repo, env.site_profile)) fab_run(role, 'git clone --branch={} {} {}'.format(env.site_profile_branch, env.site_profile_repo, env.site_profile))
print green('{} installation profile cloned in {}/{}'.format(env.site_profile, env.builddir, env.site_profile)) print green('{} installation profile cloned in {}/{}'.format(env.site_profile, env.builddir, env.site_profile))
......
from __future__ import unicode_literals
from fabric.api import task, roles, env
from fabric.colors import green
import helpers as h
@task
@roles('local')
def build():
"""
Generate the PatternLab static site
"""
role = 'local'
workspace = env.docker_workspace
host = env.site_hostname
site_root = env.docker_site_root
with h.fab_cd(role, env.patternlab_dir):
h.fab_run(role, 'touch public/styleguide/html/styleguide.html')
h.fab_run(role, 'php core/builder.php -g')