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.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +11 -0
  3. data/CHANGELOG.md +5 -0
  4. data/LICENSE.md +21 -0
  5. data/README.md +87 -0
  6. data/docs/guide.md +7 -0
  7. data/lib/toys/release/version.rb +11 -0
  8. data/lib/toys-release.rb +23 -0
  9. data/toys/.data/templates/gh-pages-404.html.erb +25 -0
  10. data/toys/.data/templates/gh-pages-empty.html.erb +11 -0
  11. data/toys/.data/templates/gh-pages-gitignore.erb +1 -0
  12. data/toys/.data/templates/gh-pages-index.html.erb +15 -0
  13. data/toys/.data/templates/release-hook-on-closed.yml.erb +34 -0
  14. data/toys/.data/templates/release-hook-on-open.yml.erb +30 -0
  15. data/toys/.data/templates/release-hook-on-push.yml.erb +32 -0
  16. data/toys/.data/templates/release-perform.yml.erb +46 -0
  17. data/toys/.data/templates/release-request.yml.erb +37 -0
  18. data/toys/.data/templates/release-retry.yml.erb +42 -0
  19. data/toys/.lib/toys/release/artifact_dir.rb +70 -0
  20. data/toys/.lib/toys/release/change_set.rb +259 -0
  21. data/toys/.lib/toys/release/changelog_file.rb +136 -0
  22. data/toys/.lib/toys/release/component.rb +388 -0
  23. data/toys/.lib/toys/release/environment_utils.rb +246 -0
  24. data/toys/.lib/toys/release/performer.rb +346 -0
  25. data/toys/.lib/toys/release/pull_request.rb +154 -0
  26. data/toys/.lib/toys/release/repo_settings.rb +855 -0
  27. data/toys/.lib/toys/release/repository.rb +661 -0
  28. data/toys/.lib/toys/release/request_logic.rb +217 -0
  29. data/toys/.lib/toys/release/request_spec.rb +188 -0
  30. data/toys/.lib/toys/release/semver.rb +112 -0
  31. data/toys/.lib/toys/release/steps.rb +580 -0
  32. data/toys/.lib/toys/release/version_rb_file.rb +91 -0
  33. data/toys/.toys.rb +5 -0
  34. data/toys/_onclosed.rb +113 -0
  35. data/toys/_onopen.rb +158 -0
  36. data/toys/_onpush.rb +57 -0
  37. data/toys/create-labels.rb +115 -0
  38. data/toys/gen-gh-pages.rb +146 -0
  39. data/toys/gen-settings.rb +46 -0
  40. data/toys/gen-workflows.rb +70 -0
  41. data/toys/perform.rb +152 -0
  42. data/toys/request.rb +162 -0
  43. data/toys/retry.rb +133 -0
  44. 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