minimal_pipeline 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/minimal_pipeline.rb +17 -0
- data/lib/minimal_pipeline/cloudformation.rb +98 -0
- data/lib/minimal_pipeline/crossing.rb +59 -0
- data/lib/minimal_pipeline/docker.rb +116 -0
- data/lib/minimal_pipeline/keystore.rb +61 -0
- metadata +135 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c77dd539e2d2d4c1a1639ed735bb8ef0d1e0802e59a1240e16af9e2baa03954d
|
4
|
+
data.tar.gz: 22fc7cc213e129f5ceada0fde056f61cfaa3722078b65b1dbc99b77d8fa48bcf
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: fd61094908f6d9407dbbf1b3e7b8218013125a71d0fe9582887547b297138acfc3952cab760661ab1856a4b80b1d99c12f5771128d6b5b31f27272ad93cd29c8
|
7
|
+
data.tar.gz: cf8b54cdedd4b8bccbbe5539cf6ff15742fc3968bc2ff74aa37f7db0bb9e77d6fd25905af81c635e5250b0d7120df3c8ef0f8cbc1c8399c91cef7c5de7891879
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'aws-sdk'
|
4
|
+
require 'crossing'
|
5
|
+
require 'keystore'
|
6
|
+
require 'open3'
|
7
|
+
require 'docker'
|
8
|
+
require 'json'
|
9
|
+
|
10
|
+
# Top level namespace for automation pipeline classes
|
11
|
+
class MinimalPipeline
|
12
|
+
# autoload libraries
|
13
|
+
autoload(:Cloudformation, 'minimal_pipeline/cloudformation')
|
14
|
+
autoload(:Crossing, 'minimal_pipeline/crossing')
|
15
|
+
autoload(:Docker, 'minimal_pipeline/docker')
|
16
|
+
autoload(:Keystore, 'minimal_pipeline/keystore')
|
17
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'aws-sdk'
|
4
|
+
|
5
|
+
class MinimalPipeline
|
6
|
+
# Here is an example of how to use this class to deploy CloudFormation Stacks.
|
7
|
+
#
|
8
|
+
# ```
|
9
|
+
# cloudformation = MinimalPipeline::Cloudformation.new
|
10
|
+
#
|
11
|
+
# cloudformation_parameters = {
|
12
|
+
# 'Vpc' => 'vpc-123456',
|
13
|
+
# 'AsgSubnets' => %w[sg-one sg-two sg-three],
|
14
|
+
# 'ElbSecurityGroup' => 'sg-123456',
|
15
|
+
# 'CertName' => 'example'
|
16
|
+
# }
|
17
|
+
#
|
18
|
+
# stack_parameters = {
|
19
|
+
# stack_name: stack_name,
|
20
|
+
# template_body: File.read('provisioning/elb.json'),
|
21
|
+
# capabilities: ['CAPABILITY_IAM'],
|
22
|
+
# parameters: cloudformation.params(cloudformation_parameters)
|
23
|
+
# }
|
24
|
+
#
|
25
|
+
# cloudformation.deploy_stack('EXAMPLE_ELB', stack_parameters)
|
26
|
+
# name = cloudformation.stack_output(stack_name, 'LoadBalancerName')
|
27
|
+
# ```
|
28
|
+
#
|
29
|
+
# You will need the following environment variables to be present:
|
30
|
+
# * `AWS_REGION` or `region`
|
31
|
+
class Cloudformation
|
32
|
+
# Instance of `Aws::CloudFormation::Client`
|
33
|
+
attr_reader(:client)
|
34
|
+
|
35
|
+
# Sets up `Aws::CloudFormation::Client`
|
36
|
+
# Requires environment variable `AWS_REGION` or `region` to be set.
|
37
|
+
def initialize
|
38
|
+
raise 'You must set env variable AWS_REGION.' if ENV['AWS_REGION'].nil?
|
39
|
+
|
40
|
+
region = ENV['AWS_REGION']
|
41
|
+
@client = Aws::CloudFormation::Client.new(region: region)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Converts a parameter Hash into a CloudFormation friendly structure
|
45
|
+
#
|
46
|
+
# @param parameters [Hash] Key value pair of parameters for a CFN stack.
|
47
|
+
# @return [Hash] CloudFormation friendly data structure of parameter
|
48
|
+
def params(parameters)
|
49
|
+
parameter_list = []
|
50
|
+
parameters.each do |k, v|
|
51
|
+
parameter_list.push(parameter_key: k, parameter_value: v)
|
52
|
+
end
|
53
|
+
parameter_list
|
54
|
+
end
|
55
|
+
|
56
|
+
# Retrieves the CloudFormation stack output of a single value
|
57
|
+
#
|
58
|
+
# @param stack [String] The name of the CloudFormation stack
|
59
|
+
# @param output [String] The name of the output to fetch the value of
|
60
|
+
# @return [String] The value of the output for the CloudFormation stack
|
61
|
+
def stack_output(stack, output)
|
62
|
+
resp = @client.describe_stacks(stack_name: stack)
|
63
|
+
|
64
|
+
raise "#{stack.upcase} stack does not exist!" if resp.stacks.empty?
|
65
|
+
|
66
|
+
resp.stacks.first.outputs.each do |stack_output|
|
67
|
+
zero_output = stack_output.output_key.casecmp(output).zero?
|
68
|
+
return stack_output.output_value if zero_output
|
69
|
+
end
|
70
|
+
|
71
|
+
raise "#{stack.upcase} stack does not have a(n) '#{output}' output!"
|
72
|
+
end
|
73
|
+
|
74
|
+
# Creates or Updates a CloudFormation stack. Checks to see if the stack
|
75
|
+
# already exists and takes the appropriate action. Pauses until a final
|
76
|
+
# stack state is reached.
|
77
|
+
|
78
|
+
# @param stack_name [String] The name of the CloudFormation stack
|
79
|
+
# @param stack_parameters [Hash] Parameters to be passed into the stack
|
80
|
+
def deploy_stack(stack_name, stack_parameters)
|
81
|
+
unless @client.describe_stacks(stack_name: stack_name).stacks.empty?
|
82
|
+
puts 'Updating the existing stack'
|
83
|
+
@client.update_stack(stack_parameters)
|
84
|
+
@client.wait_until(:stack_update_complete, stack_name: stack_name)
|
85
|
+
end
|
86
|
+
rescue Aws::CloudFormation::Errors::ValidationError => error
|
87
|
+
if error.to_s.include? 'No updates are to be performed.'
|
88
|
+
puts "Nothing to do."
|
89
|
+
elsif error.to_s.include? 'Template error'
|
90
|
+
raise error
|
91
|
+
else
|
92
|
+
puts 'Creating a new stack'
|
93
|
+
@client.create_stack(stack_parameters)
|
94
|
+
@client.wait_until(:stack_create_complete, stack_name: stack_name)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'aws-sdk'
|
4
|
+
require 'crossing'
|
5
|
+
|
6
|
+
class MinimalPipeline
|
7
|
+
# Here is an example of how to use this class to interact with Crossing.
|
8
|
+
#
|
9
|
+
# ```
|
10
|
+
# crossing = MinimalPipeline::Crossing.new
|
11
|
+
#
|
12
|
+
# # Upload
|
13
|
+
# crossing.upload_content('my-config-bucket', 'example.txt', 'foo')
|
14
|
+
#
|
15
|
+
# # Download
|
16
|
+
# content = crossing.download_file('my-config-bucket', 'example.txt')
|
17
|
+
# puts content # Outputs 'foo'
|
18
|
+
# ```
|
19
|
+
#
|
20
|
+
# You will need the following environment variables to be present:
|
21
|
+
# * `AWS_REGION` or `region`
|
22
|
+
# * `keystore_kms_id`
|
23
|
+
#
|
24
|
+
# For more information on Crossing see https://github.com/stelligent/crossing
|
25
|
+
class Crossing
|
26
|
+
def initialize
|
27
|
+
raise 'You must set env variable AWS_REGION.' if ENV['AWS_REGION'].nil?
|
28
|
+
raise '`keystore_kms_id` in environment!' \
|
29
|
+
if ENV['keystore_kms_id'].nil?
|
30
|
+
|
31
|
+
region = ENV['AWS_REGION']
|
32
|
+
kms = Aws::KMS::Client.new(region: region)
|
33
|
+
s3 = Aws::S3::Encryption::Client.new(kms_key_id: ENV['keystore_kms_id'],
|
34
|
+
kms_client: kms,
|
35
|
+
region: region)
|
36
|
+
@crossing = ::Crossing.new(s3)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Securely uploads a file to an S3 bucket
|
40
|
+
#
|
41
|
+
# @param config_bucket [String] The name of the S3 bucket
|
42
|
+
# @param filename [String] The name of the file to save content to in the
|
43
|
+
# bucket
|
44
|
+
# @param content [String] The content to store in the file
|
45
|
+
def upload_content(config_bucket, filename, content)
|
46
|
+
@crossing.put_content(config_bucket, filename, content)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Securely downloads a file from an S3 bucket
|
50
|
+
#
|
51
|
+
# @param config_bucket [String] The name of the S3 bucket
|
52
|
+
# @param filename [String] The name of the file that contains the desired
|
53
|
+
# content
|
54
|
+
# @return [String] The content that was stored in the file
|
55
|
+
def download_file(config_bucket, filename)
|
56
|
+
@crossing.get_content(config_bucket, filename)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'open3'
|
4
|
+
require 'docker'
|
5
|
+
require 'json'
|
6
|
+
require 'securerandom'
|
7
|
+
|
8
|
+
class MinimalPipeline
|
9
|
+
# Here is an example of how to use this class to manage Docker containers.
|
10
|
+
#
|
11
|
+
# ```
|
12
|
+
# docker = MinimalPipeline::Docker.new
|
13
|
+
# keystore = MinimalPipeline::Keystore.new
|
14
|
+
#
|
15
|
+
# deploy_env = ENV['DEPLOY_ENV']
|
16
|
+
# docker_repo = keystore.retrieve("#{deploy_env}_EXAMPLE_ECR_REPO")
|
17
|
+
# docker_image = "#{docker_repo}/example:latest"
|
18
|
+
# docker.build_docker_image(docker_image, 'containers/example')
|
19
|
+
# docker.push_docker_image(docker_image)
|
20
|
+
# ```
|
21
|
+
class Docker
|
22
|
+
include Rake::DSL
|
23
|
+
|
24
|
+
def initialize
|
25
|
+
end
|
26
|
+
|
27
|
+
# Finds the absolute path to a given executable
|
28
|
+
#
|
29
|
+
# @param cmd [String] The name of the executable to locate
|
30
|
+
# @return [String] The absolute path to the executable
|
31
|
+
def which(cmd)
|
32
|
+
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
33
|
+
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
34
|
+
exts.each do |ext|
|
35
|
+
exe = File.join(path, "#{cmd}#{ext}")
|
36
|
+
return exe if File.executable?(exe) && !File.directory?(exe)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
nil
|
40
|
+
end
|
41
|
+
|
42
|
+
# Outputs JSON build output lines as human readible text
|
43
|
+
#
|
44
|
+
# @param build_output [String] Raw JSON build output line
|
45
|
+
def build_output(build_output)
|
46
|
+
# Each line of the response is its own JSON structure
|
47
|
+
build_output.each_line do |l|
|
48
|
+
if (log = JSON.parse(l)) && log.key?('stream')
|
49
|
+
$stdout.puts log['stream']
|
50
|
+
end
|
51
|
+
end
|
52
|
+
rescue JSON::ParserError
|
53
|
+
$stdout.puts "Bad JSON parse\n"
|
54
|
+
$stdout.puts build_output
|
55
|
+
end
|
56
|
+
|
57
|
+
# Logs in to AWS ECR
|
58
|
+
def ecr_login
|
59
|
+
region = ENV['AWS_REGION'] || ENV['region']
|
60
|
+
`$(aws ecr get-login --region #{region})`
|
61
|
+
end
|
62
|
+
|
63
|
+
# Cleans up docker images
|
64
|
+
#
|
65
|
+
# @param image_id [String] The Docker container ID to delete
|
66
|
+
def clean_up_image(image_id)
|
67
|
+
image = ::Docker::Image.get(image_id)
|
68
|
+
image.remove(force: true)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Builds a docker image from a Dockerfile
|
72
|
+
#
|
73
|
+
# @param image_id [String] The name of the docker image
|
74
|
+
# @param dir [String] The path to the Dockerfile
|
75
|
+
# @param build_args [Hash] Additional build args to pass to Docker
|
76
|
+
# @param timeout [Integer] The Docker build timeout
|
77
|
+
def build_docker_image(image_id, dir = '.', build_args: {}, timeout: 600)
|
78
|
+
%w[HTTP_PROXY HTTPS_PROXY NO_PROXY http_proxy https_proxy
|
79
|
+
no_proxy].each do |arg|
|
80
|
+
build_args[arg] = ENV[arg] if ENV[arg]
|
81
|
+
end
|
82
|
+
|
83
|
+
args = {
|
84
|
+
'nocache' => 'true',
|
85
|
+
'pull' => 'true',
|
86
|
+
't' => image_id,
|
87
|
+
'buildargs' => JSON.dump(build_args)
|
88
|
+
}
|
89
|
+
puts "Build args: #{args.inspect}"
|
90
|
+
::Docker.options[:read_timeout] = timeout
|
91
|
+
::Docker::Image.build_from_dir(dir, args) { |v| build_output(v) }
|
92
|
+
end
|
93
|
+
|
94
|
+
# Pushes a docker image from local to AWS ECR.
|
95
|
+
# This handles login, the upload, and local cleanup of the container
|
96
|
+
#
|
97
|
+
# @param image_id [String] The name of the docker image
|
98
|
+
def push_docker_image(image_id)
|
99
|
+
ecr_login
|
100
|
+
docker_bin = which('docker')
|
101
|
+
raise "docker_push: no docker binary: #{image_id}" unless docker_bin
|
102
|
+
stdout, stderr, status = Open3.capture3(docker_bin, 'push', image_id)
|
103
|
+
raise "stdout: #{stdout}\nstderr: #{stderr}\nstatus: #{status}" \
|
104
|
+
unless status.exitstatus.zero?
|
105
|
+
clean_up_image(image_id)
|
106
|
+
end
|
107
|
+
|
108
|
+
# List all containers in the `containers` directory
|
109
|
+
# @return [Array] List of container names
|
110
|
+
def list_containers
|
111
|
+
containers = Dir.glob('containers/*')
|
112
|
+
# Grab container name from containers/name/Dockerfile
|
113
|
+
containers.map{ |container| container.split('/')[1] }
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'keystore'
|
4
|
+
require 'aws-sdk'
|
5
|
+
|
6
|
+
class MinimalPipeline
|
7
|
+
# Here is an example of how to use this class to interact with the Keystore.
|
8
|
+
#
|
9
|
+
# ```
|
10
|
+
# keystore = MinimalPipeline::Keystore.new
|
11
|
+
#
|
12
|
+
# # Store
|
13
|
+
# keystore.store('EXAMPLE_KEY', 'foo')
|
14
|
+
#
|
15
|
+
# # Retrieve
|
16
|
+
# content = keystore.retrieve('EXAMPLE_KEY')
|
17
|
+
# puts content # Outputs 'foo'
|
18
|
+
# ```
|
19
|
+
#
|
20
|
+
# You will need the following environment variables to be present:
|
21
|
+
# * `AWS_REGION` or `region`
|
22
|
+
# * `keystore_table`
|
23
|
+
# * `keystore_kms_id`
|
24
|
+
#
|
25
|
+
# For more information on the Keystore please see
|
26
|
+
# https://github.com/stelligent/keystore
|
27
|
+
class Keystore
|
28
|
+
# Initializes a `Keystore` client
|
29
|
+
# Requires environment variables `AWS_REGION` or `region` to be set.
|
30
|
+
# Also requires `keystore_kms_id` and `keystore_table`
|
31
|
+
def initialize
|
32
|
+
raise 'You must set env variable AWS_REGION.' if ENV['AWS_REGION'].nil?
|
33
|
+
raise 'Missing `keystore_table` or `keystore_kms_id` in environment!' \
|
34
|
+
if ENV['keystore_table'].nil? || ENV['keystore_kms_id'].nil?
|
35
|
+
|
36
|
+
region = ENV['AWS_REGION']
|
37
|
+
kms = Aws::KMS::Client.new(region: region)
|
38
|
+
dynamo = Aws::DynamoDB::Client.new(region: region)
|
39
|
+
@keystore = ::Keystore.new(dynamo: dynamo,
|
40
|
+
table_name: ENV['keystore_table'],
|
41
|
+
kms: kms,
|
42
|
+
key_id: ENV['keystore_kms_id'])
|
43
|
+
end
|
44
|
+
|
45
|
+
# Retrieves a value from the Keystore
|
46
|
+
#
|
47
|
+
# @param keyname [String] The name of the Keystore key
|
48
|
+
# @return [String] The value stored in the Keystore for the given key
|
49
|
+
def retrieve(keyname)
|
50
|
+
@keystore.retrieve(key: keyname)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Stores a value in the Keystore
|
54
|
+
#
|
55
|
+
# @param keyname [String] The name of the Keystore key
|
56
|
+
# @param value [String] The value to store for the given key
|
57
|
+
def store(keyname, value)
|
58
|
+
@keystore.store(key: keyname, value: value)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
metadata
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: minimal_pipeline
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mayowa Aladeojebi
|
8
|
+
- Jesse Adams
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2018-08-27 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: aws-sdk
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '2'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '2'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: crossing
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - '='
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: 0.1.8
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - '='
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: 0.1.8
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: docker-api
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - '='
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: 1.34.2
|
49
|
+
type: :runtime
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - '='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: 1.34.2
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: keystore
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - '='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 0.1.7
|
63
|
+
type: :runtime
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - '='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 0.1.7
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: rake
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
type: :runtime
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: rubocop
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - '='
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: 0.55.0
|
91
|
+
type: :runtime
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - '='
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: 0.55.0
|
98
|
+
description: Helper gem to orchestrate pipeline tasks
|
99
|
+
email:
|
100
|
+
- mayowa.aladeojebi@stelligent.com
|
101
|
+
- jesse.adams@stelligent.com
|
102
|
+
executables: []
|
103
|
+
extensions: []
|
104
|
+
extra_rdoc_files: []
|
105
|
+
files:
|
106
|
+
- lib/minimal_pipeline.rb
|
107
|
+
- lib/minimal_pipeline/cloudformation.rb
|
108
|
+
- lib/minimal_pipeline/crossing.rb
|
109
|
+
- lib/minimal_pipeline/docker.rb
|
110
|
+
- lib/minimal_pipeline/keystore.rb
|
111
|
+
homepage: https://github.com/stelligent/minimal-pipeline-gem
|
112
|
+
licenses:
|
113
|
+
- 0BSD
|
114
|
+
metadata: {}
|
115
|
+
post_install_message:
|
116
|
+
rdoc_options: []
|
117
|
+
require_paths:
|
118
|
+
- lib
|
119
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
125
|
+
requirements:
|
126
|
+
- - ">="
|
127
|
+
- !ruby/object:Gem::Version
|
128
|
+
version: '0'
|
129
|
+
requirements: []
|
130
|
+
rubyforge_project:
|
131
|
+
rubygems_version: 2.7.6
|
132
|
+
signing_key:
|
133
|
+
specification_version: 4
|
134
|
+
summary: Helper gem to manage pipeline tasks
|
135
|
+
test_files: []
|