Our configuration management discipline paid off last week, when we rolled out lots of bugfixes onto our production server. I used my deployment script to test the release-1 branch against our regression tests and copy the source code to the production server after it passed. A quick database update later, and everything worked without a hitch.
Why did it work? Here's what's behind our build system:
I think it's pretty darn good. What would make it even awesomer?
So now that we're doing all our configuration changes in source code, it makes sense to automate database updates as much as I can. Here's something I've added to drush_tools so that I can run all the schema changes from the command-line:
function drush_tools_update($command = '') { ob_start(); require_once 'includes/install.inc'; require_once 'update.php'; $ret = ob_get_contents(); drupal_load_updates(); ob_end_clean(); $list = module_list(); $update_list = array(); foreach ($list as $module) { $updates = drupal_get_schema_versions($module); if ($updates !== FALSE) { $latest = 0; $base = drupal_get_installed_schema_version($module); foreach ($updates as $update) { if ($update > $base) { if ($update > $latest) { $latest = $update; } $update_list[$module][] = $update; } } if ($latest) { sort($update_list[$module]); printf("%-30s %5d -> %5d (%s)\n", $module, $base, $latest, join(', ', $update_list[$module])); } else { printf("%-30s %5d\n", $module, $base); } } } if (count($update_list) == 0) return; if ($command != 'force' && !drush_confirm(t('Do you really want to continue?'))) { drush_die('Aborting.'); } ob_start(); foreach ($update_list as $module => $versions) { foreach ($versions as $v) { print "Running " . $module . "_update_" . $v . "\n"; update_data($module, $v); } } $updates = ob_get_contents(); cache_clear_all('*', 'cache', TRUE); cache_clear_all('*', 'cache_page', TRUE); cache_clear_all('*', 'cache_menu', TRUE); cache_clear_all('*', 'cache_filter', TRUE); drupal_clear_css_cache(); ob_end_clean(); $output = ''; if (!empty($_SESSION['update_results'])) { $output .= "The following queries were executed:\n"; foreach ($_SESSION['update_results'] as $module => $updates) { $output .= "\n" . $module . "\n--------------------------\n"; foreach ($updates as $number => $queries) { $output .= 'Update #'. $number . ":\n"; foreach ($queries as $query) { if ($query['success']) { $output .= "SUCCESS: " . $query['query'] . "\n"; } else { $output .= "FAILURE: " . $query['query'] . "\n"; } } if (!count($queries)) { $output .= "No queries\n"; } } } $output .= "\n"; print $output; unset($_SESSION['update_results']); } }To keep track of the bugfixes we'll need to make for our next release, I've created a Subversion branch called branches/release-1. Development of new features will continue on trunk, but we'll merge in the bugfixes from release-1 every so often.
There are three environments we deploy to:
Editorial changes happen on the production server, where our users update content. We would like to be able to take a snapshot of that database and use that to test our development code on the QA server or in our local development environments. Because we use Domain Access to serve multiple subdomains with shared content, it's not just a matter of using mysqldump to back up the database and copy it over. We also need to replace URLs inside the database, and we need to override domain_root using the $conf array in settings.php.
I'm the only one running Linux, so the other developers don't really benefit from the Makefiles I've defined or the tools I use. For the simpler build system we had before (all development on trunk), I wrote a deployment script that allowed users to:
The new deployment script needed to allow users to do the same, but for both branches of the code. Both branches of the code would be simultaneously available on the QA server, so the script would need to deploy the code to different directories.
After some fiddling around with the page design (because I care about making interfaces make sense!), I came up with something that looks like this:
Development Release-1 Local Database QA Database QA Deployment 988987985984983982981980979978977976975974973972971970969968 986963985984983982981980979978977976975974973972971970969968967 Production Deployment None 986963985984983982981980979978977976975974973972971970969968967 Changelog ------- r988 | somegeek | 2008-09-15 13:00:00 -0500 (Mon, 15 Sep 2008) | 2 lines Use dev.qa.example.com ------------ [more changelog entries go here] ------------------------------------------------------------------------ r986 | somegeek | 2008-09-15 11:03:38 -0500 (Mon, 15 Sep 2008) | 3 lines Starting a branch for release-1 ------------------------------------------------------------------------ [more changelog entries go here]The deployment script allows the user to get a copy of the database, deploy a copy of the database, or deploy specific revisions of branches.
Because I was having a hard time figuring out how to do ssh key-based operations from Apache (which runs as a no-login user), I use two shell scripts to do the dirty work. One shell script connects to the production server, creates a partial backup, copies the information over, and does any necessary replacements. Another shell script takes a domain name and optionally a revision, and deploys the revision from the appropriate branch.
Here's my totally small-scale PHP way to show the revisions log:
$dev_output = shell_exec("svn log $dev_url $details --limit 20"); $dev_revisions = preg_match_all('/r([0-9]+)/', $dev_output, $dev_matches);where $dev_url is the URL of the trunk in Subversion, and $details contains the username and password specified as options for the Subversion command-line.
I'm going to see if I can get my regression tests running on the server that I've got my deployment script on. Wouldn't that be awesome?
Hooray! We have running code, and we're about to make another release after our code exits quality assurance. This means, of course, that we'll need some way to differentiate the inevitable bugfixes that the next production release will require, and development of new features.
What's the best way to do this? Making a release branch seems like a good idea. Here's how I did it:
svn copy http://subversion.example.com/myproject/trunk http://subversion.example.com/myproject/branches/release-1Bugfixes for release-1 will be committed to the release-1 branch, while new development continues on trunk. Bugfixes will be periodically merged into trunk to make it easier to roll the next release, which will be the release-2 branch.
Next, I need to configure my local system so that it's easy to switch back and forth. I could work with a single source tree for local.example.com, but that means switching back and forth. The best thing to do would be to have two separate source code directories: one for trunk, and one for production releases.
svn co http://subversion.example.com/myproject/branches/release-1 /var/www/example.com-prodFor each site, I'll need
I'll also need to update my Makefile to make it easier to work. For example, the Makefile should connect me to the right database depending on which branch I'm on. How do I determine this? One way is to have an unversioned file that overrides some of the Makefile variables, and to include that file in my Makefile. I can do this by adding the following to my Makefile:
-include *.mkand then creating a dev-local.mk file that changes the values of my variables.
I'll need copies of the production database translated for the different domains, which means I need to update my deploy script and format it a little to make it easier to deal with all these options. Hmm…
This will be fun.
UPDATE: Fixed HTML tags. Thanks for pointing it out!
To make configuration management easier, we decided to make sure that all behavior-related changes are in the source code repository. So when I needed to add the reCAPTCHA module to the project, I needed to figure out how to programmatically install and enable the module with update code in another module's .install file.
Here is some sample code to do so:
/** * Install and enable the captcha module. */ function yourmodule_update_1() { $ret = array(); include_once('includes/install.inc'); module_rebuild_cache(); drupal_install_modules(array('recaptcha')); variable_set('recaptcha_public_key', 'PUBLIC KEY GOES HERE'); variable_set('recaptcha_private_key', 'SECRET KEY GOES HERE'); $ret[] = array( 'success' => true, 'query' => 'Installed recaptcha module and enabled it', ); return $ret; }I've written about using drush to evaluate PHP statements in the Drupal context using the command line before, and it turns out that Drush is also quite useful for running Simpletest scripts. Drush comes with a module that allows you to display all the available tests with "drush test list", run all the tests with "drush test run", or run specified tests with "drush test run test1,test2".
'Course, I wanted to run groups of tests and tests matching regular expressions, so I defined two new commands:
Here's the patch to make it happen:
Index: drush_simpletest.module =================================================================== --- drush_simpletest.module (revision 884) +++ drush_simpletest.module (working copy) @@ -12,9 +12,13 @@ function drush_simpletest_help($section) { switch ($section) { case 'drush:test run': - return t("Usage drush [options] test run.\n\nRun the specified specified unit tests. If is omitted, all tests are run. should be a list of classes separated by a comma. For example: PageCreationTest,PageViewTest."); + return t("Usage drush [options] test run .\n\nRun the specified unit tests. If is omitted, all tests are run. should be a list of classes separated by a comma. For example: PageCreationTest,PageViewTest."); case 'drush:test list': return t("Usage drush [options] test list.\n\nList the available tests. Use drush test run command to run them. "); + case 'drush:test group': + return t("Usage drush [options] test group .\n\nRun all unit tests in the specified groups. For example: drush test group Group1,Group2"); + case 'drush:test re': + return t("Usage drush [options] test re .\n\nRun all unit tests matching this regular expression. For example: drush test re Page.*"); } } @@ -30,10 +34,18 @@ 'callback' => 'drush_test_list', 'description' => 'List the available Simpletest test classes.', ); + $items['test re'] = array( + 'callback' => 'drush_test_re', + 'description' => 'Run one or more Simpletest tests based on regular expressions.', + ); + $items['test group'] = array( + 'callback' => 'drush_test_group', + 'description' => 'Run one or more Simpletest test groups.', + ); return $items; } -function drush_test_list() { +function drush_test_get_list() { simpletest_load(); // TODO: Refactor simpletest.module so we don't copy code from DrupalUnitTests $files = array(); @@ -60,6 +72,11 @@ $rows[] = array($class, $info['name'], truncate_utf8($info['desc'], 30, TRUE, TRUE)); } } + return $rows; +} + +function drush_test_list() { + $rows = drush_test_get_list(); return drush_print_table($rows, 0, TRUE); } @@ -75,3 +92,31 @@ } return $result; } + +function drush_test_re($expression) { + if (!$expression) { + die('You must specify a regular expression.'); + } + $rows = drush_test_get_list(); + $tests = array(); + foreach ($rows as $row) { + if (ereg($expression, $row[0])) { + $tests[] = $row[0]; + } + } + simpletest_run_tests($tests, 'text'); + return $result; +} + +function drush_test_group($groups) { + $rows = drush_test_get_list(); + $tests = array(); + $groups = explode(',', $groups); + foreach ($rows as $row) { + if (in_array($row[1], $groups)) { + $tests[] = $row[0]; + } + } + simpletest_run_tests($tests, 'text'); + return $result; +}That makes running tests so much easier and more fun!
Building on the configuration management strategy I described last time, I wrote some scripts to make it easier for other developers to migrate the production database to the QA server or to get a copy of the production database for their local system. I needed to consider the following factors:
Here's how I did it:
$conf allows you to override Drupal variables returned by variable_get.
So now, I can click on a button to migrate a sanitized copy of the production database to the QA server or to my local system. Sweet!
Drupal's coding conventions make it easier to hook into system behavior, but they also result in a lot of repetitive typing. For example, you can run code when upgrading a module by putting the code in a function named modulename_update_N() in your module's install file. I found myself scrolling up and copy-pasting stuff too many times, so I decided to automate it instead.
I've been using yasnippet for my Emacs templates. All I needed to do to automate that little update bit was to write some code that figured out what the next update number should be. Here's the snippet file I've just added (~/elisp/snippets/php-mode/drupal-mode/_update):
function `(sacha/drupal-module-name)`_update_`(sacha/drupal-module-update-number)`() { $ret = array(); $0 $ret[] = array( 'success' => true, 'query' => '$1', ); return $ret; }The relevant functions from my ~/.emacs:
(defun sacha/drupal-module-update-number () "Return the number of the next module update function. This is one more than the highest number used so far. This function should be called in a module's .install file." (save-excursion (save-restriction (widen) (goto-char (point-min)) (let ((module-name (sacha/drupal-module-name)) (max 0)) (while (re-search-forward (concat "function[ \t\n]+" module-name "_update_\\([0-9]+\\)") nil t) (setq max (max (string-to-number (match-string 1)) max))) (number-to-string (1+ max)))))) (defun sacha/drupal-module-name () "Return the Drupal module name for .module and .install files." (file-name-sans-extension (file-name-nondirectory (buffer-file-name))))I can't think of how I'd do that in Eclipse. =) Don't get me wrong–I still like Eclipse–but I heart being able to hack my editor on the fly.
I often find myself needing to variable_set something temporarily, just to try things out. The drush module provides a command-line interface that solves this problem with a little more hacking. To minimize the effect on my source tree, I've unpacked it into the sites directory for my local installation and enabled it in my test database. After I enabled the main drush module and the related modules, I tweaked drush_tools to include an insecure-but-useful eval command. Here it is:
--- sites/local.example.com/modules/drush/drush_tools.module.orig 2008-08-05 17:18:48.000000000 -0400 +++ sites/local.example.com/modules/drush/drush_tools.module 2008-08-05 17:18:55.000000000 -0400 @@ -46,6 +46,10 @@ 'callback' => 'drush_tools_sync', 'description' => 'Rsync the Drupal tree to/from another server using ssh' ); + $items['eval'] = array( + 'callback' => 'drush_tools_eval', + 'description' => 'Evaluate a command', + ); return $items; } @@ -156,3 +160,6 @@ } } +function drush_tools_eval($command) { + eval($command); +}I also added an alias to my ~/.bashrc along the lines of:
alias drush='php ~/drupal/sites/local.example.com/modules/drush/drush.php -r ~/drupal -l http://local.example.com'where ~/drupal is my multisite Drupal directory root.
After I loaded the alias with "source ~/.bashrc", I can now execute PHP statements in my Drupal context with commands like:
drush eval "variable_set('hello', 'world');"Good stuff!