Skip to content

Instantly share code, notes, and snippets.

@omares
Created March 15, 2013 15:01
Show Gist options
  • Save omares/5170464 to your computer and use it in GitHub Desktop.
Save omares/5170464 to your computer and use it in GitHub Desktop.
spawn = require('child_process').spawn
Q = require 'q'
# A capistrano command wrapper providing various helper methods for easier usage.
#
# Dependencies:
# child_process
# q: 0.9.x
class Capistrano
args = []
timeout = 10 * 60 * 1000
constructor: (@capistranoPath) ->
# Public: Set Stage for multistage deployment
#
# stage - Stage as String
#
# Returns Capistrano
setStage: (@stage) ->
return @
# Public: Add an argument that should be appended to
#
# name - Argument key as String
# value - Argument value as String
#
# Returns Capistrano
addArg: (name, value) ->
args.push {name: name, value: value}
return @
# Public: Execute the capistrano deploy command
#
# Returns a nodejs ChildProcess object
deploy: ->
@exec 'deploy'
# Public: Execute the given Capistrano command
#
# command - The to be executed capistrano command as String
#
# Returns a Q.Promise
exec: (command) ->
deferredProcess = Q.defer()
options = {cwd: @capistranoPath}
preparedArgs = @prepareArguments command
process = spawn 'cap', preparedArgs, options
t = () =>
deferredProcess.notify "Process max execution time reached. Sending SIGTERM."
process.kill "SIGKILL"
timeoutId = setTimeout t, timeout
process.stdout.on 'data', (data) =>
deferredProcess.notify data
process.stderr.on 'data', (data) =>
deferredProcess.notify data
process.on 'disconnect', (data) =>
deferredProcess.notify data
process.on 'exit', (code, signal) =>
clearTimeout timeoutId
if code is 0
deferredProcess.resolve "Exitcode #{code}"
return
if signal
rejectMessage = "Received signal #{signal}"
else
rejectMessage = "Exitcode #{code}"
deferredProcess.reject new Error rejectMessage
return deferredProcess.promise
# Internal: Prepare the capistrano command arguments in correct order
#
# command - The to be executed capistrano command as String
#
# Returns an Array of arguments in the correct order
prepareArguments: (command) ->
preparedArgs = [command]
preparedArgs.unshift @stage if @stage?
for arg in args
preparedArgs.push "-S"
preparedArgs.push "#{arg.name}=#{arg.value}"
return preparedArgs
module.exports = Capistrano
# Description:
# Allows authorized users to run a capistrano deployment via hubot.
#
# Commands:
# hubot deploy branch <branch> of <project> onto <stage> - Verbose, allows to set all parameters.
# hubot deploy <branch> <project> <stage> - Same as verbose but less words :)
# hubot deploy <project> - Quick. Defaults to master branch and production stage.
#
# Examples:
# hubot deploy branch refacotring of projectX onto staging
# hubot deploy projectX (better-branch) testing
# hubot deploy projectX
xregexp = require('xregexp').XRegExp
outputCache = require './Capistrano/OutputCache.coffee'
class CapistranoDeployment
# Only one deployment at a time is allowed.
# true if active, else false.
active = false
# XregExp patterns to react on
# Array of XregExp pattern
messagePatterns = [
'deploy (?<project>\\S*) onto (?<stage>\\S*)',
'deploy branch (?<branch>\\S*) of (?<project>\\S*) onto (?<stage>\\S*)',
'deploy (?<branch>\\S*) (?<project>\\S*) (?<stage>\\S*)'
'deploy (?<project>\\S*)'
]
# robot - A hubot Robot object
# provider - Name of the capistrano deployment data provider
constructor: (@robot, provider) ->
@loadProvider provider
@setupCommands()
setupCommands: ->
accessCheck = (response) =>
if @validateAccessRole(response.message.user) is false
response.reply "Sorry but you dont have access to the capistrano:deployment role."
return false
return true
@robot.respond /deploy.*/i, (response) =>
capistranoAargs = @parseArguments response.message.text, messagePatterns
return if capistranoAargs is null
return if accessCheck(response) is false
if @isActive()
response.reply "Sorry but only one active capistrano command at a time is allowed."
return
@execute response, capistranoAargs
@robot.respond /capistrano deployment status/i, (response) =>
return if accessCheck(response) is false
if @isActive() is false
response.reply "Currently no capistrano deployment in progress."
return
response.send outputCache.getOutput() or "Currently no capistrano output available."
# Internal: Load the capistrano deployment data provider
#
# provider - Path or name of the to be loaded provider
#
# Returns nothing
loadProvider: (provider) ->
providerPath = if provider.indexOf('/') > 0 then provider else "./Capistrano/#{provider}"
@provider = require providerPath
# Internal: Parse arguments out of input message
#
# message - Input message as String
# patterns - Array of patterns to use for argument parsing
#
# Returns Null or an Object containing the found arguments
parseArguments: (message, patterns) ->
for pattern in patterns
regex = xregexp pattern
matches = xregexp.exec message, regex
return matches if matches isnt null
return null
# Internal:
#
# Returns bool
validateAccessRole: (user) ->
if @robot.Auth?
return true
return @robot.Auth.hasRole user.name, 'capistrano:deployment'
# Public: Execute capistrano deployment, cache capistrano output and outputs state information
#
# response - Hubot Response object
# capistranoAargs - Object of capistrano args
#
# Returns nothing
execute: (response, capistranoAargs) ->
@capistranoActive()
capistranoPromise = @provider.provideCapistrano capistranoAargs.project
capistranoPromise.progress (message) =>
response.send message
.then (capistrano) =>
response.send "Starting deployment ..."
return @spawnDeployment(capistrano, capistranoAargs).progress (message) =>
outputCache.addConcatedLine message
.then (message) =>
response.send "Successfully deployed #{capistranoAargs.project}."
.catch (error) =>
response.send "Deployment failed: #{error.message or error}"
.finally () =>
response.send "Replaying capistrano output ..."
response.send outputCache.getOutputAndClear()
@capistranoInactive()
.done()
# Internal: Spawn the capistrano process
#
# args - Argumentobject
#
# Returns a Q.promise
spawnDeployment: (cap, args) ->
cap.setStage args.stage or 'production'
cap.addArg 'branch', args.branch or 'master'
cap.deploy()
# Internal: Mark the deploymend as active
# Returns nothing
capistranoActive: ->
active = true
# Internal: Mark the deploymend as inactive
# Returns nothing
capistranoInactive: ->
active = false
# Internal: Whats the state?
# Returns bool
isActive: ->
return active
module.exports = (robot) ->
new CapistranoDeployment robot, "GitProvider"
git = require 'gitty'
printf = require 'printf'
Q = require 'q'
Capistrano = require './Capistrano'
env = process.env
# Check for auth credentials in the environment and determine them by priority.
#
# Credentials are priorized in following order:
# 1. CAPISTRANO_GIT_USER and CAPISTRANO_GIT_PASSWORD
# 2. HUBOT_GITHUB_TOKEN
# 3. HUBOT_GITHUB_USER and HUBOT_GITHUB_PASSWORD
if env.CAPISTRANO_GIT_USER and env.CAPISTRANO_GIT_PASSWORD
auth =
user: env.CAPISTRANO_GIT_USER
password: env.CAPISTRANO_GIT_PASSWORD
else if env.HUBOT_GITHUB_TOKEN
auth =
user: env.HUBOT_GITHUB_TOKEN
password: null
else if env.HUBOT_GITHUB_USER and env.HUBOT_GITHUB_PASSWORD
auth =
user: env.HUBOT_GITHUB_USER
password: env.HUBOT_GITHUB_PASSWORD
else
auth = null
# Base git uri in printf format
# Use the printf "%(repo)s" argument mapping feature to specify where to place the repository name
if env.CAPISTRANO_GIT_URL
gitUrl = env.CAPISTRANO_GIT_URL
else
throw new Error "Please set the CAPISTRANO_GIT_URL environment setting to use the git provider."
# Fetches or updates the capistrano sources from git and provides a Capistrano Object referecing the directory.
#
# Dependencies:
# gitty: 1.x
# printf: 0.1.x
# q: 0.9.x
# Capistrano
class GitProvider
# gitUrl - git url where to fetch sources from
# use the printf "%(repo)s" argument mapping feature to specify where to place the repository name
# auth - Authorization data. Object consisting of user and password property.
constructor: (@gitUrl, @auth) ->
# Public: Fetches capistrano sources and returns an object wrapping the fetched sources
#
# repositoryName - Name of the github repository
#
# Returns a Q.promise
provideCapistrano: (repositoryName) ->
@deferredCapistrano = Q.defer()
repo = @getRepository repositoryName
if @isInitialized repo
Q.fcall @updateRepository, repo
else
Q.fcall @initRepository, repositoryName, repo.path
return @deferredCapistrano.promise
# Internal
#
# repositoryName - Name of the github repository
#
# Returns a gitty.Repository
getRepository: (repositoryName) ->
return new git.Repository @getWorkingDirectory repositoryName
# Internal
#
# repositoryName - Name of the github repository
#
# Returns Path to git working directory
getWorkingDirectory: (repositoryName) ->
"#{process.cwd()}/shared/capistrano/#{repositoryName}"
# Internal
#
# repository - gitty.Repository
#
# Returns Bool
isInitialized: (repository) ->
return repository.isRepository
# Internal: Update repository sources
#
# repository - gitty.Repository
#
# Returns nothing
updateRepository: (repository) =>
callback = (error, success) =>
@deferredError error if error
@deferredSuccess repository.path if success
@deferredCapistrano.notify "Updating sources ..."
repository.pull "origin", "master", callback
# Internal: Initializes an empty repository via cloning
#
# name - Repository name
# localPath - Path to working directory
#
# Returns nothing
initRepository: (name, localPath) =>
gitUrl = @getGitUrl name
console.log gitUrl
callback = (error, success) =>
@deferredError error if error
@deferredSuccess localPath if success
@deferredCapistrano.notify "Cloning repository from #{gitUrl} ..."
git.clone localPath, gitUrl, callback, @auth
# Internal: Send promise reject event
#
# message - Error message
#
# Returns nothing
deferredError: (message) ->
@deferredCapistrano.reject new Error message
# Internal: Send promise resolve event, passing Capistrano as event param
#
# path - Path to capistrano sources
#
# Returns nothing
deferredSuccess: (path) ->
@deferredCapistrano.resolve new Capistrano path
# Internal: Build github url
#
# repositoryName - Name of the github repository
#
# Returns git url
getGitUrl: (repositoryName) ->
printf @gitUrl, {repo: repositoryName}
module.exports = new GitProvider gitUrl, auth
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment