configure_trusted_publisher 0.0.1 → 0.1.2

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: c5511679a5a528ab59e7aab878e7148bd6af838fe83671deb286f41343d6e531
4
- data.tar.gz: 3db794f67c04a8f1143889741df654a5a8a74157b8fd2b143f332b0ebe0a8dca
3
+ metadata.gz: 60ef9776995d0df14b680f2784c9085b0b09038fc08838c8c879b17295c2e21a
4
+ data.tar.gz: bd3bea67d36e229955c7a380d3c9cfe26d1fe2d1860d3cea01ddb9e7c5fe3543
5
5
  SHA512:
6
- metadata.gz: a35b4d9b449812f533d86f8a6854771588afcf9d116844b81ecf4746b2265081dc42ad744f74ed69d107dc1e828b9a359ccb53f1f998e537b93f3fbdb65f5677
7
- data.tar.gz: f3892edf8c3124397b027ab5673f160f86dbfe4ae1cec82ae2af6ae865424619373c92bd8262b092dc954f03c8c65b2b9148895303085cb6c88b65af5b184db0
6
+ metadata.gz: 74dfceb0bbeb736f2ea3dd12165d5271f56d7d37b72ce41755838a363484ca100a1c8b0977327755ab8da0a3e5b561760dff11006a33d377bed35a633ebf2613
7
+ data.tar.gz: c44f734fcc3cb03775f5c24c70121e7d4db90d9889cc2eedfb1f1edb5258cff46b60edc66424ecbe8875474d6950fa29ff85ca51102d4cf2d17eb2f99e1288dd
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Samuel Giddins
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # ConfigureTrustedPublisher
2
+
3
+ A small CLI to automate the process of configuring a trusted publisher for a gem.
4
+
5
+ ## Usage
6
+
7
+ To configure a trusted publisher for a gem, run the following command:
8
+
9
+ ```console
10
+ $ gem exec configure_trusted_publisher rubygem
11
+ Configuring trusted publisher for rubygem0 in /Users/segiddins/Development/github.com/rubygems/configure_trusted_publisher for rubygems/configure_trusted_publisher
12
+ Enter your https://rubygems.org credentials.
13
+ Don't have an account yet? Create one at https://rubygems.org/sign_up
14
+ Username/email: : gem-author
15
+ Password: :
16
+
17
+ 1) Automatically when a new tag matching v* is pushed
18
+ 2) Manually by running a GitHub Action
19
+
20
+ How would you like releases for rubygem0 to be triggered? (1, 2) [2]: 2
21
+
22
+ Successfully configured trusted publisher for rubygem0:
23
+ https://rubygems.org/gems/rubygem0/trusted_publishers
24
+ ```
25
+
26
+ ## Development
27
+
28
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
29
+
30
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
31
+
32
+ ## Contributing
33
+
34
+ Bug reports and pull requests are welcome on GitHub at <https://github.com/rubygems/configure_trusted_publisher>.
35
+
36
+ ## License
37
+
38
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "configure_trusted_publisher"
5
+ require "configure_trusted_publisher/cli"
6
+
7
+ ConfigureTrustedPublisher::CLI.start(ARGV)
@@ -0,0 +1,367 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "command_kit"
4
+ require "command_kit/commands"
5
+ require "command_kit/interactive"
6
+ require "io/console"
7
+
8
+ require "json"
9
+ require "rubygems/gemcutter_utilities"
10
+
11
+ Gem.configuration.verbose = true
12
+
13
+ module ConfigureTrustedPublisher
14
+ class GemcutterUtilities
15
+ include Gem::GemcutterUtilities
16
+
17
+ attr_reader :options
18
+
19
+ def initialize(say:, ask:, ask_for_password:, terminate_interaction:, otp:)
20
+ @options = {
21
+ otp:
22
+ }
23
+ @say = say
24
+ @ask = ask
25
+ @ask_for_password = ask_for_password
26
+ @terminate_interaction = terminate_interaction
27
+ @api_keys = {}
28
+ end
29
+
30
+ def say(...)
31
+ @say.call(...)
32
+ end
33
+
34
+ def ask(...)
35
+ @ask.call(...)
36
+ end
37
+
38
+ def ask_for_password(...)
39
+ @ask_for_password.call(...)
40
+ end
41
+
42
+ def get_mfa_params(...)
43
+ { "expires_at" => (Time.now + (15 * 60)).strftime("%Y-%m-%d %H:%M %Z"), "mfa" => false }
44
+ end
45
+
46
+ def terminate_interaction(...) = @terminate_interaction.call(...)
47
+
48
+ def api_key
49
+ return @api_keys[@host] if @api_keys[@host]
50
+
51
+ if ENV["GEM_HOST_API_KEY"]
52
+ ENV["GEM_HOST_API_KEY"]
53
+ elsif options[:key]
54
+ verify_api_key options[:key]
55
+ end
56
+ end
57
+
58
+ def set_api_key(host, key)
59
+ @api_keys[host] = key
60
+ end
61
+
62
+ def rubygems_api_request(*args, **, &)
63
+ # pp(args:, **)
64
+ resp = super(*args, **) do |req|
65
+ yield(req)
66
+ # pp(req: {
67
+ # method: __method__,
68
+ # req:,
69
+ # url: req.uri,
70
+ # req_body: req.body
71
+ # })
72
+ _ = req
73
+ end
74
+ # pp(resp: {
75
+ # method: __method__,
76
+ # resp:,
77
+ # resp_body: resp.body
78
+ # })
79
+ _ = resp
80
+
81
+ resp
82
+ end
83
+
84
+ # def mfa_unauthorized?(response)
85
+ # super.tap do |result|
86
+ # pp result => {
87
+ # method: __method__,
88
+ # response:,
89
+ # response_body: response.body
90
+ # }
91
+ # end
92
+ # end
93
+
94
+ # def api_key_forbidden?(response)
95
+ # super.tap do |result|
96
+ # pp result => {
97
+ # method: __method__,
98
+ # response:,
99
+ # response_body: response.body
100
+ # }
101
+ # end
102
+ # end
103
+ end
104
+
105
+ class CLI
106
+ include CommandKit::Commands
107
+
108
+ command_name "configure_trusted_publisher"
109
+
110
+ class Rubygem < CommandKit::Command
111
+ include CommandKit::Options
112
+ include CommandKit::Interactive
113
+
114
+ option :name,
115
+ value: {
116
+ type: String
117
+ },
118
+ desc: "The name of the Rubygem to configure the trusted publisher for."
119
+
120
+ option :otp,
121
+ value: {
122
+ type: String
123
+ },
124
+ desc: "The one-time password for multi-factor authentication."
125
+
126
+ argument :repository, required: false, desc: "The repository to configure the trusted publisher for.",
127
+ usage: "REPOSITORY"
128
+
129
+ def run(repository = ".")
130
+ @gemspec_source = Bundler::Source::Gemspec.new({
131
+ "root_path" => Pathname(repository),
132
+ "path" => "."
133
+ })
134
+ rubygem_name = options[:name]
135
+ unless rubygem_name
136
+ if gemspec_source.specs.size > 1
137
+ raise "Multiple gemspecs found in #{repository}, please specify the gem name with --name"
138
+ elsif gemspec_source.specs.empty?
139
+ raise "No gemspecs found in #{repository}, please specify the gem name with --name"
140
+ end
141
+
142
+ rubygem_name = gemspec_source.specs.first.name
143
+ end
144
+
145
+ unless system("rake", "release", "--dry-run", chdir: repository, exception: false, out: File::NULL,
146
+ err: File::NULL)
147
+ abort "rake release is not configured for #{rubygem_name} in #{repository}"
148
+ end
149
+
150
+ puts "Configuring trusted publisher for #{rubygem_name} in #{File.expand_path(repository)} for " \
151
+ "#{github_repository.join('/')}"
152
+
153
+ gc = GemcutterUtilities.new(
154
+ say: ->(msg) { puts msg },
155
+ ask: ->(msg) { ask msg.chomp(":") },
156
+ ask_for_password: ->(msg) { ask_secret msg.chomp(":") },
157
+ terminate_interaction: ->(msg) { exit msg },
158
+ otp: options[:otp]
159
+ )
160
+ gc.sign_in(scope: "configure_trusted_publishers") unless gc.api_key
161
+
162
+ write_release_action(repository, rubygem_name)
163
+
164
+ owner, name = github_repository
165
+ config = {
166
+ "trusted_publisher" => {
167
+ "repository_name" => name,
168
+ "repository_owner" => owner,
169
+ "workflow_filename" => "push_gem.yml"
170
+ },
171
+ "trusted_publisher_type" => "OIDC::TrustedPublisher::GitHubAction"
172
+ }
173
+
174
+ gc.rubygems_api_request(
175
+ :get,
176
+ "api/v1/gems/#{rubygem_name}/trusted_publishers",
177
+ scope: "configure_trusted_publishers"
178
+ ) do |req|
179
+ req["Accept"] = "application/json"
180
+ req.add_field "Authorization", gc.api_key
181
+ end.then do |resp| # rubocop:disable Style/MultilineBlockChain
182
+ if resp.code != "200"
183
+ abort "Failed to get trusted publishers for #{rubygem_name} (#{resp.code.inspect}):\n#{resp.body}"
184
+ end
185
+
186
+ existing = JSON.parse(resp.body)
187
+ if (e = existing.find do |pub|
188
+ config["trusted_publisher_type"] == pub["trusted_publisher_type"] &&
189
+ config["trusted_publisher"].all? do |k, v|
190
+ pub["trusted_publisher"][k] == v
191
+ end
192
+ end)
193
+
194
+ abort "Trusted publisher for #{rubygem_name} already configured for " \
195
+ "#{e.dig('trusted_publisher', 'name').inspect}"
196
+ end
197
+ end
198
+
199
+ resp = gc.rubygems_api_request(
200
+ :post,
201
+ "api/v1/gems/#{rubygem_name}/trusted_publishers",
202
+ scope: "configure_trusted_publishers"
203
+ ) do |req|
204
+ req["Content-Type"] = "application/json"
205
+ req["Accept"] = "application/json"
206
+ req.add_field "Authorization", gc.api_key
207
+
208
+ req.body = config.to_json
209
+ end
210
+
211
+ if resp.code == "201"
212
+ puts "Successfully configured trusted publisher for #{rubygem_name}:\n " \
213
+ "#{gc.host}/gems/#{rubygem_name}/trusted_publishers"
214
+ else
215
+ abort "Failed to configure trusted publisher for #{rubygem_name}:\n#{resp.body}"
216
+ end
217
+ end
218
+
219
+ def github_repository
220
+ [
221
+ gemspec.metadata["source_code_uri"],
222
+ gemspec.metadata["homepage_uri"],
223
+ gemspec.metadata["bug_tracker_uri"],
224
+ gemspec.homepage
225
+ ].each do |uri|
226
+ next unless uri
227
+
228
+ if uri =~ %r{github.com[:/](?<owner>[^/]+)/(?<repo>[^/]+)}
229
+ return Regexp.last_match[:owner], Regexp.last_match[:repo]
230
+ end
231
+ end
232
+ raise "No GitHub repository found for #{gemspec.name}"
233
+ end
234
+
235
+ attr_reader :gemspec_source
236
+
237
+ def gemspec
238
+ gemspec_source.specs.first
239
+ end
240
+
241
+ def write_release_action(repository, rubygem_name)
242
+ tag = "Automatically when a new tag matching v* is pushed"
243
+ manual = "Manually by running a GitHub Action"
244
+ response = ask_multiple_choice(
245
+ "How would you like releases for #{rubygem_name} to be triggered?", [
246
+ tag,
247
+ manual
248
+ ],
249
+ default: "2"
250
+ )
251
+ case response
252
+ when tag
253
+ write_tag_action(repository)
254
+ when manual
255
+ write_manual_action(repository)
256
+ end
257
+ end
258
+
259
+ def write_tag_action(repository)
260
+ action_file = File.expand_path(".github/workflows/push_gem.yml", repository)
261
+ return unless check_action(action_file)
262
+
263
+ File.write(
264
+ action_file,
265
+ <<~YAML
266
+ name: Push Gem
267
+
268
+ on:
269
+ push:
270
+ tags:
271
+ - v*
272
+
273
+ permissions:
274
+ contents: read
275
+
276
+ jobs:
277
+ push:
278
+ if: github.repository == 'rubygems/configure_trusted_publisher'
279
+ runs-on: ubuntu-latest
280
+
281
+ permissions:
282
+ contents: write
283
+ id-token: write
284
+
285
+ steps:
286
+ # Set up
287
+ - name: Harden Runner
288
+ uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1
289
+ with:
290
+ egress-policy: audit
291
+
292
+ - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
293
+ - name: Set up Ruby
294
+ uses: ruby/setup-ruby@cacc9f1c0b3f4eb8a16a6bb0ed10897b43b9de49 # v1.176.0
295
+ with:
296
+ bundler-cache: true
297
+ ruby-version: ruby
298
+
299
+ # Release
300
+ - uses: rubygems/release-gem@612653d273a73bdae1df8453e090060bb4db5f31 # v1
301
+ YAML
302
+ )
303
+ puts "Created #{action_file}"
304
+ end
305
+
306
+ def write_manual_action(repository)
307
+ action_file = File.expand_path(".github/workflows/push_gem.yml", repository)
308
+ return unless check_action(action_file)
309
+
310
+ File.write(
311
+ action_file,
312
+ <<~YAML
313
+ name: Push Gem
314
+
315
+ on:
316
+ workflow_dispatch:
317
+
318
+ permissions:
319
+ contents: read
320
+
321
+ jobs:
322
+ push:
323
+ if: github.repository == 'rubygems/configure_trusted_publisher'
324
+ runs-on: ubuntu-latest
325
+
326
+ permissions:
327
+ contents: write
328
+ id-token: write
329
+
330
+ steps:
331
+ # Set up
332
+ - name: Harden Runner
333
+ uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1
334
+ with:
335
+ egress-policy: audit
336
+
337
+ - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
338
+ - name: Set up Ruby
339
+ uses: ruby/setup-ruby@cacc9f1c0b3f4eb8a16a6bb0ed10897b43b9de49 # v1.176.0
340
+ with:
341
+ bundler-cache: true
342
+ ruby-version: ruby
343
+
344
+ # Release
345
+ - uses: rubygems/release-gem@612653d273a73bdae1df8453e090060bb4db5f31 # v1
346
+ YAML
347
+
348
+ )
349
+ puts "Created #{action_file}"
350
+ end
351
+
352
+ def check_action(action_file)
353
+ return FileUtils.mkdir_p(File.dirname(action_file)) || true unless File.exist?(action_file)
354
+
355
+ response = ask_multiple_choice(
356
+ "#{action_file} already exists, overwrite?", { "y" => "Yes", "n" => "No" },
357
+ default: "n"
358
+ )
359
+ return if response == "No"
360
+
361
+ true
362
+ end
363
+ end
364
+
365
+ command Rubygem
366
+ end
367
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConfigureTrustedPublisher
4
+ VERSION = "0.1.2"
5
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "configure_trusted_publisher/version"
4
+
5
+ module ConfigureTrustedPublisher
6
+ class Error < StandardError; end
7
+ # Your code goes here...
8
+ end
metadata CHANGED
@@ -1,11 +1,11 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: configure_trusted_publisher
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Giddins
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
11
  date: 2024-05-08 00:00:00.000000000 Z
@@ -38,20 +38,27 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: 0.5.5
41
- description:
41
+ description:
42
42
  email:
43
43
  - segiddins@segiddins.me
44
- executables: []
44
+ executables:
45
+ - configure_trusted_publisher
45
46
  extensions: []
46
47
  extra_rdoc_files: []
47
- files: []
48
+ files:
49
+ - LICENSE.txt
50
+ - README.md
51
+ - exe/configure_trusted_publisher
52
+ - lib/configure_trusted_publisher.rb
53
+ - lib/configure_trusted_publisher/cli.rb
54
+ - lib/configure_trusted_publisher/version.rb
48
55
  homepage: https://github.com/rubygems/configure_trusted_publisher
49
56
  licenses:
50
57
  - MIT
51
58
  metadata:
52
59
  homepage_uri: https://github.com/rubygems/configure_trusted_publisher
53
60
  rubygems_mfa_required: 'true'
54
- post_install_message:
61
+ post_install_message:
55
62
  rdoc_options: []
56
63
  require_paths:
57
64
  - lib
@@ -66,8 +73,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
66
73
  - !ruby/object:Gem::Version
67
74
  version: '3.5'
68
75
  requirements: []
69
- rubygems_version: 3.5.10
70
- signing_key:
76
+ rubygems_version: 3.5.9
77
+ signing_key:
71
78
  specification_version: 4
72
79
  summary: A small CLI to automate the process of configuring a trusted publisher for
73
80
  a gem.