toys-release 0.6.0 → 0.7.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 +6 -0
- data/docs/guide.md +80 -0
- data/lib/toys/release/version.rb +1 -1
- data/toys/.lib/toys/release/change_set.rb +44 -10
- data/toys/.lib/toys/release/changelog_file.rb +100 -17
- data/toys/.lib/toys/release/component.rb +54 -7
- data/toys/.lib/toys/release/gemspec_file.rb +120 -0
- data/toys/.lib/toys/release/repo_settings.rb +148 -0
- data/toys/.lib/toys/release/repository.rb +4 -4
- data/toys/.lib/toys/release/request_logic.rb +10 -2
- data/toys/.lib/toys/release/request_spec.rb +114 -35
- data/toys/.lib/toys/release/semver.rb +49 -3
- data/toys/_onpush.rb +16 -5
- data/toys/request.rb +8 -1
- metadata +5 -5
- data/CLAUDE.md +0 -95
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6a183392907cf0499a8abb4df7f675146d1b52f602551f140cf12111fd809149
|
|
4
|
+
data.tar.gz: fbc2ca77613f5a9bc40c9d3fe89fc239d7feb0c43713cc4407f935abcf5b9a29
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 69fb1dc0ab0870f13ab1b6dcee252a31dc19aeeabc2d318c99baed7dd3dd45c6e7c5263090e749e00827e6f8d23c1342b4d8ee982e771c85026fe690b9770323
|
|
7
|
+
data.tar.gz: 99cc97fd617ea40ec0c75ca3cd08ac89f362d29f530c907b3b9c9dab14452607cfc10a4bfcae9c1a694d19ab6a01e2869414fd6490a4eeadebc524483e8ffd16
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Release History
|
|
2
2
|
|
|
3
|
+
### v0.7.0 / 2026-03-09
|
|
4
|
+
|
|
5
|
+
* ADDED: Support updating dependencies among released components
|
|
6
|
+
* ADDED: Configurable changelog release header format
|
|
7
|
+
* ADDED: Support optionally auto-creating release PRs on push
|
|
8
|
+
|
|
3
9
|
### v0.6.0 / 2026-02-11
|
|
4
10
|
|
|
5
11
|
* ADDED: Support retaining and/or linkifying issue/PR numbers in commit messages
|
data/docs/guide.md
CHANGED
|
@@ -851,10 +851,32 @@ rest are optional.
|
|
|
851
851
|
This can be used to modify the default build pipeline instead of redefining
|
|
852
852
|
the entire pipeline using the **steps** key.
|
|
853
853
|
|
|
854
|
+
* **auto_create_request_branches**: *array of string* (optional) --
|
|
855
|
+
A list of branch names for which release pull requests are automatically
|
|
856
|
+
created when a push occurs and no existing release PRs are open for the
|
|
857
|
+
branch. When a push is made to one of these branches and there are no
|
|
858
|
+
open release PRs targeting it, the system will automatically run the
|
|
859
|
+
release request tool to create one, including all components with
|
|
860
|
+
releasable changes. If no components have releasable changes, the
|
|
861
|
+
auto-creation is silently skipped. This setting requires
|
|
862
|
+
**required_checks** to be disabled (set to *false*). Defaults to an
|
|
863
|
+
empty array (no auto-creation).
|
|
864
|
+
|
|
854
865
|
* **breaking_change_header**: *string* (optional) --
|
|
855
866
|
A changelog entry prefix that appears when a change is marked as breaking.
|
|
856
867
|
Default is `BREAKING CHANGE`.
|
|
857
868
|
|
|
869
|
+
* **changelog_bullet**: *string* (optional) --
|
|
870
|
+
The bullet character used for changelog list items. Allowed values are
|
|
871
|
+
`*` and `-`. Default is `*`.
|
|
872
|
+
|
|
873
|
+
* **changelog_release_header_format**: *string* (optional) --
|
|
874
|
+
A format string for changelog release headers. Use `%v` for the version
|
|
875
|
+
number and [strftime](https://docs.ruby-lang.org/en/4.0/language/strftime_formatting_rdoc.html)
|
|
876
|
+
directives (e.g. `%Y`, `%m`, `%d`) for date components. The format must
|
|
877
|
+
include `%v`. Default is `### v%v / %Y-%m-%d`, which produces headers
|
|
878
|
+
like `### v1.2.3 / 2026-02-16`.
|
|
879
|
+
|
|
858
880
|
* **commit_tags**: *array of [CommitTagConfig](#commit-tag-configuration)* (optional) --
|
|
859
881
|
A set of configurations defining how to interpret
|
|
860
882
|
[conventional commit](https://conventionalcommits.org) tags, including how
|
|
@@ -1144,6 +1166,13 @@ The **name** key is required. The others are optional.
|
|
|
1144
1166
|
present, the default pipeline for the entire repository is used. (See the
|
|
1145
1167
|
**steps** key under [Top level configuration](#top-level-configuration).)
|
|
1146
1168
|
|
|
1169
|
+
* **update_dependencies**: *[UpdateDepsConfig](#update-dependencies-configuration)* (optional) --
|
|
1170
|
+
Set up automatic dependency updates, which causes this component to be
|
|
1171
|
+
updated and released if any of a specified set of dependencies is also
|
|
1172
|
+
present in the release. This supports automatically keeping "kitchen sink"
|
|
1173
|
+
libraries up to date. If this setting is not present, automatic updating is
|
|
1174
|
+
not performed for this component.
|
|
1175
|
+
|
|
1147
1176
|
* **version_rb_path**: *string* (optional) --
|
|
1148
1177
|
The path to a Ruby file that contains the current version of the component.
|
|
1149
1178
|
This file *must* include Ruby code that looks like this:
|
|
@@ -1160,6 +1189,57 @@ The **name** key is required. The others are optional.
|
|
|
1160
1189
|
module implied by the component name. For example, if the component (gem)
|
|
1161
1190
|
name is `toys-release`, this defaults to `lib/toys/release/version.rb`.
|
|
1162
1191
|
|
|
1192
|
+
#### Update dependencies configuration
|
|
1193
|
+
|
|
1194
|
+
An update-dependencies configuration describes when a component should also be
|
|
1195
|
+
released with updated dependency versions, due to one or more of those
|
|
1196
|
+
dependencies being released. It is typically used to keep "kitchen sink"
|
|
1197
|
+
libraries up to date.
|
|
1198
|
+
|
|
1199
|
+
For example, consider two components "foo_a" and "foo_b", and a "kitchen sink"
|
|
1200
|
+
component "foo_all" that depends on both the others. Suppose whenever a patch
|
|
1201
|
+
or greater release of either "foo_a" or "foo_b" happens, we also want "foo_all"
|
|
1202
|
+
to be released with its corresponding dependency bumped to the same version. We
|
|
1203
|
+
might set up the configuration like so:
|
|
1204
|
+
|
|
1205
|
+
```yaml
|
|
1206
|
+
components:
|
|
1207
|
+
- name: foo_a
|
|
1208
|
+
- name: foo_b
|
|
1209
|
+
- name: foo_all
|
|
1210
|
+
update_dependencies:
|
|
1211
|
+
dependency_semver_threshold: patch
|
|
1212
|
+
dependencies: [foo_a, foo_b]
|
|
1213
|
+
```
|
|
1214
|
+
|
|
1215
|
+
The update-dependencies configuration for a kitchen sink component can include
|
|
1216
|
+
the following keys. The **dependencies** key is required. All others are
|
|
1217
|
+
optional.
|
|
1218
|
+
|
|
1219
|
+
* **dependencies**: *array of string* (required) --
|
|
1220
|
+
A list of names of the components this component depends on.
|
|
1221
|
+
|
|
1222
|
+
* **dependency_semver_threshold**: *string* (optional) --
|
|
1223
|
+
The minimum semver level of a dependency update that should trigger an
|
|
1224
|
+
update of the kitchen sink component. For example, a threshold of `minor`
|
|
1225
|
+
would trigger an update to the kitchen sink if a minor release of a
|
|
1226
|
+
dependency occurred, but would not trigger an update to the kitchen sink if
|
|
1227
|
+
a patch release occurred.
|
|
1228
|
+
|
|
1229
|
+
Allowed values are `major`, `minor`, `patch`, `patch2`, and `all`. The
|
|
1230
|
+
`all` value indicates that every release of a dependency should trigger an
|
|
1231
|
+
update to the kitchen sink. Defaults to `minor` if not specified.
|
|
1232
|
+
|
|
1233
|
+
* **pessimistic_constraint_level**: *string* (optional) --
|
|
1234
|
+
The highest semver level allowed to float in the pessimistic dependency
|
|
1235
|
+
version constraints used to specify the dependencies. For example, a
|
|
1236
|
+
version constraint of `~> 1.0` has level `minor` because the minor version
|
|
1237
|
+
number is allowed to float, whereas the major version number is pinned.
|
|
1238
|
+
|
|
1239
|
+
Allowed values are `major`, `minor`, `patch`, `patch2`, and `exact`. The
|
|
1240
|
+
`exact` value indicates that dependencies should require the exact release
|
|
1241
|
+
version. Defaults to `minor` if not specified.
|
|
1242
|
+
|
|
1163
1243
|
### Build step configuration
|
|
1164
1244
|
|
|
1165
1245
|
A build step describes one step in the release process. Its format is a
|
data/lib/toys/release/version.rb
CHANGED
|
@@ -21,8 +21,10 @@ module Toys
|
|
|
21
21
|
@commit_tags = component_settings.commit_tags
|
|
22
22
|
@breaking_change_header = component_settings.breaking_change_header
|
|
23
23
|
@no_significant_updates_notice = component_settings.no_significant_updates_notice
|
|
24
|
+
@update_dependency_header = component_settings.update_dependency_header
|
|
24
25
|
@semver = Semver::NONE
|
|
25
26
|
@change_groups = nil
|
|
27
|
+
@updated_dependency_versions = nil
|
|
26
28
|
@inputs = []
|
|
27
29
|
@significant_shas = nil
|
|
28
30
|
@description_normalizer = create_description_normalizer(
|
|
@@ -49,7 +51,7 @@ module Toys
|
|
|
49
51
|
# Finish constructing a change set. After this method, new commit messages
|
|
50
52
|
# cannot be added.
|
|
51
53
|
#
|
|
52
|
-
def finish
|
|
54
|
+
def finish
|
|
53
55
|
raise "ChangeSet locked" if finished?
|
|
54
56
|
@semver = Semver::NONE
|
|
55
57
|
change_groups = {breaking: Group.new(@breaking_change_header)}
|
|
@@ -65,24 +67,42 @@ module Toys
|
|
|
65
67
|
change_groups[:breaking].add(input.breaks)
|
|
66
68
|
end
|
|
67
69
|
@change_groups = change_groups.values.find_all { |group| !group.empty? }
|
|
68
|
-
if @change_groups.empty? && @semver.significant?
|
|
69
|
-
@change_groups << Group.new(nil).add(@no_significant_updates_notice)
|
|
70
|
-
end
|
|
71
70
|
@significant_shas = @inputs.map(&:sha)
|
|
71
|
+
@updated_dependency_versions = {}
|
|
72
72
|
@inputs = nil
|
|
73
73
|
self
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
+
##
|
|
77
|
+
# Add a set of dependency updates.
|
|
78
|
+
# May be called only on a finished changeset.
|
|
79
|
+
#
|
|
80
|
+
# @param updates [Array<Toys::Release::RequestSpec::ResolvedComponent>]
|
|
81
|
+
# @param dependency_semver_threshold [Toys::Release::Semver] The minimum
|
|
82
|
+
# semver significance to trigger an update
|
|
83
|
+
#
|
|
84
|
+
def add_dependency_updates(updates, dependency_semver_threshold)
|
|
85
|
+
raise "ChangeSet not finished" unless finished?
|
|
86
|
+
existing_group = @change_groups.find { |group| group.header == @update_dependency_header }
|
|
87
|
+
group = existing_group || Group.new(@update_dependency_header)
|
|
88
|
+
updates.each do |resolved_update|
|
|
89
|
+
diff_semver = Toys::Release::Semver.for_diff(resolved_update.last_version, resolved_update.version)
|
|
90
|
+
next if diff_semver < dependency_semver_threshold
|
|
91
|
+
@updated_dependency_versions[resolved_update.component_name] = resolved_update.version
|
|
92
|
+
@semver = @semver.max(diff_semver)
|
|
93
|
+
group.add("Updated #{resolved_update.component_name.inspect} dependency to #{resolved_update.version}")
|
|
94
|
+
end
|
|
95
|
+
@change_groups << group unless existing_group || group.empty?
|
|
96
|
+
self
|
|
97
|
+
end
|
|
98
|
+
|
|
76
99
|
##
|
|
77
100
|
# Force a non-empty changeset even if there are no significant updates.
|
|
78
101
|
# May be called only on a finished changeset.
|
|
79
102
|
#
|
|
80
103
|
def force_release!
|
|
81
104
|
raise "ChangeSet not finished" unless finished?
|
|
82
|
-
|
|
83
|
-
@semver = Semver::PATCH
|
|
84
|
-
@change_groups << Group.new(nil).add(@no_significant_updates_notice)
|
|
85
|
-
end
|
|
105
|
+
@semver = Semver::PATCH unless @semver.significant?
|
|
86
106
|
self
|
|
87
107
|
end
|
|
88
108
|
|
|
@@ -98,7 +118,7 @@ module Toys
|
|
|
98
118
|
# the change set is finished.
|
|
99
119
|
#
|
|
100
120
|
def empty?
|
|
101
|
-
|
|
121
|
+
change_groups&.empty?
|
|
102
122
|
end
|
|
103
123
|
|
|
104
124
|
##
|
|
@@ -116,7 +136,21 @@ module Toys
|
|
|
116
136
|
# @return [Array<Group>] An array of change groups, in order. Returns nil
|
|
117
137
|
# if the change set is not finished.
|
|
118
138
|
#
|
|
119
|
-
|
|
139
|
+
def change_groups
|
|
140
|
+
if !finished?
|
|
141
|
+
nil
|
|
142
|
+
elsif @change_groups.empty? && @semver.significant?
|
|
143
|
+
@no_significant_updates_groups ||= [Group.new(nil).add(@no_significant_updates_notice)]
|
|
144
|
+
else
|
|
145
|
+
@change_groups
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
##
|
|
150
|
+
# @return [Hash{String=>Gem::Version}] Dependency version updates.
|
|
151
|
+
# Returns nil if the change set is not yet finished.
|
|
152
|
+
#
|
|
153
|
+
attr_reader :updated_dependency_versions
|
|
120
154
|
|
|
121
155
|
##
|
|
122
156
|
# Suggest a next version based on the changeset's changes.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
3
5
|
module Toys
|
|
4
6
|
module Release
|
|
5
7
|
##
|
|
@@ -16,10 +18,12 @@ module Toys
|
|
|
16
18
|
#
|
|
17
19
|
# @param path [String] File path
|
|
18
20
|
# @param environment_utils [Toys::Release::EnvironmentUtils]
|
|
21
|
+
# @param settings [Toys::Release::RepoSettings] Repository settings
|
|
19
22
|
#
|
|
20
|
-
def initialize(path, environment_utils)
|
|
23
|
+
def initialize(path, environment_utils, settings)
|
|
21
24
|
@path = path
|
|
22
25
|
@utils = environment_utils
|
|
26
|
+
@settings = settings
|
|
23
27
|
end
|
|
24
28
|
|
|
25
29
|
##
|
|
@@ -46,7 +50,7 @@ module Toys
|
|
|
46
50
|
# @return [::Gem::Version,nil] Current latest version from the changelog
|
|
47
51
|
#
|
|
48
52
|
def current_version
|
|
49
|
-
ChangelogFile.current_version_from_content(content)
|
|
53
|
+
ChangelogFile.current_version_from_content(content, header_format)
|
|
50
54
|
end
|
|
51
55
|
|
|
52
56
|
##
|
|
@@ -60,34 +64,36 @@ module Toys
|
|
|
60
64
|
def read_and_verify_latest_entry(version) # rubocop:disable Metrics/MethodLength
|
|
61
65
|
version = version.to_s
|
|
62
66
|
@utils.log("Verifying #{path} changelog content...")
|
|
63
|
-
|
|
67
|
+
expected_header = format_header(version, ::Time.now.utc)
|
|
68
|
+
version_re = ::Regexp.new("^#{ChangelogFile.header_regex(header_format, version: version)}\n$")
|
|
69
|
+
any_header_re = ::Regexp.new("^#{ChangelogFile.header_regex(header_format)}")
|
|
64
70
|
entry = []
|
|
65
71
|
state = :start
|
|
66
72
|
::File.readlines(@path).each do |line|
|
|
67
73
|
case state
|
|
68
74
|
when :start
|
|
69
75
|
case line
|
|
70
|
-
when
|
|
76
|
+
when version_re
|
|
71
77
|
entry << line
|
|
72
78
|
state = :during
|
|
73
|
-
when
|
|
79
|
+
when any_header_re
|
|
74
80
|
@utils.error("The first changelog entry in #{path} isn't for version #{version}.",
|
|
75
81
|
"It should start with:",
|
|
76
|
-
|
|
82
|
+
expected_header,
|
|
77
83
|
"But it actually starts with:",
|
|
78
84
|
line)
|
|
79
85
|
entry << line
|
|
80
86
|
state = :during
|
|
81
87
|
end
|
|
82
88
|
when :during
|
|
83
|
-
break if line
|
|
89
|
+
break if any_header_re.match?(line)
|
|
84
90
|
entry << line
|
|
85
91
|
end
|
|
86
92
|
end
|
|
87
93
|
if entry.empty?
|
|
88
94
|
@utils.error("The changelog #{path} doesn't have any entries.",
|
|
89
95
|
"The first changelog entry should start with:",
|
|
90
|
-
|
|
96
|
+
expected_header)
|
|
91
97
|
else
|
|
92
98
|
@utils.log("Changelog OK")
|
|
93
99
|
end
|
|
@@ -99,15 +105,14 @@ module Toys
|
|
|
99
105
|
#
|
|
100
106
|
# @param changeset [ChangeSet] The changeset.
|
|
101
107
|
# @param version [String] The release version.
|
|
102
|
-
# @param date [String] The date. If not provided, uses the current UTC.
|
|
103
|
-
# @param bullet [String] The bullet character for list items. Defaults to "*".
|
|
108
|
+
# @param date [Time,String,nil] The date. If not provided, uses the current UTC.
|
|
104
109
|
#
|
|
105
|
-
def append(changeset, version, date: nil
|
|
110
|
+
def append(changeset, version, date: nil)
|
|
106
111
|
@utils.log("Writing version #{version} to changelog #{path}")
|
|
107
|
-
|
|
108
|
-
|
|
112
|
+
bullet = @settings.changelog_bullet
|
|
113
|
+
header_line = format_header(version, date)
|
|
109
114
|
new_entry = [
|
|
110
|
-
|
|
115
|
+
header_line,
|
|
111
116
|
"",
|
|
112
117
|
]
|
|
113
118
|
changeset.change_groups.each do |group|
|
|
@@ -115,7 +120,7 @@ module Toys
|
|
|
115
120
|
end
|
|
116
121
|
new_entry = new_entry.join("\n")
|
|
117
122
|
old_content = content || DEFAULT_HEADER
|
|
118
|
-
new_content = old_content.sub(
|
|
123
|
+
new_content = old_content.sub(/^(#{ChangelogFile.header_regex(header_format)})$/, "#{new_entry}\n\n\\1")
|
|
119
124
|
if new_content == old_content
|
|
120
125
|
new_content = old_content.sub(/\n+\z/, "\n\n#{new_entry}\n")
|
|
121
126
|
end
|
|
@@ -127,12 +132,90 @@ module Toys
|
|
|
127
132
|
# Returns the current version from the given file content
|
|
128
133
|
#
|
|
129
134
|
# @param content [String] File contents
|
|
135
|
+
# @param header_format [String] The header format string containing
|
|
136
|
+
# `%v` for version and strftime directives for date components.
|
|
130
137
|
# @return [::Gem::Version] Latest version in the changelog
|
|
131
138
|
#
|
|
132
|
-
def self.current_version_from_content(content)
|
|
133
|
-
|
|
139
|
+
def self.current_version_from_content(content, header_format)
|
|
140
|
+
regex_str = header_regex(header_format)
|
|
141
|
+
match = ::Regexp.new(regex_str).match(content.to_s)
|
|
134
142
|
match ? ::Gem::Version.new(match[1]) : nil
|
|
135
143
|
end
|
|
144
|
+
|
|
145
|
+
##
|
|
146
|
+
# @private
|
|
147
|
+
#
|
|
148
|
+
# Converts a header format into a regular expression string for
|
|
149
|
+
# matching changelog headers. The `%v` placeholder is replaced with
|
|
150
|
+
# either a specific escaped version or a general version-capturing
|
|
151
|
+
# pattern. Strftime directives (including those with flags and width
|
|
152
|
+
# specifiers) are replaced with appropriate character class patterns.
|
|
153
|
+
#
|
|
154
|
+
# TODO: This does not correctly handle escaped percent signs (`%%`)
|
|
155
|
+
# in the format string. A `%%` sequence (which strftime interprets
|
|
156
|
+
# as a literal `%`) could have its second `%` misidentified as the
|
|
157
|
+
# start of a strftime directive. This is unlikely in practice but
|
|
158
|
+
# may warrant future investigation.
|
|
159
|
+
#
|
|
160
|
+
# @param header_format [String] The header format string.
|
|
161
|
+
# @param version [String,nil] If provided, the regex will match only
|
|
162
|
+
# this specific version. If nil, the regex captures any version.
|
|
163
|
+
# @return [String] A regular expression string (not a Regexp object).
|
|
164
|
+
#
|
|
165
|
+
def self.header_regex(header_format, version: nil)
|
|
166
|
+
version_re = version ? ::Regexp.escape(version.to_s) : "(#{VERSION_PATTERN})"
|
|
167
|
+
::Regexp.escape(header_format)
|
|
168
|
+
.gsub("%v", version_re)
|
|
169
|
+
.gsub(/%[-_0^#]?\d*([a-zA-Z])/) do
|
|
170
|
+
STRFTIME_CONVERSIONS[::Regexp.last_match(1)] || '\S+'
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# @private
|
|
175
|
+
VERSION_PATTERN = '\d+(?:\.[a-zA-Z0-9]+)+'
|
|
176
|
+
private_constant :VERSION_PATTERN
|
|
177
|
+
|
|
178
|
+
# @private
|
|
179
|
+
# Maps strftime conversion characters to regex patterns.
|
|
180
|
+
# Flags and width specifiers (e.g. %-d, %02m) are handled by
|
|
181
|
+
# header_regex via a general pattern match.
|
|
182
|
+
STRFTIME_CONVERSIONS = {
|
|
183
|
+
"Y" => '\d{4}',
|
|
184
|
+
"m" => '\d{1,2}',
|
|
185
|
+
"d" => '\d{1,2}',
|
|
186
|
+
"B" => '\w+',
|
|
187
|
+
"b" => '\w+',
|
|
188
|
+
"e" => '[\d ]\d',
|
|
189
|
+
}.freeze
|
|
190
|
+
private_constant :STRFTIME_CONVERSIONS
|
|
191
|
+
|
|
192
|
+
private
|
|
193
|
+
|
|
194
|
+
##
|
|
195
|
+
# Returns the configured header format string from settings.
|
|
196
|
+
#
|
|
197
|
+
# @return [String] A format string containing `%v` for version and
|
|
198
|
+
# strftime directives for date components.
|
|
199
|
+
#
|
|
200
|
+
def header_format
|
|
201
|
+
@settings.changelog_release_header_format
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
##
|
|
205
|
+
# Generates a formatted header line by substituting the version and
|
|
206
|
+
# formatting the date using strftime directives from the header format.
|
|
207
|
+
#
|
|
208
|
+
# @param version [String] The release version string.
|
|
209
|
+
# @param date [Time,String,nil] The release date. If nil, defaults to
|
|
210
|
+
# the current UTC time. Strings are parsed via `Time.parse`.
|
|
211
|
+
# @return [String] The formatted header line.
|
|
212
|
+
#
|
|
213
|
+
def format_header(version, date)
|
|
214
|
+
date ||= ::Time.now.utc
|
|
215
|
+
date = ::Time.parse(date) if date.is_a?(::String)
|
|
216
|
+
fmt = header_format.gsub("%v", "%%v")
|
|
217
|
+
date.strftime(fmt).gsub("%v", version.to_s)
|
|
218
|
+
end
|
|
136
219
|
end
|
|
137
220
|
end
|
|
138
221
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "toys/release/change_set"
|
|
4
4
|
require "toys/release/changelog_file"
|
|
5
|
+
require "toys/release/gemspec_file"
|
|
5
6
|
require "toys/release/version_rb_file"
|
|
6
7
|
|
|
7
8
|
module Toys
|
|
@@ -21,8 +22,9 @@ module Toys
|
|
|
21
22
|
@repository = repository
|
|
22
23
|
@settings = repository.settings.component_settings(name)
|
|
23
24
|
@utils = environment_utils
|
|
24
|
-
@changelog_file = ChangelogFile.new(changelog_path(from: :absolute), @utils)
|
|
25
|
+
@changelog_file = ChangelogFile.new(changelog_path(from: :absolute), @utils, repository.settings)
|
|
25
26
|
@version_rb_file = VersionRbFile.new(version_rb_path(from: :absolute), @utils)
|
|
27
|
+
@gemspec_file = GemspecFile.new(gemspec_path(from: :absolute), @utils)
|
|
26
28
|
@coordination_group = nil
|
|
27
29
|
end
|
|
28
30
|
|
|
@@ -43,6 +45,12 @@ module Toys
|
|
|
43
45
|
#
|
|
44
46
|
attr_reader :version_rb_file
|
|
45
47
|
|
|
48
|
+
##
|
|
49
|
+
# @return [Toys::Release::GemspecFile] The .gemspec file in this
|
|
50
|
+
# component
|
|
51
|
+
#
|
|
52
|
+
attr_reader :gemspec_file
|
|
53
|
+
|
|
46
54
|
##
|
|
47
55
|
# @return [Array<Component>] The coordination group containing this
|
|
48
56
|
# component. If this component is not coordinated, it will be part of
|
|
@@ -137,6 +145,19 @@ module Toys
|
|
|
137
145
|
file_path(settings.version_rb_path, from: from)
|
|
138
146
|
end
|
|
139
147
|
|
|
148
|
+
##
|
|
149
|
+
# Returns the path to the gemspec. It can be returned as a relative
|
|
150
|
+
# path from the component directory, a relative path from the repo root
|
|
151
|
+
# directory, or an absolute path.
|
|
152
|
+
#
|
|
153
|
+
# @param from [:directory,:repo_root,:absolute] From where (defaults to
|
|
154
|
+
# `:directory`)
|
|
155
|
+
# @return [String] The path to the gemspec
|
|
156
|
+
#
|
|
157
|
+
def gemspec_path(from: :directory)
|
|
158
|
+
file_path("#{name}.gemspec", from: from)
|
|
159
|
+
end
|
|
160
|
+
|
|
140
161
|
##
|
|
141
162
|
# Validates the component and reports any errors.
|
|
142
163
|
#
|
|
@@ -145,15 +166,39 @@ module Toys
|
|
|
145
166
|
path = directory(from: :absolute)
|
|
146
167
|
@utils.error("Missing directory #{path} for #{name}") unless ::File.directory?(path)
|
|
147
168
|
@utils.error("Missing changelog #{changelog_file.path} for #{name}") unless changelog_file.exists?
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
elsif version_rb_file.current_version.nil?
|
|
151
|
-
@utils.error("Unable to read VERSION constant from #{version_rb_file.path} for #{name}")
|
|
152
|
-
end
|
|
169
|
+
validate_version_rb_file
|
|
170
|
+
validate_gemspec_file
|
|
153
171
|
yield if block_given?
|
|
154
172
|
end
|
|
155
173
|
end
|
|
156
174
|
|
|
175
|
+
##
|
|
176
|
+
# Validates the version.rb file
|
|
177
|
+
#
|
|
178
|
+
def validate_version_rb_file
|
|
179
|
+
if !version_rb_file.exists?
|
|
180
|
+
@utils.error("Missing version #{version_rb_file.path} for #{name}")
|
|
181
|
+
elsif version_rb_file.current_version.nil?
|
|
182
|
+
@utils.error("Unable to read VERSION constant from #{version_rb_file.path} for #{name}")
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
##
|
|
187
|
+
# Validates the gemspec file if this component updates from dependencies
|
|
188
|
+
#
|
|
189
|
+
def validate_gemspec_file
|
|
190
|
+
update_deps_settings = settings.update_dependencies
|
|
191
|
+
return unless update_deps_settings
|
|
192
|
+
if gemspec_file.exists?
|
|
193
|
+
cur_deps = gemspec_file.current_dependencies
|
|
194
|
+
update_deps_settings.dependencies.each do |dep_name|
|
|
195
|
+
@utils.error("Gemspec #{gemspec_file.path} is missing #{dep_name}") unless cur_deps.key?(dep_name)
|
|
196
|
+
end
|
|
197
|
+
else
|
|
198
|
+
@utils.error("Missing gemspec #{gemspec_file.path} for #{name}")
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
157
202
|
##
|
|
158
203
|
# Returns the version of the latest release tag on the given branch.
|
|
159
204
|
#
|
|
@@ -205,7 +250,9 @@ module Toys
|
|
|
205
250
|
if at
|
|
206
251
|
path = changelog_path(from: :repo_root)
|
|
207
252
|
content = @utils.capture(["git", "show", "#{at}:#{path}"], e: true)
|
|
208
|
-
return ChangelogFile.current_version_from_content(
|
|
253
|
+
return ChangelogFile.current_version_from_content(
|
|
254
|
+
content, @repository.settings.changelog_release_header_format
|
|
255
|
+
)
|
|
209
256
|
end
|
|
210
257
|
changelog_file.current_version
|
|
211
258
|
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Toys
|
|
4
|
+
module Release
|
|
5
|
+
##
|
|
6
|
+
# Represents a gemspec file
|
|
7
|
+
#
|
|
8
|
+
class GemspecFile
|
|
9
|
+
##
|
|
10
|
+
# Transforms an input set of versions and pessimistic constraint level to
|
|
11
|
+
# the dependency syntax for rubygems.
|
|
12
|
+
#
|
|
13
|
+
# @param versions [Hash{String=>Gem::Version}] Mapping from component
|
|
14
|
+
# name to version
|
|
15
|
+
# @param dependency_semver_threshold [Semver] The semver significance
|
|
16
|
+
# threshold
|
|
17
|
+
# @param pessimistic_constraint_level [Semver] The pessimistic constraint
|
|
18
|
+
#
|
|
19
|
+
# @return [Hash{String=>Array<String>}] Mapping from component name to
|
|
20
|
+
# the rubygems version constraint syntax
|
|
21
|
+
#
|
|
22
|
+
def self.transform_version_constraints(versions,
|
|
23
|
+
dependency_semver_threshold,
|
|
24
|
+
pessimistic_constraint_level)
|
|
25
|
+
versions.transform_values do |version|
|
|
26
|
+
if pessimistic_constraint_level == Semver::NONE
|
|
27
|
+
["= #{version}"]
|
|
28
|
+
else
|
|
29
|
+
segments = version.canonical_segments.dup
|
|
30
|
+
segments.slice!((dependency_semver_threshold.segment + 1)..) if dependency_semver_threshold.segment
|
|
31
|
+
pessimistic_segments =
|
|
32
|
+
if segments.size <= pessimistic_constraint_level.segment
|
|
33
|
+
segments + ::Array.new(pessimistic_constraint_level.segment - segments.size + 1, 0)
|
|
34
|
+
else
|
|
35
|
+
segments[..pessimistic_constraint_level.segment]
|
|
36
|
+
end
|
|
37
|
+
result = ["~> #{pessimistic_segments.join('.')}"]
|
|
38
|
+
result << ">= #{segments.join('.')}" if segments.size > pessimistic_segments.size
|
|
39
|
+
result
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
##
|
|
45
|
+
# Create a gemspec file object given a file path
|
|
46
|
+
#
|
|
47
|
+
# @param path [String] File path
|
|
48
|
+
# @param environment_utils [Toys::Release::EnvironmentUtils]
|
|
49
|
+
#
|
|
50
|
+
def initialize(path, environment_utils)
|
|
51
|
+
@path = path
|
|
52
|
+
@utils = environment_utils
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
##
|
|
56
|
+
# @return [String] Path to the gemspec file
|
|
57
|
+
#
|
|
58
|
+
attr_reader :path
|
|
59
|
+
|
|
60
|
+
##
|
|
61
|
+
# @return [boolean] Whether the file exists
|
|
62
|
+
#
|
|
63
|
+
def exists?
|
|
64
|
+
path && ::File.file?(path)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
##
|
|
68
|
+
# @return [String] Current contents of the file
|
|
69
|
+
#
|
|
70
|
+
def content
|
|
71
|
+
@content ||= ::File.read(path)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
##
|
|
75
|
+
# Get the current rubygems version constraints for all dependencies
|
|
76
|
+
#
|
|
77
|
+
# @return [Hash{String=>Array<String>}] Map from component name to a
|
|
78
|
+
# possibly empty array of version constraint strings.
|
|
79
|
+
#
|
|
80
|
+
def current_dependencies
|
|
81
|
+
result = {}
|
|
82
|
+
regex = /\.add(?:_runtime)?_dependency\(?\s*["']([^"']+)["']((?:,\s*["']([^"',]+)["'])*)\s*\)?/
|
|
83
|
+
content.scan(regex) do |comp_name, expr|
|
|
84
|
+
result[comp_name] = expr.scan(/["']([^"',]+)["']/).map(&:first)
|
|
85
|
+
end
|
|
86
|
+
result
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
##
|
|
90
|
+
# Update the rubygems version constraints
|
|
91
|
+
#
|
|
92
|
+
# @param constraint_updates [Hash{String=>Array<String>}] Map from
|
|
93
|
+
# component name to a possibly empty array of version constraint
|
|
94
|
+
# strings.
|
|
95
|
+
# @return [self]
|
|
96
|
+
#
|
|
97
|
+
def update_dependencies(constraint_updates)
|
|
98
|
+
constraint_updates.each do |name, exprs|
|
|
99
|
+
escaped_name = Regexp.escape(name)
|
|
100
|
+
regex = /\.(add(?:_runtime)?_dependency)(\(?\s*)(["'])#{escaped_name}["'](?:,\s*["'][^"',]+["'])*(\s*\)?)/
|
|
101
|
+
content.sub!(regex) do
|
|
102
|
+
match = ::Regexp.last_match
|
|
103
|
+
method_name = match[1]
|
|
104
|
+
opening = match[2]
|
|
105
|
+
quote = match[3]
|
|
106
|
+
closing = match[4]
|
|
107
|
+
exprs_str = exprs.map { |str| ", #{str.inspect}" }.join
|
|
108
|
+
exprs_str.tr!('"', quote) unless quote == '"'
|
|
109
|
+
".#{method_name}#{opening}#{quote}#{name}#{quote}#{exprs_str}#{closing}"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
::File.write(path, content) if path
|
|
113
|
+
self
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# @private
|
|
117
|
+
attr_writer :content
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|