Created
March 15, 2013 15:01
-
-
Save omares/5170464 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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