A bot to rule them all!






Diego Kuperman | @freekey

https://diegok.github.io/a-bot-to-rule-them-all/

Hola

I ❤️ perl!

I ❤️ perl comunity!

(you)

I've been here long enough...

... to love IRC

And it's bot's!

CLI for chat

...

+5y ago we started a company

A supermarket to rule them all ;-)

Tiny team

Lots of pieces...

VMs API Thumbs-App Admin-App NGINX MogileFS MySQL Resque Workers MongoDB Redis ElasticSearch ...

Automate

as much as we can!

Puppet Ansible Rex Bash MrSh OpenVZ LXC PerlBrew Ubic Perl Git

Everything was "automated"

Yeah!, sure...

Lots of CLI's

And CLI's deps

Deploy one app on some ENV


$ cd /var/supers-puppet/rex/supers_deploy
$ rex -E production -b supers_checkout master
$ rex -E production -G web -b supers_install master
      

Deploy one app on some ENV


$ cd /var/supers-puppet/rex/supers_deploy
$ rex -E production -b supers_checkout master
$ rex -E production -G web -b supers_install master
      

Deploy workers to some boxes


$ mrsh @wf --\
       'source /home/deploy/perl5/perlbrew/etc/bashrc; cd somewhere;\
       git checkout $branch; git fetch; git diff --name-only FETCH_HEAD > /tmp/pull.last;\
       git merge FETCH_HEAD; cat /tmp/pull.last | ./script/ss_installer > /tmp/installer.last'

$ mrsh @wf -- 'source /home/deploy/perl5/perlbrew/etc/bashrc; ubic restart soysuper.db'
      

¯\_(ツ)_/¯

How do we automate the automation?

We need a bot

A CLI to rule them all!

Always online

It needs to run our

deploy tasks

It can run our

ops tasks

It can monitor things

... and alert Us!

gTalk/jabber

push notifications for free!

It would be nice if it can ...

It should be pluggable!

Bot::Pluggable

Bot::BasicBot::Pluggable

Bot::Backbone::Bot

...

dzil new Supers::Bot

package Supers::Bot;
# ABSTRACT: Soysuper jabber bot
use Moose;
use AnyEvent;
use AnyEvent::XMPP::IM::Connection;
use AnyEvent::XMPP::IM::Message;
require UNIVERSAL::require;

has user     => ( is => 'ro', required => 1 );
has password => ( is => 'ro', required => 1 );
has is_ready => ( is => 'rw', isa => 'Bool', default => sub{0} );

has plugins  => ( is => 'rw', isa => 'ArrayRef', default => sub{[]} );
has _plugin  => ( is => 'rw', isa => 'HashRef',  default => sub{{}} );
has _cv      => ( is => 'ro', default => sub {AnyEvent->condvar} );
has _xmpp    => (
    is   => 'rw',
    lazy => 1,
    builder => '_build_xmpp',
    clearer => 'reset_connection'
);
	  

sub _build_xmpp {
    my $self = shift;
    AnyEvent::XMPP::IM::Connection->new(
        jid              => $self->user,
        password         => $self->password,
        domain           => 'gmail.com',
        host             => 'talk.google.com',
        port             => 5223,
        old_style_ssl    => 1,
    );
}
	  

Init all the things!


sub BUILD {
    my $self = shift;
    $self->_init_plugins;
    $self->_init_events;
}

sub run {
    my $self = shift;
    $self->connect;
    $self->_cv->recv;
}

sub connect { shift->_xmpp->connect }
	  

Plugins


sub _init_plugins {
    my $self = shift;
    my $plugins = $self->plugins;
    $self->plugins([]);
    $self->register_plugin($_) for @{$plugins};
}

sub register_plugin {
    my ( $self, $name ) = @_;
    die 'Need plugin name to load!' unless $name;

    $name = "Supers::Bot::Plugin::$name" unless $name =~ /::/;
    if ( $name->use ) {
        my $plugin = eval { $name->new( bot => $self ) };
        if ($@) {
            warn "Failed to initialize plugin '$name': $@";
            return;
        }
        push @{$self->plugins}, $name;
        $self->_plugin->{$name} = $plugin;
        return $plugin;
    }
    else { warn "Plugin '$name' not loaded: $@" }
}
	  

Plugins event dispatcher


sub trigger {
    my ( $self, $event ) = ( shift, shift );

    for my $plugin_name ( @{$self->plugins} ) {
        my $plugin = $self->_plugin->{$plugin_name} || next;
        $plugin->$event(@_) if $plugin->can($event);
    }
}
	  

Plugins event dispatcher


sub trigger {
    my ( $self, $event ) = ( shift, shift );

    for my $plugin_name ( @{$self->plugins} ) {
        my $plugin = $self->_plugin->{$plugin_name} || next;
        $plugin->$event(@_) if $plugin->can($event);
    }
}
	  

XMPP events


sub _init_events {
    my $self = shift;

    $self->_xmpp->reg_cb(
        session_ready => sub {
            $self->is_ready(1);
            $self->trigger( 'ready' => 'xmpp' );
        },
        message => sub {
            my ( undef, $msg ) = @_;
            return unless $msg->body;
            $self->trigger( 'message' => $msg );
        },
        error => sub {
            my ( $conn, $error ) = @_;
            warn 'error: '. $error->string;
            $self->handle_error( $conn, $error );
        },
    );
}
	  

More events...


has _tick => ( is => 'ro', default => sub {
    my $self = shift;
    AnyEvent->timer ( after => 1, interval => 1,
        cb => sub { $self->trigger('tick') if $self->is_ready }
    );
});

has _sig_int => ( is => 'ro', default => sub {
    my $self = shift;
    AnyEvent->signal(
        signal => "INT",
        cb     => sub {
            $self->trigger('shutdown');
            $self->_cv->send;
        }
    );
});
	  

A plugin-less bot


#!/usr/bin/env perl
use Supers::Bot;

Supers::Bot->new(
    user     => 'bender@gmail.com',
    password => 'BiteMyShinyAss',
    plugins  => [] # meh!
)->run;
	  

Plugins & plugin role


package Supers::Bot::Plugin::HelloWorld;
use common::sense;
use Moose; with 'Supers::Bot::Plugin';

sub ready {
    say "Ready to run!";
    shift->send_message(
        'diego@soysuper.com' => 'Go go go soysuper!'
    );
}

sub tick {
    my $self = shift;
    say 'Should I check something on this tick?';
}

sub message {
    my ( $self, $msg ) = @_;
    say sprintf('Message from %s: %s', $msg->from, $msg->body);
    # Now do something with that!
}

__PACKAGE__->meta->make_immutable;
	  

#!/usr/bin/env perl
use Supers::Bot;

Supers::Bot->new(
    user     => 'bender@gmail.com',
    password => 'BiteMyShinyAss',
    plugins  => [qw/ HelloWorld /]
)->run;
	  

Time to grow up!

Solve real problems

Deploy things

Parse message and shell out

> deploy app@some-branch to some-env

package Supers::Bot::Plugin::DeployApps;
use Moose;
with 'Supers::Bot::Plugin';
with 'Supers::Bot::Role::Runner';

has task_running =>
    is        => 'rw',
    clearer   => 'task_done',
    predicate => 'is_running';

has last_msg => is => 'rw';

# Environments config for commands
has _envs => is => 'ro', default => sub{+{ ... }};
	  
> deploy app@some-branch to some-env

sub message {
    my ( $self, $msg ) = @_;

    if ( $msg->body =~ /
      ^deploy \s+ (\w+) (?:\@([^\s\?]+))? (?:\s+to\s+([^\s\?]+))? (\s*\?)?$
    /x ) {
        my ($app, $branch, $env, $dry) = ($1, $2||'master', $3||'pro', $4);

        return $msg->reply(
            sprintf( 'Still running `%s` for :%s:', $self->last_msg->body, $self->last_msg->from )
        ) if $self->is_running;

        $self->_deploy_app( $msg, $app, $branch, $env, $dry )
    }
    elsif ( $msg->body =~ /^ \s* (?:deploy \s* (?:\?|help) | help ) \s* $/ix ) {
			  # Give some help on this deploy command
    }
}
	  
> deploy app@some-branch to some-env

sub message {
    my ( $self, $msg ) = @_;

    if ( $msg->body =~ /
      ^deploy \s+ (\w+) (?:\@([^\s\?]+))? (?:\s+to\s+([^\s\?]+))? (\s*\?)?$
    /x ) {
        my ($app, $branch, $env, $dry) = ($1, $2||'master', $3||'pro', $4);

        return $msg->reply(
            sprintf( 'Still running `%s` for :%s:', $self->last_msg->body, $self->last_msg->from )
        ) if $self->is_running;

        $self->_deploy_app( $msg, $app, $branch, $env, $dry )
    }
    elsif ( $msg->body =~ /^ \s* (?:deploy \s* (?:\?|help) | help ) \s* $/ix ) {
			  # Give some help on this deploy command
    }
}
	  
> deploy app@some-branch to some-env

sub message {
    my ( $self, $msg ) = @_;

    if ( $msg->body =~ /
      ^deploy \s+ (\w+) (?:\@([^\s\?]+))? (?:\s+to\s+([^\s\?]+))? (\s*\?)?$
    /x ) {
        my ($app, $branch, $env, $dry) = ($1, $2||'master', $3||'pro', $4);

        return $msg->reply(
            sprintf( 'Still running `%s` for :%s:', $self->last_msg->body, $self->last_msg->from )
        ) if $self->is_running;

        $self->_deploy_app( $msg, $app, $branch, $env, $dry )
    }
    elsif ( $msg->body =~ /^ \s* (?:deploy \s* (?:\?|help) | help ) \s* $/ix ) {
			  # Give some help on this deploy command
    }
}
	  
> deploy app@some-branch to some-env

sub message {
    my ( $self, $msg ) = @_;

    if ( $msg->body =~ /
      ^deploy \s+ (\w+) (?:\@([^\s\?]+))? (?:\s+to\s+([^\s\?]+))? (\s*\?)?$
    /x ) {
        my ($app, $branch, $env, $dry) = ($1, $2||'master', $3||'pro', $4);

        return $msg->reply(
            sprintf( 'Still running `%s` for :%s:', $self->last_msg->body, $self->last_msg->from )
        ) if $self->is_running;

        $self->_deploy_app( $msg, $app, $branch, $env, $dry );
    }
    elsif ( $msg->body =~ /^ \s* (?:deploy \s* (?:\?|help) | help ) \s* $/ix ) {
			  # Give some help on this deploy command
    }
}
	  

sub _deploy_app {
    my ( $self, $msg, $app, $branch, $env_alias, $dry ) = @_;

    # Whatever we need to build the shell commands to run...
    my $cmd = _build_cmd( $app, $env, $branch );
           || return $msg->reply("Unknown application `$app` or environment `$env_alias`");

    return $msg->reply( "This would run:\n$cmd" ) if $dry;

    $msg->reply("Deploying branch `$branch` for application `$app` on `$env_alias` environment.");
    $self->last_msg($msg); $self->task_running(1);

    $self->run_command($cmd => sub{
        my ( $self, $out, $err, $exit ) = @_;
        $msg->reply( "Done deploying `$app` on `$env_alias` environment!" );
        $self->task_done;
    });
}
	  

sub _deploy_app {
    my ( $self, $msg, $app, $branch, $env_alias, $dry ) = @_;

    # Whatever we need to build the shell commands to run...
    my $cmd = _build_cmd( $app, $env, $branch );
           || return $msg->reply("Unknown application `$app` or environment `$env_alias`");

    return $msg->reply( "This would run:\n$cmd" ) if $dry;

    $msg->reply("Deploying branch `$branch` for application `$app` on `$env_alias` environment.");
    $self->last_msg($msg); $self->task_running(1);

    $self->run_command($cmd => sub{
        my ( $self, $out, $err, $exit ) = @_;
        $msg->reply( "Done deploying `$app` on `$env_alias` environment!" );
        $self->task_done;
    });
}
	  

sub _deploy_app {
    my ( $self, $msg, $app, $branch, $env_alias, $dry ) = @_;

    # Whatever we need to build the shell commands to run...
    my $cmd = _build_cmd( $app, $env, $branch );
           || return $msg->reply("Unknown application `$app` or environment `$env_alias`");

    return $msg->reply( "This would run:\n$cmd" ) if $dry;

    $msg->reply("Deploying branch `$branch` for application `$app` on `$env_alias` environment.");
    $self->last_msg($msg); $self->task_running(1);

    $self->run_command($cmd => sub{
        my ( $self, $out, $err, $exit ) = @_;
        $msg->reply( "Done deploying `$app` on `$env_alias` environment!" );
        $self->task_done;
    });
}
	  

sub _deploy_app {
    my ( $self, $msg, $app, $branch, $env_alias, $dry ) = @_;

    # Whatever we need to build the shell commands to run...
    my $cmd = _build_cmd( $app, $env, $branch );
           || return $msg->reply("Unknown application `$app` or environment `$env_alias`");

    return $msg->reply( "This would run:\n$cmd" ) if $dry;

    $msg->reply("Deploying branch `$branch` for application `$app` on `$env_alias` environment.");
    $self->last_msg($msg); $self->task_running(1);

    $self->run_command($cmd => sub{
        my ( $self, $out, $err, $exit ) = @_;
        $msg->reply( "Done deploying `$app` on `$env_alias` environment!" );
        $self->task_done;
    });
}
	  

run_command()

is provided by Runner role


package Supers::Bot::Role::Runner;
use Moose::Role;
use AnyEvent::Util;

=head2 run_command
Helper to run shell commands async.

Accept a command and optionally a callback to be called with bot,
stdout, stderr and exit status when the command finish running.

This is a wrapper around AnyEvent::Util::run_cmd, so command can
be anything accepted by it.
=cut
sub run_command {
    my $self = shift;
    my $cmd  = shift || die 'No command given!';
    my $cb   = shift;

    my $cv = run_cmd $cmd,
      "<", "/dev/null",
      ">" , \my $stdout,
      "2>", \my $stderr;

   $cv->cb(sub {
      my $exit_status = shift->recv;
      if ( $cb ) {
        $cb->($self, $stdout, $stderr, $exit_status);
      }
   });
}
	  

We got plugins for all kind of deploys

HTTP

monitoring & alerting


package Supers::Bot::Plugin::AppsMonitor;
use Moose;
with 'Supers::Bot::Plugin';
use AnyEvent::HTTP;
use Scalar::Util qw(weaken);

has operator =>
    is      => 'rw',
    default => sub{[ map {$_.'@soysuper.com'} qw/diego unlucky/ ]};

has urls =>
    is  => 'rw',
    isa => 'ArrayRef',
    default => sub {[
        [ 'http://app1.ss:3000/' => 'Aperitivos' ],
        [ 'http://app1.ss:3000/assets/img/logo-print.png' => 0],
		# ...
    ]};

has guard     => ( is => 'rw', default => sub{{}} );
has last_time => ( is => 'rw', default => sub{time} );

sub inform_op {
    my ( $self, $msg ) = @_;
    $self->send_message( $_, $msg ) for @{$self->operator};
}
	  

sub tick {
    my $self = shift;
    my $now = time;
    return unless $now - $self->last_time > 20;
    $self->last_time($now);

    if ( my $url = shift @{$self->urls} ) {
        weaken $self;
        $self->guard->{$url->[0]} = http_get( $url->[0] => sub {
            my ($body, $heads) = @_;

            if ( $heads->{Status} == 200 ) {
                if ( my $re = $url->[1] ) {
                    $self->inform_op( sprintf('Content error on %s (!~ %s)', $url->[0], $re))
                        unless !$re || $body =~ /$re/;
                }
            }
            else {
                $self->inform_op( sprintf('Error fetching %s (status is not 200)', $url->[0]) );
            }

            push @{$self->urls}, $url;
        });
    }
}

__PACKAGE__->meta->make_immutable;
	  

We happily used it for years

... and added trivial plugins!

Evolve or die

slack


package Supers::Bot;
# ABSTRACT: Soysuper jabber bot
use Moose;
use AnyEvent;
use AnyEvent::XMPP::IM::Connection;
use AnyEvent::XMPP::IM::Message;
use Mojo::SlackRTM;
Supers::Bot::SlackMessage
require UNIVERSAL::require;

has user     => ( is => 'ro', required => 1 );
has password => ( is => 'ro', required => 1 );
has slack_token => ( is => 'ro', required => 1 );

has plugins  => ( is => 'rw', isa => 'ArrayRef', default => sub{[]} );
has _plugin  => ( is => 'rw', isa => 'HashRef',  default => sub{{}} );
has _cv    => ( is => 'ro', default => sub {AnyEvent->condvar} );
has _xmpp  => ( is => 'rw', lazy => 1, builder => '_build_xmpp', clearer => 'reset_connection' );
has _slack => ( is => 'ro', lazy => 1, default => sub { Mojo::SlackRTM->new(token => shift->slack_token) } );

sub BUILD {
    my $self = shift;
    $self->_init_plugins;
    $self->_init_events;
    $self->_init_slack;
}
	  

sub _init_slack {
    my $self = shift;
    $self->_slack->on( hello   => sub { $self->trigger( 'ready' => 'slack' ) } );
    $self->_slack->on( message => sub {
        $self->trigger(
            message => Supers::Bot::SlackMessage->new(slack => $_[0], event => $_[1])
        ) unless $_[1]->{subtype}; # skip announcements ATM
    });
}
	  

package Supers::Bot::SlackMessage;
use Moose;

# ABSTRACT: Wrapper for slack message events to make it work as much as possible like xmpp ones

has slack => ( is => 'ro', required => 1 );
has event => ( is => 'ro', required => 1 );

sub reply {
    my ( $self, $response ) = @_;
    $self->slack->send_message( $self->event->{channel} => $response );
}

sub from {
    my $self = shift;
    $self->slack->find_user_name($self->event->{user});
}

sub channel {
    my $self = shift;
    $self->slack->find_channel_name($self->event->{channel});
}

sub body { shift->event->{text} }

__PACKAGE__->meta->make_immutable;
	  

We need to know when addressed!

a new plugin role was a natural move


package Supers::Bot::Role::CmdPlugin;
use Moose::Role;
with 'Supers::Bot::Plugin';
# ABSTRACT: Role to implement bot commands/plugins for Supers::Bot

sub message {
    my ( $self, $msg ) = @_;
    my ( $to_me, $cmd, @toks ) = _parse_msg($msg);
    return unless $to_me;
    if ( my $method = $self->can("cmd_$cmd") ) {
        $method->($self, $msg, [@toks]);
    }
}
	  

sub _parse_msg {
    my $msg = pop;
    my ( $cmd, @toks ) = grep {defined} split /\s+/, $msg->body;

    if ( $cmd =~ /^(bot|bender|<\@([^>]+)>):?$/i ) {
        my $name = $2 ? $msg->slack->find_user_name($2) : $1;
        return ( 0, $cmd, @toks ) unless $name =~ /^(?:bot|bender)$/;
        $cmd = shift @toks;
        return ( 1, $cmd, @toks );
    }

    return ( _is_direct_msg($msg), $cmd, @toks );
}

sub _is_direct_msg {
    my $msg = pop;
    return 1 if ref($msg) =~ /XMPP/;
    !$msg->channel && $msg->from;
}

1;
	  
> deploy app@some-branch to some-env

sub message {
    my ( $self, $msg ) = @_;

    if ( $msg->body =~ /
      ^deploy \s+ (\w+) (?:\@([^\s\?]+))? (?:\s+to\s+([^\s\?]+))? (\s*\?)?$
    /x ) {
        my ($app, $branch, $env, $dry) = ($1, $2||'master', $3||'pro', $4);

        return $msg->reply(
            sprintf( 'Still running `%s` for :%s:', $self->last_msg->body, $self->last_msg->from )
        ) if $self->is_running;

        $self->_deploy_app( $msg, $app, $branch, $env, $dry )
    }
    elsif ( $msg->body =~ /^ \s* (?:deploy \s* (?:\?|help) | help ) \s* $/ix ) {
        # Give some help on this deploy command
    }
}
	  
> deploy app@some-branch to some-env

sub cmd_deploy {
    my ( $self, $msg, $args ) = @_;
    return unless @$args;

    my ($app, $branch, $env, $try) = split(/@/, $args[0]), $arg[2]||'pro', $args[3];

    return $msg->reply(
      sprintf( 'Still running `%s` for :%s:', $self->last_msg->body, $self->last_msg->from )
    ) if $self->is_running;

    $self->_deploy_app( $msg, $app, $branch, $env, $dry )
}

sub cmd_help {
    # Give some help on this deploy command
}
	  
> deploy help

Subcommands


sub next_cmd {
    my ( $self, $msg, $args, $ns ) = @_;

    my $subcmd = shift @$args ||'';
    $ns ||= 'subcmd';

    if ( my $method = $self->can("${ns}_$subcmd") ) {
        return $method->($self, $msg, $args);
    }
    elsif ( $method = $self->can("${ns}_default") ) {
        unshift @$args, $subcmd if $subcmd;
        return $method->($self, $msg, $args);
    }
}
	  

sub cmd_deploy { shift->next_cmd( @_ => 'deploy' ) }

sub deploy_default {
    my ( $self, $msg, $args ) = @_;
    return $self->cmd_help($msg) unless @$args;

    my ($app, $branch, $env, $try) = split(/@/, $args[0]), $arg[2]||'pro', $args[3];

    return $msg->reply(
      sprintf( 'Still running `%s` for :%s:', $self->last_msg->body, $self->last_msg->from )
    ) if $self->is_running;

    $self->_deploy_app( $msg, $app, $branch, $env, $dry )
}

sub cmd_help {
    my ( $self, $msg, $args ) = @_;
    $msg->reply('Some help...');
}
	  

Plugin::Slack

Announce to slack channels


$self->bot->trigger( announce => 'Hello some channel' )
$self->bot->trigger( announce => 'Blah!' => '#random' )
	  

Plugin::Resque

Inspection and management

Plugin::Servers

Buy and renovation

Role::Config

Brain :-p

Plugin::Sensu

Improved monitoring

Plugin::Sensu trigger trick

LoopMessage

We don't want to chat anymore!

A CLI to rule them all!




package Supers::Bot;
# ABSTRACT: Soysuper jabber bot
use Moose;
use AnyEvent;
use AnyEvent::XMPP::IM::Connection;
use AnyEvent::XMPP::IM::Message;
use Mojo::SlackRTM;
use AnyEvent::HTTPD
Supers::Bot::SlackMessage
Supers::Bot::HttpMessage
require UNIVERSAL::require;

has user     => ( is => 'ro', required => 1 );
has password => ( is => 'ro', required => 1 );
has slack_token => ( is => 'ro', required => 1 );
has httpd_port  => ( is => 'ro', default => sub {6200} );

has plugins  => ( is => 'rw', isa => 'ArrayRef', default => sub{[]} );
has _plugin  => ( is => 'rw', isa => 'HashRef',  default => sub{{}} );
has _cv    => ( is => 'ro', default => sub {AnyEvent->condvar} );
has _xmpp  => ( is => 'rw', lazy => 1, builder => '_build_xmpp', clearer => 'reset_connection' );
has _slack => ( is => 'ro', lazy => 1, default => sub { Mojo::SlackRTM->new(token => shift->slack_token) } );
has _httpd => ( is => 'ro', lazy => 1, default => sub { AnyEvent::HTTPD->new(port => shift->httpd_port) } );

sub BUILD {
    my $self = shift;
    $self->_init_plugins;
    $self->_init_events;
    $self->_init_slack;
    $self->_init_httpd;
}
	  

sub _init_httpd {
    my $self = shift;

    $self->_httpd->reg_cb(
        '/' => sub {
            shift->stop_request;
            shift->respond({ content => ['text/plain', "Soysuper Bot"] })
        },
        ''  => sub {
            $self->trigger( message => Supers::Bot::HttpMessage->new(
                httpd => shift,
                req   => shift
            ))
        }
    );
}
	  

package Supers::Bot::HttpMessage;
use Moose;
has httpd   => is => 'ro', required => 1;
has req     => is => 'ro', required => 1;
has body    => is => 'ro', lazy => 1,
    default => sub { join ' ', grep {$_} split(/\//, shift->req->url->path) };
has from    => is => 'ro', default => 'api-rest';
has channel => is => 'ro', default => '';
has _res    =>
    is      => 'ro',
    isa     => 'ArrayRef[Str]',
    traits  => ['Array'],
    default => sub {[]},
    handles => {
        responses    => 'elements',
        add_response => 'push',
        has_response => 'count',
    };

sub reply { shift->add_response(shift) }

sub DEMOLISH {
    my ( $self, $is_global ) = @_;
    return if $is_global;
    $self->has_response
      ? $self->req->respond({ content => ['text/plain', join("\n\n", $self->responses)] })
      : $self->req->respond ([ 404, 'not found', { 'Content-Type' => 'text/plain' }, 'Unknown action' ]);
}

__PACKAGE__->meta->make_immutable;
	  

$ curl http://bot.ss:6200/deploy/api
	  

$ sc deploy api
	  

$ sc deploy api + deploy workers - deploy app
	  

Thanks!

Questions?


#!/usr/bin/env perl

use common::sense;
use Mojo::UserAgent;
use Data::Dump qw/pp/;

my $ua = Mojo::UserAgent->new->inactivity_timeout(360)->request_timeout(360);

for my $plan ( get_plan(@ARGV) ) { Mojo::IOLoop->delay(@$plan)->wait }

sub get_plan {
    my $base = 'http://deploy:6200/';

    map {
        my @tasks = @$_; [

        sub{
            my $delay = shift;
            say '> '. pp(\@tasks);
            $ua->get($_ => { 'ss-from' => $ENV{USER} } => $delay->begin) for map {$base.$_} @tasks;
        },
        sub{
            my $delay = shift;
            my $count = 0;
            say '- '. $tasks[$count++].":\n". $_->res->body for @_;
            say '< '. pp(\@tasks) . "\n";
        }
    ]} map {[ split m|/\+/| ]} split( m|/\-/|, join('/', @_) )
}