release_manager 0.3.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 (45) hide show
  1. checksums.yaml +7 -0
  2. data/.bash_profile +27 -0
  3. data/.dockerignore +1 -0
  4. data/.env +3 -0
  5. data/.gitignore +13 -0
  6. data/.rspec +2 -0
  7. data/CHANGELOG.md +55 -0
  8. data/Dockerfile +14 -0
  9. data/Gemfile +14 -0
  10. data/Gemfile.lock +72 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +311 -0
  13. data/Rakefile +6 -0
  14. data/app_startup_script.sh +4 -0
  15. data/bin/console +14 -0
  16. data/bin/setup +8 -0
  17. data/docker-compose.yml +37 -0
  18. data/exe/bump-changelog +5 -0
  19. data/exe/deploy-mod +5 -0
  20. data/exe/release-mod +5 -0
  21. data/exe/sandbox-create +13 -0
  22. data/lib/release_manager.rb +33 -0
  23. data/lib/release_manager/changelog.rb +130 -0
  24. data/lib/release_manager/cli/deploy_mod_cli.rb +44 -0
  25. data/lib/release_manager/cli/release_mod_cli.rb +43 -0
  26. data/lib/release_manager/cli/sandbox_create_cli.rb +138 -0
  27. data/lib/release_manager/control_mod.rb +83 -0
  28. data/lib/release_manager/control_repo.rb +35 -0
  29. data/lib/release_manager/errors.rb +13 -0
  30. data/lib/release_manager/git/credentials.rb +98 -0
  31. data/lib/release_manager/git/utilites.rb +263 -0
  32. data/lib/release_manager/logger.rb +52 -0
  33. data/lib/release_manager/module_deployer.rb +77 -0
  34. data/lib/release_manager/puppet_module.rb +211 -0
  35. data/lib/release_manager/puppetfile.rb +148 -0
  36. data/lib/release_manager/release.rb +174 -0
  37. data/lib/release_manager/sandbox.rb +272 -0
  38. data/lib/release_manager/vcs_manager.rb +22 -0
  39. data/lib/release_manager/vcs_manager/gitlab_adapter.rb +112 -0
  40. data/lib/release_manager/vcs_manager/vcs_adapter.rb +22 -0
  41. data/lib/release_manager/version.rb +3 -0
  42. data/lib/release_manager/workflow_action.rb +5 -0
  43. data/release_manager.gemspec +38 -0
  44. data/setup_repos.rb +95 -0
  45. metadata +175 -0
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Author: Corey Osman
4
+ # Purpose: release a new version of a module or r10k-control from the src branch by performing
5
+ # the following tasks:
6
+ # - bump version in metadata file
7
+ # - bump changelog version using version in metadata file
8
+ # - tag the code matching the version in the metadata file
9
+ # - push to upstream
10
+ # This script can be used on modules or r10k-control. If using on a module
11
+ # be sure to pass in the repo path using --repo. The repo is where this script
12
+ # pushes too.
13
+ #
14
+ # You should also use the -d feature which simulates a run of the script without doing
15
+ # anything harmful.
16
+ #
17
+ # Run with -h to see the help
18
+ require 'json'
19
+ require_relative 'puppet_module'
20
+
21
+ class Release
22
+ attr_reader :path, :options
23
+ include ReleaseManager::Logger
24
+
25
+ def initialize(path = Dir.getwd, options = {})
26
+ @path = path || Dir.getwd
27
+ @options = options
28
+ end
29
+
30
+ def puppet_module
31
+ @puppet_module ||= PuppetModule.new(path, upstream_repo)
32
+ end
33
+
34
+ def upstream_repo
35
+ options[:repo] || ENV['UPSTREAM_REPO']
36
+ end
37
+
38
+ # @returns [String] the version found in the metadata file
39
+ def version
40
+ dry_run? ? puppet_module.version.next : puppet_module.version
41
+ end
42
+
43
+ def tag
44
+ if dry_run?
45
+ logger.info "Would have just tagged the module to #{version}"
46
+ return
47
+ end
48
+ puppet_module.tag_module
49
+ end
50
+
51
+ def bump
52
+ if dry_run?
53
+ logger.info "Would have just bumped the version to #{version}"
54
+ return
55
+ end
56
+ puppet_module.bump_patch_version unless options[:bump]
57
+ # save the update version to the metadata file, then commit
58
+ puppet_module.commit_metadata
59
+ end
60
+
61
+ def bump_log
62
+ if dry_run?
63
+ logger.info "Would have just bumped the CHANGELOG to version #{version}"
64
+ return
65
+ end
66
+ log = Changelog.new(puppet_module.path, version, {:commit => true})
67
+ log.run
68
+ end
69
+
70
+ def push
71
+ if dry_run?
72
+ logger.info "Would have just pushed the code and tag to #{puppet_module.source}"
73
+ return
74
+ end
75
+ puppet_module.push_to_upstream
76
+ end
77
+
78
+ def dry_run?
79
+ options[:dry_run] == true
80
+ end
81
+
82
+ def auto_release?
83
+ options[:auto] || ENV['AUTO_RELEASE'] == 'true'
84
+ end
85
+
86
+ def check_requirements
87
+ begin
88
+ PuppetModule.check_requirements(puppet_module.path)
89
+ Changelog.check_requirements(puppet_module.path)
90
+ rescue NoUnreleasedLine
91
+ logger.fatal "No Unreleased line in the CHANGELOG.md file, please add a Unreleased line and retry"
92
+ exit 1
93
+ rescue UpstreamSourceMatch
94
+ logger.fatal "The upstream remote url does not match the source url in the metadata.json source"
95
+ add_upstream_remote
96
+ exit 1
97
+ rescue InvalidMetadataSource
98
+ logger.fatal "The puppet module's metadata.json source field must be a git url: ie. git@someserver.com:devops/module.git"
99
+ exit 1
100
+ rescue NoChangeLogFile
101
+ logger.fatal "CHANGELOG.md does not exist, please create one"
102
+ exit 1
103
+ end
104
+ end
105
+
106
+ # runs all the required steps to release the software
107
+ # currently this must be done manually by a release manager
108
+ #
109
+ def release
110
+ unless auto_release?
111
+ print "Have you merged your code? Did you fetch and rebase against the upstream? Want to continue (y/n)?: ".yellow
112
+ answer = gets.downcase.chomp
113
+ if answer == 'n'
114
+ return false
115
+ end
116
+ end
117
+
118
+ # updates the metadata.js file to the next version
119
+ bump
120
+ # updates the changelog to the next version based on the metadata file
121
+ bump_log
122
+ # tags the r10k-module with the version found in the metadata.json file
123
+ tag
124
+ # pushes the updated code and tags to the upstream repo
125
+ if auto_release?
126
+ push
127
+ return
128
+ end
129
+ print "Ready to release version #{version} to #{puppet_module.source}\n and forever change history(y/n)?: ".yellow
130
+ answer = gets.downcase.chomp
131
+ if answer == 'y'
132
+ push
133
+ $?.success?
134
+ else
135
+ puts "Nah, forget it, this release wasn't that cool anyways.".yellow
136
+ false
137
+ end
138
+ end
139
+
140
+ def add_upstream_remote
141
+ answer = nil
142
+ while answer !~ /y|n/
143
+ print "Ok to change your upstream remote from #{puppet_module.upstream}\n to #{puppet_module.source}? (y/n): "
144
+ answer = gets.downcase.chomp
145
+ end
146
+ puppet_module.add_upstream_remote if answer == 'y'
147
+ end
148
+
149
+ def verbose?
150
+ options[:verbose]
151
+ end
152
+
153
+ def run
154
+ begin
155
+ check_requirements
156
+ puppet_module.create_dev_branch
157
+ value = release
158
+ unless value
159
+ exit 1
160
+ end
161
+ logger.info "Releasing Version #{version} to #{puppet_module.source}"
162
+ logger.info "Version #{version} has been released successfully"
163
+ puts "This was a dry run so nothing actually happen".green if dry_run?
164
+ exit 0
165
+ rescue GitError
166
+ logger.fatal "There was an issue when running a git command"
167
+ rescue InvalidMetadataSource
168
+ logger.fatal "The puppet module's metadata.json source field must be a git url: ie. git@someserver.com:devops/module.git"
169
+ rescue ModNotFoundException
170
+ logger.fatal "Invalid module path for #{path}"
171
+ exit -1
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,272 @@
1
+ require_relative 'puppet_module'
2
+ require_relative 'control_repo'
3
+ require 'gitlab'
4
+ require 'rugged'
5
+ require 'fileutils'
6
+ require 'release_manager/logger'
7
+ require 'release_manager/vcs_manager'
8
+
9
+ class Sandbox
10
+ attr_reader :modules, :name, :repos_dir, :options,
11
+ :control_repo, :module_names, :control_repo_path, :vcs
12
+
13
+ include ReleaseManager::Logger
14
+
15
+ def initialize(name, modules, control_repo_path, repos_dir = nil, options = {})
16
+ @name = name
17
+ @repos_dir = repos_dir
18
+ @module_names = modules
19
+ @control_repo_path = control_repo_path
20
+ @vcs = ReleaseManager::VCSManager.default_instance
21
+ @options = options
22
+ end
23
+
24
+ # @param [String] repos_path - the path to the repos directory where you want to clone modules
25
+ # @return [String] the repos_path
26
+ # Creates the repos path using mkdir_p unless the path already exists
27
+ def setup_repos_dir(repos_path)
28
+ FileUtils.mkdir_p(repos_path) unless File.exists?(repos_path)
29
+ repos_path
30
+ end
31
+
32
+ # @return [String] the repos_path, defaults to ~/repos
33
+ def repos_dir
34
+ @repos_dir ||= File.expand_path(File.join(ENV['HOME'], 'repos'))
35
+ end
36
+
37
+ # @return [String] the r10k control repo path, defaults to ~/repos/r10k-control
38
+ def control_repo_path
39
+ @control_repo_path ||= File.expand_path(File.join(repos_dir, 'r10k-control'))
40
+ end
41
+
42
+ # @return [ControlRepo] - a ControlRepo object
43
+ def control_repo
44
+ @control_repo ||= ControlRepo.new(control_repo_path)
45
+ end
46
+
47
+ # @return [ControlRepo] - creates a new control repo object and clones the url unless already cloned
48
+ # @param [String] url - the url to clone and fork
49
+ def setup_control_repo(url)
50
+ # clone r10k unless already cloned
51
+ puts "## r10k-control ##".yellow
52
+ fork = create_repo_fork(url)
53
+ c = ControlRepo.create(control_repo_path, fork.ssh_url_to_repo)
54
+ c.add_remote(fork.ssh_url_to_repo, 'myfork')
55
+ c.fetch('myfork')
56
+ c.fetch('origin')
57
+ c.add_remote(url, 'upstream')
58
+ # if the user doesn't have the branch, we create from upstream
59
+ # and then checkout from the fork, we defer pushing the branch to later after updating the puppetfile
60
+ target = c.branch_exist?("upstream/#{name}") ? "upstream/#{name}" : 'upstream/dev'
61
+ # if the user has previously created the branch but doesn't exist locally, no need to create
62
+ c.create_branch(name, target)
63
+ c.checkout_branch(name)
64
+ c
65
+ end
66
+
67
+ # @return [PuppetModule] - creates a new puppet_module object and clones the url unless already cloned
68
+ # @param [ControlMod] mod - the module to clone and fork
69
+ # @param [Boolean] create_fork - defaults to true which creates a fork
70
+ # if the fork is already created, do nothing
71
+ def setup_module_repo(mod)
72
+ raise InvalidModule.new(mod) unless mod.instance_of?(ControlMod)
73
+ fork = create_repo_fork(mod.repo)
74
+ m = PuppetModule.create(File.join(repos_dir, mod.name), fork.ssh_url_to_repo, name)
75
+ m.fetch('origin')
76
+ m.add_remote(fork.ssh_url_to_repo, 'myfork')
77
+ # without the following, we risk accidently setting the upstream to the newly forked url
78
+ # this occurs because r10k-control branch contains the forked url instead of the upstream url
79
+ # we assume the metadata.source attribute contains the correct upstream url
80
+ begin
81
+ delay_source_change = false
82
+ if m.source =~ /\Agit\@/
83
+ m.add_remote(m.source, 'upstream', true)
84
+ else
85
+ logger.warn("Module's source is not defined correctly for #{m.name} should be a git url, fixing...")
86
+ # delay the changing of metadata source until we checkout the branch
87
+ delay_source_change = true
88
+ m.add_remote(mod.repo, 'upstream', true)
89
+ end
90
+ rescue ModNotFoundException => e
91
+ logger.error("Is #{mod.name} a puppet module? Can't find the metadata source")
92
+ end
93
+ # if the user doesn't have the branch, we create from upstream
94
+ # and then checkout from the fork
95
+ # if the user has previously created the branch but doesn't exist locally, no need to create
96
+ if m.remote_exists?('upstream')
97
+ target = m.branch_exist?("myfork/#{name}") ? "myfork/#{name}" : 'upstream/master'
98
+ else
99
+ # don't create from upstream since the upstream remote does not exist
100
+ # upstream does not exist because the url in the metadata source is not a git url
101
+ target = 'master'
102
+ end
103
+ m.create_branch(name, target)
104
+ m.push_branch('myfork', name)
105
+ m.checkout_branch(name)
106
+ if delay_source_change
107
+ m.source = mod.repo
108
+ m.commit_metadata_source
109
+ end
110
+ logger.info("Updating r10k-control Puppetfile to use fork: #{fork.ssh_url_to_repo} with branch: #{name}")
111
+ puppetfile.write_source(mod.name, fork.ssh_url_to_repo, name )
112
+ m
113
+ end
114
+
115
+ def setup_new_module(mod_name)
116
+ repo_url = nil
117
+ loop do
118
+ print "Please enter the git url of the source repo : ".yellow
119
+ repo_url = gets.chomp
120
+ break if repo_url =~ /git\@/
121
+ puts "Repo Url must be a git url".red
122
+ end
123
+ puppetfile.add_module(mod_name, git: repo_url)
124
+ end
125
+
126
+ # checkout and/or create branch
127
+ # get modules
128
+ # fork module unless already exists
129
+ # clone fork of module
130
+ # create branch of fork
131
+ # set module fork
132
+ # set module branch
133
+ # set upstream to original namespace
134
+ # cleanup branches
135
+ def create(r10k_url)
136
+ setup_repos_dir(repos_dir)
137
+ @control_repo = setup_control_repo(r10k_url)
138
+ # get modules we are interested in
139
+ module_names.each do | mod_name |
140
+ puts "## #{mod_name} ##".yellow
141
+ begin
142
+ mod = puppetfile.find_mod(mod_name)
143
+ setup_module_repo(mod)
144
+ rescue InvalidModuleNameException => e
145
+ logger.error(e.message)
146
+ value = nil
147
+ loop do
148
+ print "Do you want to create a new entry in the Puppetfile for the module named #{mod_name}?(y/n): ".yellow
149
+ value = gets.downcase.chomp
150
+ break if value =~ /y|n/
151
+ end
152
+ next if value == 'n'
153
+ mod = setup_new_module(mod_name)
154
+ setup_module_repo(mod)
155
+ end
156
+ end
157
+ @control_repo.checkout_branch(name)
158
+ puppetfile.write_to_file
159
+ logger.info("Committing Puppetfile changes to r10k-control branch: #{name}")
160
+ puppetfile.commit("Sandbox Creation for #{name} environment")
161
+ logger.info("Pushing new environment branch: #{name} to upstream")
162
+ puppetfile.push('upstream', name, true)
163
+ return self
164
+ end
165
+
166
+ # @param [String] url - a git url
167
+ # @return [String] a string representing the project id from gitlab
168
+ # gets the project id from gitlab using the remote API
169
+ def repo_id(url)
170
+ # ie. git@server:namespace/project.git
171
+ proj = url.match(/:(.*\/.*)\.git/)
172
+ raise RepoNotFound unless proj
173
+ # the gitlab api is supposed to encode the slash, but currently that doesn't seem to work
174
+ proj[1].gsub('/', '%2F')
175
+ end
176
+
177
+ # TODO: extract this out to an adapter
178
+ def verify_api_token
179
+ begin
180
+ Gitlab.user
181
+ rescue Exception => e
182
+ raise InvalidToken.new(e.message)
183
+ end
184
+ end
185
+
186
+ # TODO: extract this out to an adapter
187
+ # replaces namespace from the url with the supplied or default namespace
188
+ def swap_namespace(url, namespace = nil)
189
+ url.gsub(/\:([\w-]+)\//, ":#{namespace || Gitlab.user.username}/")
190
+ end
191
+
192
+ # @return [Gitlab::ObjectifiedHash] Information about the forked project
193
+ # @param [ControlMod] the module you want to fork
194
+ # TODO: extract this out to an adapter
195
+ def create_repo_fork(url, namespace = nil )
196
+ new_url = swap_namespace(url, namespace)
197
+ repo = repo_exists?(new_url)
198
+ unless repo
199
+ upstream_repo_id = repo_id(url)
200
+ logger.info("Forking project from #{url} to #{new_url}")
201
+ repo = Gitlab.create_fork(upstream_repo_id)
202
+ # gitlab lies about having completed the forking process, so lets sleep until it is actually done
203
+ loop do
204
+ sleep(1)
205
+ break if repo_exists?(repo.ssh_url_to_repo)
206
+ end
207
+ end
208
+ vcs.add_permissions(repo.id, options[:default_members])
209
+ repo
210
+ end
211
+
212
+ # @param [String] url - the git url of the repository
213
+ # @return [Boolean] returns the project object (true) if found, false otherwise
214
+ # TODO: extract this out to an adapter
215
+ def repo_exists?(url)
216
+ upstream_repo_id = repo_id(url)
217
+ begin
218
+ Gitlab.project(upstream_repo_id)
219
+ rescue
220
+ false
221
+ end
222
+ end
223
+
224
+ # @return String - the branch name that was created
225
+ # TODO: extract this out to an adapter
226
+ def create_repo_branch(repo_id, branch_name)
227
+ Gitlab.repo_create_branch(repo_id, branch_name)
228
+ end
229
+
230
+ # TODO: extract this out to an adapter
231
+ def clone_repo(mod_name, url)
232
+ path = File.join(repos_dir, mod_name)
233
+ Rugged::Repository.clone_at(url, path, checkout_branch: name)
234
+ end
235
+
236
+ # @returns [Hash[PuppetModules]] an hash of puppet modules
237
+ def modules
238
+ @modules ||= puppetfile.find_mods(module_names)
239
+ end
240
+
241
+ def puppetfile
242
+ @puppetfile ||= control_repo.puppetfile
243
+ end
244
+
245
+ def check_requirements
246
+ begin
247
+ vcs.add_ssh_key
248
+ rescue InvalidModuleNameException => e
249
+ logger.error(e.message)
250
+ exit 1
251
+ end
252
+ end
253
+
254
+ # @param [String] name - the name of the sandbox
255
+ # @param [Array[String] modules - the names of the modules that should be forked and branched
256
+ # @param [String] repos_dir - the path to the repos directory
257
+ # @param [String] control_repo_path - the url or path to the r10k control repo
258
+ # @option [String] fork_namespace - the namespace from which you fork modules to
259
+ #
260
+ # the user passes in the r10k git url or path or we assume the path
261
+ # if the user does not pass in the git url we assume the repo already exist
262
+ # if the repo doesn't exist we clone the url
263
+ def self.create(name, options)
264
+ box = Sandbox.new(name, options[:modules],
265
+ options[:r10k_repo_path],
266
+ options[:repos_path], options)
267
+ box.check_requirements
268
+ box.verify_api_token
269
+ box.create(options[:r10k_repo_url])
270
+ end
271
+
272
+ end
@@ -0,0 +1,22 @@
1
+ require 'release_manager/vcs_manager/gitlab_adapter'
2
+
3
+ module ReleaseManager
4
+ module VCSManager
5
+ def self.default_instance
6
+ ReleaseManager::VCSManager::GitlabAdapter.create
7
+ end
8
+
9
+ def self.adapter_types
10
+ [:gitlab]
11
+ end
12
+
13
+ def self.adapter_instance(type)
14
+ case type
15
+ when :gitlab
16
+ ReleaseManager::VCSManager::GitlabAdapter.create
17
+ else
18
+ default_instance
19
+ end
20
+ end
21
+ end
22
+ end