require "metaforce" require "base64" module AbuseTheForce attr_accessor :client, :can_notify begin require "terminal-notifier" @can_notify = true rescue LoadError @can_notify = false end # Write error to screen def self.pute(s, fatal=false) puts "Error: #{s}" # If fatal error, exit if fatal exit 1 end end # Write warning to screen def self.putw(s) puts "Warning: #{s}" end # builds the client instance of Metaforce def self.build_client target = Atf_Config.active_target puts target.username @client = Metaforce.new :username => target.username, :password => target.get_password, :security_token => target.security_token Metaforce.configuration.host = target.host # Supress some of the verbose logging Metaforce.configuration.log = false end # Fetches a single file from the server def self.retrieve_file(metadata_type, full_name) if @client == nil build_client end # Backup old Manifest FileUtils.copy( File.join(Atf_Config.get_project_path, 'package.xml'), File.join(Atf_Config.get_project_path, 'package.xml-bak') ) manifest = Metaforce::Manifest.new(metadata_type => [full_name]) @client.retrieve_unpackaged(manifest). extract_to(Atf_Config.get_project_path). on_complete { |job| puts "Finished retrieve #{job.id}!" }. on_error { |job| puts "Something bad happened!" }. on_poll { |job| puts "Polling for #{job.id}!" }. perform # Restore old Manifest FileUtils.move( File.join(Atf_Config.get_project_path, 'package.xml-bak'), File.join(Atf_Config.get_project_path, 'package.xml') ) end # Fetches a whole project from the server def self.retrieve_project() if @client == nil build_client end if File.file? File.join(Atf_Config.get_project_path, 'package.xml') @client.retrieve_unpackaged(File.expand_path(Atf_Config.src + '/package.xml')). extract_to(Atf_Config.get_project_path). on_complete { |job| puts "Finished retrieve #{job.id}!" }. on_error { |job| puts "Something bad happened!" }. on_poll { |job| puts "Polling for #{job.id}!" }. perform else puts "#{Atf_Config.get_project_path}: Not a valid project path" end end def self.deploy_test(dpath, test_name) options = { :run_tests => [ test_name ], :rollback_on_error => true } deploy_project(dpath, options) end def self.deploy_project(dpath=Atf_Config.get_project_path, options={ :rollback_on_error => true }) # Result hash that we can use for our results atf_result = {:success => nil, :run => nil, :failed => nil} if @client == nil build_client end # Set auto update options[:auto_update_package] = true if File.file? File.join(dpath, 'package.xml') @client.deploy(File.expand_path(dpath), options). on_complete { |job| puts "Finished deploy #{job.id}!" result = job.result if result != nil # Need messages in an array unless result.messages.kind_of? Array result.messages = [].push result.messages end # If running a test, see if there really was a success if options[:run_tests] != nil result.success = true result.messages.each do |m| result.success = result.success && m.success end end # Check if this was a test execution and successfully deployed if options[:run_tests] != nil && result.success atf_result[:success] = result.run_test_result.num_failures == "0" # Display a quick Success or Failure puts "\nTests #{atf_result[:success] ? "SUCCESS" : "FAILURE"}" # Display overview of number of successes and failures atf_result[:run] = result.run_test_result.num_tests_run atf_result[:failed] = result.run_test_result.num_failures puts "TESTS RUN: #{atf_result[:run]} FAILURES: #{atf_result[:failed]}" if result.run_test_result.failures != nil # Make sure failures is an array unless result.run_test_result.failures.kind_of? Array result.run_test_result.failures = [].push result.run_test_result.failures end result.run_test_result.failures.each do |m| # Print our error in teh format "filename:line:column type in object message" if !m.success puts "#{m.name}.#{m.method_name}: #{m.message}" puts "Stack Trace: #{m.stack_trace}" puts "" end # not success end # loop through test faiulres end # failures != nil if Atf_Config.notify == nil Atf_Config.notify = @can_notify Atf_Config.dump_settings end if @can_notify && Atf_Config.notify TerminalNotifier.notify("Tests run: #{atf_result[:run]} Failures: #{atf_result[:failed]}", :title => "AbuseTheForce", :subtitle => "Test Execution: #{(atf_result[:success] ? "SUCCESS" : "FAILURE")}" ) end else # run_test_result != nil puts "\nDeploy #{result.success ? "SUCCESS" : "FAILURE"}" atf_result[:success] = result.success # Not a test execution, so deployment # If a failed deploy, print errors if result.success == false if result.messages.any? puts "DEPLOY ERRORS: #{result.messages.reject { |m| m.success }.size}" result.messages.each do |m| # If the path is not from the project, fix it unless m.file_name.starts_with? Atf_Config.src m.file_name = m.file_name.sub(/[a-zA-Z._-]*\//, Atf_Config.src + '/') end # Print our error in the format "filename:line:column type in object message" if !m.success puts "#{m.file_name}:#{m.line_number}:#{m.column_number} #{m.problem_type} in #{m.full_name} #{m.problem}" end end else puts "DEPLOY ERRORS: UKNOWN" end end # success == false if Atf_Config.notify == nil Atf_Config.notify = @can_notify Atf_Config.dump_settings end if @can_notify && Atf_Config.notify TerminalNotifier.notify(" ", :title => "AbuseTheForce", :subtitle => "Deploy: #{(atf_result[:success] ? "SUCCESS" : "FAILURE")}" ) end end # end result.run_test_result != null else end # result != nil }. on_error { |job| puts "Something bad happened!" }. on_poll { |job| puts "Polling for #{job.id}!" }. perform else puts "#{dpath}: Not a valid project path" end return atf_result end class Atf_Config class << self attr_accessor :targets, :active_target, :src, :root_dir, :notify SETTINGS_FILE=".abusetheforce.yaml" def locate_root(path = '.') temp_path = path # Look for a settings file in this path and up to root until File.file? File.join(temp_path, SETTINGS_FILE) # If we hit root, stop if temp_path == '/' break end # Didn't find one so go up one level temp_path = File.absolute_path File.dirname(temp_path) end # If we actually found it if File.file? File.join(temp_path, SETTINGS_FILE) # Return return temp_path else # Return the original path return path end end # Loads configurations from yaml def load() # Get the project root directory @root_dir = locate_root if File.file? File.join(@root_dir, SETTINGS_FILE) settings = YAML.load_file File.join(@root_dir, SETTINGS_FILE) @targets = settings[:targets] @src = settings[:src] @notify = settings[:notify] else puts "No settings file found, creating one now" # Settings file doesn't exist # Create it @targets = {} @active_target = nil @src = './src' @notify = true dump_settings end # Set the default target @targets.values.each do |target| # Check if this one is active if target.active == true # Set it if there is no default target set yet if @active_target == nil @active_target = target else puts "Two active targets set. Using #{@active_target.print}" end end end end # write settings to a yaml file def dump_settings File.open(SETTINGS_FILE, 'w') do |out| YAML.dump( { :targets => @targets, :src => @src }, out) end end # Adds a new target to the config def add_target(target) # If there are no targets yet, use this one as the default if @active_target == nil #@targets.empty? target.active = true @active_target = target end # Push the new target @targets[target.name] = target #write out the config dump_settings puts "Target Added" target.print end # Selects one target as the active target for deployment def set_active_target(name) # Empty out current active target @active_target = nil # Go through each pair @targets.each_pair do |target_name, target| # If you find a matching item if name == target_name # Check if there is already a default if @active_target == nil # Set active target.active = true # Make it default @active_target = target end else # name != name # make not active target.active = false end end if @active_target != nil # Save to yaml dump_settings # Notify the user puts "Active target changed" @active_target.print else AbuseTheForce.pute "Target with alias #{name} was not found." end end # Sets project path from default ./src def set_project_path(ppath) if File.file? File.join(ppath, 'package.xml') @src = ppath dump_settings else pute("No package.xml found in #{ppath}", true) end end # Gets the full path to the src folder def get_project_path return File.absolute_path File.join(root_dir, src) end end end # Class for holding a target class Atf_Target attr_accessor :name, :username, :password, :security_token, :host, :active def initialize(name, username, password, security_token, host="login.salesforce.com") @name = name @username = username set_password(password) @security_token = security_token @host = host @active = false; end # TODO: Provide 2 way encryption with a lock to decode passwords def set_password(password) @password = Base64.encode64(password) end def get_password() return Base64.decode64(@password) end def print #puts "#{@name}\t#{@username}\t#{@host}\t#{(@active && 'Active') || ''}" puts "#{(@active ? '*' : ' ')} #{@name}\t\t#{@username}\t#{(@host.starts_with?('test') ? 'sandbox' : '')}\t" end end end