minimal_pipeline 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|