Completed
Push — master ( 883cd2...273a99 )
by Thomas
16:42
created

deployotron.actions.inc (1 issue)

Severity
1
<?php
2
/**
3
 * @file
4
 * Actions for Deployotron.
5
 */
6
7
namespace Deployotron {
8
9 10
  define('COLOR_RED', "\033[1;31;40m\033[1m");
10 10
  define('COLOR_YELLOW', "\033[1;33;40m\033[1m");
11 10
  define('COLOR_GREEN', "\033[1;32;40m\033[1m");
12 10
  define('COLOR_RESET', "\033[0m");
13
14
  /**
15
   * Creates actions to run.
16
   */
17
  class ActionFactory {
18
    static protected $actionMapping = array(
19
      'deploy' => array(
20
        'SanityCheck',
21
        'SiteOffline',
22
        'BackupDatabase',
23
        'DeployCode',
24
        'CreateVersionTxt',
25
        'UpdateDatabase',
26
        'ClearCache',
27
        'SiteOnline',
28
        'PurgeDatabaseBackups',
29
        'FlowdockNotificaton',
30
        'NewRelicNotificaton',
31
      ),
32
      'omg' => array(
33
        'SiteOffline',
34
        'OMGPrepare',
35
        'DeployCode',
36
        'CreateVersionTxt',
37
        'RestoreDatabase',
38
        'SiteOnline',
39
        'ClearCache',
40
      ),
41
    );
42
43
    /**
44
     * Gathers an array of actions to run.
45
     */
46 10
    static public function getActions($name, $site = NULL) {
47 10
      $actions = array();
48 10
      if (isset(static::$actionMapping[$name])) {
49 10
        foreach (static::$actionMapping[$name] as $class_name) {
50 10
          $class_name = '\\Deployotron\\Actions\\' . $class_name;
51 10
          $action = new $class_name($site);
52 10
          $actions = array_merge($actions,
53 10
                                 static::getActionCommands('pre', $action, $site),
54 10
                                 array($action),
55 10
                                 static::getActionCommands('post', $action, $site));
56 10
        }
57 10
        return $actions;
58
      }
59
    }
60
61
    /**
62
     * Gets pre/post commands for an action.
63
     */
64 10
    static protected function getActionCommands($type, $action, $site) {
65 10
      $actions = array();
66 10
      $switch = $type . '-' . $action->getSwitchSuffix();
67 10
      if ($action->enabled() && !drush_get_option('no-' . $switch, FALSE)) {
68
        // @todo drush_get_option_list() doesn't play well with commas.
69 10
        if ($commands = drush_get_option_list($switch, FALSE)) {
70 10
          foreach ($commands as $command) {
71 10
            if (!empty($command)) {
72 1
              $actions[] = new Actions\CommandAction($site, $command);
73 1
            }
74 10
          }
75 10
        }
76 10
      }
77
78 10
      return $actions;
79
    }
80
81
    /**
82
     * Output help on actions and options.
83
     */
84 1
    static public function getHelp() {
85 1
      $all_actions = array();
86 1
      drush_print('Commands');
87 1
      drush_print('--------');
88 1
      foreach (static::$actionMapping as $name => $actions) {
89 1
        drush_print(wordwrap(dt('@name runs the actions: @actions', array(
90 1
                '@name' => $name,
91 1
                '@actions' => implode(', ', $actions),
92 1
              ))));
93 1
        drush_print();
94
95 1
        $all_actions = array_merge($all_actions, $actions);
96 1
      }
97 1
      $all_actions = array_unique($all_actions);
98 1
      sort($all_actions);
99
100 1
      drush_print('Actions');
101 1
      drush_print('-------');
102 1
      foreach ($all_actions as $class_name) {
103 1
        drush_print($class_name . ':');
104 1
        $class_name = '\\Deployotron\\Actions\\' . $class_name;
105 1
        $action = new $class_name('@self');
106 1
        drush_print($action->getHelp());
107 1
        drush_print();
108
109
        // Print options.
110 1
        $options = $action->getOptions();
111 1
        if ($options) {
112 1
          drush_print_table(drush_format_help_section($options, 'options'));
113 1
          drush_print();
114 1
        }
115 1
        drush_print();
116 1
      }
117
118 1
    }
119
  }
120
121
  /**
122
   * Base class for actions.
123
   */
124
  abstract class Action {
125
    /**
126
     * Set a default short description.
127
     */
128
    protected $short = "incompletely implemented";
129
130
    /**
131
     * Default run message.
132
     */
133
    protected $runMessage = "Running action.";
134
135
    /**
136
     * Options for this action.
137
     */
138
    protected $options = array();
139
140
    /**
141
     * The site this action works on.
142
     */
143
    protected $site;
144
145
    /**
146
     * Result object of last drush command.
147
     */
148
    protected $drushResult;
149
150
    /**
151
     * Array of the output of the last executed command.
152
     */
153
    protected $shOutput;
154
155
    /**
156
     * Return code of the last executed command.
157
     */
158
    protected $shRc;
159
160
    /**
161
     * Shas of checkouts of aliases.
162
     */
163
    static protected $headShas = array();
164
165
    /**
166
     * Construct a new Action object.
167
     */
168 11
    public function __construct($site) {
169 11
      $this->site = $site;
170
171 11
      if ($this->short == "incompletely implemented") {
172 1
        drush_log(dt('Incomplete action, missing short description: @action', array('@action' => get_class($this))), 'warning');
173 1
      }
174
175 11
      if (!isset($this->runMessage)) {
176
        $this->runMessage = 'Running ' . $this->short;
177
      }
178 11
    }
179
180
    /**
181
     * Get help description.
182
     */
183 1
    public function getHelp() {
184 1
      if (isset($this->help)) {
185 1
        return $this->help;
186
      }
187 1
      if (isset($this->description)) {
188 1
        return $this->description;
189
      }
190 1
      return "No description.";
191
    }
192
193
    /**
194
     * Get the command line options for this action.
195
     *
196
     * If the command has an enable-switch, the kill-switch and command options
197
     * are classed as sub-options for nice help output. If there's no
198
     * enable-switch, all options are considered regular options.
199
     *
200
     * @return array
201
     *   Options and sub-options as defined on commands in hook_drush_command().
202
     */
203 10
    public function getOptions() {
204 10
      $options = array();
205 10
      if (isset($this->killSwitch)) {
206
        $options += array(
207 10
          $this->killSwitch => "Don't " . $this->short . (isset($this->enableSwitch) ? ' (overrides --' . $this->enableSwitch . ')' : '') . '.',
208
        );
209 10
      }
210
211 10
      $options += $this->options;
212
213
      $prest_options = array(
214
        'pre/post-*' => array(
215 10
          'pre-' . $this->getSwitchSuffix() => "Before " . $this->short . ".",
216 10
          'post-' . $this->getSwitchSuffix() => "After " . $this->short . ".",
217 10
        ),
218 10
      );
219
220 10
      if (isset($this->enableSwitch)) {
221 10
        $sub_options = $options;
222 10
        $options = array();
223
        // If the action options defines the enable switch, use it so
224
        // description and possible example-values propergate.
225 10
        if (isset($sub_options[$this->enableSwitch])) {
226 10
          $options[$this->enableSwitch] = $sub_options[$this->enableSwitch];
227 10
          unset($sub_options[$this->enableSwitch]);
228 10
        }
229
        else {
230
          $options[$this->enableSwitch] = ucfirst($this->short) . '.';
231
        }
232 10
        $options['pre/post-*'] = "Commands to run before/after action.";
233
        return array(
234 10
          'options' => $options,
235 10
          'sub-options' => (!empty($sub_options) ? array($this->enableSwitch => $sub_options) : array()) + $prest_options,
236 10
        );
237
      }
238
239 10
      $options['pre/post-*'] = "Commands to run before/after action.";
240 10
      return array('options' => $options, 'sub-options' => $prest_options);
241
    }
242
243
    /**
244
     * Get the switch suffix for --pre-/--post- switches.
245
     */
246 10
    public function getSwitchSuffix() {
247 10
      if (isset($this->switchSuffix)) {
248 10
        return $this->switchSuffix;
249
      }
250 10
      elseif (isset($this->enableSwitch)) {
251
        return $this->enableSwitch;
252
      }
253 10
      elseif (isset($this->killSwitch)) {
254 10
        return preg_replace('/^no-/', '', $this->killSwitch);
255
      }
256
      else {
257 10
        return preg_replace(array('/[^a-z0-9 ]/', '/ /'), array('', '-'), strtolower($this->short));
258
      }
259
    }
260
261
    /**
262
     * Get the short description of this action.
263
     *
264
     * If the action has a killswitch, it should work with "Don't " prepended.
265
     *
266
     * @return string
267
     *   Human readable short description of action taken.
268
     */
269 8
    public function getShort() {
270 8
      return $this->short;
271
    }
272
273
    /**
274
     * Return the message to print when running this action.
275
     *
276
     * @return string
277
     *   The Message (by Grandmaster Flash).
278
     */
279 8
    public function getRunMessage() {
280 8
      return $this->runMessage;
281
    }
282
283
    /**
284
     * Get a description of what the action would do.
285
     *
286
     * If the action subclass defines the $description property, use that, else
287
     * returns a generic message.
288
     *
289
     * If more logic than a static string is needed, subclassesare encouraged to
290
     * override this to provide a more specific description of the effect of the
291
     * action.
292
     *
293
     * Used when the user provides the --confirm switch.
294
     *
295
     * @return string
296
     *   Description of the action.
297
     */
298 10
    public function getDescription() {
299 10
      if (isset($this->description)) {
300 9
        return $this->description;
301
      }
302 1
      return dt("Run the @short action.", array('@short' => $this->short));
303
    }
304
305
    /**
306
     * Validate that the action can be run.
307
     *
308
     * @return bool
309
     *   Whether it can.
310
     */
311 9
    public function validate() {
312 9
      return TRUE;
313
    }
314
315
    /**
316
     * Run the task.
317
     *
318
     * @param array $state
319
     *   Persistent state array that actions can use to communicate with
320
     *   following actions.
321
     *
322
     * @return bool
323
     *   Success or failure.
324
     */
325 1
    public function run(ArrayObject $state) {
0 ignored issues
show
Expected type hint "array"; found "ArrayObject" for $state
Loading history...
326 1
      return FALSE;
327
    }
328
329
    /**
330
     * Roll back the task.
331
     */
332 1
    public function rollback() {
333 1
      return 'no rollback';
334
    }
335
336
    /**
337
     * Whether this action is enabled.
338
     */
339 11
    public function enabled() {
340
      // Incomplete actions are always disabled.
341 11
      if ($this->short == "incompletely implemented") {
342 1
        return FALSE;
343
      }
344
      // Disable if there is a killswitch and it is set.
345 10
      if (isset($this->killSwitch) && drush_get_option($this->killSwitch, FALSE)) {
346 2
        return FALSE;
347
      }
348
      // If there is an enable switch, let that decide.
349 10
      if (isset($this->enableSwitch)) {
350 10
        return drush_get_option($this->enableSwitch, FALSE);
351
      }
352
      // Else default to enabled.
353 10
      return TRUE;
354
    }
355
356
    /**
357
     * Execute a sh command.
358
     *
359
     * Is run on the site.
360
     *
361
     * @param string $command
362
     *   Command to run.
363
     *
364
     * @return bool
365
     *   Whether the command succeeded.
366
     */
367 9
    protected function sh($command) {
368 9
      $exec = $command;
369
      // Check that there is a remote host before trying to construct a remote
370
      // command.
371 9
      $host = drush_remote_host($this->site);
372 9
      if (!empty($host)) {
373
        $exec = drush_shell_proc_build($this->site, $command, TRUE);
374
      }
375
      else {
376
        // Else just cd to the root of the alias. This allows us to test the
377
        // code without a remote host.
378 9
        $exec = "cd " . drush_escapeshellarg($this->site['root']) . " && " . $exec;
379
      }
380 9
      return $this->shLocal($exec);
381
    }
382
383
    /**
384
     * Get the output of the most recent command.
385
     *
386
     * @return string
387
     *   The output.
388
     */
389 9
    protected function shOutput() {
390 9
      return implode("\n", $this->shOutputArray());
391
    }
392
393
    /**
394
     * Get the output of the most recent command as an array.
395
     *
396
     * @return array
397
     *   Lines of output.
398
     */
399 9
    protected function shOutputArray() {
400 9
      return $this->shOutput;
401
    }
402
403
    /**
404
     * Get the return code of the most recent command.
405
     */
406 1
    protected function shRc() {
407 1
      return $this->shRc;
408
    }
409
410
    /**
411
     * Execute a sh locally.
412
     *
413
     * Works much like drush_shell_exec, however it captures the command return
414
     * code too, and doesn't support interactive mode.
415
     *
416
     * @param string $command
417
     *   Command to run.
418
     *
419
     * @return bool
420
     *   Whether the command succeeded.
421
     */
422 9
    protected function shLocal($command) {
423 9
      $this->shOutput = array();
424 9
      $this->shRc = 0;
425 9
      $args = func_get_args();
426
      // Escape args, but not the command.
427 9
      for ($x = 1; $x < count($args); $x++) {
428
        $args[$x] = drush_escapeshellarg($args[$x]);
429
      }
430
      // Mimic drush_shell_exec(), which can take a single or multiple args.
431 9
      if (count($args) == 1) {
432 9
        $command = $args[0];
433 9
      }
434
      else {
435
        $command = call_user_func_array('sprintf', $args);
436
      }
437
438 9
      if (drush_get_context('DRUSH_VERBOSE') || drush_get_context('DRUSH_SIMULATE')) {
439
        drush_print('Executing: ' . $command, 0, STDERR);
440
      }
441 9
      if (!drush_get_context('DRUSH_SIMULATE')) {
442 9
        exec($command . ' 2>&1', $this->shOutput, $this->shRc);
443
444 9
        if (drush_get_context('DRUSH_DEBUG')) {
445
          foreach ($this->shOutput as $line) {
446
            drush_print($line, 2);
447
          }
448
        }
449 9
      }
450
451
      // Exit code 0 means success.
452 9
      return ($this->shRc == 0);
453
    }
454
455
    /**
456
     * Execute a drush command.
457
     *
458
     * Is run on the site.
459
     *
460
     * @param string $command
461
     *   Command to run.
462
     * @param string $args
463
     *   Command arguments.
464
     * @param string $options
465
     *   Command arguments.
466
     *
467
     * @return bool
468
     *   Whether the command succeeded.
469
     */
470 7
    protected function drush($command, $args = array(), $options = array()) {
471 7
      $this->drushResult = drush_invoke_process($this->site, $command, $args, $options, TRUE);
472 7
      if (!$this->drushResult || $this->drushResult['error_status'] != 0) {
473
        return FALSE;
474
      }
475 7
      return TRUE;
476
    }
477
478
    /**
479
     * Get the SHA to deploy.
480
     *
481
     * This is based on options from configuration or command.
482
     *
483
     * @return string
484
     *   The SHA to deploy.
485
     */
486 9
    protected function pickSha($options = array()) {
487
      $options = array(
488 9
        'branch' => drush_get_option('branch', NULL),
489 9
        'tag' => drush_get_option('tag', NULL),
490 9
        'sha' => drush_get_option('sha', NULL),
491 9
      );
492
493 9
      $branch = $options['branch'];
494 9
      $tag = $options['tag'];
495 9
      $sha = $options['sha'];
496
497 9
      $posibilities = array_filter(array($branch, $tag, $sha));
498 9
      if (count($posibilities) > 1) {
499 2
        drush_log(dt('More than one of branch/tag/sha specified, using @selected.', array('@selected' => !empty($sha) ? 'sha' : 'tag')), 'warning');
500 2
      }
501 9
      elseif (count($posibilities) < 1) {
502 1
        return drush_set_error(dt('You must provide at least one of --branch, --tag or --sha.'));
503
      }
504
505
      // Use rev-list to ensure we get a full SHA rather than branch/tag/partial
506
      // SHA. Ensures the existence of the branch/tag/SHA, and transforms
507
      // annotated tag SHAs to the commit they're based on (so the repo will
508
      // actually have the SHA we asked for checked out).
509 9
      if ($this->shLocal('git rev-list -1 ' . end($posibilities) . ' --')) {
510 9
        $sha = trim($this->shOutput());
511 9
      }
512
      else {
513 1
        if (!empty($sha)) {
514 1
          return drush_set_error(dt('Unknown SHA.'));
515
        }
516
        else {
517 1
          return drush_set_error(dt('Error finding SHA for ' . (!empty($branch) ? 'branch' : 'tag') . '.'));
518
        }
519
      }
520
521 9
      return $sha;
522
    }
523
524
    /**
525
     * Get the current git head revision.
526
     *
527
     * @param bool $no_cache
528
     *   Dont't use cache, but fetch anyway.
529
     */
530 9
    protected function getHead($no_cache = FALSE) {
531 9
      $name = $this->site['#name'];
532 9
      if (!isset(self::$headShas[$name]) || $no_cache) {
533 9
        self::$headShas[$name] = '';
534 9
        if ($this->sh('git rev-parse HEAD')) {
535 9
          $sha = trim($this->shOutput());
536
          // Confirm that we can see the HEAD locally.
537 9
          if ($this->shLocal('git show ' . $sha)) {
538 9
            self::$headShas[$name] = $sha;
539 9
          }
540
          else {
541
            drush_log(dt("Could not find the deployed HEAD (@sha) locally, you pulled recently?", array('@sha' => $sha)), 'warning');
542
          }
543 9
        }
544 9
      }
545 9
      return self::$headShas[$name];
546
    }
547
  }
548
}
549
550
/**
551
 * Use a namespace to keep actions together.
552
 */
553
namespace Deployotron\Actions {
554
  use Deployotron\Action;
555
556
  /**
557
   * Generic command action.
558
   *
559
   * Automatically instantiated from pre/post-* options.
560
   */
561
  class CommandAction extends Action {
562
    /**
563
     * Create action.
564
     */
565 1
    public function __construct($site, $command) {
566 1
      $this->description = 'Run command: ' . $command;
567 1
      $this->runMessage = 'Running command: ' . $command;
568 1
      $this->short = 'run ' . $command;
569
570 1
      $this->command = $command;
571 1
      parent::__construct($site);
572 1
    }
573
574
    /**
575
     * {@inheritdoc}
576
     */
577 1
    public function run(ArrayObject $state) {
578 1
      $success = $this->sh($this->command);
579 1
      drush_print($this->shOutput());
580 1
      if (!$success) {
581
        return drush_set_error(dt('Error running command "@command"', array('@command' => $this->command)));
582
      }
583 1
      return TRUE;
584
    }
585
  }
586
587
  /**
588
   * Sanity check.
589
   *
590
   * Check for locally modified files and do a dry-run checkout.
591
   */
592
  class SanityCheck extends Action {
593
    protected $runMessage = 'Fetching and sanity checking';
594
    protected $short = 'sanity check';
595
    protected $help = 'Fetches the new code and checks that the repository is clean.';
596
    protected $options = array(
597
      'insanity' => "Don't check that the server repository is clean. Might cause data loss.",
598
    );
599
600
    /**
601
     * {@inheritdoc}
602
     */
603 9
    public function getDescription() {
604 9
      if (drush_get_option('insanity', FALSE)) {
605
        return COLOR_YELLOW . dt('Skipping sanity check of deployed repo. Might cause data loss or Git failures.') . COLOR_RESET;
606
      }
607
608 9
      return dt('Run git fetch and check that the deployed git repository is clean.');
609
    }
610
611
    /**
612
     * {@inheritdoc}
613
     */
614 9
    public function validate() {
615 9
      if ($this->sha = $this->pickSha()) {
616 9
        return TRUE;
617
      }
618 1
      return FALSE;
619
    }
620
621
    /**
622
     * {@inheritdoc}
623
     */
624 8
    public function run(ArrayObject $state) {
625
      // Fetch the latest changes from upstream.
626 8
      if (!$this->sh('git fetch')) {
627
        $message = $this->shOutput();
628
        if (preg_match('/Permission denied \(publickey\)/', $message)) {
629
          drush_log(dt('Access denied to repository URL.'), 'error');
630
          drush_log(dt('Ensure that either that the user on the server has access to the repository, or use "ForwardAgent yes" in .ssh/config.'), 'error');
631
        }
632
        else {
633
          drush_print($message);
634
        }
635
        return drush_set_error(dt("Could not fetch from remote repository."));
636
      }
637
638
      // Check that we can see the sha we want to deploy.
639 8
      if (!$this->sh('git log -1 ' . $this->sha . ' --')) {
640
        $message = $this->shOutput();
641
        if (preg_match('/^fatal: bad object/', $message)) {
642
          return drush_set_error(dt('Could not find SHA, did you forget to push?'));
643
        }
644
        drush_print($message);
645
        return drush_set_error(dt('Could not find SHA remotely.'));
646
      }
647
648 8
      if (drush_get_option('insanity', FALSE)) {
649
        drush_log(dt('Skipping sanity check of deployed repo.'), 'warning');
650
        return TRUE;
651
      }
652 8
      $fallback = FALSE;
653
      // http://stackoverflow.com/questions/3878624/how-do-i-programmatically-determine-if-there-are-uncommited-changes
654
      // claims this is the proper way. I'm inclined to agree.
655
      // However, these are newer additions to Git, so fallback to git status
656
      // if they fail.
657 8
      if (!$this->sh('git diff-files --quiet --ignore-submodules --')) {
658 1
        if ($this->shRc() != 129) {
659 1
          return drush_set_error(dt('Repository not clean.'));
660
        }
661
        $fallback = TRUE;
662
      }
663 8
      if (!$fallback) {
664 8
        if (!$this->sh('git diff-index --cached --quiet HEAD --ignore-submodules --')) {
665 1
          if ($this->shRc() != 129) {
666 1
            return drush_set_error(dt('Uncommitted changes in the index.'));
667
          }
668
          $fallback = TRUE;
669
        }
670 8
      }
671 8
      if ($fallback) {
672
        if (!$this->sh('git status -s --untracked-files=no')) {
673
          return drush_set_error(dt('Error running git status -s --untracked-files=no.'));
674
        }
675
        $output = trim($this->shOutput());
676
        if (!empty($output)) {
677
          return drush_set_error(dt('Repository not clean.'));
678
        }
679
      }
680
681 8
      return TRUE;
682
    }
683
  }
684
685
  /**
686
   * Deploy code.
687
   */
688
  class DeployCode extends Action {
689
    protected $runMessage = 'Deploying';
690
    protected $killSwitch = 'no-deploy';
691
    protected $short = 'deploy code';
692
    protected $help = 'Checks out a specified branch/tag/sha on the site.';
693
    protected $options = array(
694
      'branch' => 'Branch to check out.',
695
      'tag' => 'Tag to check out.',
696
      'sha' => 'SHA to check out.',
697
    );
698
699
    /**
700
     * {@inheritdoc}
701
     */
702 9
    public function getDescription() {
703 9
      $head = $this->getHead();
704 9
      if (!$head) {
705
        return COLOR_YELLOW . dt("Cannot find the deployed HEAD.") . COLOR_RESET;
706
      }
707
708
      // Collect info and log entry for the commit we're deploying.
709 9
      $this->shLocal('git log --pretty="format:%an <%ae> @ %ci (%cr)%n%n%B" --color --no-walk ' . $this->sha);
710 9
      $commit_info = $this->shOutput();
711
712
      // Collect info and log entry for the deployed head.
713 9
      $this->shLocal('git log --pretty="format:%an <%ae> @ %ci (%cr)%n%n%B" --color --no-walk ' . $head);
714 9
      $head_info = $this->shOutput();
715
716
      // Create diffstat between the deployed commit and the one we're
717
      // deploying.
718 9
      $this->shLocal('git diff --color --stat ' . $head . ' ' . $this->sha);
719 9
      $stat = $this->shOutput();
720 9
      $stat = explode("\n", $stat);
721 9
      if (count($stat) > 20) {
722 9
        $stat = array_merge(array_slice($stat, 0, 8), array('', ' ...', ''), array_slice($stat, -8));
723 9
      }
724
725 9
      $desc = dt("Check out @sha:\n", array('@sha' => $this->sha));
726 9
      $desc .= $commit_info . "\n\n";
727
728 9
      $desc .= dt("Currently deployed: @sha\n", array('@sha' => $head));
729 9
      $desc .= $head_info . "\n\n";
730
731 9
      $desc .= dt("All changes:\n!changes", array('!changes' => implode("\n", $stat)));
732
733 9
      return $desc;
734
    }
735
736
    /**
737
     * {@inheritdoc}
738
     */
739 9
    public function validate() {
740 9
      if ($this->sha = $this->pickSha()) {
741 9
        return TRUE;
742
      }
743
      return FALSE;
744
    }
745
746
    /**
747
     * {@inheritdoc}
748
     */
749 8
    public function run(ArrayObject $state) {
750
      // Some useful information for other actions.
751 8
      $state['requested_branch'] = drush_get_option('branch', NULL);
752 8
      $state['requested_tag'] = drush_get_option('tag', NULL);
753 8
      $state['requested_sha'] = drush_get_option('sha', NULL);
754
755 8
      if (!$this->sh('git checkout ' . $this->sha)) {
756
        drush_print($this->shOutput());
757
        return drush_set_error(dt('Could not checkout code.'));
758
      }
759
760
      // An extra safety check to make sure that things are as we think.
761 8
      $deployed_sha = $this->getHead(TRUE);
762 8
      if ($deployed_sha) {
763 8
        if ($deployed_sha != $this->sha) {
764
          return drush_set_error(dt('Code not properly deployed, head is at @sha now.', array('@sha' => $deployed_sha)));
765
        }
766
        else {
767 8
          drush_log(dt('HEAD now at @sha', array('@sha' => $deployed_sha)), 'status');
768 8
          $state['deployed_sha'] = $deployed_sha;
769
        }
770 8
      }
771
      else {
772
        drush_print($this->shOutput());
773
        return drush_set_error(dt('Error confirming that the code update succceded.'));
774
      }
775
776 8
      return TRUE;
777
    }
778
  }
779
780
  /**
781
   * Set site offline.
782
   */
783
  class SiteOffline extends Action {
784
    protected $description = 'Set the site offline.';
785
    protected $runMessage = 'Setting site offline';
786
    protected $killSwitch = 'no-offline';
787
    protected $short = 'set site offline';
788
789
    /**
790
     * {@inheritdoc}
791
     */
792 7
    public function run(ArrayObject $state) {
793 7
      if (!$this->drush('variable-set', array('maintenance_mode', 1))) {
794
        return drush_set_error(dt('Error setting site offline.'));
795
      }
796 7
      return TRUE;
797
    }
798
799
    /**
800
     * {@inheritdoc}
801
     */
802
    public function rollback() {
803
      // Use the online action as rollback.
804
      $online = new SiteOnline($this->site);
805
      $online->run(new ArrayObject());
806
    }
807
  }
808
809
  /**
810
   * Set site online.
811
   */
812
  class SiteOnline extends Action {
813
    protected $description = 'Set the site online.';
814
    protected $runMessage = 'Setting site online';
815
    protected $killSwitch = 'no-offline';
816
    protected $short = 'set site online';
817
    protected $switchSuffix = 'online';
818
819
    /**
820
     * {@inheritdoc}
821
     */
822 7
    public function run(ArrayObject $state) {
823 7
      if (!$this->drush('variable-set', array('maintenance_mode', 0))) {
824
        return drush_set_error(dt('Error setting site online.'));
825
      }
826 7
      return TRUE;
827
    }
828
829
    /**
830
     * {@inheritdoc}
831
     */
832
    public function rollback() {
833
      // Use the online action as rollback.
834
      $online = new SiteOffline($this->site);
835
      $online->run(new ArrayObject());
836
    }
837
  }
838
839
  /**
840
   * Backup database.
841
   */
842
  class BackupDatabase extends Action {
843
    protected $runMessage = 'Dumping database';
844
    protected $killSwitch = 'no-dump';
845
    protected $short = 'dump database';
846
    protected $help = 'Makes a SQL dump of the site database.';
847
    protected $options = array(
848
      'dump-dir' => array(
849
        'description' => 'Directory for database dumps, defaults to /tmp.',
850
        'example-value' => 'path',
851
      ),
852
    );
853
854
    /**
855
     * Get the name of a dump.
856
     */
857 9
    public static function filename($alias, $date_str, $sha) {
858 9
      return sprintf("%s/deploy.%s.%s.%s.sql", drush_get_option('dump-dir', '/tmp'), $alias, $date_str, $sha);
859
    }
860
861
    /**
862
     * {@inheritdoc}
863
     */
864 8
    public function getDescription() {
865 8
      return dt("Dump database to @file.", array('@file' => $this->dumpFilename()));
866
    }
867
868
    /**
869
     * {@inheritdoc}
870
     */
871 7
    public function run(ArrayObject $state) {
872 7
      if (!$this->drush('sql-dump', array(), array('no-ordered-dump' => TRUE, 'result-file' => $this->dumpFilename()))) {
873
        return drush_set_error('Error dumping database.');
874
      }
875 7
      $state['database_dump'] = $this->dumpFilename();
876 7
      return TRUE;
877
    }
878
879
    /**
880
     * Figure out dump filename.
881
     */
882 8
    protected function dumpFilename() {
883
      // Because there can pass time between this is called first and last
884
      // (--confirm primarily).
885 8
      static $date;
886 8
      if (!$date) {
887 8
        $date = date('Y-m-d\TH:i:s');
888 8
      }
889
890 8
      return static::filename($this->site['#name'], $date, $this->getHead());
891
    }
892
  }
893
894
  /**
895
   * Restore database.
896
   */
897
  class RestoreDatabase extends Action {
898
    protected $runMessage = 'Restoring database';
899
    protected $short = 'restore database';
900
    protected $options = array(
901
      'file' => array(
902
        'description' => 'Database dump file to restore.',
903
        'example-value' => 'filename',
904
      ),
905
    );
906
907
    /**
908
     * {@inheritdoc}
909
     */
910 1
    public function validate() {
911 1
      if (!drush_get_option('file', NULL)) {
912
        return drush_set_error(dt('Missing file.'));
913
      }
914
915 1
      return TRUE;
916
    }
917
918
    /**
919
     * {@inheritdoc}
920
     */
921 1
    public function getDescription() {
922 1
      return dt("Restore database from @file.", array('@file' => drush_get_option('file', NULL)));
923
    }
924
925
    /**
926
     * {@inheritdoc}
927
     */
928 1
    public function run(ArrayObject $state) {
929 1
      if (!$this->drush('sql-connect', array(), array('pipe' => TRUE))) {
930
        return drush_set_error('Error getting database connection setup.');
931
      }
932
933 1
      if (!is_string($this->drushResult['object'])) {
934
        return drush_set_error('Weird result from sql-connnect.');
935
      }
936
937 1
      $command = $this->drushResult['object'];
938
939 1
      if (!$this->sh($command . " < " . drush_get_option('file', NULL))) {
940
        drush_print($this->shOutput());
941
        return drush_set_error('Error restoring database.');
942
      }
943
944 1
      return TRUE;
945
    }
946
  }
947
948
  /**
949
   * Purge old backups.
950
   */
951
  class PurgeDatabaseBackups extends Action {
952
    protected $runMessage = 'Purging old database dumps.';
953
    protected $short = 'purge old dumps';
954
    protected $options = array(
955
      'num-dumps' => array(
956
        'description' => 'Number of database dumps to keep. 0 for unlimited.',
957
        'example-value' => '5',
958
      ),
959
    );
960
    protected $switchSuffix = 'purge';
961
962
    /**
963
     * Dumps to delete.
964
     */
965
    protected $deleteDumps = array();
966
967
    /**
968
     * {@inheritdoc}
969
     */
970 9
    public function validate() {
971 9
      $dumping = TRUE;
972 9
      if (drush_get_option('no-dump', FALSE)) {
973 1
        $dumping = FALSE;
974 1
      }
975 9
      $max_dumps = drush_get_option('num-dumps', 5);
976
977 9
      if ($max_dumps > 0) {
978
        // If we're dumping a new dump, we need to keep one less than the max to
979
        // make room for the new one.
980 9
        $keep = $max_dumps - ($dumping ? 1 : 0);
981 9
        $this->sh('ls ' . BackupDatabase::filename($this->site['#name'], '*', '*'));
982 9
        $dumps = $this->shOutputArray();
983 9
        if (count($dumps) > $keep) {
984
          // Reverse sort to get the newest first.
985 1
          rsort($dumps);
986 1
          $this->deleteDumps = array_slice($dumps, $keep);
987 1
        }
988 9
      }
989
990 9
      return TRUE;
991
    }
992
993
    /**
994
     * {@inheritdoc}
995
     */
996 9
    public function getDescription() {
997 9
      if (count($this->deleteDumps)) {
998 1
        return dt("Purge the following dump files:\n@files.", array('@files' => implode("\n", $this->deleteDumps)));
999
1000
      }
1001 9
      return dt("Not purging any dumps.");
1002
    }
1003
1004
    /**
1005
     * {@inheritdoc}
1006
     */
1007 8
    public function run(ArrayObject $state) {
1008 8
      if (count($this->deleteDumps)) {
1009 1
        $this->sh('rm ' . implode(" ", array_map('drush_escapeshellarg', $this->deleteDumps)));
1010 1
      }
1011
1012
      // We don't consider failure to delete dumps serious enough to fail the
1013
      // deployment.
1014 8
      return TRUE;
1015
    }
1016
  }
1017
1018
  /**
1019
   * Update database.
1020
   */
1021
  class UpdateDatabase extends Action {
1022
    protected $description = 'Runs database updates (as with update.php).';
1023
    protected $runMessage = 'Running database updates';
1024
    protected $killSwitch = 'no-updb';
1025
    protected $short = 'update database schema';
1026
1027
    /**
1028
     * {@inheritdoc}
1029
     */
1030 7
    public function run(ArrayObject $state) {
1031 7
      if (!$this->drush('updb', array(), array('yes' => TRUE))) {
1032
        return drush_set_error(dt('Error running database updates.'));
1033
      }
1034 7
      return TRUE;
1035
    }
1036
  }
1037
1038
  /**
1039
   * Clear cache.
1040
   */
1041
  class ClearCache extends Action {
1042
    protected $description = 'Clear all Drupal caches.';
1043
    protected $runMessage = 'Clearing caches';
1044
    protected $short = 'cache clear';
1045
    protected $killSwitch = 'no-cc-all';
1046
1047
    /**
1048
     * {@inheritdoc}
1049
     */
1050 7
    public function run(ArrayObject $state) {
1051 7
      if (!$this->drush('cc', array('all'), array())) {
1052
        return drush_set_error(dt('Error clearing cache.'));
1053
      }
1054 7
      return TRUE;
1055
    }
1056
  }
1057
1058
  /**
1059
   * Prepare OMG.
1060
   */
1061
  class OMGPrepare extends Action {
1062
    protected $description = 'Prepare restore.';
1063
    protected $runMessage = 'Preparing';
1064
    protected $short = 'preparing';
1065
    protected $help = 'Prepares restoring by looking for dumps to import.';
1066
    protected $options = array(
1067
      'dump-dir' => array(
1068
        'description' => 'Directory for database dumps.',
1069
        'example-value' => 'path',
1070
      ),
1071
    );
1072
    protected $switchSuffix = 'prepare';
1073
1074
    /**
1075
     * {@inheritdoc}
1076
     */
1077 1
    public function validate() {
1078
      // Try to find some dumps and give them as options for restoring.
1079 1
      $regex = '{^deploy\.' . preg_quote($this->site['#name']) . '\.(\d+-\d+-\d+T\d+:\d+:\d+)\.([0-9a-f]+)\.sql$}';
1080 1
      $this->sh('ls ' . drush_get_option('dump-dir', '/tmp'));
1081 1
      $listing = $this->shOutput();
1082 1
      $dumps = array();
1083 1
      foreach (array_reverse(explode("\n", $listing)) as $line) {
1084 1
        if (preg_match($regex, $line, $matches)) {
1085 1
          $dumps[$matches[1]] = $matches[2];
1086 1
        }
1087 1
      }
1088
1089 1
      if (!empty($dumps)) {
1090 1
        $date = drush_choice($dumps, dt('Please select a dump.'), '!key (!value)');
1091 1
        if ($date) {
1092 1
          $sha = $dumps[$date];
1093 1
          $file = 'deploy.' . $this->site['#name'] . '.' . $date . '.' . $sha . '.sql';
1094
          // We simply set the options so the other actions will see them. The
1095
          // DeployCode action will check that the SHA is available locally
1096
          // before validating, so we'll avoid the worst if dumps get mixed up.
1097 1
          drush_set_option('sha', $sha);
1098 1
          drush_set_option('file', drush_get_option('dump-dir', '/tmp') . '/' . $file);
1099 1
          return TRUE;
1100
        }
1101
        else {
1102
          return drush_set_error(dt('Aborting.'));
1103
        }
1104
      }
1105
      else {
1106
        return drush_set_error(dt('No database dumps found.'));
1107
      }
1108
    }
1109
1110
    /**
1111
     * {@inheritdoc}
1112
     */
1113 1
    public function run(ArrayObject $state) {
1114
      // Doing nothing.
1115 1
      return TRUE;
1116
    }
1117
  }
1118
1119
  /**
1120
   * Create a VERSION.txt file.
1121
   */
1122
  class CreateVersionTxt extends Action {
1123
    protected $description = 'Create VERSION.txt.';
1124
    protected $runMessage = 'Creating VERSION.txt';
1125
    protected $short = 'create VERSION.txt';
1126
    protected $killSwitch = 'no-create-version-txt';
1127
    protected $switchSuffix = 'version-txt';
1128
1129
    /**
1130
     * {@inheritdoc}
1131
     */
1132 8
    public function run(ArrayObject $state) {
1133 8
      if (!empty($state['deployed_sha'])) {
1134
        // Ask git which tags points to this commit.
1135 8
        $this->shLocal('git tag --points-at ' . $state['deployed_sha']);
1136 8
        $tags = implode(', ', $this->shOutputArray());
1137
1138 8
        $version_txt = array();
1139 8
        $version_txt[] = 'Deployment info';
1140 8
        $version_txt[] = '---------------';
1141 8
        if (!empty($state['requested_branch'])) {
1142 5
          $version_txt[] = 'Branch: ' . $state['requested_branch'];
1143 5
        }
1144 8
        if (!empty($tags)) {
1145 6
          $version_txt[] = 'Tags: ' . $tags;
1146 6
        }
1147 8
        $version_txt[] = 'SHA: ' . $state['deployed_sha'];
1148 8
        $version_txt[] = 'Time of deployment: ' . date('r');
1149 8
        $version_txt[] = 'Deployer: ' . $_SERVER['USER'] . '@' . php_uname('n');
1150
1151
        // Delete any pre-existing VERSION.txt file and create a new one.
1152 8
        $this->sh('rm VERSION.txt');
1153
        // You'd think that echo would do the job, but it's not consistent
1154
        // across shells. Bash and most /bin/echo requires the -n option to
1155
        // expand \n to a newline, while /bin/sh built-in echo doesn't and
1156
        // prints the -n as part of the output. But printf works the same and is
1157
        // POSIX 7, which should cover our bases.
1158 8
        $this->sh('printf ' . escapeshellarg(implode("\\n", $version_txt) . "\\n") . ' > VERSION.txt');
1159 8
      }
1160
      else {
1161 1
        drush_log(dt('No version deployed, not creating/updating VERSION.txt.'), 'warning');
1162
      }
1163 8
      return TRUE;
1164
    }
1165
  }
1166
1167
  /**
1168
   * Send a notification to Flowdock.
1169
   */
1170
  class FlowdockNotificaton extends Action {
1171
    protected $description = 'Send Flowdock notification.';
1172
    protected $runMessage = 'Sending Flowdock notification.';
1173
    protected $short = 'send Flowdock notification';
1174
    protected $enableSwitch = 'flowdock-token';
1175
    protected $options = array(
1176
      'flowdock-token' => array(
1177
        'description' => 'Flowdock token.',
1178
        'example-value' => 'token',
1179
      ),
1180
    );
1181
    protected $switchSuffix = 'flowdock';
1182
1183
    /**
1184
     * {@inheritdoc}
1185
     */
1186 2
    public function run(ArrayObject $state) {
1187 2
      if (!empty($state['deployed_sha'])) {
1188 1
        $this->shLocal('git tag --points-at ' . $state['deployed_sha']);
1189 1
        $tags = implode(', ', $this->shOutputArray());
1190
1191 1
        $subject = 'Deployment to ' . $this->site['remote-user'] . '@' .
1192 1
          $this->site['remote-host'] . ':' . $this->site['root'];
1193
1194 1
        $body = 'SHA: ' . $state['deployed_sha'] .
1195 1
          (!empty($state['requested_branch']) ? '<br />Branch: ' . $state['requested_branch'] : '') .
1196 1
          (!empty($tags) ? '<br />Tags: ' . $tags : '');
1197
1198
        $data = array(
1199 1
          'source' => 'Deployotron',
1200 1
          'from_address' => '[email protected]',
1201 1
          'subject' => $subject,
1202 1
          'content' => $body,
1203 1
          'tags' => array('deployotron', $this->getFlowdockTag()),
1204 1
        );
1205 1
        $data = json_encode($data);
1206
1207 1
        $service_url = 'https://api.flowdock.com/v1/messages/team_inbox/' . drush_get_option('flowdock-token', '');
1208
1209 1
        $curl = curl_init($service_url);
1210 1
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);
1211 1
        curl_setopt($curl, CURLOPT_POST, TRUE);
1212 1
        curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
1213 1
        curl_setopt($curl, CURLOPT_HTTPHEADER, array(
1214 1
            'Content-Type: application/json',
1215 1
            'Content-Length: ' . strlen($data),
1216
          )
1217 1
        );
1218
1219 1
        $curl_response = curl_exec($curl);
1220 1
        if ($curl_response != '{}') {
1221 1
          drush_log(dt('Unexpected response from Flowdock: !response', array('!response' => $curl_response)), 'warning');
1222 1
        }
1223 1
        curl_close($curl);
1224 1
      }
1225
      else {
1226 1
        drush_log(dt('No version deployed, not sending Flowdock notification.'), 'warning');
1227
      }
1228 2
      return TRUE;
1229
    }
1230
1231
    /**
1232
     * Get a Flowdock tag for the site.
1233
     */
1234 1
    private function getFlowdockTag() {
1235 1
      return preg_replace('/[^[:alnum:]_\-]/', '_', $this->site['#name']);
1236
    }
1237
  }
1238
1239
  /**
1240
   * Send a notification to New Relic.
1241
   */
1242
  class NewRelicNotificaton extends Action {
1243
    protected $description = 'Send New Relic notification.';
1244
    protected $runMessage = 'Sending New Relic notification.';
1245
    protected $short = 'send new relic notification';
1246
    protected $enableSwitch = 'newrelic-api-key';
1247
    protected $options = array(
1248
      'newrelic-app-name' => array(
1249
        'description' => 'New Relic application name.',
1250
        'example-value' => 'name',
1251
      ),
1252
      'newrelic-app-id' => array(
1253
        'description' => 'New Relic application id.',
1254
        'example-value' => 'id',
1255
      ),
1256
      'newrelic-api-key' => array(
1257
        'description' => 'New Relic API key.',
1258
        'example-value' => 'key',
1259
      ),
1260
    );
1261
    protected $switchSuffix = 'newrelic';
1262
1263
    /**
1264
     * {@inheritdoc}
1265
     */
1266
    public function validate() {
1267
      if (!drush_get_option('newrelic-app-name', NULL) && !drush_get_option('newrelic-app-id', NULL)) {
1268
        return drush_set_error('Need at least one of --newrelic-app-name or --newrelic-app-id');
1269
      }
1270
      // The api-key must have been specified as its the enable switch.
1271
      return TRUE;
1272
    }
1273
1274
    /**
1275
     * {@inheritdoc}
1276
     */
1277
    public function run(ArrayObject $state) {
1278
      if (!empty($state['deployed_sha'])) {
1279
        $this->shLocal('git tag --points-at ' . $state['deployed_sha']);
1280
        $tags = implode(', ', $this->shOutputArray());
1281
1282
        $body = 'SHA: ' . $state['deployed_sha'] .
1283
          (!empty($state['requested_branch']) ? "\nBranch: " . $state['requested_branch'] : '') .
1284
          (!empty($tags) ? "\nTags: " . $tags : '');
1285
1286
        $deployment = array(
1287
          'description' => $body,
1288
          'revision' => $state['deployed_sha'],
1289
          // @todo 'changelog' => '',
1290
          'user' => $_SERVER['USER'] . '@' . php_uname('n'),
1291
        );
1292
1293
        $app_name = drush_get_option('newrelic-app-name', NULL);
1294
        if ($app_name) {
1295
          $deployment['app_name'] = $app_name;
1296
        }
1297
1298
        $app_id = drush_get_option('newrelic-app-id', NULL);
1299
        if ($app_id) {
1300
          $deployment['application_id'] = $app_id;
1301
        }
1302
1303
        $data = http_build_query(array('deployment' => $deployment));
1304
        $service_url = 'https://api.newrelic.com/deployments.xml';
1305
1306
        $curl = curl_init($service_url);
1307
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);
1308
        curl_setopt($curl, CURLOPT_POST, TRUE);
1309
        curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
1310
        curl_setopt($curl, CURLOPT_HTTPHEADER, array(
1311
            'Content-Type: application/x-www-form-urlencoded',
1312
            'Content-Length: ' . strlen($data),
1313
            'x-api-key: ' . drush_get_option('newrelic-api-key'),
1314
          )
1315
        );
1316
1317
        $curl_response = curl_exec($curl);
1318
        if ($curl_response === FALSE) {
1319
          drush_log(dt('Curl failed: !response', array('!response' => curl_error($curl))), 'error');
1320
        }
1321
        elseif (!$curl_response) {
1322
          drush_log(dt('New Relic notification failed for some reason.'), 'error');
1323
        }
1324
        curl_close($curl);
1325
      }
1326
      else {
1327
        drush_log(dt('No version deployed, not sending New Relic notification.'), 'warning');
1328
      }
1329
      return TRUE;
1330
    }
1331
  }
1332
}
1333