capsulecd 1.0.0
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/.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
|