Issues (7)

deployotron.actions.inc (6 issues)

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 2
    public function rollback() {
333 2
      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 3
        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
     * Find the tag pointed to by a SHA.
457
     *
458
     * @param string $sha
459
     *   The SHA to lookup.
460
     *
461
     * @return bool
462
     *   Whether the command succeeded.
463
     */
464
    protected function gitPointsAt ($sha) {
0 ignored issues
show
Space before opening parenthesis of function definition prohibited
Loading history...
465
      $success = $this->shLocal('git tag --points-at ' . $sha);
466
467
      // If it failed we try a fallback command for older versions of
468
      // git.
469
      if (!$success) {
470 7
          $success = $this->shLocal('git show-ref --tags -d | grep ^' . $sha . ' | sed -e \'s,.* refs/tags/,,\' -e \'s/\^{}//\'');
0 ignored issues
show
Line indented incorrectly; expected 8 spaces, found 10
Loading history...
471 7
      }
472 7
473
      return $success;
474
    }
475 7
476
    /**
477
     * Execute a drush command.
478
     *
479
     * Is run on the site.
480
     *
481
     * @param string $command
482
     *   Command to run.
483
     * @param string $args
484
     *   Command arguments.
485
     * @param string $options
486 9
     *   Command arguments.
487
     *
488 9
     * @return bool
489 9
     *   Whether the command succeeded.
490 9
     */
491 9
    protected function drush($command, $args = array(), $options = array()) {
492
      $this->drushResult = drush_invoke_process($this->site, $command, $args, $options, TRUE);
493 9
      if (!$this->drushResult || $this->drushResult['error_status'] != 0) {
494 9
        return FALSE;
495 9
      }
496
      return TRUE;
497 9
    }
498 9
499 2
    /**
500 2
     * Get the SHA to deploy.
501 9
     *
502 1
     * This is based on options from configuration or command.
503
     *
504
     * @return string
505
     *   The SHA to deploy.
506
     */
507
    protected function pickSha($options = array()) {
508
      $options = array(
509 9
        'branch' => drush_get_option('branch', NULL),
510 9
        'tag' => drush_get_option('tag', NULL),
511 9
        'sha' => drush_get_option('sha', NULL),
512
      );
513 1
514 1
      $branch = $options['branch'];
515
      $tag = $options['tag'];
516
      $sha = $options['sha'];
517 1
518
      $posibilities = array_filter(array($branch, $tag, $sha));
519
      if (count($posibilities) > 1) {
520
        drush_log(dt('More than one of branch/tag/sha specified, using @selected.', array('@selected' => !empty($sha) ? 'sha' : 'tag')), 'warning');
521 9
      }
522
      elseif (count($posibilities) < 1) {
523
        return drush_set_error(dt('You must provide at least one of --branch, --tag or --sha.'));
524
      }
525
526
      // Use rev-list to ensure we get a full SHA rather than branch/tag/partial
527
      // SHA. Ensures the existence of the branch/tag/SHA, and transforms
528
      // annotated tag SHAs to the commit they're based on (so the repo will
529
      // actually have the SHA we asked for checked out).
530 9
      if ($this->shLocal('git rev-list -1 ' . end($posibilities) . ' --')) {
531 9
        $sha = trim($this->shOutput());
532 9
      }
533 9
      else {
534 9
        if (!empty($sha)) {
535 9
          return drush_set_error(dt('Unknown SHA.'));
536
        }
537 9
        else {
538 9
          return drush_set_error(dt('Error finding SHA for ' . (!empty($branch) ? 'branch' : 'tag') . '.'));
539 9
        }
540
      }
541
542
      return $sha;
543 9
    }
544 9
545 9
    /**
546
     * Get the current git head revision.
547
     *
548
     * @param bool $no_cache
549
     *   Dont't use cache, but fetch anyway.
550
     */
551
    protected function getHead($no_cache = FALSE) {
552
      $name = $this->site['#name'];
553
      if (!isset(self::$headShas[$name]) || $no_cache) {
554
        self::$headShas[$name] = '';
555
        if ($this->sh('git rev-parse HEAD')) {
556
          $sha = trim($this->shOutput());
557
          // Confirm that we can see the HEAD locally.
558
          if ($this->shLocal('git show -s ' . $sha)) {
559
            self::$headShas[$name] = $sha;
560
          }
561
          else {
562
            drush_log(dt("Could not find the deployed HEAD (@sha) locally, you pulled recently?", array('@sha' => $sha)), 'warning');
563
          }
564
        }
565 1
      }
566 1
      return self::$headShas[$name];
567 1
    }
568 1
  }
569
}
570 1
571 1
/**
572 1
 * Use a namespace to keep actions together.
573
 */
574
namespace Deployotron\Actions {
575
  use Deployotron\Action;
576
577 1
  /**
578 1
   * Generic command action.
579 1
   *
580 1
   * Automatically instantiated from pre/post-* options.
581 1
   */
582
  class CommandAction extends Action {
583 1
    /**
584
     * Create action.
585
     */
586
    public function __construct($site, $command) {
587
      $this->description = 'Run command: ' . $command;
588
      $this->runMessage = 'Running command: ' . $command;
589
      $this->short = 'run ' . $command;
590
591
      $this->command = $command;
592
      parent::__construct($site);
593
    }
594
595
    /**
596
     * {@inheritdoc}
597
     */
598
    public function run(\ArrayObject $state) {
599
      $success = $this->sh($this->command);
600
      drush_print($this->shOutput());
601
      if (!$success) {
602
        return drush_set_error(dt('Error running command "@command"', array('@command' => $this->command)));
603 9
      }
604 9
      return TRUE;
605
    }
606
  }
607
608 9
  /**
609
   * Sanity check.
610
   *
611
   * Check for locally modified files and do a dry-run checkout.
612
   */
613
  class SanityCheck extends Action {
614 9
    protected $runMessage = 'Fetching and sanity checking';
615 9
    protected $short = 'sanity check';
616 9
    protected $help = 'Fetches the new code and checks that the repository is clean.';
617
    protected $options = array(
618 1
      'insanity' => "Don't check that the server repository is clean. Might cause data loss.",
619
    );
620
621
    /**
622
     * {@inheritdoc}
623
     */
624 8
    public function getDescription() {
625
      if (drush_get_option('insanity', FALSE)) {
626 8
        return COLOR_YELLOW . dt('Skipping sanity check of deployed repo. Might cause data loss or Git failures.') . COLOR_RESET;
627
      }
628
629
      return dt('Run git fetch and check that the deployed git repository is clean.');
630
    }
631
632
    /**
633
     * {@inheritdoc}
634
     */
635
    public function validate() {
636
      if ($this->sha = $this->pickSha()) {
637
        return TRUE;
638
      }
639 8
      return FALSE;
640
    }
641
642
    /**
643
     * {@inheritdoc}
644
     */
645
    public function run(\ArrayObject $state) {
646
      // Fetch the latest changes from upstream.
647
      if (!$this->sh('git fetch')) {
648 8
        $message = $this->shOutput();
649
        if (preg_match('/Permission denied \(publickey\)/', $message)) {
650
          drush_log(dt('Access denied to repository URL.'), 'error');
651
          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');
652 8
        }
653
        else {
654
          drush_print($message);
655
        }
656
        return drush_set_error(dt("Could not fetch from remote repository."));
657 8
      }
658 1
659 1
      // Check that we can see the sha we want to deploy.
660
      if (!$this->sh('git log -1 ' . $this->sha . ' --')) {
661
        $message = $this->shOutput();
662
        if (preg_match('/^fatal: bad object/', $message)) {
663 8
          return drush_set_error(dt('Could not find SHA, did you forget to push?'));
664 8
        }
665 1
        drush_print($message);
666 1
        return drush_set_error(dt('Could not find SHA remotely.'));
667
      }
668
669
      if (drush_get_option('insanity', FALSE)) {
670 8
        drush_log(dt('Skipping sanity check of deployed repo.'), 'warning');
671 8
        return TRUE;
672
      }
673
      $fallback = FALSE;
674
      // http://stackoverflow.com/questions/3878624/how-do-i-programmatically-determine-if-there-are-uncommited-changes
0 ignored issues
show
Line exceeds 80 characters; contains 119 characters
Loading history...
675
      // claims this is the proper way. I'm inclined to agree.
676
      // However, these are newer additions to Git, so fallback to git status
677
      // if they fail.
678
      if (!$this->sh('git diff-files --quiet --ignore-submodules --')) {
679
        if ($this->shRc() != 129) {
680
          return drush_set_error(dt('Remote git checkout not clean.'));
681 8
        }
682
        $fallback = TRUE;
683
      }
684
      if (!$fallback) {
685
        if (!$this->sh('git diff-index --cached --quiet HEAD --ignore-submodules --')) {
686
          if ($this->shRc() != 129) {
687
            return drush_set_error(dt('Uncommitted changes in the index.'));
688
          }
689
          $fallback = TRUE;
690
        }
691
      }
692
      if ($fallback) {
693
        if (!$this->sh('git status -s --untracked-files=no')) {
694
          return drush_set_error(dt('Error running git status -s --untracked-files=no.'));
695
        }
696
        $output = trim($this->shOutput());
697
        if (!empty($output)) {
698
          return drush_set_error(dt('Remote git checkout not clean.'));
699
        }
700
      }
701
702 9
      return TRUE;
703 9
    }
704 9
  }
705
706
  /**
707
   * Deploy code.
708
   */
709 9
  class DeployCode extends Action {
710 9
    protected $runMessage = 'Deploying';
711
    protected $killSwitch = 'no-deploy';
712
    protected $short = 'deploy code';
713 9
    protected $help = 'Checks out a specified branch/tag/sha on the site.';
714 9
    protected $options = array(
715
      'branch' => 'Branch to check out.',
716
      'tag' => 'Tag to check out.',
717
      'sha' => 'SHA to check out.',
718 9
    );
719 9
720 9
    /**
721 9
     * {@inheritdoc}
722 9
     */
723 9
    public function getDescription() {
724
      $head = $this->getHead();
725 9
      if (!$head) {
726 9
        return COLOR_YELLOW . dt("Cannot find the deployed HEAD.") . COLOR_RESET;
727
      }
728 9
729 9
      // Collect info and log entry for the commit we're deploying.
730
      $this->shLocal('git log --pretty="format:%an <%ae> @ %ci (%cr)%n%n%B" --color --no-walk ' . $this->sha);
731 9
      $commit_info = $this->shOutput();
732
733 9
      // Collect info and log entry for the deployed head.
734
      $this->shLocal('git log --pretty="format:%an <%ae> @ %ci (%cr)%n%n%B" --color --no-walk ' . $head);
735
      $head_info = $this->shOutput();
736
737
      // Create diffstat between the deployed commit and the one we're
738
      // deploying.
739 9
      $this->shLocal('git diff --color --stat ' . $head . ' ' . $this->sha);
740 9
      $stat = $this->shOutput();
741 9
      $stat = explode("\n", $stat);
742
      if (count($stat) > 20) {
743
        $stat = array_merge(array_slice($stat, 0, 8), array('', ' ...', ''), array_slice($stat, -8));
744
      }
745
746
      $desc = dt("Check out @sha:\n", array('@sha' => $this->sha));
747
      $desc .= $commit_info . "\n\n";
748
749 8
      $desc .= dt("Currently deployed: @sha\n", array('@sha' => $head));
750
      $desc .= $head_info . "\n\n";
751 8
752 8
      $desc .= dt("All changes:\n!changes", array('!changes' => implode("\n", $stat)));
753 8
754
      return $desc;
755 8
    }
756 1
757 1
    /**
758
     * {@inheritdoc}
759
     */
760
    public function validate() {
761 8
      if ($this->sha = $this->pickSha()) {
762 8
        return TRUE;
763 8
      }
764
      return FALSE;
765
    }
766
767 8
    /**
768 8
     * {@inheritdoc}
769
     */
770 8
    public function run(\ArrayObject $state) {
771
      // Some useful information for other actions.
772
      $state['requested_branch'] = drush_get_option('branch', NULL);
773
      $state['requested_tag'] = drush_get_option('tag', NULL);
774
      $state['requested_sha'] = drush_get_option('sha', NULL);
775
776 8
      if (!$this->sh('git checkout ' . $this->sha)) {
777
        drush_print($this->shOutput());
778
        return drush_set_error(dt('Could not checkout code.'));
779
      }
780
781
      // An extra safety check to make sure that things are as we think.
782
      $deployed_sha = $this->getHead(TRUE);
783
      if ($deployed_sha) {
784
        if ($deployed_sha != $this->sha) {
785
          return drush_set_error(dt('Code not properly deployed, head is at @sha now.', array('@sha' => $deployed_sha)));
786
        }
787
        else {
788
          drush_log(dt('HEAD now at @sha', array('@sha' => $deployed_sha)), 'status');
789
          $state['deployed_sha'] = $deployed_sha;
790
        }
791
      }
792 7
      else {
793 7
        drush_print($this->shOutput());
794
        return drush_set_error(dt('Error confirming that the code update succceded.'));
795
      }
796 7
797
      return TRUE;
798
    }
799
  }
800
801
  /**
802 2
   * Set site offline.
803
   */
804 2
  class SiteOffline extends Action {
805 2
    protected $description = 'Set the site offline.';
806 2
    protected $runMessage = 'Setting site offline';
807
    protected $killSwitch = 'no-offline';
808
    protected $short = 'set site offline';
809
810
    /**
811
     * {@inheritdoc}
812
     */
813
    public function run(\ArrayObject $state) {
814
      if (!$this->drush('variable-set', array('maintenance_mode', 1))) {
815
        return drush_set_error(dt('Error setting site offline.'));
816
      }
817
      return TRUE;
818
    }
819
820
    /**
821
     * {@inheritdoc}
822 7
     */
823 7
    public function rollback() {
824
      // Use the online action as rollback.
825
      $online = new SiteOnline($this->site);
826 7
      $online->run(new \ArrayObject());
827
    }
828
  }
829
830
  /**
831
   * Set site online.
832
   */
833
  class SiteOnline extends Action {
834
    protected $description = 'Set the site online.';
835
    protected $runMessage = 'Setting site online';
836
    protected $killSwitch = 'no-offline';
837
    protected $short = 'set site online';
838
    protected $switchSuffix = 'online';
839
840
    /**
841
     * {@inheritdoc}
842
     */
843
    public function run(\ArrayObject $state) {
844
      if (!$this->drush('variable-set', array('maintenance_mode', 0))) {
845
        return drush_set_error(dt('Error setting site online.'));
846
      }
847
      return TRUE;
848
    }
849
850
    /**
851
     * {@inheritdoc}
852
     */
853
    public function rollback() {
854
      // Use the online action as rollback.
855
      $online = new SiteOffline($this->site);
856
      $online->run(new \ArrayObject());
857 9
    }
858 9
  }
859
860
  /**
861
   * Backup database.
862
   */
863
  class BackupDatabase extends Action {
864 8
    protected $runMessage = 'Dumping database';
865 8
    protected $killSwitch = 'no-dump';
866
    protected $short = 'dump database';
867
    protected $help = 'Makes a SQL dump of the site database.';
868
    protected $options = array(
869
      'dump-dir' => array(
870
        'description' => 'Directory for database dumps, defaults to /tmp.',
871 7
        'example-value' => 'path',
872 7
      ),
873
    );
874
875 7
    /**
876 7
     * Get the name of a dump.
877
     */
878
    public static function filename($alias, $date_str, $sha) {
879
      return sprintf("%s/deploy.%s.%s.%s.sql", drush_get_option('dump-dir', '/tmp'), $alias, $date_str, $sha);
880
    }
881
882 8
    /**
883
     * {@inheritdoc}
884
     */
885 8
    public function getDescription() {
886 8
      return dt("Dump database to @file.", array('@file' => $this->dumpFilename()));
887 8
    }
888 8
889
    /**
890 8
     * {@inheritdoc}
891
     */
892
    public function run(\ArrayObject $state) {
893
      if (!$this->drush('sql-dump', array(), array('no-ordered-dump' => TRUE, 'result-file' => $this->dumpFilename()))) {
894
        return drush_set_error('Error dumping database.');
895
      }
896
      $state['database_dump'] = $this->dumpFilename();
897
      return TRUE;
898
    }
899
900
    /**
901
     * Figure out dump filename.
902
     */
903
    protected function dumpFilename() {
904
      // Because there can pass time between this is called first and last
905
      // (--confirm primarily).
906
      static $date;
907
      if (!$date) {
908
        $date = date('Y-m-d\TH:i:s');
909
      }
910 1
911 1
      return static::filename($this->site['#name'], $date, $this->getHead());
912
    }
913
  }
914
915 1
  /**
916
   * Restore database.
917
   */
918
  class RestoreDatabase extends Action {
919
    protected $runMessage = 'Restoring database';
920
    protected $short = 'restore database';
921 1
    protected $options = array(
922 1
      'file' => array(
923
        'description' => 'Database dump file to restore.',
924
        'example-value' => 'filename',
925
      ),
926
    );
927
928 1
    /**
929 1
     * {@inheritdoc}
930
     */
931
    public function validate() {
932
      if (!drush_get_option('file', NULL)) {
933 1
        return drush_set_error(dt('Missing file.'));
934
      }
935
936
      return TRUE;
937 1
    }
938
939 1
    /**
940
     * {@inheritdoc}
941
     */
942
    public function getDescription() {
943
      return dt("Restore database from @file.", array('@file' => drush_get_option('file', NULL)));
944 1
    }
945
946
    /**
947
     * {@inheritdoc}
948
     */
949
    public function run(\ArrayObject $state) {
950
      if (!$this->drush('sql-connect', array(), array('pipe' => TRUE))) {
951
        return drush_set_error('Error getting database connection setup.');
952
      }
953
954
      if (!is_string($this->drushResult['object'])) {
955
        return drush_set_error('Weird result from sql-connnect.');
956
      }
957
958
      $command = $this->drushResult['object'];
959
960
      if (!$this->sh($command . " < " . drush_get_option('file', NULL))) {
961
        drush_print($this->shOutput());
962
        return drush_set_error('Error restoring database.');
963
      }
964
965
      return TRUE;
966
    }
967
  }
968
969
  /**
970 9
   * Purge old backups.
971 9
   */
972 9
  class PurgeDatabaseBackups extends Action {
973 1
    protected $runMessage = 'Purging old database dumps.';
974 1
    protected $short = 'purge old dumps';
975 9
    protected $options = array(
976
      'num-dumps' => array(
977 9
        'description' => 'Number of database dumps to keep. 0 for unlimited.',
978
        'example-value' => '5',
979
      ),
980 9
    );
981 9
    protected $switchSuffix = 'purge';
982 9
983 9
    /**
984
     * Dumps to delete.
985 2
     */
986 2
    protected $deleteDumps = array();
987 2
988 9
    /**
989
     * {@inheritdoc}
990 9
     */
991
    public function validate() {
992
      $dumping = TRUE;
993
      if (drush_get_option('no-dump', FALSE)) {
994
        $dumping = FALSE;
995
      }
996 9
      $max_dumps = drush_get_option('num-dumps', 5);
997 9
998 2
      if ($max_dumps > 0) {
999
        // If we're dumping a new dump, we need to keep one less than the max to
1000
        // make room for the new one.
1001 9
        $keep = $max_dumps - ($dumping ? 1 : 0);
1002
        $this->sh('ls ' . BackupDatabase::filename($this->site['#name'], '*', '*'));
1003
        $dumps = $this->shOutputArray();
1004
        if (count($dumps) > $keep) {
1005
          // Reverse sort to get the newest first.
1006
          rsort($dumps);
1007 8
          $this->deleteDumps = array_slice($dumps, $keep);
1008 8
        }
1009 2
      }
1010 2
1011
      return TRUE;
1012
    }
1013
1014 8
    /**
1015
     * {@inheritdoc}
1016
     */
1017
    public function getDescription() {
1018
      if (count($this->deleteDumps)) {
1019
        return dt("Purge the following dump files:\n@files.", array('@files' => implode("\n", $this->deleteDumps)));
1020
1021
      }
1022
      return dt("Not purging any dumps.");
1023
    }
1024
1025
    /**
1026
     * {@inheritdoc}
1027
     */
1028
    public function run(\ArrayObject $state) {
1029
      if (count($this->deleteDumps)) {
1030 7
        $this->sh('rm ' . implode(" ", array_map('drush_escapeshellarg', $this->deleteDumps)));
1031 7
      }
1032
1033
      // We don't consider failure to delete dumps serious enough to fail the
1034 7
      // deployment.
1035
      return TRUE;
1036
    }
1037
  }
1038
1039
  /**
1040
   * Update database.
1041
   */
1042
  class UpdateDatabase extends Action {
1043
    protected $description = 'Runs database updates (as with update.php).';
1044
    protected $runMessage = 'Running database updates';
1045
    protected $killSwitch = 'no-updb';
1046
    protected $short = 'update database schema';
1047
1048
    /**
1049
     * {@inheritdoc}
1050 7
     */
1051 7
    public function run(\ArrayObject $state) {
1052
      if (!$this->drush('updb', array(), array('yes' => TRUE))) {
1053
        return drush_set_error(dt('Error running database updates.'));
1054 7
      }
1055
      return TRUE;
1056
    }
1057
  }
1058
1059
  /**
1060
   * Clear cache.
1061
   */
1062
  class ClearCache extends Action {
1063
    protected $description = 'Clear all Drupal caches.';
1064
    protected $runMessage = 'Clearing caches';
1065
    protected $short = 'cache clear';
1066
    protected $killSwitch = 'no-cc-all';
1067
1068
    /**
1069
     * {@inheritdoc}
1070
     */
1071
    public function run(\ArrayObject $state) {
1072
      if (!$this->drush('cc', array('all'), array())) {
1073
        return drush_set_error(dt('Error clearing cache.'));
1074
      }
1075
      return TRUE;
1076
    }
1077 1
  }
1078
1079 1
  /**
1080 1
   * Prepare OMG.
1081 1
   */
1082 1
  class OMGPrepare extends Action {
1083 1
    protected $description = 'Prepare restore.';
1084 1
    protected $runMessage = 'Preparing';
1085 1
    protected $short = 'preparing';
1086 1
    protected $help = 'Prepares restoring by looking for dumps to import.';
1087 1
    protected $options = array(
1088
      'dump-dir' => array(
1089 1
        'description' => 'Directory for database dumps.',
1090 1
        'example-value' => 'path',
1091 1
      ),
1092 1
    );
1093 1
    protected $switchSuffix = 'prepare';
1094
1095
    /**
1096
     * {@inheritdoc}
1097 1
     */
1098 1
    public function validate() {
1099 1
      // Try to find some dumps and give them as options for restoring.
1100
      $regex = '{^deploy\.' . preg_quote($this->site['#name']) . '\.(\d+-\d+-\d+T\d+:\d+:\d+)\.([0-9a-f]+)\.sql$}';
1101
      $this->sh('ls ' . drush_get_option('dump-dir', '/tmp'));
1102
      $listing = $this->shOutput();
1103
      $dumps = array();
1104
      foreach (array_reverse(explode("\n", $listing)) as $line) {
1105
        if (preg_match($regex, $line, $matches)) {
1106
          $dumps[$matches[1]] = $matches[2];
1107
        }
1108
      }
1109
1110
      if (!empty($dumps)) {
1111
        $date = drush_choice($dumps, dt('Please select a dump.'), '!key (!value)');
1112
        if ($date) {
1113 1
          $sha = $dumps[$date];
1114
          $file = 'deploy.' . $this->site['#name'] . '.' . $date . '.' . $sha . '.sql';
1115 1
          // We simply set the options so the other actions will see them. The
1116
          // DeployCode action will check that the SHA is available locally
1117
          // before validating, so we'll avoid the worst if dumps get mixed up.
1118
          drush_set_option('sha', $sha);
1119
          drush_set_option('file', drush_get_option('dump-dir', '/tmp') . '/' . $file);
1120
          return TRUE;
1121
        }
1122
        else {
1123
          return drush_set_error(dt('Aborting.'));
1124
        }
1125
      }
1126
      else {
1127
        return drush_set_error(dt('No database dumps found.'));
1128
      }
1129
    }
1130
1131
    /**
1132 8
     * {@inheritdoc}
1133 8
     */
1134
    public function run(\ArrayObject $state) {
1135 8
      // Doing nothing.
1136 8
      return TRUE;
1137
    }
1138 8
  }
1139 8
1140 8
  /**
1141 8
   * Create a VERSION.txt file.
1142 5
   */
1143 5
  class CreateVersionTxt extends Action {
1144 8
    protected $description = 'Create VERSION.txt.';
1145 7
    protected $runMessage = 'Creating VERSION.txt';
1146 7
    protected $short = 'create VERSION.txt';
1147 8
    protected $killSwitch = 'no-create-version-txt';
1148 8
    protected $switchSuffix = 'version-txt';
1149 8
1150
    /**
1151
     * {@inheritdoc}
1152 8
     */
1153
    public function run(\ArrayObject $state) {
0 ignored issues
show
run uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
1154
      if (!empty($state['deployed_sha'])) {
1155
        // Ask git which tags points to this commit.
1156
        $this->gitPointsAt($state['deployed_sha']);
1157
        $tags = implode(', ', $this->shOutputArray());
1158 8
1159 8
        $version_txt = array();
1160
        $version_txt[] = 'Deployment info';
1161 1
        $version_txt[] = '---------------';
1162
        if (!empty($state['requested_branch'])) {
1163 8
          $version_txt[] = 'Branch: ' . $state['requested_branch'];
1164
        }
1165
        if (!empty($tags)) {
1166
          $version_txt[] = 'Tags: ' . $tags;
1167
        }
1168
        $version_txt[] = 'SHA: ' . $state['deployed_sha'];
1169
        $version_txt[] = 'Time of deployment: ' . date('r');
1170
        $version_txt[] = 'Deployer: ' . $_SERVER['USER'] . '@' . php_uname('n');
1171
1172
        // Delete any pre-existing VERSION.txt file and create a new one.
1173
        $this->sh('rm VERSION.txt');
1174
        // You'd think that echo would do the job, but it's not consistent
1175
        // across shells. Bash and most /bin/echo requires the -n option to
1176
        // expand \n to a newline, while /bin/sh built-in echo doesn't and
1177
        // prints the -n as part of the output. But printf works the same and is
1178
        // POSIX 7, which should cover our bases.
1179
        $this->sh('printf ' . escapeshellarg(implode("\\n", $version_txt) . "\\n") . ' > VERSION.txt');
1180
      }
1181
      else {
1182
        drush_log(dt('No version deployed, not creating/updating VERSION.txt.'), 'warning');
1183
      }
1184
      return TRUE;
1185
    }
1186 2
  }
1187 2
1188 1
  /**
1189 1
   * Send a notification to Flowdock.
1190
   */
1191 1
  class FlowdockNotificaton extends Action {
1192 1
    protected $description = 'Send Flowdock notification.';
1193
    protected $runMessage = 'Sending Flowdock notification.';
1194 1
    protected $short = 'send Flowdock notification';
1195 1
    protected $enableSwitch = 'flowdock-token';
1196 1
    protected $options = array(
1197
      'flowdock-token' => array(
1198
        'description' => 'Flowdock token.',
1199 1
        'example-value' => 'token',
1200 1
      ),
1201 1
    );
1202 1
    protected $switchSuffix = 'flowdock';
1203 1
1204 1
    /**
1205 1
     * {@inheritdoc}
1206
     */
1207 1
    public function run(\ArrayObject $state) {
1208
      if (!empty($state['deployed_sha'])) {
1209 1
        $this->gitPointsAt($state['deployed_sha']);
1210 1
        $tags = implode(', ', $this->shOutputArray());
1211 1
1212 1
        $subject = 'Deployment to ' . $this->site['remote-user'] . '@' .
1213 1
          $this->site['remote-host'] . ':' . $this->site['root'];
1214 1
1215 1
        $body = 'SHA: ' . $state['deployed_sha'] .
1216
          (!empty($state['requested_branch']) ? '<br />Branch: ' . $state['requested_branch'] : '') .
1217 1
          (!empty($tags) ? '<br />Tags: ' . $tags : '');
1218
1219 1
        $data = array(
1220 1
          'source' => 'Deployotron',
1221 1
          'from_address' => '[email protected]',
1222 1
          'subject' => $subject,
1223 1
          'content' => $body,
1224 1
          'tags' => array('deployotron', $this->getFlowdockTag()),
1225
        );
1226 1
        $data = json_encode($data);
1227
1228 2
        $service_url = 'https://api.flowdock.com/v1/messages/team_inbox/' . drush_get_option('flowdock-token', '');
1229
1230
        $curl = curl_init($service_url);
1231
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);
1232
        curl_setopt($curl, CURLOPT_POST, TRUE);
1233
        curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
1234 1
        curl_setopt($curl, CURLOPT_HTTPHEADER, array(
1235 1
            'Content-Type: application/json',
1236
            'Content-Length: ' . strlen($data),
1237
          )
1238
        );
1239
1240
        $curl_response = curl_exec($curl);
1241
        if ($curl_response != '{}') {
1242
          drush_log(dt('Unexpected response from Flowdock: !response', array('!response' => $curl_response)), 'warning');
1243
        }
1244
        curl_close($curl);
1245
      }
1246
      else {
1247
        drush_log(dt('No version deployed, not sending Flowdock notification.'), 'warning');
1248
      }
1249
      return TRUE;
1250
    }
1251
1252
    /**
1253
     * Get a Flowdock tag for the site.
1254
     */
1255
    private function getFlowdockTag() {
1256
      return preg_replace('/[^[:alnum:]_\-]/', '_', $this->site['#name']);
1257
    }
1258
  }
1259
1260
  /**
1261
   * Send a notification to New Relic.
1262
   */
1263
  class NewRelicNotificaton extends Action {
1264
    protected $description = 'Send New Relic notification.';
1265
    protected $runMessage = 'Sending New Relic notification.';
1266
    protected $short = 'send new relic notification';
1267
    protected $enableSwitch = 'newrelic-api-key';
1268
    protected $options = array(
1269
      'newrelic-app-name' => array(
1270
        'description' => 'New Relic application name.',
1271
        'example-value' => 'name',
1272
      ),
1273
      'newrelic-app-id' => array(
1274
        'description' => 'New Relic application id.',
1275
        'example-value' => 'id',
1276
      ),
1277
      'newrelic-api-key' => array(
1278
        'description' => 'New Relic API key.',
1279
        'example-value' => 'key',
1280
      ),
1281
    );
1282
    protected $switchSuffix = 'newrelic';
1283
1284
    /**
1285
     * {@inheritdoc}
1286
     */
1287
    public function validate() {
1288
      if (!drush_get_option('newrelic-app-name', NULL) && !drush_get_option('newrelic-app-id', NULL)) {
1289
        return drush_set_error('Need at least one of --newrelic-app-name or --newrelic-app-id');
1290
      }
1291
      // The api-key must have been specified as its the enable switch.
1292
      return TRUE;
1293
    }
1294
1295
    /**
1296
     * {@inheritdoc}
1297
     */
1298
    public function run(\ArrayObject $state) {
0 ignored issues
show
run uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
1299
      if (!empty($state['deployed_sha'])) {
1300
        $this->gitPointsAt($state['deployed_sha']);
1301
        $tags = implode(', ', $this->shOutputArray());
1302
1303
        $body = 'SHA: ' . $state['deployed_sha'] .
1304
          (!empty($state['requested_branch']) ? "\nBranch: " . $state['requested_branch'] : '') .
1305
          (!empty($tags) ? "\nTags: " . $tags : '');
1306
1307
        $deployment = array(
1308
          'description' => $body,
1309
          'revision' => $state['deployed_sha'],
1310
          // @todo 'changelog' => '',
1311
          'user' => $_SERVER['USER'] . '@' . php_uname('n'),
1312
        );
1313
1314
        $app_name = drush_get_option('newrelic-app-name', NULL);
1315
        if ($app_name) {
1316
          $deployment['app_name'] = $app_name;
1317
        }
1318
1319
        $app_id = drush_get_option('newrelic-app-id', NULL);
1320
        if ($app_id) {
1321
          $deployment['application_id'] = $app_id;
1322
        }
1323
1324
        $data = http_build_query(array('deployment' => $deployment));
1325
        $service_url = 'https://api.newrelic.com/deployments.xml';
1326
1327
        $curl = curl_init($service_url);
1328
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);
1329
        curl_setopt($curl, CURLOPT_POST, TRUE);
1330
        curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
1331
        curl_setopt($curl, CURLOPT_HTTPHEADER, array(
1332
            'Content-Type: application/x-www-form-urlencoded',
1333
            'Content-Length: ' . strlen($data),
1334
            'x-api-key: ' . drush_get_option('newrelic-api-key'),
1335
          )
1336
        );
1337
1338
        $curl_response = curl_exec($curl);
1339
        if ($curl_response === FALSE) {
1340
          drush_log(dt('Curl failed: !response', array('!response' => curl_error($curl))), 'error');
1341
        }
1342
        elseif (!$curl_response) {
1343
          drush_log(dt('New Relic notification failed for some reason.'), 'error');
1344
        }
1345
        curl_close($curl);
1346
      }
1347
      else {
1348
        drush_log(dt('No version deployed, not sending New Relic notification.'), 'warning');
1349
      }
1350
      return TRUE;
1351
    }
1352
  }
1353
}
1354