toys-release 0.1.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 +7 -0
- data/.yardopts +11 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.md +21 -0
- data/README.md +87 -0
- data/docs/guide.md +7 -0
- data/lib/toys/release/version.rb +11 -0
- data/lib/toys-release.rb +23 -0
- data/toys/.data/templates/gh-pages-404.html.erb +25 -0
- data/toys/.data/templates/gh-pages-empty.html.erb +11 -0
- data/toys/.data/templates/gh-pages-gitignore.erb +1 -0
- data/toys/.data/templates/gh-pages-index.html.erb +15 -0
- data/toys/.data/templates/release-hook-on-closed.yml.erb +34 -0
- data/toys/.data/templates/release-hook-on-open.yml.erb +30 -0
- data/toys/.data/templates/release-hook-on-push.yml.erb +32 -0
- data/toys/.data/templates/release-perform.yml.erb +46 -0
- data/toys/.data/templates/release-request.yml.erb +37 -0
- data/toys/.data/templates/release-retry.yml.erb +42 -0
- data/toys/.lib/toys/release/artifact_dir.rb +70 -0
- data/toys/.lib/toys/release/change_set.rb +259 -0
- data/toys/.lib/toys/release/changelog_file.rb +136 -0
- data/toys/.lib/toys/release/component.rb +388 -0
- data/toys/.lib/toys/release/environment_utils.rb +246 -0
- data/toys/.lib/toys/release/performer.rb +346 -0
- data/toys/.lib/toys/release/pull_request.rb +154 -0
- data/toys/.lib/toys/release/repo_settings.rb +855 -0
- data/toys/.lib/toys/release/repository.rb +661 -0
- data/toys/.lib/toys/release/request_logic.rb +217 -0
- data/toys/.lib/toys/release/request_spec.rb +188 -0
- data/toys/.lib/toys/release/semver.rb +112 -0
- data/toys/.lib/toys/release/steps.rb +580 -0
- data/toys/.lib/toys/release/version_rb_file.rb +91 -0
- data/toys/.toys.rb +5 -0
- data/toys/_onclosed.rb +113 -0
- data/toys/_onopen.rb +158 -0
- data/toys/_onpush.rb +57 -0
- data/toys/create-labels.rb +115 -0
- data/toys/gen-gh-pages.rb +146 -0
- data/toys/gen-settings.rb +46 -0
- data/toys/gen-workflows.rb +70 -0
- data/toys/perform.rb +152 -0
- data/toys/request.rb +162 -0
- data/toys/retry.rb +133 -0
- metadata +106 -0
data/toys/_onclosed.rb
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
desc "Process a release pull request"
|
|
4
|
+
|
|
5
|
+
long_desc \
|
|
6
|
+
"This tool is called by a GitHub Actions workflow after a release pull" \
|
|
7
|
+
" request is closed. If the pull request was merged, the requested" \
|
|
8
|
+
" release is performed and the pull request is updated with the results." \
|
|
9
|
+
" If the pull request was closed without being merged, the pull request" \
|
|
10
|
+
" is marked as aborted. This tool also ensures that release branches are" \
|
|
11
|
+
" deleted once their PRs are closed."
|
|
12
|
+
|
|
13
|
+
flag :event_path, "--event-path=VAL" do
|
|
14
|
+
default ::ENV["GITHUB_EVENT_PATH"]
|
|
15
|
+
desc "Path to the pull request closed event JSON file"
|
|
16
|
+
end
|
|
17
|
+
flag :rubygems_api_key, "--rubygems-api-key=VAL" do
|
|
18
|
+
desc "Set the Rubygems API key"
|
|
19
|
+
long_desc \
|
|
20
|
+
"Use the given Rubygems API key when pushing to Rubygems. Deprecated;" \
|
|
21
|
+
" prefer just setting the `GEM_HOST_API_KEY` environment variable."
|
|
22
|
+
end
|
|
23
|
+
flag :enable_releases, "--enable-releases=VAL" do
|
|
24
|
+
desc "Deprecated and unused"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
include :exec
|
|
28
|
+
include :terminal, styled: true
|
|
29
|
+
|
|
30
|
+
def run
|
|
31
|
+
setup
|
|
32
|
+
|
|
33
|
+
delete_release_branch
|
|
34
|
+
check_for_release_pr
|
|
35
|
+
if @pull_request.merged?
|
|
36
|
+
handle_release_merged
|
|
37
|
+
else
|
|
38
|
+
handle_release_aborted
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def setup
|
|
43
|
+
require "json"
|
|
44
|
+
require "toys/release/environment_utils"
|
|
45
|
+
require "toys/release/pull_request"
|
|
46
|
+
require "toys/release/repo_settings"
|
|
47
|
+
require "toys/release/repository"
|
|
48
|
+
|
|
49
|
+
::Dir.chdir(context_directory)
|
|
50
|
+
::ENV["GEM_HOST_API_KEY"] = rubygems_api_key unless rubygems_api_key.to_s.empty?
|
|
51
|
+
|
|
52
|
+
@utils = Toys::Release::EnvironmentUtils.new(self)
|
|
53
|
+
@settings = Toys::Release::RepoSettings.load_from_environment(@utils)
|
|
54
|
+
@repository = Toys::Release::Repository.new(@utils, @settings)
|
|
55
|
+
|
|
56
|
+
@utils.error("GitHub event path missing") unless event_path
|
|
57
|
+
pr_resource = ::JSON.parse(::File.read(event_path))["pull_request"]
|
|
58
|
+
@pull_request = Toys::Release::PullRequest.new(@repository, pr_resource)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def delete_release_branch
|
|
62
|
+
source_ref = @pull_request.head_ref
|
|
63
|
+
if @repository.release_related_branch?(source_ref)
|
|
64
|
+
logger.info("Deleting release branch #{source_ref} ...")
|
|
65
|
+
exec(["git", "push", "--delete", "origin", source_ref])
|
|
66
|
+
logger.info("Deleted.")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def check_for_release_pr
|
|
71
|
+
unless @pull_request.labels.include?(@settings.release_pending_label)
|
|
72
|
+
logger.info("PR #{@pull_request.number} does not have the release pending label. Ignoring.")
|
|
73
|
+
exit
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def handle_release_aborted
|
|
78
|
+
logger.info("Updating release PR #{@pull_request.number} to mark it as aborted.")
|
|
79
|
+
@pull_request.update(labels: @settings.release_aborted_label, state: "closed")
|
|
80
|
+
@pull_request.add_comment("Release PR closed without merging.")
|
|
81
|
+
logger.info "Done."
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def handle_release_merged
|
|
85
|
+
setup_git
|
|
86
|
+
github_check_errors = @repository.wait_github_checks
|
|
87
|
+
unless github_check_errors.empty?
|
|
88
|
+
@utils.error("GitHub checks failed", *github_check_errors)
|
|
89
|
+
end
|
|
90
|
+
performer = create_performer
|
|
91
|
+
performer.perform_pr_releases
|
|
92
|
+
performer.report_results
|
|
93
|
+
if performer.error?
|
|
94
|
+
@utils.error("Releases reported failure")
|
|
95
|
+
else
|
|
96
|
+
puts("All releases completed successfully", :bold, :green)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def setup_git
|
|
101
|
+
exec(["git", "fetch", "--depth=2", "origin", "+#{@pull_request.merge_commit_sha}:refs/heads/release/current"],
|
|
102
|
+
e: true)
|
|
103
|
+
exec(["git", "switch", "release/current"], e: true)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def create_performer
|
|
107
|
+
require "toys/release/performer"
|
|
108
|
+
dry_run = /^t/i.match?(::ENV["TOYS_RELEASE_DRY_RUN"].to_s)
|
|
109
|
+
Toys::Release::Performer.new(@repository,
|
|
110
|
+
release_pr: @pull_request,
|
|
111
|
+
git_remote: "origin",
|
|
112
|
+
dry_run: dry_run)
|
|
113
|
+
end
|
data/toys/_onopen.rb
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
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/_onpush.rb
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
desc "Update open releases after a push"
|
|
4
|
+
|
|
5
|
+
long_desc \
|
|
6
|
+
"This tool is called by a GitHub Actions workflow after a commit is pushed" \
|
|
7
|
+
" to a releasable branch. It adds a warning note to any relevant open" \
|
|
8
|
+
" releases that additional commits have been added."
|
|
9
|
+
|
|
10
|
+
include :exec
|
|
11
|
+
include :terminal, styled: true
|
|
12
|
+
|
|
13
|
+
def run
|
|
14
|
+
setup
|
|
15
|
+
update_open_release_prs
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def setup
|
|
19
|
+
::Dir.chdir(context_directory)
|
|
20
|
+
|
|
21
|
+
require "toys/release/environment_utils"
|
|
22
|
+
require "toys/release/repo_settings"
|
|
23
|
+
require "toys/release/repository"
|
|
24
|
+
|
|
25
|
+
@utils = Toys::Release::EnvironmentUtils.new(self)
|
|
26
|
+
@settings = Toys::Release::RepoSettings.load_from_environment(@utils)
|
|
27
|
+
@repository = Toys::Release::Repository.new(@utils, @settings)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def update_open_release_prs
|
|
31
|
+
push_branch = @repository.current_branch
|
|
32
|
+
logger.info("Searching for open release PRs targeting branch #{push_branch} ...")
|
|
33
|
+
pr_message = nil
|
|
34
|
+
@repository.find_release_prs(branch: push_branch).each do |pull|
|
|
35
|
+
pr_message ||= build_pr_message
|
|
36
|
+
logger.info("Updating PR #{pull.number} ...")
|
|
37
|
+
pull.add_comment(pr_message)
|
|
38
|
+
end
|
|
39
|
+
if pr_message
|
|
40
|
+
logger.info("Finished updating existing release PRs.")
|
|
41
|
+
else
|
|
42
|
+
logger.info("No existing release PRs target branch #{push_branch}.")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def build_pr_message
|
|
47
|
+
commit_message = capture(["git", "log", "-1", "--pretty=%B"], e: true)
|
|
48
|
+
<<~STR
|
|
49
|
+
WARNING: An additional commit was added while this release PR was open.
|
|
50
|
+
You may need to add to the changelog, or close this PR and prepare a new one.
|
|
51
|
+
|
|
52
|
+
Commit link: https://github.com/#{@settings.repo_path}/commit/#{@repository.current_sha}
|
|
53
|
+
|
|
54
|
+
Message:
|
|
55
|
+
#{commit_message}
|
|
56
|
+
STR
|
|
57
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
desc "Create GitHub labels for releases"
|
|
4
|
+
|
|
5
|
+
long_desc \
|
|
6
|
+
"This tool ensures that the proper GitHub labels are present for the" \
|
|
7
|
+
" release automation scripts."
|
|
8
|
+
|
|
9
|
+
flag :yes, "--yes", "-y" do
|
|
10
|
+
desc "Automatically answer yes to all confirmations"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
include :exec
|
|
14
|
+
include :terminal, styled: true
|
|
15
|
+
|
|
16
|
+
def run
|
|
17
|
+
require "json"
|
|
18
|
+
require "cgi"
|
|
19
|
+
require "toys/release/environment_utils"
|
|
20
|
+
require "toys/release/repo_settings"
|
|
21
|
+
|
|
22
|
+
@utils = Toys::Release::EnvironmentUtils.new(self)
|
|
23
|
+
@settings = Toys::Release::RepoSettings.load_from_environment(@utils)
|
|
24
|
+
|
|
25
|
+
unless @settings.enable_release_automation?
|
|
26
|
+
puts "Release automation disabled in settings."
|
|
27
|
+
unless yes || confirm("Create labels anyway? ", default: false)
|
|
28
|
+
@utils.error("Aborted.")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
expected_labels = create_expected_labels
|
|
33
|
+
cur_labels = load_existing_labels
|
|
34
|
+
update_labels cur_labels, expected_labels
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def create_expected_labels
|
|
38
|
+
[
|
|
39
|
+
{
|
|
40
|
+
"name" => @settings.release_pending_label,
|
|
41
|
+
"color" => "ddeeff",
|
|
42
|
+
"description" => "Automated release is pending",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name" => @settings.release_error_label,
|
|
46
|
+
"color" => "ffdddd",
|
|
47
|
+
"description" => "Automated release failed with an error",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name" => @settings.release_aborted_label,
|
|
51
|
+
"color" => "eeeeee",
|
|
52
|
+
"description" => "Automated release was aborted",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name" => @settings.release_complete_label,
|
|
56
|
+
"color" => "ddffdd",
|
|
57
|
+
"description" => "Automated release completed successfully",
|
|
58
|
+
},
|
|
59
|
+
]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def load_existing_labels
|
|
63
|
+
output = capture(["gh", "api", "repos/#{@settings.repo_path}/labels",
|
|
64
|
+
"-H", "Accept: application/vnd.github.v3+json"])
|
|
65
|
+
::JSON.parse(output)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def update_labels(cur_labels, expected_labels)
|
|
69
|
+
expected_labels.each do |expected|
|
|
70
|
+
cur = cur_labels.find { |label| label["name"] == expected["name"] }
|
|
71
|
+
if cur
|
|
72
|
+
if cur["color"] != expected["color"] || cur["description"] != expected["description"]
|
|
73
|
+
update_label(expected)
|
|
74
|
+
end
|
|
75
|
+
else
|
|
76
|
+
create_label(expected)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
release_related_labels = [
|
|
80
|
+
@settings.release_pending_label,
|
|
81
|
+
@settings.release_error_label,
|
|
82
|
+
@settings.release_aborted_label,
|
|
83
|
+
@settings.release_complete_label,
|
|
84
|
+
]
|
|
85
|
+
cur_labels.each do |cur|
|
|
86
|
+
next unless release_related_labels.include?(cur["name"])
|
|
87
|
+
delete_label(cur) unless expected_labels.find { |label| label["name"] == cur["name"] }
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def create_label(label)
|
|
92
|
+
label_name = label["name"]
|
|
93
|
+
return unless yes || confirm("Label \"#{label_name}\" doesn't exist. Create? ", default: true)
|
|
94
|
+
body = ::JSON.dump(label)
|
|
95
|
+
exec(["gh", "api", "repos/#{@settings.repo_path}/labels", "--input", "-",
|
|
96
|
+
"-H", "Accept: application/vnd.github.v3+json"],
|
|
97
|
+
in: [:string, body], out: :null, e: true)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def update_label(label)
|
|
101
|
+
label_name = label["name"]
|
|
102
|
+
return unless yes || confirm("Update fields of \"#{label_name}\"? ", default: true)
|
|
103
|
+
body = ::JSON.dump(color: label["color"], description: label["description"])
|
|
104
|
+
exec(["gh", "api", "-XPATCH", "repos/#{@settings.repo_path}/labels/#{label_name}",
|
|
105
|
+
"--input", "-", "-H", "Accept: application/vnd.github.v3+json"],
|
|
106
|
+
in: [:string, body], out: :null, e: true)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def delete_label(label)
|
|
110
|
+
label_name = label["name"]
|
|
111
|
+
return unless yes || confirm("Label \"#{label_name}\" unrecognized. Delete? ", default: true)
|
|
112
|
+
exec(["gh", "api", "-XDELETE", "repos/#{@settings.repo_path}/labels/#{label_name}",
|
|
113
|
+
"-H", "Accept: application/vnd.github.v3+json"],
|
|
114
|
+
out: :null, e: true)
|
|
115
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
desc "Generate gh-pages documentation site"
|
|
4
|
+
|
|
5
|
+
long_desc "This tool generates an initial gh-pages documentation site."
|
|
6
|
+
|
|
7
|
+
flag :working_dir, "--working-dir=PATH" do
|
|
8
|
+
desc "Set the working directory for the gh-pages checkout"
|
|
9
|
+
end
|
|
10
|
+
flag :git_remote, "--git-remote=REMOTE" do
|
|
11
|
+
desc "Use the specified remote (default is `origin`)"
|
|
12
|
+
default "origin"
|
|
13
|
+
end
|
|
14
|
+
flag :yes, "--yes", "-y" do
|
|
15
|
+
desc "Automatically answer yes to all confirmations"
|
|
16
|
+
end
|
|
17
|
+
flag :dry_run
|
|
18
|
+
|
|
19
|
+
include :exec
|
|
20
|
+
include :terminal, styled: true
|
|
21
|
+
|
|
22
|
+
# Context for ERB templates
|
|
23
|
+
class ErbContext
|
|
24
|
+
def initialize(data)
|
|
25
|
+
data.each { |name, value| instance_variable_set("@#{name}", value) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.get(data)
|
|
29
|
+
new(data).instance_eval { binding }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def run
|
|
34
|
+
setup
|
|
35
|
+
generate_gh_pages
|
|
36
|
+
push_gh_pages
|
|
37
|
+
cleanup
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def setup
|
|
41
|
+
require "erb"
|
|
42
|
+
require "fileutils"
|
|
43
|
+
require "toys/release/artifact_dir"
|
|
44
|
+
require "toys/release/environment_utils"
|
|
45
|
+
require "toys/release/repo_settings"
|
|
46
|
+
require "toys/release/repository"
|
|
47
|
+
|
|
48
|
+
::Dir.chdir(context_directory)
|
|
49
|
+
@utils = Toys::Release::EnvironmentUtils.new(self)
|
|
50
|
+
@settings = Toys::Release::RepoSettings.load_from_environment(@utils)
|
|
51
|
+
@repository = Toys::Release::Repository.new(@utils, @settings)
|
|
52
|
+
@artifact_dir = Toys::Release::ArtifactDir.new(working_dir)
|
|
53
|
+
@gh_pages_dir = @repository.checkout_separate_dir(
|
|
54
|
+
branch: "gh-pages", remote: git_remote, dir: @artifact_dir.get("gh-pages"),
|
|
55
|
+
gh_token: ::ENV["GITHUB_TOKEN"], create: true
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def cleanup
|
|
60
|
+
@artifact_dir.cleanup
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def generate_gh_pages
|
|
64
|
+
relevant_component_settings = @settings.all_component_settings.find_all(&:gh_pages_enabled)
|
|
65
|
+
url_base = "#{@settings.repo_owner}.github.io/#{@settings.repo_name}"
|
|
66
|
+
default_url = "https://#{url_base}/#{relevant_component_settings.first.gh_pages_directory}/latest"
|
|
67
|
+
|
|
68
|
+
::Dir.chdir(@gh_pages_dir) do
|
|
69
|
+
::File.write(".nojekyll", "")
|
|
70
|
+
generate_file("gh-pages-gitignore.erb", ".gitignore", {})
|
|
71
|
+
|
|
72
|
+
data = {
|
|
73
|
+
relevant_component_settings: relevant_component_settings,
|
|
74
|
+
url_base: url_base,
|
|
75
|
+
default_url: default_url,
|
|
76
|
+
}
|
|
77
|
+
generate_file("gh-pages-404.html.erb", "404.html", data) do |content, old_content|
|
|
78
|
+
update_versions(content, old_content)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
generate_file("gh-pages-index.html.erb", "index.html", {default_url: default_url})
|
|
82
|
+
|
|
83
|
+
relevant_component_settings.each do |component_settings|
|
|
84
|
+
generate_file("gh-pages-empty.html.erb", "#{component_settings.gh_pages_directory}/v0.0.0/index.html",
|
|
85
|
+
{component_settings: component_settings}, remove_dir: true)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
puts "Files generated into #{@gh_pages_dir}", :bold
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def push_gh_pages
|
|
92
|
+
::Dir.chdir(@gh_pages_dir) do
|
|
93
|
+
@repository.git_commit("Generated initial gh-pages", signoff: @settings.signoff_commits?)
|
|
94
|
+
if dry_run
|
|
95
|
+
puts "DRY RUN: Skipped git push.", :green, :bold
|
|
96
|
+
else
|
|
97
|
+
@utils.exec(["git", "push", git_remote, "gh-pages"], e: true)
|
|
98
|
+
puts "Pushed gh-pages.", :green, :bold
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def generate_file(template, destination, data, remove_dir: false)
|
|
104
|
+
old_content = file_generation_confirmations(destination, remove_dir)
|
|
105
|
+
return if old_content == :cancel
|
|
106
|
+
template_path = find_data("templates/#{template}")
|
|
107
|
+
raise "Unable to find template #{template}" unless template_path
|
|
108
|
+
erb = ::ERB.new(::File.read(template_path))
|
|
109
|
+
content = erb.result(ErbContext.get(data))
|
|
110
|
+
content = yield(content, old_content) if block_given? && old_content
|
|
111
|
+
::File.write(destination, content)
|
|
112
|
+
puts "Wrote #{destination}."
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def file_generation_confirmations(destination, remove_dir)
|
|
116
|
+
old_content = nil
|
|
117
|
+
if ::File.readable?(destination)
|
|
118
|
+
old_content = ::File.read(destination)
|
|
119
|
+
puts "Destination file #{destination} exists.", :yellow, :bold
|
|
120
|
+
return :cancel unless yes || confirm("Overwrite? ", default: true)
|
|
121
|
+
else
|
|
122
|
+
return :cancel unless yes || confirm("Create file #{destination}? ", default: true)
|
|
123
|
+
end
|
|
124
|
+
dir = ::File.dirname(destination)
|
|
125
|
+
unless dir == "."
|
|
126
|
+
if remove_dir && ::File.directory?(dir)
|
|
127
|
+
puts "Old version directory #{dir} exists.", :yellow, :bold
|
|
128
|
+
return :cancel unless yes || confirm("Remove? ", default: true)
|
|
129
|
+
::FileUtils.remove_entry(dir, true)
|
|
130
|
+
end
|
|
131
|
+
::FileUtils.mkdir_p(dir)
|
|
132
|
+
end
|
|
133
|
+
old_content
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def update_versions(content, old_content)
|
|
137
|
+
@settings.all_component_settings.each do |component_settings|
|
|
138
|
+
next unless component_settings.gh_pages_enabled
|
|
139
|
+
match = /#{component_settings.gh_pages_version_var} = "([\w.]+)";/.match(old_content)
|
|
140
|
+
if match
|
|
141
|
+
content.sub!("#{component_settings.gh_pages_version_var} = \"0.0.0\";",
|
|
142
|
+
"#{component_settings.gh_pages_version_var} = \"#{match[1]}\";")
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
content
|
|
146
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|