configure_trusted_publisher 0.0.1 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c5511679a5a528ab59e7aab878e7148bd6af838fe83671deb286f41343d6e531
4
- data.tar.gz: 3db794f67c04a8f1143889741df654a5a8a74157b8fd2b143f332b0ebe0a8dca
3
+ metadata.gz: fa9e10544502d63d23b7677b099af4a53e2ae4436cd4cb8e5134cfcd41e541b7
4
+ data.tar.gz: f7608581cf5d2cc748630bb8fdf56771e7ce3e6ec8ce93385e30d2f721a7d2e3
5
5
  SHA512:
6
- metadata.gz: a35b4d9b449812f533d86f8a6854771588afcf9d116844b81ecf4746b2265081dc42ad744f74ed69d107dc1e828b9a359ccb53f1f998e537b93f3fbdb65f5677
7
- data.tar.gz: f3892edf8c3124397b027ab5673f160f86dbfe4ae1cec82ae2af6ae865424619373c92bd8262b092dc954f03c8c65b2b9148895303085cb6c88b65af5b184db0
6
+ metadata.gz: 71ea60d2723ca3d2c49eb7b1a6d3c1ab2bf1b6a95024e5414efd1ddbe57c71d17e1a54bd6c96d75f4c20b32182bf7dc349c7144105b0a4863c7b0ec73cabd62a
7
+ data.tar.gz: aeac3236be08f91c5e479e5fe2151b4a58be1c8f9c59d957204ee5fbe147041f1621b0aa26be99a0fa48063898dbaf0140e48f4b7783a2b90156b38dff52e81f
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,368 @@
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 "bundler"
9
+ require "json"
10
+ require "rubygems/gemcutter_utilities"
11
+
12
+ Gem.configuration.verbose = true
13
+
14
+ module ConfigureTrustedPublisher
15
+ class GemcutterUtilities
16
+ include Gem::GemcutterUtilities
17
+
18
+ attr_reader :options
19
+
20
+ def initialize(say:, ask:, ask_for_password:, terminate_interaction:, otp:)
21
+ @options = {
22
+ otp:
23
+ }
24
+ @say = say
25
+ @ask = ask
26
+ @ask_for_password = ask_for_password
27
+ @terminate_interaction = terminate_interaction
28
+ @api_keys = {}
29
+ end
30
+
31
+ def say(...)
32
+ @say.call(...)
33
+ end
34
+
35
+ def ask(...)
36
+ @ask.call(...)
37
+ end
38
+
39
+ def ask_for_password(...)
40
+ @ask_for_password.call(...)
41
+ end
42
+
43
+ def get_mfa_params(...)
44
+ { "expires_at" => (Time.now + (15 * 60)).strftime("%Y-%m-%d %H:%M %Z"), "mfa" => false }
45
+ end
46
+
47
+ def terminate_interaction(...) = @terminate_interaction.call(...)
48
+
49
+ def api_key
50
+ return @api_keys[@host] if @api_keys[@host]
51
+
52
+ if ENV["GEM_HOST_API_KEY"]
53
+ ENV["GEM_HOST_API_KEY"]
54
+ elsif options[:key]
55
+ verify_api_key options[:key]
56
+ end
57
+ end
58
+
59
+ def set_api_key(host, key)
60
+ @api_keys[host] = key
61
+ end
62
+
63
+ def rubygems_api_request(*args, **, &)
64
+ # pp(args:, **)
65
+ resp = super(*args, **) do |req|
66
+ yield(req)
67
+ # pp(req: {
68
+ # method: __method__,
69
+ # req:,
70
+ # url: req.uri,
71
+ # req_body: req.body
72
+ # })
73
+ _ = req
74
+ end
75
+ # pp(resp: {
76
+ # method: __method__,
77
+ # resp:,
78
+ # resp_body: resp.body
79
+ # })
80
+ _ = resp
81
+
82
+ resp
83
+ end
84
+
85
+ # def mfa_unauthorized?(response)
86
+ # super.tap do |result|
87
+ # pp result => {
88
+ # method: __method__,
89
+ # response:,
90
+ # response_body: response.body
91
+ # }
92
+ # end
93
+ # end
94
+
95
+ # def api_key_forbidden?(response)
96
+ # super.tap do |result|
97
+ # pp result => {
98
+ # method: __method__,
99
+ # response:,
100
+ # response_body: response.body
101
+ # }
102
+ # end
103
+ # end
104
+ end
105
+
106
+ class CLI
107
+ include CommandKit::Commands
108
+
109
+ command_name "configure_trusted_publisher"
110
+
111
+ class Rubygem < CommandKit::Command
112
+ include CommandKit::Options
113
+ include CommandKit::Interactive
114
+
115
+ option :name,
116
+ value: {
117
+ type: String
118
+ },
119
+ desc: "The name of the Rubygem to configure the trusted publisher for."
120
+
121
+ option :otp,
122
+ value: {
123
+ type: String
124
+ },
125
+ desc: "The one-time password for multi-factor authentication."
126
+
127
+ argument :repository, required: false, desc: "The repository to configure the trusted publisher for.",
128
+ usage: "REPOSITORY"
129
+
130
+ def run(repository = ".")
131
+ @gemspec_source = Bundler::Source::Gemspec.new({
132
+ "root_path" => Pathname(repository),
133
+ "path" => "."
134
+ })
135
+ rubygem_name = options[:name]
136
+ unless rubygem_name
137
+ if gemspec_source.specs.size > 1
138
+ raise "Multiple gemspecs found in #{repository}, please specify the gem name with --name"
139
+ elsif gemspec_source.specs.empty?
140
+ raise "No gemspecs found in #{repository}, please specify the gem name with --name"
141
+ end
142
+
143
+ rubygem_name = gemspec_source.specs.first.name
144
+ end
145
+
146
+ unless system("rake", "release", "--dry-run", chdir: repository, exception: false, out: File::NULL,
147
+ err: File::NULL)
148
+ abort "rake release is not configured for #{rubygem_name} in #{repository}"
149
+ end
150
+
151
+ puts "Configuring trusted publisher for #{rubygem_name} in #{File.expand_path(repository)} for " \
152
+ "#{github_repository.join('/')}"
153
+
154
+ gc = GemcutterUtilities.new(
155
+ say: ->(msg) { puts msg },
156
+ ask: ->(msg) { ask msg.chomp(":") },
157
+ ask_for_password: ->(msg) { ask_secret msg.chomp(":") },
158
+ terminate_interaction: ->(msg) { exit msg },
159
+ otp: options[:otp]
160
+ )
161
+ gc.sign_in(scope: "configure_trusted_publishers") unless gc.api_key
162
+
163
+ write_release_action(repository, rubygem_name)
164
+
165
+ owner, name = github_repository
166
+ config = {
167
+ "trusted_publisher" => {
168
+ "repository_name" => name,
169
+ "repository_owner" => owner,
170
+ "workflow_filename" => "push_gem.yml"
171
+ },
172
+ "trusted_publisher_type" => "OIDC::TrustedPublisher::GitHubAction"
173
+ }
174
+
175
+ gc.rubygems_api_request(
176
+ :get,
177
+ "api/v1/gems/#{rubygem_name}/trusted_publishers",
178
+ scope: "configure_trusted_publishers"
179
+ ) do |req|
180
+ req["Accept"] = "application/json"
181
+ req.add_field "Authorization", gc.api_key
182
+ end.then do |resp| # rubocop:disable Style/MultilineBlockChain
183
+ if resp.code != "200"
184
+ abort "Failed to get trusted publishers for #{rubygem_name} (#{resp.code.inspect}):\n#{resp.body}"
185
+ end
186
+
187
+ existing = JSON.parse(resp.body)
188
+ if (e = existing.find do |pub|
189
+ config["trusted_publisher_type"] == pub["trusted_publisher_type"] &&
190
+ config["trusted_publisher"].all? do |k, v|
191
+ pub["trusted_publisher"][k] == v
192
+ end
193
+ end)
194
+
195
+ abort "Trusted publisher for #{rubygem_name} already configured for " \
196
+ "#{e.dig('trusted_publisher', 'name').inspect}"
197
+ end
198
+ end
199
+
200
+ resp = gc.rubygems_api_request(
201
+ :post,
202
+ "api/v1/gems/#{rubygem_name}/trusted_publishers",
203
+ scope: "configure_trusted_publishers"
204
+ ) do |req|
205
+ req["Content-Type"] = "application/json"
206
+ req["Accept"] = "application/json"
207
+ req.add_field "Authorization", gc.api_key
208
+
209
+ req.body = config.to_json
210
+ end
211
+
212
+ if resp.code == "201"
213
+ puts "Successfully configured trusted publisher for #{rubygem_name}:\n " \
214
+ "#{gc.host}/gems/#{rubygem_name}/trusted_publishers"
215
+ else
216
+ abort "Failed to configure trusted publisher for #{rubygem_name}:\n#{resp.body}"
217
+ end
218
+ end
219
+
220
+ def github_repository
221
+ [
222
+ gemspec.metadata["source_code_uri"],
223
+ gemspec.metadata["homepage_uri"],
224
+ gemspec.metadata["bug_tracker_uri"],
225
+ gemspec.homepage
226
+ ].each do |uri|
227
+ next unless uri
228
+
229
+ if uri =~ %r{github.com[:/](?<owner>[^/]+)/(?<repo>[^/]+)}
230
+ return Regexp.last_match[:owner], Regexp.last_match[:repo]
231
+ end
232
+ end
233
+ raise "No GitHub repository found for #{gemspec.name}"
234
+ end
235
+
236
+ attr_reader :gemspec_source
237
+
238
+ def gemspec
239
+ gemspec_source.specs.first
240
+ end
241
+
242
+ def write_release_action(repository, rubygem_name)
243
+ tag = "Automatically when a new tag matching v* is pushed"
244
+ manual = "Manually by running a GitHub Action"
245
+ response = ask_multiple_choice(
246
+ "How would you like releases for #{rubygem_name} to be triggered?", [
247
+ tag,
248
+ manual
249
+ ],
250
+ default: "2"
251
+ )
252
+ case response
253
+ when tag
254
+ write_tag_action(repository)
255
+ when manual
256
+ write_manual_action(repository)
257
+ end
258
+ end
259
+
260
+ def write_tag_action(repository)
261
+ action_file = File.expand_path(".github/workflows/push_gem.yml", repository)
262
+ return unless check_action(action_file)
263
+
264
+ File.write(
265
+ action_file,
266
+ <<~YAML
267
+ name: Push Gem
268
+
269
+ on:
270
+ push:
271
+ tags:
272
+ - v*
273
+
274
+ permissions:
275
+ contents: read
276
+
277
+ jobs:
278
+ push:
279
+ if: github.repository == 'rubygems/configure_trusted_publisher'
280
+ runs-on: ubuntu-latest
281
+
282
+ permissions:
283
+ contents: write
284
+ id-token: write
285
+
286
+ steps:
287
+ # Set up
288
+ - name: Harden Runner
289
+ uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1
290
+ with:
291
+ egress-policy: audit
292
+
293
+ - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
294
+ - name: Set up Ruby
295
+ uses: ruby/setup-ruby@cacc9f1c0b3f4eb8a16a6bb0ed10897b43b9de49 # v1.176.0
296
+ with:
297
+ bundler-cache: true
298
+ ruby-version: ruby
299
+
300
+ # Release
301
+ - uses: rubygems/release-gem@612653d273a73bdae1df8453e090060bb4db5f31 # v1
302
+ YAML
303
+ )
304
+ puts "Created #{action_file}"
305
+ end
306
+
307
+ def write_manual_action(repository)
308
+ action_file = File.expand_path(".github/workflows/push_gem.yml", repository)
309
+ return unless check_action(action_file)
310
+
311
+ File.write(
312
+ action_file,
313
+ <<~YAML
314
+ name: Push Gem
315
+
316
+ on:
317
+ workflow_dispatch:
318
+
319
+ permissions:
320
+ contents: read
321
+
322
+ jobs:
323
+ push:
324
+ if: github.repository == 'rubygems/configure_trusted_publisher'
325
+ runs-on: ubuntu-latest
326
+
327
+ permissions:
328
+ contents: write
329
+ id-token: write
330
+
331
+ steps:
332
+ # Set up
333
+ - name: Harden Runner
334
+ uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1
335
+ with:
336
+ egress-policy: audit
337
+
338
+ - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
339
+ - name: Set up Ruby
340
+ uses: ruby/setup-ruby@cacc9f1c0b3f4eb8a16a6bb0ed10897b43b9de49 # v1.176.0
341
+ with:
342
+ bundler-cache: true
343
+ ruby-version: ruby
344
+
345
+ # Release
346
+ - uses: rubygems/release-gem@612653d273a73bdae1df8453e090060bb4db5f31 # v1
347
+ YAML
348
+
349
+ )
350
+ puts "Created #{action_file}"
351
+ end
352
+
353
+ def check_action(action_file)
354
+ return FileUtils.mkdir_p(File.dirname(action_file)) || true unless File.exist?(action_file)
355
+
356
+ response = ask_multiple_choice(
357
+ "#{action_file} already exists, overwrite?", { "y" => "Yes", "n" => "No" },
358
+ default: "n"
359
+ )
360
+ return if response == "No"
361
+
362
+ true
363
+ end
364
+ end
365
+
366
+ command Rubygem
367
+ end
368
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConfigureTrustedPublisher
4
+ VERSION = "0.1.3"
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.3
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.