├── .gitignore ├── prepare_dir.sh ├── github_comment.sh ├── cleanup.sh ├── clone_site.sh ├── README.md ├── INSTALL.md └── jgd.drush.inc /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store* 2 | ehthumbs.db 3 | Thumbs.db 4 | -------------------------------------------------------------------------------- /prepare_dir.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | # The directory this script is in. 5 | REAL_PATH=`readlink -f "${BASH_SOURCE[0]}"` 6 | SCRIPT_DIR=`dirname "$REAL_PATH"` 7 | 8 | usage() { 9 | cat $SCRIPT_DIR/README.md | 10 | # Remove ticks and stars. 11 | sed -e "s/[\`|\*]//g" 12 | } 13 | 14 | GHPRID= 15 | MERGE_BRANCH="master" 16 | while getopts “hxi:m:” OPTION; do 17 | case $OPTION in 18 | h) 19 | usage 20 | exit 21 | ;; 22 | i) 23 | GHPRID=$OPTARG 24 | ;; 25 | m) 26 | MERGE_BRANCH=$OPTARG 27 | ;; 28 | x) 29 | set -x 30 | ;; 31 | ?) 32 | usage 33 | exit 34 | ;; 35 | esac 36 | done 37 | 38 | # Remove the switches we parsed above. 39 | shift `expr $OPTIND - 1` 40 | 41 | # Now, parse the arguments. 42 | WEBROOT=${1:-$WORKSPACE} 43 | 44 | # If we're missing some of these variables, show the usage and throw an error. 45 | if [[ -z $WEBROOT ]] || [[ -z $GHPRID ]]; then 46 | usage 47 | exit 1 48 | fi 49 | 50 | if [[ -z $WORKSPACE ]]; then 51 | echo "This script must be executed from within a proper Jenkins job." 52 | exit 1 53 | fi 54 | 55 | # This is the directory of the checked out pull request, from Jenkins. 56 | ORIGINAL_DIR="${WORKSPACE}/new_pull_request" 57 | # The directory where the checked out pull request will reside. 58 | ACTUAL_DIR="${WORKSPACE}/${GHPRID}-actual" 59 | # The directory where the docroot will be symlinked to. 60 | DOCROOT=$WEBROOT/$GHPRID 61 | # The command will attempt to merge master with the pull request. 62 | BRANCH="jenkins-pull-request-$GHPRID" 63 | 64 | # Remove the existing .git dir if it exists. 65 | rm -rf $ACTUAL_DIR/.git 66 | # Copy the new_pull_request directory. 67 | rsync -a ${ORIGINAL_DIR}/ $ACTUAL_DIR 68 | # Now remove the new_pull_request directory. 69 | rm -rf $ORIGINAL_DIR 70 | # Create a symlink to the docroot, if it doesn't already exist. 71 | if [ ! -h $DOCROOT -a -d $ACTUAL_DIR ]; 72 | then 73 | ln -sf $ACTUAL_DIR/docroot $DOCROOT 74 | fi 75 | 76 | cd $ACTUAL_DIR 77 | git checkout -b $BRANCH 78 | # Clean out any untracked files and directories. 79 | git clean -d -f 80 | # Now checkout the merge branch. 81 | git checkout $MERGE_BRANCH 82 | git pull 83 | git merge $BRANCH -m "Jenkins test merge into master." 84 | 85 | echo "Checked out a new branch for this pull request, and merged it to $MERGE_BRANCH." 86 | -------------------------------------------------------------------------------- /github_comment.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | # The directory this script is in. 5 | REAL_PATH=`readlink -f "${BASH_SOURCE[0]}"` 6 | SCRIPT_DIR=`dirname "$REAL_PATH"` 7 | 8 | usage() { 9 | cat << EOF 10 | usage: github_comment.sh -a <[github_account]/[github_project]> -i -b <<< "" 11 | 12 | OPTIONS 13 | 14 | <[github_account]/[github_project]> 15 | 16 | -a 17 | This is the project owner account and project, as you might see in the github 18 | URL. For instance, if you were going to post a comment on 19 | https://github.com/q0rban/foo, you would set -a to q0rban/foo. 20 | -i 21 | The github issue number. 22 | -b 23 | The body of the comment. 24 | 25 | ARGUMENTS 26 | 27 | 28 | The Github authentication token. Pass this to stdin so that the value cannot 29 | be seen in ps. If using this within Jenkins, it is highly recommended to use 30 | the 'Inject passwords to the build environment as environment variables' 31 | option that comes with the Environment Injector Plugin. 32 | 33 | EOF 34 | } 35 | 36 | # Parse options. 37 | WEBROOT=$WORKSPACE 38 | DRUSH="drush" 39 | VERBOSE="" 40 | 41 | while getopts "ha:i:b:" OPTION; do 42 | case $OPTION in 43 | h) 44 | usage 45 | exit 1 46 | ;; 47 | a) 48 | ACCOUNT_PROJECT=$OPTARG 49 | ;; 50 | i) 51 | ISSUE_NUMBER=$OPTARG 52 | ;; 53 | b) 54 | BODY=$OPTARG 55 | ;; 56 | ?) 57 | usage 58 | exit 59 | ;; 60 | esac 61 | done 62 | 63 | # Remove the switches we parsed above from the arguments. 64 | shift `expr $OPTIND - 1` 65 | 66 | # Grab the token from STDIN. 67 | read TOKEN 68 | 69 | # If we're missing some of these variables, show the usage and throw an error. 70 | if [[ -z $TOKEN ]] || [[ -z $ACCOUNT_PROJECT ]] || [[ -z $ISSUE_NUMBER ]] || [[ -z $BODY ]]; then 71 | usage 72 | exit 1 73 | fi 74 | 75 | # Ensure curl exists. 76 | command -v curl >/dev/null 2>&1 || { 77 | echo >&2 "You must have cURL installed for this command to work properly."; 78 | exit 1; 79 | } 80 | 81 | URL="https://api.github.com/repos/$ACCOUNT_PROJECT/issues/$ISSUE_NUMBER/comments" 82 | PUBLIC_URL="http://github.com/$ACCOUNT_PROJECT/issues/$ISSUE_NUMBER" 83 | # Escape all single quotes. 84 | BODY=${BODY//\'/\\\'} 85 | # Encode to json. PHP makes this pretty easy. Maybe there's something better? 86 | DATA=`php -r "print json_encode(array('body' => '$BODY'));"` 87 | # Now make the actual call to github. 88 | OUTPUT=`curl -H "Authorization: token $TOKEN" -d "$DATA" $URL` 89 | # Escape all single quotes again. 90 | OUTPUT=${OUTPUT//\'/\\\'} 91 | # Check for errors 92 | ERROR=`php -r "\\$json = json_decode('$OUTPUT'); isset(\\$json->message) ? print \\$json->message : NULL;"` 93 | 94 | if [[ -z $ERROR ]]; then 95 | echo "Comment posted successfully to $PUBLIC_URL." 96 | exit 0 97 | fi 98 | 99 | echo "Failed to post comment. Reason: $ERROR." 100 | exit 1 101 | -------------------------------------------------------------------------------- /cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | # The directory this script is in. 5 | REAL_PATH=`readlink -f "${BASH_SOURCE[0]}"` 6 | SCRIPT_DIR=`dirname "$REAL_PATH"` 7 | 8 | usage() { 9 | cat $SCRIPT_DIR/README.md | 10 | # Remove ticks and stars. 11 | sed -e "s/[\`|\*]//g" 12 | } 13 | 14 | # Parse options. 15 | DRUSH="drush" 16 | WEBROOT=$WORKSPACE 17 | VERBOSE="" 18 | CLONE="table" 19 | 20 | while getopts “hi:l:d:u:cvx” OPTION; do 21 | case $OPTION in 22 | h) 23 | usage 24 | exit 1 25 | ;; 26 | i) 27 | GHPRID=$OPTARG 28 | ;; 29 | l) 30 | WEBROOT=$OPTARG 31 | ;; 32 | d) 33 | DRUSH=$OPTARG 34 | ;; 35 | u) 36 | URIS=$OPTARG 37 | ;; 38 | c) 39 | CLONE="database" 40 | ;; 41 | v) 42 | VERBOSE="--verbose" 43 | ;; 44 | x) 45 | set -x 46 | ;; 47 | ?) 48 | usage 49 | exit 50 | ;; 51 | esac 52 | done 53 | 54 | # Remove the switches we parsed above from the arguments. 55 | shift `expr $OPTIND - 1` 56 | 57 | # If we're missing some of these variables, show the usage and throw an error. 58 | if [[ -z $WEBROOT ]] || [[ -z $GHPRID ]]; then 59 | usage 60 | exit 1 61 | fi 62 | 63 | # Put drush in verbose mode, if requested, and include our script dir so we have 64 | # access to our custom drush commands. 65 | DRUSH="$DRUSH $VERBOSE --include=$SCRIPT_DIR" 66 | # The docroot of the new Drupal directory. 67 | DOCROOT="$WEBROOT/$GHPRID" 68 | # The real directory of the docroot. 69 | REAL_DOCROOT=`readlink -f "$DOCROOT"` 70 | # The path of the repository. 71 | ACTUAL_PATH=`dirname "$REAL_DOCROOT"` 72 | # The unique prefix to use for just this pull request. 73 | DB_PREFIX="pr_${GHPRID}_" 74 | # The drush options for the Drupal destination site. Eventually, we could open 75 | # this up to allow users to specify a drush site alias, but for now, we'll just 76 | # manually specify the root and uri options. 77 | DESTINATION="--root=$DOCROOT" 78 | 79 | # If $URIS is currently empty, attempt to load them from the sa command. 80 | if [[ -z $URIS ]]; then 81 | # Check to make sure drush is working properly, and can access the source site. 82 | URIS=`eval $DRUSH $DESTINATION sa` 83 | # Run this through grep separately, turning off error reporting in case of an 84 | # empty string. TODO: figure out how to do this with bash pattern substitution. 85 | set +e 86 | URIS=`echo "$URIS" | grep -v ^@` 87 | set -e 88 | 89 | # If we didn't get any aliases, throw an error and quit. 90 | if [[ -z $URIS ]]; then 91 | echo "No sites found at $DOCROOT." 92 | exit 1 93 | fi 94 | fi 95 | 96 | # Delete all prefixed tables. 97 | for URI in $URIS; do 98 | DESTINATION="$DESTINATION --uri=$URI" 99 | if [ "$CLONE" == "database" ]; then 100 | DATABASE=`$DRUSH $DESTINATION status database_name --format=list` 101 | $DRUSH $DESTINATION sqlq "DROP DATABASE $DATABASE" 102 | else 103 | $DRUSH $DESTINATION --yes drop-prefixed-tables $DB_PREFIX 104 | fi 105 | done; 106 | 107 | # Remove the symlink. 108 | rm $DOCROOT 109 | echo "Removed the $DOCROOT symlink." 110 | # Remove the repository. 111 | rm -rf $ACTUAL_PATH 112 | echo "Removed $ACTUAL_PATH" 113 | -------------------------------------------------------------------------------- /clone_site.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | # The directory this script is in. 5 | REAL_PATH=`readlink -f "${BASH_SOURCE[0]}"` 6 | SCRIPT_DIR=`dirname "$REAL_PATH"` 7 | 8 | usage() { 9 | cat $SCRIPT_DIR/README.md | 10 | # Remove ticks and stars. 11 | sed -e "s/[\`|\*]//g" 12 | } 13 | 14 | # Parse options. 15 | WEBROOT=$WORKSPACE 16 | DRUSH="drush" 17 | WEBGROUP= 18 | VERBOSE= 19 | GHPRID= 20 | EXTRA_SETTINGS= 21 | HARDLINKS= 22 | DEBUG= 23 | CLONE="table" 24 | 25 | while getopts “hH:i:l:d:g:e:cvx” OPTION; do 26 | case $OPTION in 27 | h) 28 | usage 29 | exit 30 | ;; 31 | H) 32 | if [[ ! -d $OPTARG ]]; then 33 | echo "The $OPTARG directory does not exist. Please choose a directory that contains an identical copy of the files directory." 34 | exit 1 35 | fi 36 | HARDLINKS="--link-dest=\"$OPTARG\"" 37 | ;; 38 | i) 39 | GHPRID=$OPTARG 40 | ;; 41 | l) 42 | WEBROOT=$OPTARG 43 | ;; 44 | d) 45 | DRUSH=$OPTARG 46 | ;; 47 | e) 48 | EXTRA_SETTINGS="--extra-settings=\"$OPTARG\"" 49 | ;; 50 | c) 51 | CLONE="database" 52 | ;; 53 | v) 54 | VERBOSE="--verbose" 55 | ;; 56 | x) 57 | set -x 58 | DEBUG="--debug" 59 | ;; 60 | g) 61 | WEBGROUP=$OPTARG 62 | ;; 63 | ?) 64 | usage 65 | exit 66 | ;; 67 | esac 68 | done 69 | 70 | # Remove the switches we parsed above from the arguments. 71 | shift `expr $OPTIND - 1` 72 | 73 | # Now, parse arguments. 74 | SOURCE=$1 75 | URL=${2:-http://default} 76 | 77 | # If we're missing some of these variables, show the usage and throw an error. 78 | if [[ -z $WEBROOT ]]; then 79 | echo "You must specify a webroot." 80 | exit 1 81 | fi 82 | if [[ -z $SOURCE ]]; then 83 | echo "You must specify a source alias." 84 | exit 1 85 | fi 86 | if [[ -z $GHPRID ]]; then 87 | echo "You must specify a github pull request id." 88 | exit 1 89 | fi 90 | 91 | # Put drush in verbose mode, if requested, and include our script dir so we have 92 | # access to our custom drush commands. 93 | DRUSH="$DRUSH $VERBOSE $DEBUG --include=$SCRIPT_DIR" 94 | # The docroot of the new Drupal directory. 95 | DOCROOT=$WEBROOT/$GHPRID 96 | # The base prefix to use for the database tables. 97 | PREFIX="pr_" 98 | # The unique prefix to use for just this pull request. 99 | DB_PREFIX="${PREFIX}${GHPRID}_" 100 | DB_SUFFIX="_${GHPRID}" 101 | # The drush options for the Drupal destination site. Eventually, we could open 102 | # this up to allow users to specify a drush site alias, but for now, we'll just 103 | # manually specify the root and uri options. 104 | DESTINATION="--root=$DOCROOT --uri=$URL" 105 | 106 | # Check to make sure drush is working properly, and can access the source site. 107 | $DRUSH $SOURCE status --quiet 108 | 109 | if [ "$CLONE" == "database" ]; then 110 | # Copy the existing settings.php to the new site, but add a database name suffix. 111 | $DRUSH $DESTINATION --yes $EXTRA_SETTINGS --database clone-settings-php $SOURCE $DB_SUFFIX 112 | 113 | # Copy the database to the new location 114 | $DRUSH $DESTINATION sql-sync --create-db --yes $SOURCE @self 115 | else 116 | # Copy the existing settings.php to the new site, but add a database prefix. 117 | $DRUSH $DESTINATION --yes $EXTRA_SETTINGS clone-settings-php $SOURCE $DB_PREFIX 118 | 119 | # Drop all database tables with this prefix first, in case the environment is 120 | # being rebuilt, and new tables were created in the environment. 121 | $DRUSH $SOURCE --yes drop-prefixed-tables $DB_PREFIX 122 | 123 | # Copy all the database tables, using the new prefix. 124 | $DRUSH $SOURCE --yes clone-db-prefix $DB_PREFIX $PREFIX 125 | fi 126 | 127 | # If we have the registry-rebuild command available, let's go ahead and use it 128 | # first. If modules or classes have changed names or directories, then the 129 | # following drush commands will fail. 130 | if [[ -n `eval $DRUSH $DESTINATION help --pipe | grep registry-rebuild` ]]; then 131 | eval $DRUSH $DESTINATION registry-rebuild 132 | fi 133 | 134 | # Now, rsync the files over. If we have a webgroup, set the sticky bit on the 135 | # directory before rsyncing. We then rsync with --no-p, --no-o, and 136 | # --omit-dir-times, to avoid errors. There are dynamically created directories 137 | # we can exclude as well, such as css, js, styles, etc. 138 | if [[ $WEBGROUP ]]; then 139 | DESTINATION_FILES=`$DRUSH $DESTINATION dd files` 140 | mkdir -p -m 2775 "$DESTINATION_FILES" 141 | chgrp $WEBGROUP $DESTINATION_FILES 142 | fi 143 | $DRUSH $DESTINATION -y rsync $HARDLINKS \ 144 | $SOURCE:%files @self:%files \ 145 | --omit-dir-times --no-p --no-o \ 146 | --exclude-paths="css:js:styles:imagecache:ctools:tmp" 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![jenktocat](https://api.monosnap.com/image/download?id=9PMKonRKS2i1el0vJZ7cuK1oQ) 2 | 3 | ## Overview 4 | Do you build Drupal sites? Do you use GitHub to manage your code for these projects? 5 | Do you use Jenkins to automate continuous integration? Well you should do all of these things! 6 | And if you do, then you should also use Lullabot's Magical Jenkins/Github/Drupal Scripts™. 7 | 8 | These scripts will build your repository's branches so that you can test them in a full 9 | Drupal environment. No more local checkout of a branch, running updates, and synchronizing 10 | your database and files in order to test a branch. Just install the scripts and let the 11 | machine do it for you automatically every time you create a new pull request. 12 | 13 | Brought to you by your friends at Lullabot! 14 | 15 | If you'd like to read more, check out 16 | [this article on Lullabot.com](http://www.lullabot.com/blog/article/github-pull-request-builder-drupal). 17 | 18 | ## Installation 19 | Please see INSTALL.md 20 | 21 | ## Usage 22 | This script should be executed within Jenkins, and will fail otherwise. 23 | 24 | First, call the directory preparer, which moves the pull request to your 25 | webroot, and merges it into master. 26 | 27 | `prepare_dir.sh` `[-himvx]` `` 28 | 29 | Then, call the site cloning script, which uses drush to clone an existing 30 | staging site. 31 | 32 | `clone_site.sh` `[-deghHilvxc]` `` `` 33 | 34 | To clean up afterwards, call cleanup.sh using the same pull request ID and 35 | location of your webroot. 36 | 37 | `cleanup.sh` `[-dhiluvxc]` 38 | 39 | ## What does it do? 40 | - Moves the checked out repository to a unique directory in the workspace. 41 | - Creates a symlink to the docroot of the drupal directory in the webroot. 42 | - Creates a new branch from the pull request and merges that branch to 43 | master. 44 | - Copies the settings.php from an existing site to this new drupal site. 45 | - Clones the database from the source site, prefixing any tables with a 46 | unique identifier to this pull request. 47 | - Rsyncs the files directory from the source site. 48 | 49 | ## Requirements 50 | - Drush 51 | - A web accessible URL for the pull request. The location of the docroot for 52 | this URL should be specified with the -l option. 53 | - An existing Drupal 7 site with a site alias, and empty prefix line in the 54 | database array in settings.php 55 | - A Jenkins job that checks out the Pull Request to 'new_pull_request' directory 56 | inside the job workspace. 57 | 58 | ## Arguments 59 | `` 60 | 61 | Location of the parent directory of the web root this site will be hosted at. 62 | Defaults to the job workspace. Note, the Jenkins user must have write 63 | permissions to this directory. 64 | 65 | `` 66 | 67 | The drush alias to the site whose database and files will be cloned to build 68 | the pull request test environment. 69 | 70 | `` 71 | 72 | The parent URL that the destination site will be visible at. Defaults to 73 | 'http://default'. The domain name the site will be set up at. Note, the site 74 | will be in a subdirectory of this domain using the Pull Request ID, so if the 75 | Pull Request ID is 234, and you pass https://www.example.com/foo, the site 76 | will be at https://www.example.com/foo/234. 77 | 78 | ## Options 79 | * `-c` Optional. Specifies that the full database is cloned instead of using 80 | table prefixes. 81 | * `-e` Optional. Extra settings to be concatenated to the settings.php file 82 | when it is cloned. Only used in clone_site.sh. 83 | * `-g` Optional. The http server's group, such as www-data or apache. This is 84 | only used in clone_site.sh to ensure that the file permissions are set 85 | properly on the Drupal file directory. 86 | * `-h` Show this message 87 | * `-H` The directory to pass to --link-dir during rsync. This is only used in 88 | clone_site.sh to use a shared files directory to create hardlinks from. 89 | This is useful for sites that have large file directories, to avoid 90 | eating up disk space. It is recommended to keep this directory synced 91 | regularly with the stage files dir. See `man rsync` for more details. 92 | * `-i` The Github pull request issue number. 93 | * `-l` The location of the parent directory of the web root. Same as 94 | ``. 95 | * `-m` The branch the pull request should be merged to. Defaults to 'master'. 96 | This is only used with prepare_dir.sh. 97 | * `-d` The path to drush. Defaults to drush. 98 | * `-u` Optional. URIs of the sites to clean up when running cleanup.sh. Useful 99 | when there are symlinks in /sites. 100 | * `-v` Verbose mode, passed to all drush commands. 101 | * `-x` Turn on bash xtrace and drush debug mode. 102 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | ## Jenkins Github Drupal Pull Request Builder.sh 2 | 3 | ### Steps to Install: 4 | 5 | ```bash 6 | cd ~ 7 | git clone git://github.com/Lullabot/jenkins_github_drupal.git 8 | sudo mv jenkins_github_drupal /usr/local/share 9 | ``` 10 | 11 | #### If you'd like easy access to the scripts in this repo, create symlinks. 12 | 13 | ```bash 14 | sudo ln -s /usr/local/share/jenkins_github_drupal/cleanup.sh \ 15 | /usr/local/bin/jgd-cleanup 16 | sudo ln -s /usr/local/share/jenkins_github_drupal/clone_site.sh \ 17 | /usr/local/bin/jgd-clone-site 18 | sudo ln -s /usr/local/share/jenkins_github_drupal/github_comment.sh \ 19 | /usr/local/bin/jgd-github-comment 20 | sudo ln -s /usr/local/share/jenkins_github_drupal/prepare_dir.sh \ 21 | /usr/local/bin/jgd-prepare-dir 22 | ``` 23 | 24 | #### Test out the scripts, by calling each of them with the help option. 25 | 26 | ```bash 27 | jgd-cleanup -h 28 | jgd-clone-site -h 29 | jgd-github-comment -h 30 | jgd-prepare-dir -h 31 | ``` 32 | 33 | #### Install some Jenkins plugins we'll need: 34 | 35 | 1. Log into Jenkins 36 | 1. Go to Manage Jenkins 37 | 1. Click on Manage Plugins 38 | 1. Click on Available Plugins 39 | 1. Install these plugins 40 | * EnvInject Plugin 41 | * Github pull requests builder 42 | 43 | Note: _You must restart Jenkins after installing these plugins, otherwise expect errors._ 44 | 45 | Some nice to have Plugins: 46 | 47 | * AnsiColor plugin, to show Drush colored output for errors and warnings. 48 | * Log Parser plugin, with Drush rules to log errors and warnings in Jenkins based on the same from drush command output. 49 | 50 | After the Log Parser plugin is installed, you'll need to add Drush rules. See https://gist.github.com/4236526 for instructions. 51 | 52 | #### Configure Github pull request builder plugin 53 | 1. Go back to 'Manage Jenkins' and click on 'Configure System' 54 | 1. Down at the bottom under Github pull requests builder, enter the credentials for Github "bot" user, for instance, `serverbots`. 55 | 1. Enter the admin list. 'Admins' are users that can kick off builds of the jobs by typing comments on the Pull Request. Separate each user with spaces. 56 | 1. Click Advanced in that same area if you'd like to change the regex expression to look for to kick off jobs 57 | 58 | #### Setting up the Pull Request environment 59 | You need to have a webroot that Jenkins can write to for this job. This requires an Apache Vhost or Nginx server spec that allows subdirectories, and following symlinks. For example, say you have a staging site at http://stage.example.com. You might want to create another site at http://pull-request.example.com, or http://pr.stage.example.com, etc. You then need a directory that is writable by the Jenkins user that this vhost points to. Note that if you are executing this job on a Jenkins node, the user the command is being executed as may not be jenkins. Adjust the following accordingly. You will also want to make sure that the Jenkins user is in the web group, such as www-data, or apache, or whatever group your web server runs as. 60 | 61 | 1. Add the jenkins user to the www-data group. 62 | * `sudo usermod -a -G jenkins www-data` 63 | 1. Create pull-requests/example.com 64 | * `sudo mkdir -p /var/www/pull-requests/example.com` 65 | 1. Make the Jenkins user the owner of this directory. 66 | * `sudo chown -R jenkins:www-data /var/www/pull-requests` 67 | 1. Optionally, preserve the www-data group on this directory. 68 | * `sudo chmod 2775 /var/www/pull-requests` 69 | 70 | #### Create a new Jenkins job 71 | As always, name the job with underscores, not spaces. For instance: `Stage_Example_Pull_Request_Builder`. I like to prefix all job names consistently, based on Environment and Project, so you can set up groups that match on these names in Jenkins easily. 72 | 73 | Note: _While you are creating and editing the job, it is best to save often, as some of these plugins seem a bit flaky._ 74 | 75 | 1. Choose Build a free-style software project, unless you already have a Pull Request job you can clone. 76 | 1. Under Source Code Management, choose 'Git' 77 | 1. Enter your github repository URL, the URL you use to clone the repo. 78 | 1. Click Advanced underneath this field 79 | 1. Under Refspec, enter `+refs/pull/*:refs/remotes/origin/pr/*` 80 | 1. Branch Specifier should be `${sha1}` 81 | 1. Click Advanced under the branch 82 | 1. Set Local subdirectory for repo (optional) to `new_pull_request` 83 | 1. Scroll down to Build Triggers 84 | 1. Click Github pull requests builder 85 | 1. Under Build Environment, click Inject passwords to the build as environment variables 86 | 1. Click Add 87 | 1. Type `GITHUB_TOKEN` under name 88 | 1. Enter your bot user's github token in password. If you need to create one, do the following, replacing `[github-bot-username]` with your github bot username: 89 | * `curl -u [github-bot-username] https://api.github.com/authorizations -d '{"scopes":["repo"]}'` 90 | 1. Under Build Steps, choose `Execute Shell` 91 | 1. See https://gist.github.com/4236407 for an example script to call. 92 | 1. If you have the Log Parser plugin, under Post-Build-Actions choose `Console output (build log) parsing` 93 | 1. Choose `Drush`, and mark failed on error, unstable on warning. 94 | 95 | #### Build a job to tear down the environment 96 | 97 | Often you'd like to be able to tear down these environments after they are tested and merged. You can create a Jenkins job to handle this for you with minimal effort. 98 | 99 | 1. Create a new job, choosing, Build a free-style software project again. 100 | 1. Name the job example_com_pull_request_tear_down or something like that. Just try to keep it consistent, mkay? ;) 101 | 1. Check the This build is parameterized checkbox. 102 | 1. Click Add Parameter and choose String Parameter. 103 | 1. Under name specify `GHPRID` 104 | 1. Under Description specify The github pull request ID. 105 | 1. Click Add build step under Build. 106 | 1. Choose Execute shell. 107 | 1. In the shell, do something like the following: 108 | * `jgd-cleanup -i "$GHPRID" -l "/srv/www/pull-requests/example.com"` 109 | 1. If you have the Log Parser plugin, under Post-Build-Actions choose `Console output (build log) parsing` 110 | 1. Choose `Drush`, and mark failed on error, unstable on warning. 111 | -------------------------------------------------------------------------------- /jgd.drush.inc: -------------------------------------------------------------------------------- 1 | 'Clones a database to itself, adding a table prefix.', 28 | 'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_DATABASE, 29 | 'arguments' => array( 30 | 'prefix' => dt('The database table prefix to use.'), 31 | 'ignored_prefix' => dt('The prefix of tables to not clone, useful if the database has been cloned previously.'), 32 | ), 33 | 'required-arguments' => 1, 34 | ); 35 | $items['drop-prefixed-tables'] = array( 36 | 'description' => 'Drops all tables in a database matching a specified prefix.', 37 | 'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_DATABASE, 38 | 'arguments' => array( 39 | 'prefix' => dt('The prefix of tables to delete.'), 40 | ), 41 | ); 42 | $items['clone-settings-php'] = array( 43 | 'description' => "Clone a settings.php file to this site.", 44 | 'bootstrap' => DRUSH_BOOTSTRAP_DRUSH, 45 | 'arguments' => array( 46 | 'site_alias' => dt('Drush site alias of the site to clone from.'), 47 | 'prefix / suffix' => dt('An optional table prefix or database name suffix to add to the database array in the settings.php file.'), 48 | ), 49 | 'options' => array( 50 | 'extra-settings' => dt('A string of extra settings to be added to the settings.php file.'), 51 | 'database' => dt('Use a database instead of using tables.'), 52 | ), 53 | 'required-arguments' => 1, 54 | ); 55 | 56 | return $items; 57 | } 58 | 59 | /** 60 | * Command callback for clone-db-prefix. 61 | */ 62 | function drush_jgd_clone_db_prefix($prefix, $ignored_prefix = NULL) { 63 | $dt_args['@prefix'] = $prefix; 64 | 65 | if (!drush_confirm(dt('All database tables prefixed with @prefix will be destroyed and recreated. There is no undo. Are you sure you want to proceed?', $dt_args))) { 66 | return FALSE; 67 | } 68 | 69 | $creds = drush_get_context('DRUSH_DB_CREDENTIALS'); 70 | $db_name = $creds['name']; 71 | 72 | if (!isset($ignored_prefix)) { 73 | $ignored_prefix = $prefix; 74 | } 75 | 76 | $sql = "SHOW TABLES WHERE tables_in_$db_name NOT LIKE :ignored"; 77 | $args = array(':ignored' => db_like($ignored_prefix) . '%'); 78 | $tables = db_query($sql, $args)->fetchCol(); 79 | 80 | if (empty($tables)) { 81 | drush_log(dt('There were no database tables to clone.'), 'error'); 82 | return FALSE; 83 | } 84 | 85 | dlm($tables); 86 | 87 | try { 88 | foreach ($tables as $table) { 89 | $dt_args['@new-table'] = $new_table_name = "$prefix$table"; 90 | $dt_args['@table'] = $table; 91 | 92 | // Drop the existing table, if it's there. 93 | drush_log(dt('Dropping table @new-table.', $dt_args)); 94 | // We can't use db_drop_table, as it may prefix it again. 95 | db_query("DROP TABLE IF EXISTS $new_table_name"); 96 | 97 | // Create the new table. 98 | drush_log(dt('Creating table @new-table from @table.', $dt_args)); 99 | db_query("CREATE TABLE $new_table_name LIKE $table"); 100 | 101 | // Insert all the data into the new table. 102 | drush_log(dt('Copying data from @table to @new-table.', $dt_args)); 103 | db_query("INSERT INTO $new_table_name SELECT * FROM $table"); 104 | 105 | // If we got this far, we succeeded! 106 | $dt_args['@successes']++; 107 | } 108 | } 109 | catch (Exception $e) { 110 | drush_log((string) $e, 'error'); 111 | return FALSE; 112 | } 113 | 114 | drush_log(dt('Cloned @successes database tables, prefixing with @prefix.', $dt_args), 'completed'); 115 | } 116 | 117 | function drush_jgd_drop_prefixed_tables($prefix) { 118 | $dt_args['@prefix'] = $prefix; 119 | 120 | if (!drush_confirm(dt('All database tables prefixed with @prefix will be destroyed. There is no undo. Are you sure you want to proceed?', $dt_args))) { 121 | return FALSE; 122 | } 123 | 124 | if (!$prefix) { 125 | drush_set_error('NO_DB_PREFIX', dt('You must specify a database prefix.')); 126 | return FALSE; 127 | } 128 | 129 | $creds = drush_get_context('DRUSH_DB_CREDENTIALS'); 130 | $db_name = $creds['name']; 131 | 132 | $sql = "SHOW TABLES LIKE :prefix"; 133 | $tables = db_query($sql, array(':prefix' => db_like($prefix) . '%'))->fetchCol(); 134 | 135 | if (empty($tables)) { 136 | drush_log(dt('There were no database tables to remove.'), 'status'); 137 | return; 138 | } 139 | 140 | dlm($tables); 141 | 142 | try { 143 | // We can't use db_drop_table, as it may prefix the table name again. 144 | foreach ($tables as $table) { 145 | db_query("DROP TABLE IF EXISTS $table"); 146 | } 147 | } 148 | catch (Exception $e) { 149 | drush_log((string) $e, 'error'); 150 | return FALSE; 151 | } 152 | 153 | $dt_args['@count'] = count($tables); 154 | drush_log(dt('Deleted @count database tables with prefix @prefix.', $dt_args), 'completed'); 155 | } 156 | 157 | /** 158 | * Command callback for clone-settings-php. 159 | */ 160 | function drush_jgd_clone_settings_php($site_alias, $prefix = NULL) { 161 | // Normalize the alias to have a leading @ symbol. 162 | $dt_args['@alias'] = $site_alias = '@' . ltrim($site_alias, '@'); 163 | $dt_args['@prefix'] = $prefix; 164 | 165 | if (!($source_record = drush_sitealias_get_record($site_alias))) { 166 | drush_log(dt('No @alias alias was found.', $dt_args), 'error'); 167 | return FALSE; 168 | } 169 | dlm($source_record); 170 | 171 | $destination_record = drush_sitealias_get_record('@self'); 172 | dlm($destination_record); 173 | 174 | // Fetch the site's directory. 175 | $results = drush_invoke_process($site_alias, 'dd', array('%site'), array('pipe' => TRUE), array('integrate' => FALSE)); 176 | 177 | // Check to make sure the data we need is in the results. 178 | if (empty($results['output'])) { 179 | drush_log(dt('No source settings.php found from alias @alias.', $dt_args), 'error'); 180 | return FALSE; 181 | } 182 | $dt_args['@source-settings'] 183 | = $source_settings_path 184 | = "{$results['output']}/settings.php"; 185 | 186 | // If the source settings.php file doesn't exist, log an error and exit. 187 | if (!file_exists($source_settings_path)) { 188 | drush_log(dt('No source settings.php found at @source-settings.', $dt_args), 'error'); 189 | return FALSE; 190 | } 191 | 192 | // If we don't have read perms to the file, log an error and exit. 193 | if (!($source_settings = file_get_contents($source_settings_path))) { 194 | drush_log(dt('This command needs read privileges to file @source-settings.', $dt_args), 'error'); 195 | return FALSE; 196 | } 197 | 198 | // If there's a database prefix, attempt to use it. 199 | if (isset($prefix)) { 200 | if (drush_get_option('database')) { 201 | // Match a new line with spaces and a database option. 202 | $pattern = "%(\n\ *)'database' => '([a-zA-Z0-9_]+)',%"; 203 | // Replace with the following. 204 | $replacement = "\${1}'database' => '\${2}$prefix',"; 205 | // Here's the error message if this fails. 206 | $error_message = 'The database name suffix @prefix was not added to settings.php. You will need to do this by hand.'; 207 | } 208 | else { 209 | // Match a new line with spaces and an empty prefix string. 210 | $pattern = "%(\n\ *)'prefix' => '',%"; 211 | // Replace with the following. 212 | $replacement = "\${1}'prefix' => '$prefix',"; 213 | // Here's the error message if this fails. 214 | $error_message = 'The database table prefix @prefix was not added to settings.php. You will need to do this by hand.'; 215 | } 216 | $destination_settings = preg_replace($pattern, $replacement, $source_settings); 217 | // If nothing changed, log a warning. 218 | if ($destination_settings == $source_settings) { 219 | drush_log(dt($error_message), 'warning'); 220 | // Unset the prefix, so the completed message won't say it was added. 221 | unset($prefix); 222 | } 223 | } 224 | // Otherwise, the settings are the same. 225 | else { 226 | $destination_settings = $source_settings; 227 | } 228 | 229 | // Remove the http:// or https:// from the front of the string. 230 | $patterns[] = '%^https?://%i'; 231 | $replacements[] = ''; 232 | // Replace all slashes with dots. 233 | $patterns[] = '%/%'; 234 | $replacements[] = '.'; 235 | $conf_path = preg_replace($patterns, $replacements, $destination_record['uri']); 236 | 237 | // Check for additional settings to concatenate to the string. 238 | $extra_settings = drush_get_option('extra-settings', ''); 239 | 240 | // If this is drupal multisite, ($conf_path is not 'default'), hardcode the 241 | // file directory path into settings.php, so that there's no accidental file 242 | // directory sharing going on. 243 | if ($conf_path !== 'default') { 244 | $extra_settings .= "\n\$conf['file_public_path'] = 'sites/$conf_path/files';"; 245 | } 246 | 247 | // If we've got extra 248 | if (is_string($extra_settings) && !empty($extra_settings)) { 249 | $destination_settings .= "\n// Added by the Github pull request builder\n"; 250 | $destination_settings .= "$extra_settings\n"; 251 | } 252 | 253 | // TODO: add an option here for a settings destination. 254 | $dt_args['@dir-destination'] 255 | = $destination_settings_dir 256 | = "{$destination_record['root']}/sites/$conf_path"; 257 | $dt_args['@destination'] 258 | = $destination_settings_path 259 | = "$destination_settings_dir/settings.php"; 260 | 261 | // Log a warning and confirm continuation if the settings.php file exists. 262 | if (file_exists($destination_settings_path)) { 263 | $message = 'The destination settings.php file already exists at @destination.'; 264 | drush_log(dt($message, $dt_args), 'warning'); 265 | if (!drush_confirm('Would you like to continue?')) { 266 | return FALSE; 267 | } 268 | } 269 | 270 | // Create the directory if necessary. 271 | if (!file_exists($destination_settings_dir)) { 272 | if (!drush_mkdir($destination_settings_dir)) { 273 | drush_log(dt('Unable to create directory @dir-destination', $dt_args), 'error'); 274 | return FALSE; 275 | } 276 | drush_log(dt('Created directory @dir-destination.', $dt_args), 'success'); 277 | } 278 | 279 | // If we can't create the settings.php file, log an error and quit. 280 | if (!file_put_contents($destination_settings_path, $destination_settings)) { 281 | drush_log(dt('Unable to create file @destination.', $dt_args), 'error'); 282 | return FALSE; 283 | } 284 | 285 | // Winning! 286 | $message = !isset($prefix) ? 287 | 'Settings from @alias saved to @destination.' : 288 | 'Settings from @alias saved to @destination with database table prefix @prefix.'; 289 | drush_log(dt($message, $dt_args), 'completed'); 290 | // If this is in pipe mode, print the conf directory. 291 | drush_print_pipe($destination_settings_dir); 292 | } 293 | --------------------------------------------------------------------------------