toys-release 0.1.1 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -0
- data/README.md +4 -4
- data/docs/guide.md +826 -1
- data/lib/toys/release/version.rb +1 -1
- data/toys/.data/templates/release-request.yml.erb +1 -1
- data/toys/.lib/toys/release/artifact_dir.rb +21 -1
- data/toys/.lib/toys/release/change_set.rb +8 -8
- data/toys/.lib/toys/release/component.rb +24 -85
- data/toys/.lib/toys/release/environment_utils.rb +29 -6
- data/toys/.lib/toys/release/performer.rb +48 -43
- data/toys/.lib/toys/release/pipeline.rb +577 -0
- data/toys/.lib/toys/release/pull_request.rb +0 -2
- data/toys/.lib/toys/release/repo_settings.rb +494 -332
- data/toys/.lib/toys/release/repository.rb +7 -8
- data/toys/.lib/toys/release/request_spec.rb +1 -1
- data/toys/.lib/toys/release/steps.rb +295 -441
- data/toys/.toys.rb +2 -2
- data/toys/_onclosed.rb +9 -1
- data/toys/gen-config.rb +137 -0
- data/toys/gen-workflows.rb +23 -6
- data/toys/perform.rb +7 -5
- data/toys/retry.rb +20 -14
- metadata +5 -6
- data/toys/.data/templates/release-hook-on-open.yml.erb +0 -30
- data/toys/_onopen.rb +0 -158
- data/toys/gen-settings.rb +0 -46
data/toys/.toys.rb
CHANGED
data/toys/_onclosed.rb
CHANGED
|
@@ -83,12 +83,13 @@ end
|
|
|
83
83
|
|
|
84
84
|
def handle_release_merged
|
|
85
85
|
setup_git
|
|
86
|
+
report_release_starting
|
|
86
87
|
github_check_errors = @repository.wait_github_checks
|
|
87
88
|
unless github_check_errors.empty?
|
|
88
89
|
@utils.error("GitHub checks failed", *github_check_errors)
|
|
89
90
|
end
|
|
90
91
|
performer = create_performer
|
|
91
|
-
performer.perform_pr_releases
|
|
92
|
+
performer.perform_pr_releases unless performer.error?
|
|
92
93
|
performer.report_results
|
|
93
94
|
if performer.error?
|
|
94
95
|
@utils.error("Releases reported failure")
|
|
@@ -103,6 +104,13 @@ def setup_git
|
|
|
103
104
|
exec(["git", "switch", "release/current"], e: true)
|
|
104
105
|
end
|
|
105
106
|
|
|
107
|
+
def report_release_starting
|
|
108
|
+
reports = ["Starting release automation."]
|
|
109
|
+
url = @utils.current_workflow_run_url
|
|
110
|
+
reports << "See #{url} for execution logs." if url
|
|
111
|
+
@pull_request.add_comment(reports.join(" "))
|
|
112
|
+
end
|
|
113
|
+
|
|
106
114
|
def create_performer
|
|
107
115
|
require "toys/release/performer"
|
|
108
116
|
dry_run = /^t/i.match?(::ENV["TOYS_RELEASE_DRY_RUN"].to_s)
|
data/toys/gen-config.rb
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
desc "Generate an initial config file"
|
|
4
|
+
|
|
5
|
+
long_desc \
|
|
6
|
+
"This tool generates an initial config file for this repo." \
|
|
7
|
+
" You will generally need to make additional edits to this file after" \
|
|
8
|
+
" initial generation."
|
|
9
|
+
|
|
10
|
+
flag :repo, "--repo=REPO" do
|
|
11
|
+
desc "GitHub repo owner and name (e.g. dazuma/toys)"
|
|
12
|
+
end
|
|
13
|
+
flag :git_user, "--git-user=NAME" do
|
|
14
|
+
default ""
|
|
15
|
+
desc "User name for git commits (defaults to the git user.name config)"
|
|
16
|
+
end
|
|
17
|
+
flag :git_email, "--git-email=EMAIL" do
|
|
18
|
+
default ""
|
|
19
|
+
desc "User email for git commits (defaults to the git user.email config)"
|
|
20
|
+
end
|
|
21
|
+
flag :file_path, "-o PATH", "--output=PATH" do
|
|
22
|
+
desc "Output file path (defaults to .toys/.data/releases.yml)"
|
|
23
|
+
end
|
|
24
|
+
flag :yes, "--yes", "-y" do
|
|
25
|
+
desc "Automatically answer yes to all confirmations"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
include :exec, e: true
|
|
29
|
+
include :terminal, styled: true
|
|
30
|
+
include :fileutils
|
|
31
|
+
|
|
32
|
+
def run
|
|
33
|
+
setup
|
|
34
|
+
interpret_github_repo
|
|
35
|
+
interpret_git_user
|
|
36
|
+
check_file_path
|
|
37
|
+
gems_and_dirs = find_gems
|
|
38
|
+
confirm_with_user
|
|
39
|
+
mkdir_p(::File.dirname(file_path))
|
|
40
|
+
::File.open(file_path, "w") do |file|
|
|
41
|
+
write_settings(file, gems_and_dirs)
|
|
42
|
+
end
|
|
43
|
+
puts("Wrote initial config file to #{file_path}.", :green, :bold)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def setup
|
|
47
|
+
require "toys/release/environment_utils"
|
|
48
|
+
@utils = Toys::Release::EnvironmentUtils.new(self)
|
|
49
|
+
cd(@utils.repo_root_directory)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def interpret_github_repo
|
|
53
|
+
return if repo
|
|
54
|
+
current_guess = nil
|
|
55
|
+
capture(["git", "remote", "-v"]).split("\n").each do |line|
|
|
56
|
+
match = %r{^(\S+)\s+git@github\.com:([^/.\s]+/[^/.\s]+)(?:\.git)?}.match(line)
|
|
57
|
+
current_guess = match[2] if match && (match[1] == "origin" || current_guess.nil?)
|
|
58
|
+
match = %r{^(\S+)\s+https://github\.com/([^/.\s]+/[^/.\s]+)(?:\.git)?}.match(line)
|
|
59
|
+
current_guess = match[2] if match && (match[1] == "origin" || current_guess.nil?)
|
|
60
|
+
end
|
|
61
|
+
if current_guess.nil?
|
|
62
|
+
puts "Unable to determine the GitHub repo associated with this repository.", :red, :bold
|
|
63
|
+
exit(1)
|
|
64
|
+
end
|
|
65
|
+
puts "GitHub repository inferred to be #{current_guess}."
|
|
66
|
+
puts "If this is incorrect, specify the correct repo using the --repo= flag."
|
|
67
|
+
set(:repo, current_guess)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def interpret_git_user
|
|
71
|
+
if git_user.empty?
|
|
72
|
+
set(:git_user, capture(["git", "config", "get", "user.name"]).strip)
|
|
73
|
+
if git_user.empty?
|
|
74
|
+
puts "Unable to determine git user.name. Using a hard-coded fallback", :yellow
|
|
75
|
+
set(:git_user, "Example User")
|
|
76
|
+
else
|
|
77
|
+
puts "Using the current git user.name of #{git_user}"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
if git_email.empty?
|
|
81
|
+
set(:git_email, capture(["git", "config", "get", "user.email"]).strip)
|
|
82
|
+
if git_email.empty?
|
|
83
|
+
puts "Unable to determine git user.email. Using a hard-coded fallback", :yellow
|
|
84
|
+
set(:git_email, "hello@example.com")
|
|
85
|
+
else
|
|
86
|
+
puts "Using the current git user.email of #{git_email}"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def check_file_path
|
|
92
|
+
set(:file_path, ::File.join(".toys", ".data", "releases.yml")) unless file_path
|
|
93
|
+
if ::File.readable?(file_path)
|
|
94
|
+
puts "Cannot overwrite existing file: #{file_path}", :red, :bold
|
|
95
|
+
exit(1)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def confirm_with_user
|
|
100
|
+
exit unless yes || confirm("Create config file #{file_path}? ", default: true)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def find_gems
|
|
104
|
+
toplevel = ::Dir.glob("*.gemspec")
|
|
105
|
+
subdirs = ::Dir.glob("*/*.gemspec")
|
|
106
|
+
if toplevel.size > 1
|
|
107
|
+
puts "Unexpected: Found multiple gemspecs at the top level.", :red, :bold
|
|
108
|
+
exit(1)
|
|
109
|
+
end
|
|
110
|
+
if toplevel.size == 1 && subdirs.empty?
|
|
111
|
+
path = toplevel.first
|
|
112
|
+
puts "Found #{path} at the toplevel of the repo."
|
|
113
|
+
[[::File.basename(path, ".gemspec"), "."]]
|
|
114
|
+
elsif toplevel.empty? && !subdirs.empty?
|
|
115
|
+
subdirs.map do |path|
|
|
116
|
+
puts "Found #{path} in the repo."
|
|
117
|
+
[::File.basename(path, ".gemspec"), ::File.dirname(path)]
|
|
118
|
+
end
|
|
119
|
+
else
|
|
120
|
+
puts "Unexpected: Found gemspecs at the toplevel and in subdirectories.", :red, :bold
|
|
121
|
+
exit(1)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def write_settings(file, gems_and_dirs)
|
|
126
|
+
file.puts("repo: #{repo}")
|
|
127
|
+
file.puts("git_user_name: #{git_user}")
|
|
128
|
+
file.puts("git_user_email: #{git_email}")
|
|
129
|
+
file.puts("# Insert additional repo-level settings here.")
|
|
130
|
+
file.puts
|
|
131
|
+
file.puts("gems:")
|
|
132
|
+
gems_and_dirs.sort_by(&:first).each do |(name, dir)|
|
|
133
|
+
file.puts(" - name: #{name}")
|
|
134
|
+
file.puts(" directory: #{dir}")
|
|
135
|
+
file.puts(" # Insert additional gem-level settings here.")
|
|
136
|
+
end
|
|
137
|
+
end
|
data/toys/gen-workflows.rb
CHANGED
|
@@ -5,12 +5,16 @@ desc "Generate GitHub Actions workflow files"
|
|
|
5
5
|
long_desc \
|
|
6
6
|
"This tool generates workflow files for GitHub Actions."
|
|
7
7
|
|
|
8
|
+
flag :workflows_dir, "-o PATH", "--output=PATH" do
|
|
9
|
+
desc "Output directory (defaults to .github/workflows)"
|
|
10
|
+
end
|
|
8
11
|
flag :yes, "--yes", "-y" do
|
|
9
12
|
desc "Automatically answer yes to all confirmations"
|
|
10
13
|
end
|
|
11
14
|
|
|
12
|
-
include :exec
|
|
15
|
+
include :exec, e: true
|
|
13
16
|
include :terminal, styled: true
|
|
17
|
+
include :fileutils
|
|
14
18
|
|
|
15
19
|
# Context for ERB templates
|
|
16
20
|
class ErbContext
|
|
@@ -24,21 +28,35 @@ class ErbContext
|
|
|
24
28
|
end
|
|
25
29
|
|
|
26
30
|
def run
|
|
31
|
+
setup
|
|
32
|
+
user_confirmation
|
|
33
|
+
generate_all_files
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def setup
|
|
27
37
|
require "erb"
|
|
38
|
+
require "fileutils"
|
|
28
39
|
require "toys/release/environment_utils"
|
|
29
40
|
require "toys/release/repo_settings"
|
|
30
41
|
|
|
31
42
|
@utils = Toys::Release::EnvironmentUtils.new(self)
|
|
43
|
+
cd(@utils.repo_root_directory)
|
|
32
44
|
@settings = Toys::Release::RepoSettings.load_from_environment(@utils)
|
|
33
45
|
|
|
46
|
+
set(:workflows_dir, ::File.join(".github", "workflows")) if workflows_dir.to_s.empty?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def user_confirmation
|
|
34
50
|
unless @settings.enable_release_automation?
|
|
35
51
|
puts "Release automation disabled in settings."
|
|
36
52
|
unless yes || confirm("Create workflow files anyway? ", default: false)
|
|
37
53
|
@utils.error("Aborted.")
|
|
38
54
|
end
|
|
39
55
|
end
|
|
56
|
+
end
|
|
40
57
|
|
|
41
|
-
|
|
58
|
+
def generate_all_files
|
|
59
|
+
mkdir_p(workflows_dir)
|
|
42
60
|
files = [
|
|
43
61
|
"release-hook-on-closed.yml",
|
|
44
62
|
"release-hook-on-push.yml",
|
|
@@ -46,12 +64,11 @@ def run
|
|
|
46
64
|
"release-request.yml",
|
|
47
65
|
"release-retry.yml",
|
|
48
66
|
]
|
|
49
|
-
|
|
50
|
-
files.each { |name| generate(name) }
|
|
67
|
+
files.each { |name| generate_file(name) }
|
|
51
68
|
end
|
|
52
69
|
|
|
53
|
-
def
|
|
54
|
-
destination = ::File.join(
|
|
70
|
+
def generate_file(name)
|
|
71
|
+
destination = ::File.join(workflows_dir, name)
|
|
55
72
|
if ::File.readable?(destination)
|
|
56
73
|
puts "Destination file #{destination} exists.", :yellow, :bold
|
|
57
74
|
return unless yes || confirm("Overwrite? ", default: true)
|
data/toys/perform.rb
CHANGED
|
@@ -106,11 +106,13 @@ def run
|
|
|
106
106
|
|
|
107
107
|
setup_arguments
|
|
108
108
|
setup_performer
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
109
|
+
unless @performer.error?
|
|
110
|
+
components.each do |component_spec|
|
|
111
|
+
name, version = component_spec.split(/[:=]/, 2)
|
|
112
|
+
confirmation_ui(name, version)
|
|
113
|
+
gem_version = version ? ::Gem::Version.new(version) : nil
|
|
114
|
+
@performer.perform_adhoc_release(name, assert_version: gem_version)
|
|
115
|
+
end
|
|
114
116
|
end
|
|
115
117
|
puts @performer.build_report_text
|
|
116
118
|
end
|
data/toys/retry.rb
CHANGED
|
@@ -18,12 +18,13 @@ flag_group desc: "Flags" do
|
|
|
18
18
|
"Run in dry-run mode, where checks are made and releases are built" \
|
|
19
19
|
" but are not pushed."
|
|
20
20
|
end
|
|
21
|
-
flag :
|
|
22
|
-
desc "The directory to use for
|
|
21
|
+
flag :work_dir, "--work-dir=VAL" do
|
|
22
|
+
desc "The directory to use for artifacts and temporary files"
|
|
23
23
|
long_desc \
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
"If provided, the given directory path is used for artifacts and" \
|
|
25
|
+
" temporary files, and is left intact after the release so the" \
|
|
26
|
+
" artifacts can be inspected. If omitted, a new temporary directory" \
|
|
27
|
+
" is created, and is automatically deleted after the release."
|
|
27
28
|
end
|
|
28
29
|
flag :git_remote, "--git-remote=VAL" do
|
|
29
30
|
default "origin"
|
|
@@ -84,13 +85,13 @@ def setup_objects
|
|
|
84
85
|
@repo_settings = Toys::Release::RepoSettings.load_from_environment(@utils)
|
|
85
86
|
@repository = Toys::Release::Repository.new(@utils, @repo_settings)
|
|
86
87
|
@repository.git_set_user_info
|
|
88
|
+
@pull_request = @repository.load_pr(release_pr)
|
|
87
89
|
end
|
|
88
90
|
|
|
89
91
|
def verify_release_pr
|
|
90
|
-
@
|
|
91
|
-
@utils.error("Could not load pull request ##{release_pr}") unless @pr_info
|
|
92
|
+
@utils.error("Could not load pull request ##{release_pr}") unless @pull_request
|
|
92
93
|
expected_labels = [@repo_settings.release_pending_label, @repo_settings.release_error_label]
|
|
93
|
-
return if @
|
|
94
|
+
return if @pull_request.labels.any? { |label| expected_labels.include?(label) }
|
|
94
95
|
warning = "PR #{release_pr} doesn't have the release pending or release error label."
|
|
95
96
|
if yes
|
|
96
97
|
logger.warn(warning)
|
|
@@ -102,12 +103,17 @@ def verify_release_pr
|
|
|
102
103
|
end
|
|
103
104
|
|
|
104
105
|
def setup_params
|
|
105
|
-
[
|
|
106
|
+
[
|
|
107
|
+
:git_remote,
|
|
108
|
+
:release_ref,
|
|
109
|
+
:rubygems_api_key,
|
|
110
|
+
:work_dir,
|
|
111
|
+
].each do |key|
|
|
106
112
|
set(key, nil) if get(key).to_s.empty?
|
|
107
113
|
end
|
|
108
|
-
::ENV["GEM_HOST_API_KEY"] = rubygems_api_key if rubygems_api_key
|
|
109
114
|
set(:dry_run, /^t/i.match?(::ENV["TOYS_RELEASE_DRY_RUN"].to_s)) if dry_run.nil?
|
|
110
|
-
|
|
115
|
+
::ENV["GEM_HOST_API_KEY"] = rubygems_api_key if rubygems_api_key
|
|
116
|
+
set :release_ref, @repository.current_sha(release_ref || @pull_request.merge_commit_sha)
|
|
111
117
|
end
|
|
112
118
|
|
|
113
119
|
def create_performer
|
|
@@ -116,14 +122,14 @@ def create_performer
|
|
|
116
122
|
release_pr: release_pr,
|
|
117
123
|
enable_prechecks: enable_prechecks,
|
|
118
124
|
git_remote: git_remote,
|
|
119
|
-
|
|
125
|
+
work_dir: work_dir,
|
|
120
126
|
dry_run: dry_run)
|
|
121
127
|
end
|
|
122
128
|
|
|
123
129
|
def perform_pending_releases
|
|
124
|
-
@repository.wait_github_checks(release_ref) if enable_prechecks
|
|
130
|
+
@repository.wait_github_checks(ref: release_ref) if enable_prechecks
|
|
125
131
|
performer = create_performer
|
|
126
|
-
performer.perform_pr_releases
|
|
132
|
+
performer.perform_pr_releases unless performer.error?
|
|
127
133
|
performer.report_results
|
|
128
134
|
if performer.error?
|
|
129
135
|
@utils.error("Releases reported failure")
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: toys-release
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel Azuma
|
|
@@ -48,7 +48,6 @@ files:
|
|
|
48
48
|
- toys/.data/templates/gh-pages-gitignore.erb
|
|
49
49
|
- toys/.data/templates/gh-pages-index.html.erb
|
|
50
50
|
- toys/.data/templates/release-hook-on-closed.yml.erb
|
|
51
|
-
- toys/.data/templates/release-hook-on-open.yml.erb
|
|
52
51
|
- toys/.data/templates/release-hook-on-push.yml.erb
|
|
53
52
|
- toys/.data/templates/release-perform.yml.erb
|
|
54
53
|
- toys/.data/templates/release-request.yml.erb
|
|
@@ -59,6 +58,7 @@ files:
|
|
|
59
58
|
- toys/.lib/toys/release/component.rb
|
|
60
59
|
- toys/.lib/toys/release/environment_utils.rb
|
|
61
60
|
- toys/.lib/toys/release/performer.rb
|
|
61
|
+
- toys/.lib/toys/release/pipeline.rb
|
|
62
62
|
- toys/.lib/toys/release/pull_request.rb
|
|
63
63
|
- toys/.lib/toys/release/repo_settings.rb
|
|
64
64
|
- toys/.lib/toys/release/repository.rb
|
|
@@ -69,11 +69,10 @@ files:
|
|
|
69
69
|
- toys/.lib/toys/release/version_rb_file.rb
|
|
70
70
|
- toys/.toys.rb
|
|
71
71
|
- toys/_onclosed.rb
|
|
72
|
-
- toys/_onopen.rb
|
|
73
72
|
- toys/_onpush.rb
|
|
74
73
|
- toys/create-labels.rb
|
|
74
|
+
- toys/gen-config.rb
|
|
75
75
|
- toys/gen-gh-pages.rb
|
|
76
|
-
- toys/gen-settings.rb
|
|
77
76
|
- toys/gen-workflows.rb
|
|
78
77
|
- toys/perform.rb
|
|
79
78
|
- toys/request.rb
|
|
@@ -82,10 +81,10 @@ homepage: https://github.com/dazuma/toys
|
|
|
82
81
|
licenses:
|
|
83
82
|
- MIT
|
|
84
83
|
metadata:
|
|
85
|
-
changelog_uri: https://dazuma.github.io/toys/gems/toys-release/v0.
|
|
84
|
+
changelog_uri: https://dazuma.github.io/toys/gems/toys-release/v0.3.0/file.CHANGELOG.html
|
|
86
85
|
source_code_uri: https://github.com/dazuma/toys/tree/main/toys-release
|
|
87
86
|
bug_tracker_uri: https://github.com/dazuma/toys/issues
|
|
88
|
-
documentation_uri: https://dazuma.github.io/toys/gems/toys-release/v0.
|
|
87
|
+
documentation_uri: https://dazuma.github.io/toys/gems/toys-release/v0.3.0
|
|
89
88
|
rdoc_options: []
|
|
90
89
|
require_paths:
|
|
91
90
|
- lib
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
name: "[release hook] Check commit messages"
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
pull_request_target:
|
|
5
|
-
types: [opened, edited, synchronize, reopened]
|
|
6
|
-
|
|
7
|
-
jobs:
|
|
8
|
-
release-check-commits:
|
|
9
|
-
if: ${{ github.repository == '<%= @settings.repo_path %>' }}
|
|
10
|
-
env:
|
|
11
|
-
ruby_version: "3.4"
|
|
12
|
-
runs-on: ubuntu-latest
|
|
13
|
-
steps:
|
|
14
|
-
- name: Install Ruby ${{ env.ruby_version }}
|
|
15
|
-
uses: ruby/setup-ruby@v1
|
|
16
|
-
with:
|
|
17
|
-
ruby-version: ${{ env.ruby_version }}
|
|
18
|
-
- name: Checkout repo
|
|
19
|
-
uses: actions/checkout@v5
|
|
20
|
-
with:
|
|
21
|
-
ref: refs/pull/${{ github.event.pull_request.number }}/merge
|
|
22
|
-
- name: Install Toys
|
|
23
|
-
run: "gem install --no-document toys"
|
|
24
|
-
- name: Check commit messages
|
|
25
|
-
env:
|
|
26
|
-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
27
|
-
run: |
|
|
28
|
-
toys release _onopen --verbose \
|
|
29
|
-
"--event-path=${{ github.event_path }}" \
|
|
30
|
-
< /dev/null
|
data/toys/_onopen.rb
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
desc "Check a pull request"
|
|
4
|
-
|
|
5
|
-
long_desc \
|
|
6
|
-
"This tool is called by a GitHub Actions workflow when any pull request" \
|
|
7
|
-
" is opened or synchronized. It checks the commit messages and/or pull" \
|
|
8
|
-
" request title (as appropriate) for conventional commit style."
|
|
9
|
-
|
|
10
|
-
flag :event_path, "--event-path=VAL" do
|
|
11
|
-
default ::ENV["GITHUB_EVENT_PATH"]
|
|
12
|
-
desc "Path to the pull request event JSON file"
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
include :exec
|
|
16
|
-
include :terminal, styled: true
|
|
17
|
-
|
|
18
|
-
def run
|
|
19
|
-
setup
|
|
20
|
-
lint_commit_messages if @utils.commit_lint_active?
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def setup
|
|
24
|
-
::Dir.chdir(context_directory)
|
|
25
|
-
|
|
26
|
-
require "json"
|
|
27
|
-
require "toys/release/environment_utils"
|
|
28
|
-
require "toys/release/pull_request"
|
|
29
|
-
require "toys/release/repo_settings"
|
|
30
|
-
require "toys/release/repository"
|
|
31
|
-
|
|
32
|
-
@utils = Toys::Release::EnvironmentUtils.new(self)
|
|
33
|
-
@settings = Toys::Release::RepoSettings.load_from_environment(@utils)
|
|
34
|
-
@repository = Toys::Release::Repository.new(@utils, @settings)
|
|
35
|
-
|
|
36
|
-
@utils.error("GitHub event path missing") unless event_path
|
|
37
|
-
pr_resource = ::JSON.parse(::File.read(event_path))["pull_request"]
|
|
38
|
-
@pull_request = Toys::Release::PullRequest.new(@repository, pr_resource)
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def lint_commit_messages
|
|
42
|
-
errors = []
|
|
43
|
-
shas = find_shas
|
|
44
|
-
if shas.size == 1 || !@settings.commit_lint_merge.intersection(["merge", "rebase"]).empty?
|
|
45
|
-
lint_sha_messages(shas, errors)
|
|
46
|
-
end
|
|
47
|
-
if shas.size > 1 && @settings.commit_lint_merge.include?("squash")
|
|
48
|
-
lint_pr_message(errors)
|
|
49
|
-
end
|
|
50
|
-
if errors.empty?
|
|
51
|
-
puts "No conventional commit format problems found.", :green, :bold
|
|
52
|
-
else
|
|
53
|
-
report_lint_errors(errors)
|
|
54
|
-
if @settings.commit_lint_fail_checks?
|
|
55
|
-
@utils.error("Failing due to conventional commit format problems")
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def find_shas
|
|
61
|
-
@repository.git_unshallow("origin", branch: @pull_request.head_sha)
|
|
62
|
-
log = capture(["git", "log", "#{@pull_request.base_sha}..#{@pull_request.head_sha}", "--format=%H"], e: true)
|
|
63
|
-
shas = log.split("\n").reverse
|
|
64
|
-
shas.find_all do |sha|
|
|
65
|
-
parents = capture(["git", "show", "-s", "--pretty=%p", sha], e: true).strip.split
|
|
66
|
-
@utils.log("Omitting merge commit #{sha}") if parents.size > 1
|
|
67
|
-
parents.size == 1
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def lint_sha_messages(shas, errors)
|
|
72
|
-
shas.each do |sha|
|
|
73
|
-
@utils.log("Checking commit #{sha} ...")
|
|
74
|
-
message = capture(["git", "log", "#{sha}^..#{sha}", "--format=%B"], e: true).strip
|
|
75
|
-
lint_message(message) do |err|
|
|
76
|
-
@utils.warning("Commit #{sha}: #{err}")
|
|
77
|
-
suggestion = "Please consider amending the commit message."
|
|
78
|
-
if @settings.commit_lint_merge == ["squash"]
|
|
79
|
-
suggestion += " Alternately, because this pull request will be squashed when merged, you" \
|
|
80
|
-
" can add multiple commits, and instead make sure the pull request _title_" \
|
|
81
|
-
" conforms to the Conventional Commit format."
|
|
82
|
-
end
|
|
83
|
-
err =
|
|
84
|
-
[
|
|
85
|
-
"The message for commit #{sha} does not conform to the Conventional Commit format.",
|
|
86
|
-
"",
|
|
87
|
-
"```",
|
|
88
|
-
] + message.split("\n") + [
|
|
89
|
-
"```",
|
|
90
|
-
"",
|
|
91
|
-
err,
|
|
92
|
-
suggestion,
|
|
93
|
-
]
|
|
94
|
-
errors << err
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def lint_pr_message(errors)
|
|
100
|
-
@utils.log("Checking Pull request title ...")
|
|
101
|
-
lint_message(@pull_request.title) do |err|
|
|
102
|
-
@utils.warning("PR title: #{err}")
|
|
103
|
-
header = "The pull request title does not conform to the Conventional Commit format."
|
|
104
|
-
header +=
|
|
105
|
-
if @settings.commit_lint_merge == ["squash"]
|
|
106
|
-
" (The title will be used as the merge commit message when this pull request is merged.)"
|
|
107
|
-
else
|
|
108
|
-
" (The title may be used as the merge commit message if this pull request is squashed" \
|
|
109
|
-
" when merged.)"
|
|
110
|
-
end
|
|
111
|
-
errors << [
|
|
112
|
-
header,
|
|
113
|
-
"",
|
|
114
|
-
"```",
|
|
115
|
-
@pull_request.title,
|
|
116
|
-
"```",
|
|
117
|
-
"",
|
|
118
|
-
err,
|
|
119
|
-
]
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def lint_message(message)
|
|
124
|
-
lines = message.split("\n")
|
|
125
|
-
matches = /^([\w-]+)(?:\(([^()]+)\))?!?:\s(.+)$/.match(lines.first)
|
|
126
|
-
unless matches
|
|
127
|
-
yield "The first line should follow the form `<type>: <description>`."
|
|
128
|
-
return
|
|
129
|
-
end
|
|
130
|
-
allowed_types = @settings.commit_lint_allowed_types
|
|
131
|
-
if allowed_types && !allowed_types.include?(matches[1].downcase)
|
|
132
|
-
yield "The type `#{matches[1]}` is not allowed by this repository." \
|
|
133
|
-
" Please use one of the types: `#{allowed_types.inspect}`."
|
|
134
|
-
end
|
|
135
|
-
if lines.size > 1 && !lines[1].empty?
|
|
136
|
-
yield "You may not use multiple conventional commit formatted lines." \
|
|
137
|
-
" If you want to include a body or footers in your commit message," \
|
|
138
|
-
" they must be separated from the main message by a blank line." \
|
|
139
|
-
" If you are making multiple semantic changes, please use separate" \
|
|
140
|
-
" commits/pull requets."
|
|
141
|
-
end
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def report_lint_errors(errors)
|
|
145
|
-
header = <<~STR
|
|
146
|
-
Please use [Conventional Commit](https://conventionalcommits.org/) format \
|
|
147
|
-
for commit messages and pull request titles. The automated linter found \
|
|
148
|
-
the following problems in this pull request:
|
|
149
|
-
STR
|
|
150
|
-
lines = [header]
|
|
151
|
-
errors.each do |error_lines|
|
|
152
|
-
lines << "" << " * #{error_lines.first}"
|
|
153
|
-
error_lines[1..].each do |err_line|
|
|
154
|
-
lines << (err_line.empty? ? "" : " #{err_line}")
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
@pull_request.add_comment(lines.join("\n"))
|
|
158
|
-
end
|
data/toys/gen-settings.rb
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
desc "Generate initial settings file"
|
|
4
|
-
|
|
5
|
-
long_desc \
|
|
6
|
-
"This tool generates an initial settings file for this repo." \
|
|
7
|
-
" You will generally need to make additional edits to this file after" \
|
|
8
|
-
" initial generation."
|
|
9
|
-
|
|
10
|
-
required_arg :repo do
|
|
11
|
-
desc "GitHub repo owner and name (e.g. dazuma/toys)"
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
flag :yes, "--yes", "-y" do
|
|
15
|
-
desc "Automatically answer yes to all confirmations"
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
include :exec
|
|
19
|
-
include :terminal, styled: true
|
|
20
|
-
|
|
21
|
-
def run
|
|
22
|
-
file_path = ::File.join(context_directory, ".toys", ".data", "releases.yml")
|
|
23
|
-
if ::File.readable?(file_path)
|
|
24
|
-
puts "Cannot overwrite existing file: #{file_path}", :red, :bold
|
|
25
|
-
exit(1)
|
|
26
|
-
end
|
|
27
|
-
return unless yes || confirm("Create file #{file_path}? ", default: true)
|
|
28
|
-
::File.open(file_path, "w") do |file|
|
|
29
|
-
write_settings(file)
|
|
30
|
-
end
|
|
31
|
-
puts("Wrote initial settings file: #{file_path}.", :green, :bold)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def write_settings(file)
|
|
35
|
-
file.puts("repo: #{repo}")
|
|
36
|
-
file.puts("# Insert additional repo-level settings here.")
|
|
37
|
-
file.puts
|
|
38
|
-
file.puts("gems:")
|
|
39
|
-
::Dir.glob("**/*.gemspec").each do |gemspec|
|
|
40
|
-
gem_name = ::File.basename(gemspec, ".gemspec")
|
|
41
|
-
file.puts(" - name: #{gem_name}")
|
|
42
|
-
dir = ::File.dirname(gemspec)
|
|
43
|
-
file.puts(" directory: #{dir}") if dir != gem_name
|
|
44
|
-
file.puts(" # Insert additional gem-level settings here.")
|
|
45
|
-
end
|
|
46
|
-
end
|