diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..45a9174 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,2 @@ +Metrics/BlockLength: + Enabled: false diff --git a/Dockerfile b/Dockerfile index 1f4b90b..ba95a06 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,4 +5,4 @@ COPY . /usr/src/app RUN cd /usr/src/app && bundle install -ENTRYPOINT ["ruby", "/usr/src/app/app.rb"] \ No newline at end of file +ENTRYPOINT ["/usr/src/app/entrypoint.sh"] diff --git a/Gemfile b/Gemfile index b5aabfc..e72c38f 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,9 @@ source 'https://rubygems.org' do gem 'aws-sdk-states' -end \ No newline at end of file + + group :test do + gem 'nokogiri' + gem 'rspec' + gem 'rspec-mocks' + end +end diff --git a/Gemfile.lock b/Gemfile.lock index 7d84981..bc02bca 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -16,13 +16,34 @@ GEM aws-sigv4 (~> 1.1) aws-sigv4 (1.5.2) aws-eventstream (~> 1, >= 1.0.2) + diff-lcs (1.5.1) jmespath (1.6.2) + nokogiri (1.16.2-arm64-darwin) + racc (~> 1.4) + racc (1.7.3) + rspec (3.13.0) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.0) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.1) PLATFORMS arm64-darwin-21 + arm64-darwin-23 DEPENDENCIES aws-sdk-states! + nokogiri! + rspec! + rspec-mocks! BUNDLED WITH 2.3.7 diff --git a/app.rb b/app.rb index 0ce4a5a..1605a5e 100644 --- a/app.rb +++ b/app.rb @@ -1,6 +1,12 @@ +# frozen_string_literal: true + +$LOAD_PATH.unshift File.expand_path('.', 'lib') + require 'aws-sdk-states' require 'json' +require 'deployment' + STDOUT.sync = true execution_arn = ENV['EXECUTION_ARN'] @@ -12,60 +18,21 @@ aws_client = Aws::States::Client.new(region: 'eu-west-1') -def deployment_status(aws_client, execution_arn) - aws_client.describe_execution({ execution_arn: execution_arn }).status -end - -def failure_reason(aws_client, execution_arn) - resp = aws_client.get_execution_history({ - execution_arn: execution_arn, - max_results: 2, - reverse_order: true - }) - - event_type = resp.events[1].type - if event_type == "LambdaFunctionFailed" - error_message = JSON.parse(resp.events[1].lambda_function_failed_event_details.cause)['errorMessage'] - if error_message.include? "Pre flight checks failed" - preflight_checks_output = error_message.lines[1] - forward_deploy_check_result = preflight_checks_output.match(/ForwardDeployCheck=>"(.*?)"/i).captures[0] - if forward_deploy_check_result == "FAILED" - deploy_fail_reason = "Forward deploy check FAILED. No need to panic! "\ - "This likely means your commit has already been deployed as part of a previous deploy. "\ - "To confirm you can check whether your SHA is a parent commit to the currently deployed SHA. "\ - "You can figure out the currently deployed SHA by following this guide https://www.notion.so/freeagent/Deployment-Runbooks-29796221387e40b7abbb217d7d33c4ac?pvs=4#3bfa2ab5d3ab4c33b7a46522027f94bb" - return deploy_fail_reason - end - end - deploy_fail_reason = error_message - elsif event_type == "FailStateEntered" - error_message = JSON.parse(JSON.parse(resp.events[1].state_entered_event_details.input)['Error']['Cause'])['errorMessage'] - if error_message.include? "ECS" and error_message.include? "IN_PROGRESS" - deploy_fail_reason = "Sidekiq workers failed to start or failed to stabilise." - else - deploy_fail_reason = "Failure message: #{error_message}. Please investigate further here if required https://eu-west-1.console.aws.amazon.com/states/home?region=eu-west-1#/executions/details/#{execution_arn}" - end - else - deploy_fail_reason = "Uncaught failure. Please investigate here https://eu-west-1.console.aws.amazon.com/states/home?region=eu-west-1#/executions/details/#{execution_arn}" - end - - return deploy_fail_reason -end +deploy = Deployment::Deployment.new(aws_client, execution_arn) -# One of: "RUNNING", "SUCCEEDED", "FAILED", "TIMED_OUT", "ABORTED" -if deployment_status(aws_client, execution_arn) == 'RUNNING' +if deploy.running? puts 'Deployment in progress...🔄' puts "Monitor at https://eu-west-1.console.aws.amazon.com/states/home?region=eu-west-1#/executions/details/#{execution_arn}" - sleep 15 until deployment_status(aws_client, execution_arn) != 'RUNNING' end -deploy_status = deployment_status(aws_client, execution_arn) -if %w[FAILED TIMED_OUT ABORTED].include?(deploy_status) - puts "Deployment Failure Status: #{deploy_status} ❌" +sleep 10 while deploy.running? + +if deploy.succeeded? + puts 'Deployment Successful 🎉' +else + puts "Deployment Failure Status: #{deploy.status} ❌" File.open(ENV['GITHUB_OUTPUT'], 'a') do |f| - f.puts "deployment_failed=true" - f.puts "deployment_failure_reason=#{failure_reason(aws_client, execution_arn)}" + f.puts 'deployment_failed=true' + f.puts "deployment_failure_reason=#{deploy.failure_reason}" end -elsif deploy_status == 'SUCCEEDED' - puts 'Deployment Successful 🎉' end diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 0000000..7b62309 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +cd /usr/src/app && bundle exec ruby app.rb diff --git a/lib/deployment.rb b/lib/deployment.rb new file mode 100644 index 0000000..7df44f4 --- /dev/null +++ b/lib/deployment.rb @@ -0,0 +1,3 @@ +require 'deployment/deployment' +require 'deployment/events/fail_state_entered' +require 'deployment/events/lambda_function_failed' diff --git a/lib/deployment/deployment.rb b/lib/deployment/deployment.rb new file mode 100644 index 0000000..59abd6e --- /dev/null +++ b/lib/deployment/deployment.rb @@ -0,0 +1,61 @@ +module Deployment + class Deployment + def initialize(aws_client, execution_arn) + @aws_client = aws_client + @execution_arn = execution_arn + end + + def status + @aws_client.describe_execution({ execution_arn: @execution_arn }).status + end + + def running? + status == 'RUNNING' + end + + def succeeded? + status == 'SUCCEEDED' + end + + def aborted? + status == 'ABORTED' + end + + def timed_out? + status == 'TIMED_OUT' + end + + def failed? + status == 'FAILED' + end + + def failure_reason + raise 'Deploy still runnning' if running? + raise 'Deploy succeeded' if succeeded? + + event_type = fail_event.type + + case event_type + when 'LambdaFunctionFailed' + Events::LambdaFunctionFailed.new(fail_event).error + when 'FailStateEntered' + Events::FailStateEntered.new(fail_event, @execution_arn).error + else + "Uncaught failure. Please investigate here https://eu-west-1.console.aws.amazon.com/states/home?region=eu-west-1#/executions/details/#{@execution_arn}" + end + end + + private + + def fail_event + # The penultimate event holds the failure reason + @aws_client.get_execution_history( + { + execution_arn: @execution_arn, + max_results: 2, + reverse_order: true + } + ).events.last + end + end +end diff --git a/lib/deployment/events/fail_state_entered.rb b/lib/deployment/events/fail_state_entered.rb new file mode 100644 index 0000000..c14c5d8 --- /dev/null +++ b/lib/deployment/events/fail_state_entered.rb @@ -0,0 +1,36 @@ +module Deployment + module Events + class FailStateEntered + def initialize(event, execution_arn) + @event = event + @execution_arn = execution_arn + end + + def error + if sidekiq_error? + 'Sidekiq workers failed to start or failed to stabilise.' + else + "Failure message: #{error_message}. Please investigate further here if required https://eu-west-1.console.aws.amazon.com/states/home?region=eu-west-1#/executions/details/#{@execution_arn}" + end + end + + private + + def error_message + @error_message ||= JSON.parse(JSON.parse(@event.state_entered_event_details.input)['Error']['Cause'])['errorMessage'] + end + + def in_progress? + error_message.include? 'IN_PROGRESS' + end + + def ecs_error? + error_message.include? 'ECS' + end + + def sidekiq_error? + ecs_error? && in_progress? + end + end + end +end diff --git a/lib/deployment/events/lambda_function_failed.rb b/lib/deployment/events/lambda_function_failed.rb new file mode 100644 index 0000000..5d7618b --- /dev/null +++ b/lib/deployment/events/lambda_function_failed.rb @@ -0,0 +1,42 @@ +module Deployment + module Events + class LambdaFunctionFailed + def initialize(event) + @event = event + end + + def error + if preflight_checks_failed? && forward_deploy_check_failed? + 'Forward deploy check FAILED. No need to panic! '\ + 'This likely means your commit has already been deployed as part of a previous deploy. '\ + 'To confirm you can check whether your SHA is a parent commit to the currently deployed SHA. '\ + 'You can figure out the currently deployed SHA by following this guide https://www.notion.so/freeagent/Deployment-Runbooks-29796221387e40b7abbb217d7d33c4ac?pvs=4#3bfa2ab5d3ab4c33b7a46522027f94bb' + else + error_message + end + end + + private + + def error_message + @error_message ||= JSON.parse(@event.lambda_function_failed_event_details.cause)['errorMessage'] + end + + def preflight_checks_output + error_message.lines[1] + end + + def preflight_checks_failed? + error_message.include? 'Pre flight checks failed' + end + + def forward_deploy_check_result + preflight_checks_output.match(/ForwardDeployCheck=>"(.*?)"/i).captures[0] + end + + def forward_deploy_check_failed? + forward_deploy_check_result == 'FAILED' + end + end + end +end diff --git a/spec/app_spec.rb b/spec/app_spec.rb new file mode 100644 index 0000000..9eb67e8 --- /dev/null +++ b/spec/app_spec.rb @@ -0,0 +1,117 @@ +require 'rspec' +require 'stringio' + +require 'aws-sdk-states' + +RSpec.describe 'app' do + let(:mock_client) { double('Aws::States::Client') } + let(:execution_arn) { 'arn:aws:states:eu-west-1:123456789012:execution:my-execution-flow' } + let(:test_github_log) { Tempfile.new('github_output') } + + before(:each) do + ENV['EXECUTION_ARN'] = execution_arn + ENV['GITHUB_OUTPUT'] = test_github_log.path + allow(Aws::States::Client).to receive(:new).and_return(mock_client) + allow(mock_client).to receive(:describe_execution).with(any_args).and_return(desc_exec_resp) + allow(mock_client).to receive(:get_execution_history).with(any_args).and_return(get_exec_history) + end + + context 'when deployment was successful' do + let(:desc_exec_resp) { Aws::States::Types::DescribeExecutionOutput.new(status: 'SUCCEEDED') } + let(:get_exec_history) {} + + it 'outputs "Deployment Successful 🎉"' do + $stdout = StringIO.new + + # Execute the script + load "#{__dir__}/../app.rb" + + # Assert the output + expect($stdout.string).to eq("Deployment Successful 🎉\n") + end + end + + context 'when forward deploy check failed' do + let(:desc_exec_resp) { Aws::States::Types::DescribeExecutionOutput.new(status: 'FAILED') } + let(:get_exec_history) do + Aws::States::Types::GetExecutionHistoryOutput.new( + events: [ + '', + Aws::States::Types::HistoryEvent.new( + type: 'LambdaFunctionFailed', + lambda_function_failed_event_details: Aws::States::Types::LambdaFunctionFailedEventDetails.new( + cause: ' + { + "errorMessage": "Pre flight checks failed:\n{\"Checks\"=>{:RequiredParameters=>\"PASSED\", :CommitCheck=>\"PASSED\", :ScheduleCheck=>\"PASSED\", :ForwardDeployCheck=>\"FAILED\"}, \"Status\"=>\"FAILED\"}", + "errorType": "Function", + "stackTrace": [ + "/var/task/pre_flight_checks.rb:145:in `handler" + ] + }' + ) + ) + ] + ) + end + + it 'outputs that it failed' do + $stdout = StringIO.new + + # Execute the script + load "#{__dir__}/../app.rb" + + # Assert the output + expect($stdout.string).to eq("Deployment Failure Status: FAILED ❌\n") + expect(File.readlines(test_github_log.path)).to eq( + [ + "deployment_failed=true\n", + 'deployment_failure_reason=' \ + 'Forward deploy check FAILED. No need to panic! '\ + 'This likely means your commit has already been deployed as part of a previous deploy. '\ + 'To confirm you can check whether your SHA is a parent commit to the currently deployed SHA. '\ + "You can figure out the currently deployed SHA by following this guide https://www.notion.so/freeagent/Deployment-Runbooks-29796221387e40b7abbb217d7d33c4ac?pvs=4#3bfa2ab5d3ab4c33b7a46522027f94bb\n" + ] + ) + end + end + + context 'when sidekiq failed to start' do + let(:desc_exec_resp) { Aws::States::Types::DescribeExecutionOutput.new(status: 'FAILED') } + let(:get_exec_history) do + Aws::States::Types::GetExecutionHistoryOutput.new( + events: [ + '', + Aws::States::Types::HistoryEvent.new( + type: 'FailStateEntered', + state_entered_event_details: Aws::States::Types::StateEnteredEventDetails.new( + input: ' + { + "Error": { + "Cause":"{\"errorMessage\":\"ECS deployment status: IN_PROGRESS\",\"errorType\":\"Function\",\"stackTrace\":[\"/var/task/ecs_deployment_handler.rb:49:in `handler\"]}", + "error":"Function","resource":"invoke","resourceType":"lambda}" + } + } + ' + ) + ) + ] + ) + end + + it 'outputs that it failed' do + $stdout = StringIO.new + + # Execute the script + load "#{__dir__}/../app.rb" + + # Assert the output + expect($stdout.string).to eq("Deployment Failure Status: FAILED ❌\n") + expect(File.readlines(test_github_log.path)).to eq( + [ + "deployment_failed=true\n", + "deployment_failure_reason=Sidekiq workers failed to start or failed to stabilise.\n" + ] + ) + end + end +end diff --git a/spec/deployment_spec.rb b/spec/deployment_spec.rb new file mode 100644 index 0000000..ceff5af --- /dev/null +++ b/spec/deployment_spec.rb @@ -0,0 +1,208 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Deployment::Deployment do + subject(:deployment) { Deployment::Deployment.new(mock_client, execution_arn) } + + let(:mock_client) { double('Aws::States::Client') } + let(:execution_arn) { 'arn:aws:states:eu-west-1:123456789012:execution:my-execution-flow' } + let(:get_exec_history) { [] } + + before(:each) do + allow(Aws::States::Client).to receive(:new).and_return(mock_client) + allow(mock_client).to receive(:describe_execution).with(any_args).and_return(desc_exec_resp) + allow(mock_client).to receive(:get_execution_history).with(any_args).and_return(get_exec_history) + end + + context 'when deploy is running' do + let(:desc_exec_resp) { Aws::States::Types::DescribeExecutionOutput.new(status: 'RUNNING') } + + describe 'status' do + it 'returns RUNNING' do + expect(deployment.status).to eq('RUNNING') + end + end + + describe 'running?' do + it 'returns true' do + expect(deployment.running?).to be true + end + end + + describe 'failure_reason' do + it 'should raise an exception' do + expect { deployment.failure_reason }.to raise_error(RuntimeError) + end + end + end + + context 'when deploy succeeded' do + let(:desc_exec_resp) { Aws::States::Types::DescribeExecutionOutput.new(status: 'SUCCEEDED') } + + describe 'status' do + it 'returns SUCCEEDED' do + expect(deployment.status).to eq('SUCCEEDED') + end + end + + describe 'succeeded?' do + it 'returns true' do + expect(deployment.succeeded?).to be true + end + end + + describe 'running?' do + it 'returns false' do + expect(deployment.running?).to be false + end + end + + describe 'failure_reason' do + it 'should raise an exception' do + expect { deployment.failure_reason }.to raise_error(RuntimeError) + end + end + end + + context 'when deploy failed' do + let(:desc_exec_resp) { Aws::States::Types::DescribeExecutionOutput.new(status: 'FAILED') } + + describe 'status' do + it 'returns FAILED' do + expect(deployment.status).to eq('FAILED') + end + end + + describe 'failed?' do + it 'returns true' do + expect(deployment.failed?).to be true + end + end + + describe 'running?' do + it 'returns false' do + expect(deployment.running?).to be false + end + end + end + + context 'when deploy has been aborted' do + let(:desc_exec_resp) { Aws::States::Types::DescribeExecutionOutput.new(status: 'ABORTED') } + + describe 'status' do + it 'returns ABORTED' do + expect(deployment.status).to eq('ABORTED') + end + end + + describe 'aborted?' do + it 'returns true' do + expect(deployment.aborted?).to be true + end + end + + describe 'running?' do + it 'returns false' do + expect(deployment.running?).to be false + end + end + end + + context 'when deploy has timed out' do + let(:desc_exec_resp) { Aws::States::Types::DescribeExecutionOutput.new(status: 'TIMED_OUT') } + + describe 'status' do + it 'returns TIMED_OUT' do + expect(deployment.status).to eq('TIMED_OUT') + end + end + + describe 'timed_out?' do + it 'returns true' do + expect(deployment.timed_out?).to be true + end + end + + describe 'running?' do + it 'returns false' do + expect(deployment.running?).to be false + end + end + end + + context 'when deploy has failed' do + let(:desc_exec_resp) { Aws::States::Types::DescribeExecutionOutput.new(status: 'FAILED') } + + describe 'status' do + it 'returns FAILED' do + expect(deployment.status).to eq('FAILED') + end + end + + describe 'failed?' do + it 'returns true' do + expect(deployment.failed?).to be true + end + end + + describe 'running?' do + it 'returns false' do + expect(deployment.running?).to be false + end + end + + context 'when due to LambdaFunctionFailed event' do + let(:get_exec_history) do + Aws::States::Types::GetExecutionHistoryOutput.new( + events: [ + '', + Aws::States::Types::HistoryEvent.new(type: 'LambdaFunctionFailed') + ] + ) + end + + describe 'failure_reason' do + it 'calls LambdaFunctionFailedError.new(fail_event).error' do + expect_any_instance_of(Deployment::Events::LambdaFunctionFailed).to receive(:error) + subject.failure_reason + end + end + end + + context 'when due to FailStateEntered event' do + let(:get_exec_history) do + Aws::States::Types::GetExecutionHistoryOutput.new( + events: [ + '', + Aws::States::Types::HistoryEvent.new(type: 'FailStateEntered') + ] + ) + end + + describe 'failure_reason' do + it 'calls FailStateEntered.new(fail_event).error' do + expect_any_instance_of(Deployment::Events::FailStateEntered).to receive(:error) + subject.failure_reason + end + end + end + + context 'when due to and unknown event' do + let(:get_exec_history) do + Aws::States::Types::GetExecutionHistoryOutput.new( + events: [ + '', + Aws::States::Types::HistoryEvent.new(type: 'UnknownError') + ] + ) + end + + describe 'failure_reason' do + it 'returns a generic error message' do + expect(subject.failure_reason).to start_with('Uncaught failure.') + end + end + end + end +end diff --git a/spec/fail_state_entered_spec.rb b/spec/fail_state_entered_spec.rb new file mode 100644 index 0000000..d9aa4f3 --- /dev/null +++ b/spec/fail_state_entered_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Deployment::Events::FailStateEntered do + subject(:deployment) { Deployment::Events::FailStateEntered.new(event, execution_arn) } + + let(:execution_arn) { 'arn:aws:states:eu-west-1:123456789012:execution:my-execution-flow' } + + context 'when Sidekiq has failed to start' do + let(:event) do + Aws::States::Types::HistoryEvent.new( + type: 'FailStateEntered', + state_entered_event_details: Aws::States::Types::StateEnteredEventDetails.new( + input: ' + { + "Error": { + "Cause":"{\"errorMessage\":\"ECS deployment status: IN_PROGRESS\",\"errorType\":\"Function\",\"stackTrace\":[\"/var/task/ecs_deployment_handler.rb:49:in `handler\"]}", + "error":"Function", + "resource":"invoke", + "resourceType":"lambda" + } + } + ' + ) + ) + end + + describe(:error) do + it 'returns the correct error message' do + expect(deployment.error).to eq('Sidekiq workers failed to start or failed to stabilise.') + end + end + end + + context 'when there is some other failure' do + let(:event) do + Aws::States::Types::HistoryEvent.new( + type: 'FailStateEntered', + state_entered_event_details: Aws::States::Types::StateEnteredEventDetails.new( + input: ' + { + "Error": { + "Cause":"{\"errorMessage\":\"Some other error\"}", + "error":"Function", + "resource":"invoke", + "resourceType":"lambda" + } + } + ' + ) + ) + end + + describe(:error) do + it 'returns the error message' do + expect(deployment.error).to start_with('Failure message: Some other error') + end + end + end +end diff --git a/spec/lambda_function_failed_spec.rb b/spec/lambda_function_failed_spec.rb new file mode 100644 index 0000000..d17fb08 --- /dev/null +++ b/spec/lambda_function_failed_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Deployment::Events::LambdaFunctionFailed do + subject(:deployment) { Deployment::Events::LambdaFunctionFailed.new(event) } + + context 'forward deploy pre-flight check failed' do + let(:event) do + Aws::States::Types::HistoryEvent.new( + type: 'LambdaFunctionFailed', + lambda_function_failed_event_details: Aws::States::Types::LambdaFunctionFailedEventDetails.new( + cause: ' + { + "errorMessage": "Pre flight checks failed:\n{\"Checks\"=>{:RequiredParameters=>\"PASSED\", :CommitCheck=>\"PASSED\", :ScheduleCheck=>\"PASSED\", :ForwardDeployCheck=>\"FAILED\"}, \"Status\"=>\"FAILED\"}", + "errorType": "Function", + "stackTrace": [ + "/var/task/pre_flight_checks.rb:145:in `handler" + ] + }' + ) + ) + end + + describe(:error) do + it 'returns the correct error message' do + expect(deployment.error).to start_with('Forward deploy check FAILED.') + end + end + end + + context 'some other pre-flight check failed' do + let(:event) do + Aws::States::Types::HistoryEvent.new( + type: 'LambdaFunctionFailed', + lambda_function_failed_event_details: Aws::States::Types::LambdaFunctionFailedEventDetails.new( + cause: ' + { + "errorMessage": "Pre flight checks failed:\n{\"Checks\"=>{:RequiredParameters=>\"FAILED\", :CommitCheck=>\"PASSED\", :ScheduleCheck=>\"PASSED\", :ForwardDeployCheck=>\"PASSED\"}, \"Status\"=>\"FAILED\"}", + "errorType": "Function", + "stackTrace": [ + "/var/task/pre_flight_checks.rb:145:in `handler" + ] + }' + ) + ) + end + + describe(:error) do + it 'returns a generic Pre flight checks failed error message' do + expect(deployment.error).to start_with('Pre flight checks failed:') + end + end + end + + context 'some other function failed' do + let(:event) do + Aws::States::Types::HistoryEvent.new( + type: 'LambdaFunctionFailed', + lambda_function_failed_event_details: Aws::States::Types::LambdaFunctionFailedEventDetails.new( + cause: ' + { + "errorMessage": "Some other error", + "errorType": "Function", + "stackTrace": [ + "/var/task/another_function.rb:145:in `handler" + ] + }' + ) + ) + end + + describe(:error) do + it 'returns the error message' do + expect(deployment.error).to start_with('Some other error') + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..b4ce74d --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,3 @@ +$LOAD_PATH.unshift File.expand_path('../', 'lib') +require 'deployment' +require 'aws-sdk-states'