capsulecd 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.coveralls.yml +2 -0
- data/.dockerignore +5 -0
- data/.gitignore +95 -0
- data/.rspec +3 -0
- data/.simplecov +9 -0
- data/Dockerfile +16 -0
- data/Dockerfile.chef +26 -0
- data/Dockerfile.javascript +7 -0
- data/Dockerfile.node +7 -0
- data/Dockerfile.python +7 -0
- data/Dockerfile.ruby +4 -0
- data/FEATURES.md +12 -0
- data/Gemfile +26 -0
- data/LICENSE.md +22 -0
- data/README.md +227 -0
- data/Rakefile +43 -0
- data/bin/capsulecd +4 -0
- data/capsulecd.gemspec +27 -0
- data/circle.yml +24 -0
- data/lib/capsulecd/base/common/git_utils.rb +90 -0
- data/lib/capsulecd/base/common/validation_utils.rb +22 -0
- data/lib/capsulecd/base/configuration.rb +151 -0
- data/lib/capsulecd/base/engine.rb +163 -0
- data/lib/capsulecd/base/runner/circleci.rb +37 -0
- data/lib/capsulecd/base/runner/default.rb +38 -0
- data/lib/capsulecd/base/source/github.rb +183 -0
- data/lib/capsulecd/base/transform_engine.rb +62 -0
- data/lib/capsulecd/chef/chef_engine.rb +172 -0
- data/lib/capsulecd/chef/chef_helper.rb +29 -0
- data/lib/capsulecd/cli.rb +64 -0
- data/lib/capsulecd/error.rb +51 -0
- data/lib/capsulecd/javascript/javascript_engine.rb +213 -0
- data/lib/capsulecd/node/node_engine.rb +141 -0
- data/lib/capsulecd/python/python_engine.rb +157 -0
- data/lib/capsulecd/ruby/ruby_engine.rb +191 -0
- data/lib/capsulecd/ruby/ruby_helper.rb +60 -0
- data/lib/capsulecd/version.rb +3 -0
- data/lib/capsulecd.rb +16 -0
- data/logo.svg +1 -0
- data/spec/fixtures/chef/cookbook_analogj_test/CHANGELOG.md +3 -0
- data/spec/fixtures/chef/cookbook_analogj_test/Gemfile +18 -0
- data/spec/fixtures/chef/cookbook_analogj_test/LICENSE +21 -0
- data/spec/fixtures/chef/cookbook_analogj_test/README.md +13 -0
- data/spec/fixtures/chef/cookbook_analogj_test/Rakefile +1 -0
- data/spec/fixtures/chef/cookbook_analogj_test/Thorfile +12 -0
- data/spec/fixtures/chef/cookbook_analogj_test/chefignore +94 -0
- data/spec/fixtures/chef/cookbook_analogj_test/metadata.rb +5 -0
- data/spec/fixtures/chef/cookbook_analogj_test/recipes/default.rb +6 -0
- data/spec/fixtures/incorrect_configuration.yml +4 -0
- data/spec/fixtures/javascript/javascript_analogj_test/LICENSE +21 -0
- data/spec/fixtures/javascript/javascript_analogj_test/README.md +2 -0
- data/spec/fixtures/javascript/javascript_analogj_test/package.json +19 -0
- data/spec/fixtures/node/npm_analogj_test/LICENSE +21 -0
- data/spec/fixtures/node/npm_analogj_test/README.md +2 -0
- data/spec/fixtures/node/npm_analogj_test/package.json +19 -0
- data/spec/fixtures/python/pip_analogj_test/LICENSE +21 -0
- data/spec/fixtures/python/pip_analogj_test/MANIFEST.in +1 -0
- data/spec/fixtures/python/pip_analogj_test/README.md +1 -0
- data/spec/fixtures/python/pip_analogj_test/VERSION +1 -0
- data/spec/fixtures/python/pip_analogj_test/setup.cfg +5 -0
- data/spec/fixtures/python/pip_analogj_test/setup.py +80 -0
- data/spec/fixtures/python/pip_analogj_test/tox.ini +14 -0
- data/spec/fixtures/ruby/gem_analogj_test/Gemfile +4 -0
- data/spec/fixtures/ruby/gem_analogj_test/LICENSE.txt +21 -0
- data/spec/fixtures/ruby/gem_analogj_test/README.md +41 -0
- data/spec/fixtures/ruby/gem_analogj_test/Rakefile +6 -0
- data/spec/fixtures/ruby/gem_analogj_test/bin/console +14 -0
- data/spec/fixtures/ruby/gem_analogj_test/bin/setup +8 -0
- data/spec/fixtures/ruby/gem_analogj_test/gem_analogj_test.gemspec +25 -0
- data/spec/fixtures/ruby/gem_analogj_test/lib/gem_analogj_test/version.rb +3 -0
- data/spec/fixtures/ruby/gem_analogj_test/lib/gem_analogj_test.rb +5 -0
- data/spec/fixtures/ruby/gem_analogj_test/spec/gem_analogj_test_spec.rb +7 -0
- data/spec/fixtures/ruby/gem_analogj_test/spec/spec_helper.rb +2 -0
- data/spec/fixtures/ruby/gem_analogj_test-0.1.4.gem +0 -0
- data/spec/fixtures/sample_chef_configuration.yml +8 -0
- data/spec/fixtures/sample_configuration.yml +7 -0
- data/spec/fixtures/sample_global_configuration.yml +23 -0
- data/spec/fixtures/sample_node_configuration.yml +7 -0
- data/spec/fixtures/sample_python_configuration.yml +8 -0
- data/spec/fixtures/sample_repo_configuration.yml +22 -0
- data/spec/fixtures/sample_ruby_configuration.yml +5 -0
- data/spec/fixtures/vcr_cassettes/chef_build_step.yml +636 -0
- data/spec/fixtures/vcr_cassettes/gem_build_step.yml +653 -0
- data/spec/fixtures/vcr_cassettes/gem_build_step_without_version_rb.yml +653 -0
- data/spec/fixtures/vcr_cassettes/integration_chef.yml +1399 -0
- data/spec/fixtures/vcr_cassettes/integration_node.yml +1388 -0
- data/spec/fixtures/vcr_cassettes/integration_python.yml +1388 -0
- data/spec/fixtures/vcr_cassettes/integration_ruby.yml +1377 -0
- data/spec/fixtures/vcr_cassettes/node_build_step.yml +647 -0
- data/spec/fixtures/vcr_cassettes/pip_build_step.yml +653 -0
- data/spec/lib/capsulecd/base/configuration_spec.rb +75 -0
- data/spec/lib/capsulecd/base/engine_spec.rb +51 -0
- data/spec/lib/capsulecd/base/source/github_spec.rb +253 -0
- data/spec/lib/capsulecd/base/transform_engine_spec.rb +55 -0
- data/spec/lib/capsulecd/chef/chef_engine_spec.rb +114 -0
- data/spec/lib/capsulecd/cli_spec.rb +57 -0
- data/spec/lib/capsulecd/node/node_engine_spec.rb +113 -0
- data/spec/lib/capsulecd/python/python_engine_spec.rb +118 -0
- data/spec/lib/capsulecd/ruby/ruby_engine_spec.rb +128 -0
- data/spec/spec_helper.rb +105 -0
- data/spec/support/file_system.rb +21 -0
- data/spec/support/package_types.rb +11 -0
- metadata +281 -0
@@ -0,0 +1,183 @@
|
|
1
|
+
require 'octokit'
|
2
|
+
require 'uri'
|
3
|
+
require 'git'
|
4
|
+
require 'capsulecd'
|
5
|
+
require 'pp'
|
6
|
+
|
7
|
+
module CapsuleCD
|
8
|
+
module Source
|
9
|
+
module Github
|
10
|
+
# all of these instance variables are available for use within hooks
|
11
|
+
attr_accessor :source_client
|
12
|
+
attr_accessor :source_git_base_info
|
13
|
+
attr_accessor :source_git_head_info
|
14
|
+
attr_accessor :source_git_parent_path
|
15
|
+
attr_accessor :source_git_local_path
|
16
|
+
attr_accessor :source_git_local_branch
|
17
|
+
attr_accessor :source_git_remote
|
18
|
+
attr_accessor :source_release_commit
|
19
|
+
attr_accessor :source_release_artifacts
|
20
|
+
|
21
|
+
# define the Source API methods
|
22
|
+
|
23
|
+
# configure method will generate an authenticated client that can be used to comunicate with Github
|
24
|
+
# MUST set @source_git_parent_path
|
25
|
+
# MUST set @source_client
|
26
|
+
def source_configure
|
27
|
+
puts 'github source_configure'
|
28
|
+
fail CapsuleCD::Error::SourceAuthenticationFailed, 'Missing github access token' unless @config.source_github_access_token
|
29
|
+
|
30
|
+
@source_release_commit = nil
|
31
|
+
@source_release_artifacts = []
|
32
|
+
|
33
|
+
@source_git_parent_path = @config.source_git_parent_path || Dir.mktmpdir
|
34
|
+
Octokit.auto_paginate = true
|
35
|
+
Octokit.configure do |c|
|
36
|
+
c.api_endpoint = @config.source_github_api_endpoint if @config.source_github_api_endpoint
|
37
|
+
c.web_endpoint = @config.source_github_web_endpoint if @config.source_github_web_endpoint
|
38
|
+
end
|
39
|
+
@source_client = Octokit::Client.new(access_token: @config.source_github_access_token)
|
40
|
+
end
|
41
|
+
|
42
|
+
# all capsule CD processing will be kicked off via a payload. In Github's case, the payload is the webhook data.
|
43
|
+
# should check if the pull request opener even has permissions to create a release.
|
44
|
+
# all sources should process the payload by downloading a git repository that contains the master branch merged with the test branch
|
45
|
+
# MUST set source_git_local_path
|
46
|
+
# MUST set source_git_local_branch
|
47
|
+
# MUST set source_git_head_info
|
48
|
+
# REQUIRES source_git_parent_path
|
49
|
+
def source_process_push_payload(payload)
|
50
|
+
# set the processed head info
|
51
|
+
@source_git_head_info = payload['head']
|
52
|
+
CapsuleCD::ValidationUtils.validate_repo_payload(@source_git_head_info)
|
53
|
+
|
54
|
+
# set the remote url, with embedded token
|
55
|
+
uri = URI.parse(@source_git_head_info['repo']['clone_url'])
|
56
|
+
uri.user = @config.source_github_access_token
|
57
|
+
@source_git_remote = uri.to_s
|
58
|
+
@source_git_local_branch = @source_git_head_info['repo']['ref']
|
59
|
+
# clone the merged branch
|
60
|
+
# https://sethvargo.com/checkout-a-github-pull-request/
|
61
|
+
# https://coderwall.com/p/z5rkga/github-checkout-a-pull-request-as-a-branch
|
62
|
+
@source_git_local_path = CapsuleCD::GitUtils.clone(@source_git_parent_path, @source_git_head_info['repo']['name'], @source_git_remote)
|
63
|
+
CapsuleCD::GitUtils.checkout(@source_git_local_path, @source_git_head_info['repo']['sha1'])
|
64
|
+
end
|
65
|
+
|
66
|
+
# all capsule CD processing will be kicked off via a payload. In Github's case, the payload is the pull request data.
|
67
|
+
# should check if the pull request opener even has permissions to create a release.
|
68
|
+
# all sources should process the payload by downloading a git repository that contains the master branch merged with the test branch
|
69
|
+
# MUST set source_git_local_path
|
70
|
+
# MUST set source_git_local_branch
|
71
|
+
# MUST set source_git_base_info
|
72
|
+
# MUST set source_git_head_info
|
73
|
+
# REQUIRES source_client
|
74
|
+
# REQUIRES source_git_parent_path
|
75
|
+
def source_process_pull_request_payload(payload)
|
76
|
+
puts 'github source_process_payload'
|
77
|
+
|
78
|
+
# validate the github specific payload options
|
79
|
+
unless (payload['state'] == 'open')
|
80
|
+
fail CapsuleCD::Error::SourcePayloadUnsupported, 'Pull request has an invalid action'
|
81
|
+
end
|
82
|
+
unless (payload['base']['repo']['default_branch'] == payload['base']['ref'])
|
83
|
+
fail CapsuleCD::Error::SourcePayloadUnsupported, 'Pull request is not being created against the default branch of this repository (usually master)'
|
84
|
+
end
|
85
|
+
# check the payload push user.
|
86
|
+
|
87
|
+
# TODO: figure out how to do optional authenication. possible options, Source USER, token based auth, no auth when used with capsulecd.com.
|
88
|
+
# unless @source_client.collaborator?(payload['base']['repo']['full_name'], payload['user']['login'])
|
89
|
+
#
|
90
|
+
# @source_client.add_comment(payload['base']['repo']['full_name'], payload['number'], CapsuleCD::BotUtils.pull_request_comment)
|
91
|
+
# fail CapsuleCD::Error::SourceUnauthorizedUser, 'Pull request was opened by an unauthorized user'
|
92
|
+
# end
|
93
|
+
|
94
|
+
# set the processed base/head info,
|
95
|
+
@source_git_base_info = payload['base']
|
96
|
+
@source_git_head_info = payload['head']
|
97
|
+
CapsuleCD::ValidationUtils.validate_repo_payload(@source_git_base_info)
|
98
|
+
CapsuleCD::ValidationUtils.validate_repo_payload(@source_git_head_info)
|
99
|
+
|
100
|
+
# set the remote url, with embedded token
|
101
|
+
uri = URI.parse(payload['base']['repo']['clone_url'])
|
102
|
+
uri.user = @config.source_github_access_token
|
103
|
+
@source_git_remote = uri.to_s
|
104
|
+
|
105
|
+
# clone the merged branch
|
106
|
+
# https://sethvargo.com/checkout-a-github-pull-request/
|
107
|
+
# https://coderwall.com/p/z5rkga/github-checkout-a-pull-request-as-a-branch
|
108
|
+
@source_git_local_path = CapsuleCD::GitUtils.clone(@source_git_parent_path, @source_git_head_info['repo']['name'], @source_git_remote)
|
109
|
+
@source_git_local_branch = "pr_#{payload['number']}"
|
110
|
+
CapsuleCD::GitUtils.fetch(@source_git_local_path, "refs/pull/#{payload['number']}/merge", @source_git_local_branch)
|
111
|
+
CapsuleCD::GitUtils.checkout(@source_git_local_path, @source_git_local_branch)
|
112
|
+
|
113
|
+
# show a processing message on the github PR.
|
114
|
+
@source_client.create_status(payload['base']['repo']['full_name'], @source_git_head_info['sha'], 'pending',
|
115
|
+
context: 'CapsuleCD',
|
116
|
+
target_url: 'http://www.github.com/AnalogJ/capsulecd',
|
117
|
+
description: 'Started processing package. Pull request will be merged automatically when complete.')
|
118
|
+
end
|
119
|
+
|
120
|
+
# REQUIRES source_client
|
121
|
+
# REQUIRES source_release_commit
|
122
|
+
# REQUIRES source_git_local_path
|
123
|
+
# REQUIRES source_git_local_branch
|
124
|
+
# REQUIRES source_git_base_info
|
125
|
+
# REQUIRES source_git_head_info
|
126
|
+
# REQUIRES source_release_artifacts
|
127
|
+
# REQUIRES source_git_parent_path
|
128
|
+
def source_release
|
129
|
+
puts 'github source_release'
|
130
|
+
|
131
|
+
# push the version bumped metadata file + newly created files to
|
132
|
+
CapsuleCD::GitUtils.push(@source_git_local_path, @source_git_local_branch, @source_git_base_info['ref'])
|
133
|
+
# sleep because github needs time to process the new tag.
|
134
|
+
sleep 5
|
135
|
+
|
136
|
+
# calculate the release sha
|
137
|
+
release_sha = ('0' * (40 - @source_release_commit.sha.strip.length)) + @source_release_commit.sha.strip
|
138
|
+
|
139
|
+
# get the release changelog
|
140
|
+
release_body = CapsuleCD::GitUtils.generate_changelog(@source_git_local_path, @source_git_base_info['sha'], @source_git_head_info['sha'], @source_git_base_info['repo']['full_name'])
|
141
|
+
|
142
|
+
release = @source_client.create_release(@source_git_base_info['repo']['full_name'], @source_release_commit.name, target_commitish: release_sha,
|
143
|
+
name: @source_release_commit.name,
|
144
|
+
body: release_body)
|
145
|
+
|
146
|
+
@source_release_artifacts.each do |release_artifact|
|
147
|
+
@source_client.upload_asset(release[:url], release_artifact[:path], name: release_artifact[:name])
|
148
|
+
end
|
149
|
+
|
150
|
+
FileUtils.remove_entry_secure @source_git_parent_path if Dir.exists?(@source_git_parent_path)
|
151
|
+
# set the pull request status
|
152
|
+
@source_client.create_status(@source_git_base_info['repo']['full_name'], @source_git_head_info['sha'], 'success',
|
153
|
+
context: 'CapsuleCD',
|
154
|
+
target_url: 'http://www.github.com/AnalogJ/capsulecd',
|
155
|
+
description: 'Pull-request was successfully merged, new release created.')
|
156
|
+
end
|
157
|
+
|
158
|
+
# requires @source_client
|
159
|
+
# requires @source_git_parent_path
|
160
|
+
# requires @source_git_base_info
|
161
|
+
# requires @source_git_head_info
|
162
|
+
def source_notify(step, status='pending')
|
163
|
+
|
164
|
+
@source_client.create_status(@source_git_base_info['repo']['full_name'], @source_git_head_info['sha'], status,
|
165
|
+
context: 'CapsuleCD',
|
166
|
+
target_url: 'http://www.github.com/AnalogJ/capsulecd',
|
167
|
+
description: "Started '#{step}' step. Pull request will be merged automatically when complete.")
|
168
|
+
|
169
|
+
yield
|
170
|
+
|
171
|
+
rescue => ex
|
172
|
+
puts 'github source_process_failure'
|
173
|
+
FileUtils.remove_entry_secure @source_git_parent_path if Dir.exists?(@source_git_parent_path)
|
174
|
+
@source_client.create_status(@source_git_base_info['repo']['full_name'], @source_git_head_info['sha'], 'failure',
|
175
|
+
context: 'CapsuleCD',
|
176
|
+
target_url: 'http://www.github.com/AnalogJ/capsulecd',
|
177
|
+
description: ex.message.slice!(0..135))
|
178
|
+
raise
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
module CapsuleCD
|
3
|
+
module EngineExtension
|
4
|
+
end
|
5
|
+
|
6
|
+
class TransformEngine
|
7
|
+
def initialize()
|
8
|
+
end
|
9
|
+
|
10
|
+
def transform(engine, config_file, type = :repo) #type can only be :repo or :global
|
11
|
+
@engine = engine
|
12
|
+
@type = type
|
13
|
+
|
14
|
+
unless File.exists?(config_file)
|
15
|
+
puts 'no configuration file found, no engine hooks'
|
16
|
+
return
|
17
|
+
end
|
18
|
+
|
19
|
+
#parse the config file and generate a module file that we can use as part of our engine
|
20
|
+
@config = YAML.load(File.open(config_file).read)
|
21
|
+
populate_engine_extension
|
22
|
+
register_extension
|
23
|
+
end
|
24
|
+
|
25
|
+
def populate_engine_extension()
|
26
|
+
# lets loop though all the keys, and raise an error if the hooks specified are not available.
|
27
|
+
if @type == :repo
|
28
|
+
@config.keys.each do |key|
|
29
|
+
fail CapsuleCD::Error::EngineTransformUnavailableStep, key + ' cannot be overridden by repo capsule.yml file.' if %w(source_configure source_process_pull_request_payload source_process_push_payload runner_retrieve_payload).include?(key)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# yeah yeah, metaprogramming is evil. But actually its really just a powerful tool, albeit a really complicated one.
|
34
|
+
# Like any tool, you can use it incorrectly. With great power comes.. yada yada.
|
35
|
+
# In general metaprogramming is bad because it makes it hard to reason about your code. In this case we're using it
|
36
|
+
# to allow other developers to override our engine steps, and/or attach to our hooks. In this case its their
|
37
|
+
# responsibility not to f*&^ it all up.
|
38
|
+
|
39
|
+
# http://www.monkeyandcrow.com/blog/building_classes_dynamically/
|
40
|
+
# https://rubymonk.com/learning/books/5-metaprogramming-ruby-ascent/chapters/24-eval/lessons/68-class-eval
|
41
|
+
# https://www.ruby-forum.com/topic/207350
|
42
|
+
@config.each do |step, value|
|
43
|
+
next unless %w(source_configure source_process_pull_request_payload source_process_push_payload runner_retrieve_payload build_step test_step package_step source_release release_step).include?(step)
|
44
|
+
|
45
|
+
value.each do |prefix, method_script|
|
46
|
+
EngineExtension.class_eval(<<-METHOD
|
47
|
+
def #{prefix == 'override' ? '' : prefix+ '_'}#{step};
|
48
|
+
#{method_script};
|
49
|
+
end
|
50
|
+
METHOD
|
51
|
+
)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# at this point the EngineExtension module should be populated with all the hooks and methods.
|
57
|
+
# now we need to add them to the engine.
|
58
|
+
def register_extension
|
59
|
+
@engine.class.prepend(CapsuleCD::EngineExtension)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
require 'semverly'
|
2
|
+
require 'open3'
|
3
|
+
require 'bundler'
|
4
|
+
require_relative '../base/engine'
|
5
|
+
require_relative 'chef_helper'
|
6
|
+
require 'base64'
|
7
|
+
require 'fileutils'
|
8
|
+
|
9
|
+
module CapsuleCD
|
10
|
+
module Chef
|
11
|
+
class ChefEngine < Engine
|
12
|
+
def build_step
|
13
|
+
super
|
14
|
+
# validate that the chef metadata.rb file exists
|
15
|
+
|
16
|
+
unless File.exist?(@source_git_local_path + '/metadata.rb')
|
17
|
+
fail CapsuleCD::Error::BuildPackageInvalid, 'metadata.rb file is required to process Chef cookbook'
|
18
|
+
end
|
19
|
+
|
20
|
+
# bump up the chef cookbook version
|
21
|
+
metadata_str = CapsuleCD::Chef::ChefHelper.read_repo_metadata(@source_git_local_path)
|
22
|
+
chef_metadata = CapsuleCD::Chef::ChefHelper.parse_metadata(metadata_str)
|
23
|
+
next_version = bump_version(SemVer.parse(chef_metadata.version))
|
24
|
+
|
25
|
+
new_metadata_str = metadata_str.gsub(/(version\s+['"])[0-9\.]+(['"])/, "\\1#{next_version}\\2")
|
26
|
+
CapsuleCD::Chef::ChefHelper.write_repo_metadata(@source_git_local_path, new_metadata_str)
|
27
|
+
|
28
|
+
# TODO: check if this cookbook name and version already exist.
|
29
|
+
|
30
|
+
# check for/create any required missing folders/files
|
31
|
+
# Berksfile.lock and Gemfile.lock are not required to be commited, but they should be.
|
32
|
+
unless File.exist?(@source_git_local_path + '/Rakefile')
|
33
|
+
File.open(@source_git_local_path + '/Rakefile', 'w') { |file| file.write('task :test') }
|
34
|
+
end
|
35
|
+
unless File.exist?(@source_git_local_path + '/Berksfile')
|
36
|
+
File.open(@source_git_local_path + '/Berksfile', 'w') { |file| file.write('site :opscode') }
|
37
|
+
end
|
38
|
+
unless File.exist?(@source_git_local_path + '/Gemfile')
|
39
|
+
File.open(@source_git_local_path + '/Gemfile', 'w') { |file| file.write('source "https://rubygems.org"') }
|
40
|
+
end
|
41
|
+
unless File.exist?(@source_git_local_path + '/spec')
|
42
|
+
FileUtils.mkdir(@source_git_local_path + '/spec')
|
43
|
+
end
|
44
|
+
unless File.exist?(@source_git_local_path + '/.gitignore')
|
45
|
+
CapsuleCD::GitUtils.create_gitignore(@source_git_local_path, ['ChefCookbook'])
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_step
|
50
|
+
super
|
51
|
+
|
52
|
+
# the cookbook has already been downloaded. lets make sure all its dependencies are available.
|
53
|
+
Open3.popen3('berks install', chdir: @source_git_local_path) do |_stdin, stdout, stderr, external|
|
54
|
+
{ stdout: stdout, stderr: stderr }. each do |name, stream_buffer|
|
55
|
+
Thread.new do
|
56
|
+
until (line = stream_buffer.gets).nil?
|
57
|
+
puts "#{name} -> #{line}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
# wait for process
|
62
|
+
external.join
|
63
|
+
unless external.value.success?
|
64
|
+
fail CapsuleCD::Error::TestDependenciesError, 'berks install failed. Check cookbook dependencies'
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# lets download all its gem dependencies
|
69
|
+
Bundler.with_clean_env do
|
70
|
+
Open3.popen3('bundle install', chdir: @source_git_local_path) do |_stdin, stdout, stderr, external|
|
71
|
+
{ stdout: stdout, stderr: stderr }. each do |name, stream_buffer|
|
72
|
+
Thread.new do
|
73
|
+
until (line = stream_buffer.gets).nil?
|
74
|
+
puts "#{name} -> #{line}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
# wait for process
|
79
|
+
external.join
|
80
|
+
unless external.value.success?
|
81
|
+
fail CapsuleCD::Error::TestDependenciesError, 'bundle install failed. Check gem dependencies'
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# run test command
|
86
|
+
test_cmd = @config.engine_cmd_test || 'rake test'
|
87
|
+
Open3.popen3(test_cmd, chdir: @source_git_local_path) do |_stdin, stdout, stderr, external|
|
88
|
+
{ stdout: stdout, stderr: stderr }. each do |name, stream_buffer|
|
89
|
+
Thread.new do
|
90
|
+
until (line = stream_buffer.gets).nil?
|
91
|
+
puts "#{name} -> #{line}"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
# wait for process
|
96
|
+
external.join
|
97
|
+
unless external.value.success?
|
98
|
+
fail CapsuleCD::Error::TestRunnerError, test_cmd + ' failed. Check log for exact error'
|
99
|
+
end
|
100
|
+
end unless @config.engine_disable_test
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def package_step
|
105
|
+
super
|
106
|
+
metadata_str = CapsuleCD::Chef::ChefHelper.read_repo_metadata(@source_git_local_path)
|
107
|
+
chef_metadata = CapsuleCD::Chef::ChefHelper.parse_metadata(metadata_str)
|
108
|
+
next_version = SemVer.parse(chef_metadata.version)
|
109
|
+
# commit changes to the cookbook. (test run occurs before this, and it should clean up any instrumentation files, created,
|
110
|
+
# as they will be included in the commmit and any release artifacts)
|
111
|
+
CapsuleCD::GitUtils.commit(@source_git_local_path, "(v#{next_version}) Automated packaging of release by CapsuleCD")
|
112
|
+
@source_release_commit = CapsuleCD::GitUtils.tag(@source_git_local_path, "v#{next_version}")
|
113
|
+
end
|
114
|
+
|
115
|
+
# this step should push the release to the package repository (ie. npm, chef supermarket, rubygems)
|
116
|
+
def release_step
|
117
|
+
super
|
118
|
+
puts @source_git_parent_path
|
119
|
+
pem_path = File.expand_path('~/client.pem')
|
120
|
+
knife_path = File.expand_path('~/knife.rb')
|
121
|
+
|
122
|
+
unless @config.chef_supermarket_username || @config.chef_supermarket_key
|
123
|
+
fail CapsuleCD::Error::ReleaseCredentialsMissing, 'cannot deploy cookbook to supermarket, credentials missing'
|
124
|
+
return
|
125
|
+
end
|
126
|
+
|
127
|
+
# knife is really sensitive to folder names. The cookbook name MUST match the folder name otherwise knife throws up
|
128
|
+
# when doing a knife cookbook share. So we're going to make a new tmp directory, create a subdirectory with the EXACT
|
129
|
+
# cookbook name, and then copy the cookbook contents into it. Yeah yeah, its pretty nasty, but blame Chef.
|
130
|
+
metadata_str = CapsuleCD::Chef::ChefHelper.read_repo_metadata(@source_git_local_path)
|
131
|
+
chef_metadata = CapsuleCD::Chef::ChefHelper.parse_metadata(metadata_str)
|
132
|
+
|
133
|
+
Dir.mktmpdir {|tmp_parent_path|
|
134
|
+
# create cookbook folder
|
135
|
+
tmp_local_path = File.join(tmp_parent_path, chef_metadata.name)
|
136
|
+
FileUtils.mkdir_p(tmp_local_path)
|
137
|
+
FileUtils.copy_entry(@source_git_local_path, tmp_local_path)
|
138
|
+
|
139
|
+
# write the knife.rb config jfile.
|
140
|
+
File.open(knife_path, 'w+') do |file|
|
141
|
+
file.write(<<-EOT.gsub(/^\s+/, '')
|
142
|
+
node_name "#{@config.chef_supermarket_username }" # Replace with the login name you use to login to the Supermarket.
|
143
|
+
client_key "#{pem_path}" # Define the path to wherever your client.pem file lives. This is the key you generated when you signed up for a Chef account.
|
144
|
+
cookbook_path [ '#{tmp_parent_path}' ] # Directory where the cookbook you're uploading resides.
|
145
|
+
EOT
|
146
|
+
)
|
147
|
+
end
|
148
|
+
|
149
|
+
File.open(pem_path, 'w+') do |file|
|
150
|
+
file.write(@config.chef_supermarket_key)
|
151
|
+
end
|
152
|
+
|
153
|
+
command = "knife cookbook site share #{chef_metadata.name} #{@config.chef_supermarket_type} -c #{knife_path}"
|
154
|
+
Open3.popen3(command) do |_stdin, stdout, stderr, external|
|
155
|
+
{ stdout: stdout, stderr: stderr }. each do |name, stream_buffer|
|
156
|
+
Thread.new do
|
157
|
+
until (line = stream_buffer.gets).nil?
|
158
|
+
puts "#{name} -> #{line}"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
# wait for process
|
163
|
+
external.join
|
164
|
+
unless external.value.success?
|
165
|
+
fail CapsuleCD::Error::ReleasePackageError, 'knife cookbook upload to supermarket failed'
|
166
|
+
end
|
167
|
+
end
|
168
|
+
}
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'ridley'
|
2
|
+
require 'berkshelf'
|
3
|
+
|
4
|
+
module CapsuleCD
|
5
|
+
module Chef
|
6
|
+
class ChefHelper
|
7
|
+
def self.read_repo_metadata(repo_path, metadata_filename = 'metadata.rb')
|
8
|
+
File.read("#{repo_path}/#{metadata_filename}")
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.write_repo_metadata(repo_path, metadata_str, metadata_filename = 'metadata.rb')
|
12
|
+
File.open("#{repo_path}/#{metadata_filename}", 'w') { |file| file.write(metadata_str) }
|
13
|
+
end
|
14
|
+
def self.parse_metadata(metadata_str)
|
15
|
+
chef_metadata = Ridley::Chef::Cookbook::Metadata.new
|
16
|
+
chef_metadata.instance_eval(metadata_str)
|
17
|
+
chef_metadata
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.parse_berksfile_lock_from_repo(repo_path, lockfile_filename = 'Berksfile.lock')
|
21
|
+
Berkshelf::Lockfile.from_file("#{repo_path}/#{lockfile_filename}")
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.parse_berksfile_from_repo(repo_path, berksfile_filename = 'Berksfile')
|
25
|
+
Berkshelf::Lockfile.from_file("#{repo_path}/#{berksfile_filename}")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'capsulecd'
|
3
|
+
require 'pp'
|
4
|
+
|
5
|
+
module CapsuleCD
|
6
|
+
# The command line interface for CapsuleCD.
|
7
|
+
class Cli < Thor
|
8
|
+
|
9
|
+
##
|
10
|
+
# Run
|
11
|
+
##
|
12
|
+
desc 'start', 'Start a new CapsuleCD package pipeline '
|
13
|
+
option :runner,
|
14
|
+
type: :string,
|
15
|
+
default: 'default', # can be :none, :circleci or :shippable (check the readme for why other hosted providers arn't supported.)
|
16
|
+
desc: 'The cloud CI runner that is running this PR. (Used to determine the Environmental Variables to parse)'
|
17
|
+
|
18
|
+
option :source,
|
19
|
+
type: :string,
|
20
|
+
default: 'default',
|
21
|
+
desc: 'The source for the code, used to determine which git endpoint to clone from, and create releases on'
|
22
|
+
|
23
|
+
option :package_type,
|
24
|
+
type: :string,
|
25
|
+
default: 'default',
|
26
|
+
desc: 'The type of package being built.'
|
27
|
+
|
28
|
+
option :dry_run,
|
29
|
+
type: :boolean,
|
30
|
+
default: false,
|
31
|
+
desc: 'Specifies that no changes should be pushed to source and no package will be released'
|
32
|
+
|
33
|
+
option :config_file,
|
34
|
+
type: :string,
|
35
|
+
default: nil,
|
36
|
+
desc: 'Specifies the location of the config file'
|
37
|
+
|
38
|
+
# Begin processing
|
39
|
+
def start
|
40
|
+
# parse runner from env
|
41
|
+
engine_opts = {}
|
42
|
+
|
43
|
+
engine_opts[:runner] = options[:runner].to_sym # TODO: we cant modify the hash sent by Thor, so we'll duplicate it
|
44
|
+
engine_opts[:source] = options[:source].to_sym
|
45
|
+
engine_opts[:package_type] = options[:package_type].to_sym
|
46
|
+
engine_opts[:dry_run] = options[:dry_run]
|
47
|
+
puts '###########################################################################################'
|
48
|
+
puts '# Configuration '
|
49
|
+
puts '###########################################################################################'
|
50
|
+
pp engine_opts
|
51
|
+
|
52
|
+
if engine_opts[:package_type] == :default
|
53
|
+
|
54
|
+
engine = CapsuleCD::Engine.new(engine_opts)
|
55
|
+
else
|
56
|
+
package_type = engine_opts[:package_type].to_s
|
57
|
+
require_relative "#{package_type}/#{package_type}_engine"
|
58
|
+
engine = CapsuleCD.const_get(package_type.capitalize).const_get("#{package_type.capitalize}Engine").new(engine_opts)
|
59
|
+
end
|
60
|
+
|
61
|
+
engine.start
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module CapsuleCD
|
2
|
+
# The collection of Minimart specific errors.
|
3
|
+
module Error
|
4
|
+
class BaseError < StandardError; end
|
5
|
+
|
6
|
+
# Raised when the config file specifies a hook/override for a step when the type is :repo
|
7
|
+
class EngineTransformUnavailableStep < BaseError; end
|
8
|
+
|
9
|
+
# Raised when the source is not specified
|
10
|
+
class SourceUnspecifiedError < BaseError; end
|
11
|
+
|
12
|
+
# Raised when capsule cannot create an authenticated client for the source.
|
13
|
+
class SourceAuthenticationFailed < BaseError; end
|
14
|
+
|
15
|
+
# Raised when there is an error parsing the repo payload format.
|
16
|
+
class SourcePayloadFormatError < BaseError; end
|
17
|
+
|
18
|
+
# Raised when a source payload is unsupported/action is invalid
|
19
|
+
class SourcePayloadUnsupported < BaseError; end
|
20
|
+
|
21
|
+
# Raised when the user who started the packaging is unauthorized (non-collaborator)
|
22
|
+
class SourceUnauthorizedUser < BaseError; end
|
23
|
+
|
24
|
+
# Raised when the package is missing certain required files (ie metadata.rb, package.json, setup.py, etc)
|
25
|
+
class BuildPackageInvalid < BaseError; end
|
26
|
+
|
27
|
+
# Raised when the source could not be compiled or build for any reason
|
28
|
+
class BuildPackageFailed < BaseError; end
|
29
|
+
|
30
|
+
# Raised when package dependencies fail to install correctly.
|
31
|
+
class TestDependenciesError < BaseError; end
|
32
|
+
|
33
|
+
# Raised when the package test runner fails
|
34
|
+
class TestRunnerError < BaseError; end
|
35
|
+
|
36
|
+
# Raised when credentials required to upload/deploy new package are missing.
|
37
|
+
class ReleaseCredentialsMissing < BaseError; end
|
38
|
+
|
39
|
+
# Raised when an error occurs while uploading package.
|
40
|
+
class ReleasePackageError < BaseError; end
|
41
|
+
|
42
|
+
# Gracefully handle any errors raised by CapsuleCD, and exit with a failure
|
43
|
+
# status code.
|
44
|
+
# @param [CapsuleCD::Error::BaseError] ex
|
45
|
+
def self.handle_exception(ex)
|
46
|
+
puts ex.message
|
47
|
+
# Configuration.output.puts_red(ex.message)
|
48
|
+
exit false
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|