capsulecd 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +2 -0
  3. data/.dockerignore +5 -0
  4. data/.gitignore +95 -0
  5. data/.rspec +3 -0
  6. data/.simplecov +9 -0
  7. data/Dockerfile +16 -0
  8. data/Dockerfile.chef +26 -0
  9. data/Dockerfile.javascript +7 -0
  10. data/Dockerfile.node +7 -0
  11. data/Dockerfile.python +7 -0
  12. data/Dockerfile.ruby +4 -0
  13. data/FEATURES.md +12 -0
  14. data/Gemfile +26 -0
  15. data/LICENSE.md +22 -0
  16. data/README.md +227 -0
  17. data/Rakefile +43 -0
  18. data/bin/capsulecd +4 -0
  19. data/capsulecd.gemspec +27 -0
  20. data/circle.yml +24 -0
  21. data/lib/capsulecd/base/common/git_utils.rb +90 -0
  22. data/lib/capsulecd/base/common/validation_utils.rb +22 -0
  23. data/lib/capsulecd/base/configuration.rb +151 -0
  24. data/lib/capsulecd/base/engine.rb +163 -0
  25. data/lib/capsulecd/base/runner/circleci.rb +37 -0
  26. data/lib/capsulecd/base/runner/default.rb +38 -0
  27. data/lib/capsulecd/base/source/github.rb +183 -0
  28. data/lib/capsulecd/base/transform_engine.rb +62 -0
  29. data/lib/capsulecd/chef/chef_engine.rb +172 -0
  30. data/lib/capsulecd/chef/chef_helper.rb +29 -0
  31. data/lib/capsulecd/cli.rb +64 -0
  32. data/lib/capsulecd/error.rb +51 -0
  33. data/lib/capsulecd/javascript/javascript_engine.rb +213 -0
  34. data/lib/capsulecd/node/node_engine.rb +141 -0
  35. data/lib/capsulecd/python/python_engine.rb +157 -0
  36. data/lib/capsulecd/ruby/ruby_engine.rb +191 -0
  37. data/lib/capsulecd/ruby/ruby_helper.rb +60 -0
  38. data/lib/capsulecd/version.rb +3 -0
  39. data/lib/capsulecd.rb +16 -0
  40. data/logo.svg +1 -0
  41. data/spec/fixtures/chef/cookbook_analogj_test/CHANGELOG.md +3 -0
  42. data/spec/fixtures/chef/cookbook_analogj_test/Gemfile +18 -0
  43. data/spec/fixtures/chef/cookbook_analogj_test/LICENSE +21 -0
  44. data/spec/fixtures/chef/cookbook_analogj_test/README.md +13 -0
  45. data/spec/fixtures/chef/cookbook_analogj_test/Rakefile +1 -0
  46. data/spec/fixtures/chef/cookbook_analogj_test/Thorfile +12 -0
  47. data/spec/fixtures/chef/cookbook_analogj_test/chefignore +94 -0
  48. data/spec/fixtures/chef/cookbook_analogj_test/metadata.rb +5 -0
  49. data/spec/fixtures/chef/cookbook_analogj_test/recipes/default.rb +6 -0
  50. data/spec/fixtures/incorrect_configuration.yml +4 -0
  51. data/spec/fixtures/javascript/javascript_analogj_test/LICENSE +21 -0
  52. data/spec/fixtures/javascript/javascript_analogj_test/README.md +2 -0
  53. data/spec/fixtures/javascript/javascript_analogj_test/package.json +19 -0
  54. data/spec/fixtures/node/npm_analogj_test/LICENSE +21 -0
  55. data/spec/fixtures/node/npm_analogj_test/README.md +2 -0
  56. data/spec/fixtures/node/npm_analogj_test/package.json +19 -0
  57. data/spec/fixtures/python/pip_analogj_test/LICENSE +21 -0
  58. data/spec/fixtures/python/pip_analogj_test/MANIFEST.in +1 -0
  59. data/spec/fixtures/python/pip_analogj_test/README.md +1 -0
  60. data/spec/fixtures/python/pip_analogj_test/VERSION +1 -0
  61. data/spec/fixtures/python/pip_analogj_test/setup.cfg +5 -0
  62. data/spec/fixtures/python/pip_analogj_test/setup.py +80 -0
  63. data/spec/fixtures/python/pip_analogj_test/tox.ini +14 -0
  64. data/spec/fixtures/ruby/gem_analogj_test/Gemfile +4 -0
  65. data/spec/fixtures/ruby/gem_analogj_test/LICENSE.txt +21 -0
  66. data/spec/fixtures/ruby/gem_analogj_test/README.md +41 -0
  67. data/spec/fixtures/ruby/gem_analogj_test/Rakefile +6 -0
  68. data/spec/fixtures/ruby/gem_analogj_test/bin/console +14 -0
  69. data/spec/fixtures/ruby/gem_analogj_test/bin/setup +8 -0
  70. data/spec/fixtures/ruby/gem_analogj_test/gem_analogj_test.gemspec +25 -0
  71. data/spec/fixtures/ruby/gem_analogj_test/lib/gem_analogj_test/version.rb +3 -0
  72. data/spec/fixtures/ruby/gem_analogj_test/lib/gem_analogj_test.rb +5 -0
  73. data/spec/fixtures/ruby/gem_analogj_test/spec/gem_analogj_test_spec.rb +7 -0
  74. data/spec/fixtures/ruby/gem_analogj_test/spec/spec_helper.rb +2 -0
  75. data/spec/fixtures/ruby/gem_analogj_test-0.1.4.gem +0 -0
  76. data/spec/fixtures/sample_chef_configuration.yml +8 -0
  77. data/spec/fixtures/sample_configuration.yml +7 -0
  78. data/spec/fixtures/sample_global_configuration.yml +23 -0
  79. data/spec/fixtures/sample_node_configuration.yml +7 -0
  80. data/spec/fixtures/sample_python_configuration.yml +8 -0
  81. data/spec/fixtures/sample_repo_configuration.yml +22 -0
  82. data/spec/fixtures/sample_ruby_configuration.yml +5 -0
  83. data/spec/fixtures/vcr_cassettes/chef_build_step.yml +636 -0
  84. data/spec/fixtures/vcr_cassettes/gem_build_step.yml +653 -0
  85. data/spec/fixtures/vcr_cassettes/gem_build_step_without_version_rb.yml +653 -0
  86. data/spec/fixtures/vcr_cassettes/integration_chef.yml +1399 -0
  87. data/spec/fixtures/vcr_cassettes/integration_node.yml +1388 -0
  88. data/spec/fixtures/vcr_cassettes/integration_python.yml +1388 -0
  89. data/spec/fixtures/vcr_cassettes/integration_ruby.yml +1377 -0
  90. data/spec/fixtures/vcr_cassettes/node_build_step.yml +647 -0
  91. data/spec/fixtures/vcr_cassettes/pip_build_step.yml +653 -0
  92. data/spec/lib/capsulecd/base/configuration_spec.rb +75 -0
  93. data/spec/lib/capsulecd/base/engine_spec.rb +51 -0
  94. data/spec/lib/capsulecd/base/source/github_spec.rb +253 -0
  95. data/spec/lib/capsulecd/base/transform_engine_spec.rb +55 -0
  96. data/spec/lib/capsulecd/chef/chef_engine_spec.rb +114 -0
  97. data/spec/lib/capsulecd/cli_spec.rb +57 -0
  98. data/spec/lib/capsulecd/node/node_engine_spec.rb +113 -0
  99. data/spec/lib/capsulecd/python/python_engine_spec.rb +118 -0
  100. data/spec/lib/capsulecd/ruby/ruby_engine_spec.rb +128 -0
  101. data/spec/spec_helper.rb +105 -0
  102. data/spec/support/file_system.rb +21 -0
  103. data/spec/support/package_types.rb +11 -0
  104. 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