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.
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