releasinator 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 194855b8293cef00a51d7cf0db1d92ffc40b8431
4
+ data.tar.gz: 3a83dfbf189daeed8dd226c6b58618d946d10a6b
5
+ SHA512:
6
+ metadata.gz: cb334694d56c8b3152eb861d57b6861b8a7605a1ee2278916589b334f577b5abc8e8e1bc154632b25dd5abda40223c8c048add31764545b3a0526315eaed288e
7
+ data.tar.gz: 3e0c022e3146461f65cf51642a094606cf572c0e371543f8e3d9ad20bfb6e5b154739bfc9de08f915343beed4571a9745b04ce25db1005236acf9a81474f2642
data/.gitignore ADDED
File without changes
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in releasinator.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,44 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ releasinator (0.1.0)
5
+ colorize
6
+ configatron
7
+ json
8
+ octokit (~> 4.0)
9
+ semantic
10
+ vandamme
11
+
12
+ GEM
13
+ remote: https://rubygems.org/
14
+ specs:
15
+ addressable (2.4.0)
16
+ colorize (0.7.7)
17
+ configatron (4.5.0)
18
+ faraday (0.9.2)
19
+ multipart-post (>= 1.2, < 3)
20
+ github-markup (1.4.0)
21
+ json (1.8.3)
22
+ multipart-post (2.0.0)
23
+ octokit (4.3.0)
24
+ sawyer (~> 0.7.0, >= 0.5.3)
25
+ rake (11.1.2)
26
+ redcarpet (3.3.4)
27
+ sawyer (0.7.0)
28
+ addressable (>= 2.3.5, < 2.5)
29
+ faraday (~> 0.8, < 0.10)
30
+ semantic (1.4.1)
31
+ vandamme (0.0.11)
32
+ github-markup (~> 1.3)
33
+ redcarpet (~> 3.3.2)
34
+
35
+ PLATFORMS
36
+ ruby
37
+
38
+ DEPENDENCIES
39
+ bundler (~> 1.11)
40
+ rake (~> 11.1)
41
+ releasinator!
42
+
43
+ BUNDLED WITH
44
+ 1.11.2
data/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # Releasinator
2
+
3
+ ## Problem
4
+
5
+ When automating an SDK release process, many teams and languages have different ideas. The release process is a hurdle that makes it hard for new project members to ramp up. One shouldn't have to read a `release_process.md` to release an open source SDK.
6
+
7
+ ## Solution
8
+
9
+ The releasinator corrects this by enforcing standard must-have release files, being configurable where necessary, and reducing the ramp-up hurdle.
10
+
11
+ ## Making it work
12
+
13
+ ### Install
14
+
15
+ 1. Clone this repo
16
+ 2. Run `bundle install` from terminal. It should download dependencies.
17
+
18
+ ### Setup
19
+
20
+ 1. Set the environment variable `GITHUB_TOKEN` with a [generated personal access token](https://github.com/settings/tokens) with `repo` scope.
21
+
22
+ ### Usage
23
+
24
+ 1. `cd` to directory of the repo you'd like to release
25
+ 2. Run `rake -t -f <path to releasinator>/Rakefile`
26
+
27
+ ## Conventions
28
+
29
+ The releasinator enforces certain conventions. If a filename doesn't exactly match the convention, it is renamed to match, and automatically committed. The conventions are documented below:
30
+
31
+ 1. `README.md`
32
+ 2. `LICENSE`: See [here](http://stackoverflow.com/questions/5678462/should-i-provide-a-license-txt-or-copying-txt-file-in-my-project) for a relevant StackOverflow post on this discussion. This project has chosen to exclude the `.txt` extension to match other popular projects, and GitHub defaults.
33
+ 3. `CONTRIBUTING.md`: See [a relevant GitHub blog post](https://github.com/blog/1184-contributing-guidelines) for relevant information. This project has chosen to include the `.md` extension, as these files can get a bit unwieldy without formatting.
34
+ 4. `CHANGELOG.md` - This file is the source of truth for the releasinator. The file should be organized with the most recent release on top, and formatted correctly. The latest release is the one used when the releasinator executes, so it is a precondition that the `CHANGELOG.md` has been edited and committed **prior to releasing**.
35
+ 1. Releases either are contained within an Alt-H2 (`------`) or `##H2` format. Any other format will be rejected.
36
+ 2. Each release MUST start with the release version, and may contain any following text, such as the date, and/or any release summary.
37
+
38
+ ## Behind the Scenes
39
+
40
+ #### Validations
41
+
42
+ 1. ✓ Validate git version.
43
+ 1. ✓ Validate git's cleanliness (no untracked, unstaged, or uncommitted files).
44
+ 1. ✓ Validate current git branch is up to date with the latest version on server.
45
+ 1. ✓ Validate current git branch is `master` (if no git flow), or `develop` or `release/<Release number>` if using git flow.
46
+ 1. ✓ Validate the presence of`.gitignore`, adding it if needed, and adding any needed lines.
47
+ 1. ✓ Validate the presence of the `README.md` file.
48
+ 1. ✓ Validate the presence of the `CHANGELOG.md` file.
49
+ 1. ✓ Validate the `CHANGELOG.md` file is properly formatted.
50
+ 1. ✓ Validate semver release number sequence in `CHANGELOG.md`. (Note: cannot detect duplicate versions, due to the underlying Hash implementation of the parsing library.)
51
+ 1. ✓ Validate the presence of `LICENSE`.
52
+ 1. ✓ Validate the presence of `CONTRIBUTING.md`.
53
+ 1. ✓ Validate the presence of `.github/ISSUE_TEMPLATE.md`.
54
+ 1. ✓ Validate `LICENSE` and `CONTRIBUTING.md` are referenced in `README.md`.
55
+ 1. ✓ Validate all submodules are up to date with the latest master version.
56
+ 1. ✓ Validate all files are committed to git (`git ls-files --others --exclude-standard`).
57
+ 1. ✓ Validate user has valid access_tokens as environment variables for all repos (public & enterprise). Public github requires `GITHUB_TOKEN`, while enterprise tokens add a modified version of their domain as a prefix. For example, github.example.com requires `GITHUB_EXAMPLE_COM_GITHUB_TOKEN`.
58
+ 1. ✓ Validate user has permissions to push to repo, and downstream repos.
59
+ 1. ✓ Validate anything as defined by the configuration. Examples:
60
+ * Compiling with the right version of the platform.
61
+ * Upstream library dependencies are the latest available.
62
+
63
+ #### Package manager credential management
64
+ 1. Validate permissions to publish to package manager
65
+ 2. If permissions are needed, retrieve them from the secret credential repo, and run any subsequent repo steps.
66
+
67
+ #### Performs internal-only tasks:
68
+
69
+ 1. ✓ Confirm user has completed all manual tasks (aka pre-release checklist).
70
+ 1. ✓ Confirm overwrite of local tags already matching version.
71
+ 1. ✓ Create local tag.
72
+ 1. ✓ Run build command, as specified in the config file.
73
+
74
+ #### Performs external-facing tasks:
75
+
76
+ 1. ✓ Clone/reset any downstream external distribution repos.
77
+ 2. ✓ Copy to destination repo:
78
+
79
+ * `README.md`
80
+ * `CHANGELOG.md`
81
+ * `LICENSE`
82
+ * `CONTRIBUTING.md`
83
+ * `.github/ISSUE_TEMPLATE.md`
84
+ * Any other distribution files. This will be configured.
85
+
86
+ 3. ✓ Modify any dependency/versions in `README.md`, source, or sample apps to the latest version. The location and regex of these will be configured.
87
+ 4. ✓ Build code and/or Sample apps.
88
+ * TODO If build fails, pause build and give developer a chance to fix the downstream build and retry without completely starting over.
89
+
90
+ 5. ✓ Push to appropriate package manager (nexus/cocoapods/composer/npm/nuget/rubygem/pypi).
91
+ 6. ✓ Confirm overwrite of remote tags already matching version.
92
+ 7. ✓ Create root and downstream repo tag and GitHub release. The name of the release is the tag, and the annotated description and GitHub release is the relevant section of the `CHANGELOG.md`.
93
+ 8. ✓ Create downstream GitHub branch.
94
+ 9. ✓ Correctly handle git flow branches and merging.
95
+ 9. ✓ Push to external repo (once live in external package manager).
96
+ 10. ✓ Create PRs into any downstream dependencies, including the release notes in the PR. Examples:
97
+
98
+ * Create a PR into Braintree when mSDK/OTC release.
99
+ * Create a PR into Cordova when mSDK release.
100
+ * Create a PR into any other framework that has a direct dependency on this repo.
101
+
102
+ 11. TODO add those same release notes within the release notes of the downstream repo.
103
+ 11. Assemble a draft of news, and publish (where possible, i.e. send email, tweet, whatever).
104
+
105
+ #### Open items:
106
+
107
+ * Might need a different name - [releasinator is taken](https://qconnewyork.com/ny2015/system/files/presentation-slides/QCon%20Distributing%20a%20Mobile%20Team.pdf)
108
+
109
+ ### Example build files
110
+
111
+ * https://github.paypal.com/SDK-R/PayPal-Android-SDK/blob/develop/fabfile.py
112
+ * https://github.paypal.com/SDK-R/PayPal-iOS-SDK/blob/develop/fabfile.py
113
+ * https://github.braintreeps.com/braintree/braintree-android/blob/master/Rakefile
114
+
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ import "./lib/tasks/releasinator.rake"
@@ -0,0 +1,55 @@
1
+ require 'colorize'
2
+ require_relative 'printer'
3
+
4
+ module Releasinator
5
+ class CommandProcessor
6
+ def self.command(command, live_output=false)
7
+ puts Time.now.utc.iso8601 + ": " + "#{Dir.pwd}".bold + " exec:" + " #{command}".bold
8
+ if live_output
9
+ puts "...with live output (forked process)".bold
10
+
11
+ return_code = nil
12
+ r, io = IO.pipe
13
+ pid = fork do
14
+ return_code = system(command, :out => io, :err => io)
15
+ if !return_code
16
+ Printer.fail("Execution failure.")
17
+ abort()
18
+ end
19
+ end
20
+ io.close
21
+ r.each_line{|l| puts l.strip.white}
22
+
23
+ Process.wait(pid)
24
+ fork_exitstatus = $?.exitstatus
25
+ if 0 != fork_exitstatus
26
+ Printer.fail("Forked process failed with exitstatus:#{fork_exitstatus}")
27
+ abort()
28
+ end
29
+ else
30
+ output = `#{command}`
31
+ exitstatus = $?.exitstatus
32
+ if 0 != exitstatus
33
+ Printer.fail("Process failed with exitstatus:#{exitstatus}")
34
+ abort()
35
+ end
36
+
37
+ output
38
+ end
39
+ end
40
+
41
+ # waits for the input command to return non-empty output.
42
+ def self.wait_for(command_to_execute, wait_for_seconds=30)
43
+ while "" == CommandProcessor.command(command_to_execute)
44
+ puts "Returned empty output. Sleeping #{wait_for_seconds} seconds."
45
+ wait_for_seconds.times do
46
+ print "."
47
+ sleep 1
48
+ end
49
+ puts
50
+ end
51
+
52
+ Printer.success("Returned non-empty output.")
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,44 @@
1
+ require 'configatron'
2
+ require_relative 'default_config'
3
+ require_relative 'command_processor'
4
+ require_relative 'git_util'
5
+
6
+ RELEASINATOR_NAME = "releasinator"
7
+ CONFIG_FILE_NAME = ".#{RELEASINATOR_NAME}.rb"
8
+
9
+ module Releasinator
10
+ class ConfigHash < Hash
11
+ def initialize(verbose, trace)
12
+ update({:releasinator_name => RELEASINATOR_NAME})
13
+ update({:verbose => verbose})
14
+ update({:trace => trace})
15
+
16
+ require_file_name = "./.#{RELEASINATOR_NAME}.rb"
17
+ begin
18
+ require require_file_name
19
+ rescue LoadError
20
+ is_git_already_clean = GitUtil.is_clean_git?
21
+ puts "It looks like this is your first time using #{RELEASINATOR_NAME} on this project.".yellow
22
+ puts "A default '#{CONFIG_FILE_NAME}' file has been created for you.".yellow
23
+ out_file = File.new("#{CONFIG_FILE_NAME}", "w")
24
+ out_file.write(DEFAULT_CONFIG)
25
+ out_file.close
26
+ require require_file_name
27
+
28
+ # dpn't want to commit other files
29
+ if is_git_already_clean
30
+ puts "adding default #{CONFIG_FILE_NAME} to git".yellow
31
+ CommandProcessor.command("git add #{CONFIG_FILE_NAME}")
32
+ CommandProcessor.command("git commit -m \"#{RELEASINATOR_NAME}: add default config\"")
33
+ end
34
+ end
35
+
36
+ configatron.lock!
37
+ loaded_config_hash = configatron.to_h
38
+
39
+ update(loaded_config_hash)
40
+
41
+ puts "loaded config:" + self.to_s if verbose
42
+ end
43
+ end
44
+ end
data/lib/copy_file.rb ADDED
@@ -0,0 +1,10 @@
1
+ module Releasinator
2
+ class CopyFile
3
+ attr_reader :source_file, :target_name, :target_dir
4
+ def initialize(source_file, target_name, target_dir)
5
+ @source_file = source_file
6
+ @target_name = target_name
7
+ @target_dir = target_dir
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ module Releasinator
2
+ class CurrentRelease
3
+ attr_reader :release, :changelog
4
+ def initialize(release, changelog)
5
+ @release = release
6
+ @changelog = changelog
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,34 @@
1
+ DEFAULT_CONFIG = %(configatron.product_name = "test product"
2
+
3
+ # List of items to confirm from the person releasing. Required, but empty list is ok.
4
+ configatron.prerelease_checklist_items = [
5
+ ]
6
+
7
+ # The directory where all distributed docs are. Default is '.'
8
+ # configatron.base_docs_dir = '.'
9
+
10
+ def build_method
11
+ CommandProcessor.command("ls -al")
12
+ end
13
+
14
+ # The command that builds the sdk. Required.
15
+ configatron.build_method = method(:build_method)
16
+
17
+ def publish_to_package_manager(version)
18
+ sleep 1
19
+ puts "done publishing yay!"
20
+ end
21
+
22
+ # The method that publishes the sdk to the package manager. Required.
23
+ configatron.publish_to_package_manager_method = method(:publish_to_package_manager)
24
+
25
+
26
+ def wait_for_package_manager(version)
27
+ end
28
+
29
+ # The method that waits for the package manager to be done. Required
30
+ configatron.wait_for_package_manager_method = method(:wait_for_package_manager)
31
+
32
+ # Whether to publish the root repo to GitHub. Required.
33
+ configatron.release_to_github = true
34
+ )
@@ -0,0 +1,18 @@
1
+ module Releasinator
2
+ class DownstreamRepo
3
+ attr_reader :name, :url, :branch, :options
4
+ def initialize(name, url, branch, options={})
5
+ @name = name
6
+ @url = url
7
+ @branch = branch
8
+ @options = options
9
+ # options are:
10
+ # :new_branch_name (if set, ignores :release_to_github)
11
+ # :release_to_github
12
+ # :files_to_copy
13
+ # :full_file_sync
14
+ # :post_copy_methods
15
+ # :build_methods
16
+ end
17
+ end
18
+ end
data/lib/git_util.rb ADDED
@@ -0,0 +1,67 @@
1
+ require_relative 'command_processor'
2
+
3
+ module Releasinator
4
+ class GitUtil
5
+ def self.is_clean_git?
6
+ any_changes = CommandProcessor.command("git status --porcelain")
7
+ '' == any_changes
8
+ end
9
+
10
+ def self.get_current_branch
11
+ CommandProcessor.command("git symbolic-ref --short HEAD").strip
12
+ end
13
+
14
+ def self.untracked_files
15
+ CommandProcessor.command("git ls-files --others --exclude-standard")
16
+ end
17
+
18
+ def self.diff
19
+ CommandProcessor.command("git diff")
20
+ end
21
+
22
+ def self.cached
23
+ CommandProcessor.command("git diff --cached")
24
+ end
25
+
26
+ def self.repo_url
27
+ CommandProcessor.command("git config --get remote.origin.url").strip
28
+ end
29
+
30
+ def self.delete_branch(branch_name)
31
+ if has_branch? branch_name
32
+ CommandProcessor.command("git branch -D #{branch_name}")
33
+ end
34
+ end
35
+
36
+ def self.has_branch?(branch_name)
37
+ "" != CommandProcessor.command("git branch --list #{branch_name}").strip
38
+ end
39
+
40
+ def self.has_remote_branch?(branch_name)
41
+ "" != CommandProcessor.command("git branch --list -r #{branch_name}").strip
42
+ end
43
+
44
+ def self.checkout(branch_name)
45
+ if get_current_branch != branch_name
46
+ CommandProcessor.command("git checkout #{branch_name}")
47
+ end
48
+ end
49
+
50
+ def self.init_gh_pages
51
+ if !has_branch? "gh-pages"
52
+ if has_remote_branch? "origin/gh-pages"
53
+ checkout("gh-pages")
54
+ else
55
+ CommandProcessor.command("git checkout --orphan gh-pages")
56
+ CommandProcessor.command("GLOBIGNORE='.git' git rm -rf *")
57
+ #http://stackoverflow.com/questions/19363795/git-rm-doesnt-remove-all-files-in-one-go
58
+ CommandProcessor.command("GLOBIGNORE='.git' git rm -rf *")
59
+ CommandProcessor.command("touch README.md")
60
+ CommandProcessor.command("git add .")
61
+ CommandProcessor.command("git commit -am \"Initial gh-pages commit\"")
62
+ CommandProcessor.command("git push -u origin gh-pages")
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,45 @@
1
+ require 'colorize'
2
+ require 'octokit'
3
+ require_relative 'printer'
4
+
5
+ module Releasinator
6
+ class GitHubRepo
7
+ attr_reader :url, :org, :repo, :domain, :client
8
+
9
+ def initialize(url)
10
+ @url = url
11
+ if url.start_with?("https")
12
+ # https: "https://github.com/braebot/test.git"
13
+ slash_split = url.split("/")
14
+ @domain = slash_split[2]
15
+ @repo = slash_split.last.split(".git").first
16
+ slash_split.pop
17
+ @org = slash_split.last
18
+ else
19
+ # ssh: git@github.com:braebot/test.git"
20
+ colon_split = url.split(":")
21
+ at_split = colon_split.first.split("@")
22
+ @domain = at_split.last
23
+ slash_split = colon_split.last.split("/")
24
+ @org = slash_split.first
25
+ @repo = slash_split.last.split(".git").first
26
+ end
27
+
28
+ if @domain == "github.com"
29
+ check_token("GITHUB_TOKEN")
30
+ @client = Octokit::Client.new(:access_token => ENV["GITHUB_TOKEN"])
31
+ else
32
+ env_key = "#{@domain.gsub(".", "_").upcase}_GITHUB_TOKEN"
33
+ check_token(env_key)
34
+ @client = Octokit::Client.new(:access_token => ENV[env_key], :api_endpoint => "https://#{@domain}/api/v3/")
35
+ end
36
+ end
37
+
38
+ def check_token(token_param)
39
+ if !ENV[token_param]
40
+ Printer.fail("#{token_param} environment variable required. Please set this to your personal access token.")
41
+ abort()
42
+ end
43
+ end
44
+ end
45
+ end
data/lib/printer.rb ADDED
@@ -0,0 +1,21 @@
1
+ require_relative 'command_processor'
2
+
3
+ module Releasinator
4
+ class Printer
5
+ def self.success(msg)
6
+ puts "\xE2\x9C\x94\xEF\xB8\x8E ".light_green + "SUCCESS: #{msg}".green
7
+ end
8
+
9
+ def self.fail(msg)
10
+ puts "\xE2\x9C\x98 FAILURE: #{msg}".red
11
+ end
12
+
13
+ def self.check_proceed(warning_msg, abort_msg)
14
+ puts "#{warning_msg} Continue? (Y/n)".yellow
15
+ if 'n' == STDIN.gets.strip
16
+ self.fail(abort_msg)
17
+ abort()
18
+ end
19
+ end
20
+ end
21
+ end
data/lib/publisher.rb ADDED
@@ -0,0 +1,47 @@
1
+ require 'octokit'
2
+ require 'colorize'
3
+ require_relative 'github_repo'
4
+ require_relative 'printer'
5
+
6
+ module Releasinator
7
+ class Publisher
8
+ def initialize(config)
9
+ @config = config
10
+ end
11
+
12
+ def publish_draft(repo_url, release)
13
+ github_repo = GitHubRepo.new(repo_url)
14
+
15
+ begin
16
+ # https://github.com/octokit/octokit.rb/blob/master/spec/octokit/client/releases_spec.rb#L18
17
+ github_release = github_repo.client.create_release "#{github_repo.org}/#{github_repo.repo}",
18
+ release.release,
19
+ :name => release.release,
20
+ :body => release.changelog,
21
+ :draft => true,
22
+ :prerelease => false
23
+ puts github_release.inspect if @config[:trace]
24
+ rescue => error
25
+ #This will fail if the user does not have push permissions.
26
+ Printer.fail(error.inspect)
27
+ abort()
28
+ end
29
+ end
30
+
31
+ def publish_pull_request(repo_url, release, product_name, base, head)
32
+ begin
33
+ github_repo = GitHubRepo.new(repo_url)
34
+ github_pull_request = github_repo.client.create_pull_request "#{github_repo.org}/#{github_repo.repo}",
35
+ base,
36
+ head,
37
+ "Update #{product_name} to #{release.release}",
38
+ release.changelog
39
+ puts github_pull_request.inspect if @config[:trace]
40
+ rescue => error
41
+ #This will fail if there's already a pull request
42
+ Printer.fail(error.inspect)
43
+ abort()
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,3 @@
1
+ module Releasinator
2
+ VERSION = "0.1.0"
3
+ end