configure_trusted_publisher 0.0.1 → 0.1.2

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: 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.