gem_kit-release 0.2.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7c57bae0a6fbc182e752ab68d4e5eaa6f1441354e9f906d9ee6d336c8c51c9e4
4
- data.tar.gz: 73cc9921720bad183c573a5cd7bdb077bd118ed6f3eb2d1a0f417f0c9a1bf9de
3
+ metadata.gz: 7c85c52c48c2f4ef893784a7e3f5cd6f1dd35fbf6e74e46de4eef4bc7c205821
4
+ data.tar.gz: 994048430d10430d4d3f47ca3a57a214200459a75205685adc921cc63e6d58cf
5
5
  SHA512:
6
- metadata.gz: 507b97ad08897e529ba1d471b696beeeb8af74fdd782e7067de2ac0f6ffa51f20010d782de808727083781b4e4b766fa7327cf122b300b284e88441e2d2899ef
7
- data.tar.gz: 1fc2b130909d685e609f30e54a59faa8ad8e047ec057be23a5cfbea5648d1919a4b28fbf2bf2076239a7af4a3523ccd9b49cdc899333b0d1cd7d1e8bab113080
6
+ metadata.gz: 58349a775e00a64b0e023f6cd53016527bcc3ed440c9143ef53321da8dd03a8e5b2009ad90cf8d8d84827e7158f639943a544cf15446095c59b0407bc00dabe6
7
+ data.tar.gz: 5014510b87f87df7047b62f288182586fae3278133fd92d1f50a24f93b6294ed986e92a67c3fb3da15c9bb86c47b8d1ef6231b886edcd4d422df7885aba7cdc6
data/README.md CHANGED
@@ -65,7 +65,7 @@ gem kit changelog --write # have an AI CLI write the entry
65
65
  gem kit changelog # lint it
66
66
  gem kit deprecations # what is still outstanding
67
67
  gem kit release --dry-run # run the gates
68
- gem kit tag --push # tag it
68
+ gem kit release # ...then build, push and tag
69
69
  ```
70
70
 
71
71
  | Command | What it does |
@@ -105,21 +105,28 @@ module GemKit
105
105
  Commands::Deprecations.new(options).call(version)
106
106
  end
107
107
 
108
- desc "release", "Gate, build and push this gem"
108
+ desc "release", "Gate, build, push and tag this gem"
109
109
  long_desc <<~TXT
110
- Two gates, both before `gem build` runs:
110
+ Three gates, all before `gem build` runs:
111
111
 
112
112
  the changelog must have a non-empty, correctly formatted section for this
113
- version, sitting at the top of the released list; and nothing promised to
114
- disappear in this version may still be in the tree.
113
+ version, sitting at the top of the released list; nothing promised to
114
+ disappear in this version may still be in the tree; and the working tree
115
+ must be committed, because a gem built from uncommitted files is a gem
116
+ whose source exists nowhere.
117
+
118
+ Then builds the gemspec, pushes it, and tags the release — after the push,
119
+ so a tag never names a version that failed to publish. Requires RubyGems
120
+ push credentials.
115
121
 
116
- Then builds the gemspec and pushes it. Requires RubyGems push credentials.
117
122
  --dry-run runs the gates and stops, which is what belongs in CI.
118
123
  TXT
119
124
  method_option :dry_run, type: :boolean, default: false, aliases: "-n",
120
125
  desc: "Run the gates and stop"
121
- method_option :tag, type: :boolean, default: false, aliases: "-t",
122
- desc: "Tag the release after pushing"
126
+ method_option :tag, type: :boolean, default: true,
127
+ desc: "Tag the release and push the tag (--no-tag to skip)"
128
+ method_option :allow_dirty, type: :boolean, default: false,
129
+ desc: "Release even with uncommitted changes"
123
130
  def release
124
131
  Commands::Release.new(options).call
125
132
  end
@@ -5,15 +5,15 @@ require_relative "command"
5
5
  module GemKit
6
6
  module Release
7
7
  module Commands
8
- # Gate, build, push. The gates run before anything is built, because a
9
- # half-done release is worse than a refused one.
8
+ # Gate, build, push, tag. The gates run before anything is built, because
9
+ # a half-done release is worse than a refused one.
10
10
  class Release < Command
11
11
 
12
12
 
13
13
 
14
14
  def call
15
15
  version = project.version.to_s
16
- problems = gate.release_problems(version)
16
+ problems = gate.release_problems(version, allow_dirty: options[:allow_dirty])
17
17
 
18
18
  refuse("Refusing to release #{project.name} #{version}:", problems) unless problems.empty?
19
19
 
@@ -35,7 +35,11 @@ module GemKit
35
35
 
36
36
  say("Released #{project.name} #{version}")
37
37
 
38
- Tag.new(options).call if options[:tag]
38
+ # Tagging is part of releasing, not a flag someone remembers. It runs
39
+ # after the push rather than before, so a tag never points at a
40
+ # version that failed to publish — and it pushes, because the gem is
41
+ # public by now and a tag only this machine has is half a release.
42
+ Tag.new(options.merge(push: true)).call unless options[:tag] == false
39
43
  end
40
44
  end
41
45
  end
@@ -56,6 +60,39 @@ describe "gem_kit/release/commands/release" do
56
60
  end
57
61
  end
58
62
 
63
+ # A gem built from an uncommitted tree matches nothing in git.
64
+ it "refuses while the working tree is dirty, before building anything" do
65
+ with_gem do |dir|
66
+ system("git init -q #{dir}")
67
+
68
+ status, _out, err = invoke(["release"], dir)
69
+
70
+ status.should == 1
71
+ err.should.match(/uncommitted change/)
72
+ err.should.match(/--allow-dirty/)
73
+ Dir[File.join(dir, "*.gem")].should.be.empty
74
+ end
75
+ end
76
+
77
+ it "--allow-dirty releases anyway" do
78
+ with_gem do |dir|
79
+ system("git init -q #{dir}")
80
+
81
+ # --dry-run stops before the push, which is as far as a spec should go.
82
+ invoke(["release", "--dry-run", "--allow-dirty"], dir).first.should == 0
83
+ end
84
+ end
85
+
86
+ it "is satisfied once the tree is committed" do
87
+ with_gem do |dir|
88
+ system("git init -q #{dir}")
89
+ system("git -C #{dir} add -A")
90
+ system("git -C #{dir} -c user.name=t -c user.email=t@t commit -q -m x")
91
+
92
+ invoke(["release", "--dry-run"], dir).first.should == 0
93
+ end
94
+ end
95
+
59
96
  it "refuses without a changelog entry, before building anything" do
60
97
  with_gem(changelog: :none) do |dir|
61
98
  status, _out, err = invoke(["release"], dir)
@@ -47,14 +47,39 @@ module GemKit
47
47
  version ? changelog.release_problems(version) : changelog.problems
48
48
  end
49
49
 
50
+ # What is in the working tree but not in git. A gem built from an
51
+ # uncommitted tree is a gem whose source exists nowhere — and `bump` and
52
+ # `changelog --write` leave exactly two such files behind, which is
53
+ # precisely the moment someone reaches for `release`.
54
+ #
55
+ # A directory that is not a git repository is not a problem: this gate
56
+ # has nothing to say about it.
57
+ def working_tree_problems
58
+ return [] unless git?
59
+
60
+ dirty = Dir.chdir(project.root) { `git status --porcelain`.lines.map(&:strip) }
61
+ return [] if dirty.empty?
62
+
63
+ ["#{dirty.size} uncommitted change(s) — the gem would match nothing in git:",
64
+ *dirty.first(10).map { |line| " #{line}" },
65
+ *(dirty.size > 10 ? [" … and #{dirty.size - 10} more"] : []),
66
+ "",
67
+ "The bump and the changelog belong in one commit. Or pass --allow-dirty."]
68
+ end
69
+
70
+ def git?
71
+ Dir.chdir(project.root) { system("git rev-parse --git-dir >/dev/null 2>&1") }
72
+ end
73
+
50
74
  # Everything standing between the project and releasing `version`.
51
- def release_problems(version = project.version)
75
+ def release_problems(version = project.version, allow_dirty: false)
52
76
  problems = []
53
77
 
54
78
  changelog_problems(version).each { |problem| problems << problem }
55
79
  deprecation_problems(version).each do |problem|
56
80
  problems << "deprecation due in #{version}: #{problem}"
57
81
  end
82
+ working_tree_problems.each { |problem| problems << problem } unless allow_dirty
58
83
 
59
84
  problems
60
85
  end
@@ -76,10 +101,10 @@ describe "gem_kit/release/gate" do
76
101
 
77
102
  # A Project stub: the Gate only asks it for a changelog path and a version,
78
103
  # and to load the library (a no-op here — the specs drive the registry).
79
- stub_project = lambda do |changelog_path, version: "1.0.0"|
80
- Struct.new(:changelog_path, :version) do
104
+ stub_project = lambda do |changelog_path, version: "1.0.0", root: nil|
105
+ Struct.new(:changelog_path, :version, :root) do
81
106
  def load! = true
82
- end.new(changelog_path, Gem::Version.new(version))
107
+ end.new(changelog_path, Gem::Version.new(version), root || File.dirname(changelog_path))
83
108
  end
84
109
 
85
110
  clean_changelog = <<~MD
@@ -155,6 +180,36 @@ describe "gem_kit/release/gate" do
155
180
  end
156
181
  end
157
182
 
183
+ # The gate that catches what `bump` and `changelog --write` leave behind.
184
+ it "reports an uncommitted working tree, and clears once it is committed" do
185
+ Dir.mktmpdir do |dir|
186
+ File.write(File.join(dir, "CHANGELOG.md"), clean_changelog)
187
+ system("git init -q #{dir}")
188
+
189
+ isolated.call do
190
+ gate = GemKit::Release::Gate.new(stub_project.call(File.join(dir, "CHANGELOG.md"), root: dir))
191
+
192
+ gate.release_problems("1.0.0").first.should.match(/uncommitted change/)
193
+ gate.release_problems("1.0.0", allow_dirty: true).should == []
194
+
195
+ system("git -C #{dir} add -A")
196
+ system("git -C #{dir} -c user.name=t -c user.email=t@t commit -q -m x")
197
+ gate.release_problems("1.0.0").should == []
198
+ end
199
+ end
200
+ end
201
+
202
+ it "says nothing about a directory that is not a git repository" do
203
+ Dir.mktmpdir do |dir|
204
+ File.write(File.join(dir, "CHANGELOG.md"), clean_changelog)
205
+
206
+ isolated.call do
207
+ GemKit::Release::Gate.new(stub_project.call(File.join(dir, "CHANGELOG.md"), root: dir))
208
+ .working_tree_problems.should == []
209
+ end
210
+ end
211
+ end
212
+
158
213
  it "checks changelog format alone when given no version" do
159
214
  Dir.mktmpdir do |dir|
160
215
  path = File.join(dir, "CHANGELOG.md")
@@ -8,8 +8,7 @@ The whole process, in order:
8
8
  <%= step("gem kit changelog#{gem_flag} --write") %># 3. write the entry
9
9
  <%= step("gem kit changelog#{gem_flag} <VERSION>") %># 4. check it
10
10
  <%= step(%(git commit -am "Release ...")) %># 5. commit the bump + changelog
11
- <%= step("gem kit release#{gem_flag}") %># 6. build and push
12
- <%= step("gem kit tag#{gem_flag} --push") %># 7. tag it
11
+ <%= step("gem kit release#{gem_flag}") %># 6. gate, build, push, tag
13
12
  ```
14
13
 
15
14
  Steps 2 and 6 are gates: they refuse to proceed when something is missing.
@@ -119,27 +118,30 @@ unwritten in git is the failure this process exists to prevent.
119
118
  ## 6. Release
120
119
 
121
120
  ```sh
122
- gem kit release<%= gem_flag %> # or: gem kit release<%= gem_flag %> --dry-run, which is the CI check
121
+ gem kit release<%= gem_flag %> # or --dry-run, which is the CI check
123
122
  ```
124
123
 
125
- Two gates, both before `gem build` runs:
124
+ Three gates, all before `gem build` runs:
126
125
 
127
126
  1. **Changelog** — this version needs its own non-empty, correctly formatted
128
127
  section, sitting at the top of the released list.
129
128
  2. **Deprecations** — nothing promised to disappear in this version may still
130
129
  be in the tree.
130
+ 3. **The working tree** — everything committed. A gem built from uncommitted
131
+ files is a gem whose source exists nowhere, and step 2 and step 3 above
132
+ leave exactly two such files behind. `--allow-dirty` overrides it.
131
133
 
132
- Then `gem build` and `gem push`. Requires RubyGems push credentials.
134
+ Then `gem build`, `gem push`, and the tag — `<%= tag_prefix %><%= version %>`,
135
+ created and pushed *after* the gem, so a tag never names a version that failed
136
+ to publish. `--no-tag` skips it.
133
137
 
134
- ## 7. Tag
138
+ Requires RubyGems push credentials.
135
139
 
136
- ```sh
137
- gem kit tag<%= gem_flag %> --push
138
- ```
140
+ The tag matters beyond bookkeeping: it is what the next
141
+ `gem kit changelog<%= gem_flag %> --write` uses to find the commit range, so a
142
+ missing one makes the following release's changelog harder to write.
139
143
 
140
- Creates `<%= tag_prefix %><%= version %>`, refusing if it already exists. The tag is also what
141
- the next `gem kit changelog<%= gem_flag %> --write` uses to find the commit range, so a missing
142
- one makes the following release's changelog harder to write.
144
+ `gem kit tag<%= gem_flag %>` still exists for tagging on its own.
143
145
 
144
146
  ## When something goes wrong
145
147
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GemKit
4
4
  module Release
5
- VERSION = "0.2.2"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gem_kit-release
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan Kidd