rake-gem-maintenance 0.1.7 → 0.2.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/CLAUDE.md +7 -7
- data/Gemfile.lock +2 -2
- data/README.md +9 -9
- data/TODO.md +4 -1
- data/lib/rake/gem/maintenance/api_key_renewer.rb +37 -35
- data/lib/rake/gem/maintenance/ci_environment.rb +7 -5
- data/lib/rake/gem/maintenance/credential_store.rb +46 -44
- data/lib/rake/gem/maintenance/gem_publisher.rb +86 -84
- data/lib/rake/gem/maintenance/gem_push.rb +44 -42
- data/lib/rake/gem/maintenance/install_tasks.rb +5 -5
- data/lib/rake/gem/maintenance/otp_provider.rb +33 -31
- data/lib/rake/gem/maintenance/renew_api_key_task.rb +111 -109
- data/lib/rake/gem/maintenance/repos.rb +81 -79
- data/lib/rake/gem/maintenance/ruby_gems_api_key_creator.rb +39 -37
- data/lib/rake/gem/maintenance/upgrade_task.rb +245 -242
- data/lib/rake/gem/maintenance/version.rb +4 -2
- data/lib/rake/gem/maintenance/version_bump_task.rb +83 -81
- data/lib/rake/gem/maintenance/woodpecker_secret_store.rb +69 -67
- data/scripts/ci_publish_rubygems.rb +3 -3
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1025716123e134ab6db2ffcfc5655a59208dd038691b73e270c92c6bd35ea977
|
|
4
|
+
data.tar.gz: 1445566f68985cdd2b915c9958ec9daddaf07caa5002c62277481b72a122d561
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ba35f363319b323fe927da2cc3a9e8934d7b4310f5995b31d1cc91a759b34c8462262c449ef59fa1debe3c35615c733ef8f0dc96e6f270b592a76f1f6cc76153
|
|
7
|
+
data.tar.gz: 892b319be15819eb07cf5f254e9e7144e8387393a586442e9831585602600ade156e6d6c7c0b1dc41b6e60ee9e58b411c10a1bcebbff5e08f8510ec56f32ffad
|
data/CLAUDE.md
CHANGED
|
@@ -22,7 +22,7 @@ rake rubocop
|
|
|
22
22
|
|
|
23
23
|
## Architecture
|
|
24
24
|
|
|
25
|
-
### Core Classes (lib/rake/
|
|
25
|
+
### Core Classes (lib/rake/gem/maintenance/)
|
|
26
26
|
|
|
27
27
|
- **UpgradeTask** — Rake::TaskLib subclass defining upgrade:* tasks (branch, gems, commit, push, auto)
|
|
28
28
|
- Default repositories: rubygems.org
|
|
@@ -36,21 +36,21 @@ rake rubocop
|
|
|
36
36
|
|
|
37
37
|
### Entry Points
|
|
38
38
|
|
|
39
|
-
- `rake/
|
|
40
|
-
- `rake/
|
|
39
|
+
- `rake/gem/maintenance.rb` — requires all task classes
|
|
40
|
+
- `rake/gem/maintenance/install_tasks.rb` — auto-instantiates UpgradeTask and VersionBumpTask with defaults
|
|
41
41
|
|
|
42
42
|
### Quick Usage
|
|
43
43
|
|
|
44
44
|
```ruby
|
|
45
45
|
# Internal gems only (cbp-org.internal)
|
|
46
|
-
Rake::
|
|
46
|
+
Rake::Gem::Maintenance::InternalUpgradeTask.new
|
|
47
47
|
|
|
48
48
|
# Both repositories
|
|
49
|
-
Rake::
|
|
49
|
+
Rake::Gem::Maintenance::DualUpgradeTask.new
|
|
50
50
|
|
|
51
51
|
# Default (rubygems.org) - can also use Repos module
|
|
52
|
-
Rake::
|
|
53
|
-
t.gem_repositories = Rake::
|
|
52
|
+
Rake::Gem::Maintenance::UpgradeTask.new do |t|
|
|
53
|
+
t.gem_repositories = Rake::Gem::Maintenance::Repos.internal # or Repos.all
|
|
54
54
|
end
|
|
55
55
|
```
|
|
56
56
|
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
rake-gem-maintenance (0.
|
|
4
|
+
rake-gem-maintenance (0.2.0)
|
|
5
5
|
bundler-audit
|
|
6
6
|
gem-release
|
|
7
7
|
rake
|
|
@@ -254,7 +254,7 @@ CHECKSUMS
|
|
|
254
254
|
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
|
|
255
255
|
rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
|
|
256
256
|
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
|
|
257
|
-
rake-gem-maintenance (0.
|
|
257
|
+
rake-gem-maintenance (0.2.0)
|
|
258
258
|
rb-fsevent (0.11.2) sha256=43900b972e7301d6570f64b850a5aa67833ee7d87b458ee92805d56b7318aefe
|
|
259
259
|
rb-inotify (0.11.1) sha256=a0a700441239b0ff18eb65e3866236cd78613d6b9f78fea1f9ac47a85e47be6e
|
|
260
260
|
rdoc (7.2.0) sha256=8650f76cd4009c3b54955eb5d7e3a075c60a57276766ebf36f9085e8c9f23192
|
data/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Rake::
|
|
1
|
+
# Rake::Gem::Maintenance
|
|
2
2
|
|
|
3
3
|
[](https://github.com/cbroult/rake-gem-maintenance/actions/workflows/main.yml)
|
|
4
4
|
[](https://github.com/cbroult/rake-gem-maintenance/network/updates)
|
|
@@ -18,7 +18,7 @@ gem "rake-gem-maintenance"
|
|
|
18
18
|
Add to your Rakefile for default behavior:
|
|
19
19
|
|
|
20
20
|
```ruby
|
|
21
|
-
require "rake/
|
|
21
|
+
require "rake/gem/maintenance/install_tasks"
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
This defines:
|
|
@@ -33,15 +33,15 @@ This defines:
|
|
|
33
33
|
## Customization
|
|
34
34
|
|
|
35
35
|
```ruby
|
|
36
|
-
require "rake/
|
|
36
|
+
require "rake/gem/maintenance"
|
|
37
37
|
|
|
38
|
-
Rake::
|
|
38
|
+
Rake::Gem::Maintenance::UpgradeTask.new do |t|
|
|
39
39
|
t.main_branch = "develop"
|
|
40
40
|
t.upgrade_branch = "chore/upgrade-deps"
|
|
41
41
|
t.commit_message = "chore: upgrade dependencies"
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
Rake::
|
|
44
|
+
Rake::Gem::Maintenance::VersionBumpTask.new do |t|
|
|
45
45
|
t.default_type = "minor"
|
|
46
46
|
end
|
|
47
47
|
```
|
|
@@ -76,7 +76,7 @@ The file is `0600` (owner-read-only on Unix). The **password is never written to
|
|
|
76
76
|
|
|
77
77
|
### Step 2 — All future local runs are automatic
|
|
78
78
|
|
|
79
|
-
Any project using `require "rake/
|
|
79
|
+
Any project using `require "rake/gem/maintenance/install_tasks"` automatically reads the
|
|
80
80
|
credential file at startup and sets `GEM_HOST_API_KEY` and `RUBYGEMS_OTP_SEED` in the process
|
|
81
81
|
environment. Running `rake upgrade` needs no manual credential setup from this point on.
|
|
82
82
|
|
|
@@ -106,10 +106,10 @@ See [features/upgrade_task/renew_api_key.feature](features/upgrade_task/renew_ap
|
|
|
106
106
|
```ruby
|
|
107
107
|
require "rake/gem/maintenance"
|
|
108
108
|
|
|
109
|
-
Rake::
|
|
110
|
-
Rake::
|
|
109
|
+
Rake::Gem::Maintenance::Repos.rubygems_api_key_env_var = "MY_RUBYGEMS_KEY"
|
|
110
|
+
Rake::Gem::Maintenance::Repos.rubygems_otp_seed_env_var = "MY_OTP_SEED"
|
|
111
111
|
|
|
112
|
-
Rake::
|
|
112
|
+
Rake::Gem::Maintenance::UpgradeTask.new
|
|
113
113
|
```
|
|
114
114
|
|
|
115
115
|
See [features/upgrade_task/repos_configuration.feature](features/upgrade_task/repos_configuration.feature)
|
data/TODO.md
CHANGED
|
@@ -1 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
# Packwerk
|
|
2
|
+
|
|
3
|
+
Packwerk is Shopify's package enforcement tool designed for large Rails monoliths.
|
|
4
|
+
With ~15 files in this library gem, the overhead is not warranted — close without implementing.
|
|
@@ -1,47 +1,49 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Rake
|
|
4
|
-
module
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
4
|
+
module Gem
|
|
5
|
+
module Maintenance
|
|
6
|
+
# Renews a rubygems.org API key using credentials from env vars and
|
|
7
|
+
# persists the new key to Woodpecker CI when server details are available.
|
|
8
|
+
class ApiKeyRenewer
|
|
9
|
+
def initialize(otp_provider:,
|
|
10
|
+
username_env_var: "RUBYGEMS_USERNAME",
|
|
11
|
+
password_env_var: "RUBYGEMS_PASSWORD")
|
|
12
|
+
@otp_provider = otp_provider
|
|
13
|
+
@username_env_var = username_env_var
|
|
14
|
+
@password_env_var = password_env_var
|
|
15
|
+
end
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
def renew(repository)
|
|
18
|
+
username = env_credential(@username_env_var)
|
|
19
|
+
password = env_credential(@password_env_var)
|
|
20
|
+
return nil if username.nil? || password.nil?
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
otp = @otp_provider.otp_for(repository[:name], otp_seed_env_var: repository[:otp_seed_env_var])
|
|
23
|
+
new_key = RubyGemsApiKeyCreator.new(host: repository.fetch(:url, "https://rubygems.org"))
|
|
24
|
+
.create(username, password, otp: otp)
|
|
25
|
+
persist_to_woodpecker(new_key)
|
|
26
|
+
new_key
|
|
27
|
+
rescue StandardError
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
private
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
def env_credential(var)
|
|
34
|
+
value = ENV.fetch(var, nil)
|
|
35
|
+
value&.empty? ? nil : value
|
|
36
|
+
end
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
def persist_to_woodpecker(new_key)
|
|
39
|
+
server = ENV.fetch("WOODPECKER_SERVER", nil)
|
|
40
|
+
token = ENV.fetch("WOODPECKER_TOKEN", nil)
|
|
41
|
+
return unless server && token
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
org = ENV.fetch("WOODPECKER_ORG", "cbp-org")
|
|
44
|
+
WoodpeckerSecretStore.new(server: server, org: org, token: token)
|
|
45
|
+
.store("rubygems_api_key", new_key)
|
|
46
|
+
end
|
|
45
47
|
end
|
|
46
48
|
end
|
|
47
49
|
end
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Rake
|
|
4
|
-
module
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
module Gem
|
|
5
|
+
module Maintenance
|
|
6
|
+
# Detects whether the current process is running inside a CI environment.
|
|
7
|
+
module CIEnvironment
|
|
8
|
+
def self.ci?
|
|
9
|
+
ENV["CI"].to_s != ""
|
|
10
|
+
end
|
|
9
11
|
end
|
|
10
12
|
end
|
|
11
13
|
end
|
|
@@ -5,60 +5,62 @@ require "fileutils"
|
|
|
5
5
|
require "rubygems"
|
|
6
6
|
|
|
7
7
|
module Rake
|
|
8
|
-
module
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
module Gem
|
|
9
|
+
module Maintenance
|
|
10
|
+
# Persists rubygems.org credentials (username, API key, OTP seed) to a local config file.
|
|
11
|
+
class CredentialStore
|
|
12
|
+
def self.default_path
|
|
13
|
+
base = if ::Gem.win_platform?
|
|
14
|
+
ENV.fetch("APPDATA", File.expand_path("~"))
|
|
15
|
+
else
|
|
16
|
+
ENV.fetch("XDG_CONFIG_HOME", File.join(Dir.home, ".config"))
|
|
17
|
+
end
|
|
18
|
+
File.join(base, "rake-gem-maintenance", "credentials.yml")
|
|
19
|
+
end
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
def initialize(path: self.class.default_path)
|
|
22
|
+
@path = path
|
|
23
|
+
end
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
attr_reader :path
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
def read
|
|
28
|
+
return {} unless File.exist?(@path)
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
YAML.safe_load_file(@path, symbolize_names: true) || {}
|
|
31
|
+
rescue StandardError
|
|
32
|
+
{}
|
|
33
|
+
end
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
def write(credentials)
|
|
36
|
+
FileUtils.mkdir_p(File.dirname(@path))
|
|
37
|
+
File.write(@path, credentials.transform_keys(&:to_s).to_yaml)
|
|
38
|
+
File.chmod(0o600, @path) unless ::Gem.win_platform?
|
|
39
|
+
end
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
def apply_to_env(username_env_var:, api_key_env_var:)
|
|
42
|
+
creds = read
|
|
43
|
+
set_env_if_absent(username_env_var, creds[:username])
|
|
44
|
+
set_env_if_absent("RUBYGEMS_OTP_SEED", creds[:rubygems_otp_seed])
|
|
45
|
+
set_env_if_absent(api_key_env_var, creds[:gem_host_api_key])
|
|
46
|
+
end
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
def update(username:, api_key:, api_key_env_var:)
|
|
49
|
+
otp_seed = ENV.fetch("RUBYGEMS_OTP_SEED", nil)
|
|
50
|
+
updated = read.merge(username: username, gem_host_api_key: api_key)
|
|
51
|
+
updated[:rubygems_otp_seed] = otp_seed if otp_seed && !otp_seed.empty?
|
|
52
|
+
write(updated)
|
|
53
|
+
ENV[api_key_env_var] = api_key
|
|
54
|
+
end
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
private
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
def set_env_if_absent(env_var, value)
|
|
59
|
+
return unless value && !value.empty?
|
|
60
|
+
return if (existing = ENV.fetch(env_var, nil)) && !existing.empty?
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
ENV[env_var] = value
|
|
63
|
+
end
|
|
62
64
|
end
|
|
63
65
|
end
|
|
64
66
|
end
|
|
@@ -4,113 +4,115 @@ require "json"
|
|
|
4
4
|
require "open-uri"
|
|
5
5
|
|
|
6
6
|
module Rake
|
|
7
|
-
module
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
7
|
+
module Gem
|
|
8
|
+
module Maintenance
|
|
9
|
+
# Publishes gems to multiple gem repositories, with version checking and warning handling.
|
|
10
|
+
class GemPublisher
|
|
11
|
+
attr_reader :repositories, :warnings, :failed_repositories, :successful_repos
|
|
12
|
+
|
|
13
|
+
def initialize(repositories = default_repositories,
|
|
14
|
+
otp_provider: OtpProvider.new,
|
|
15
|
+
ci_environment: CIEnvironment)
|
|
16
|
+
@repositories = repositories
|
|
17
|
+
@otp_provider = otp_provider
|
|
18
|
+
@ci_environment = ci_environment
|
|
19
|
+
@warnings = []
|
|
20
|
+
@failed_pushes = []
|
|
21
|
+
@failed_repositories = []
|
|
22
|
+
@published_files = []
|
|
23
|
+
@successful_repos = []
|
|
24
|
+
end
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
def default_repositories
|
|
27
|
+
[{ name: "rubygems", url: "https://rubygems.org" }]
|
|
28
|
+
end
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
def publish(gem_file)
|
|
31
|
+
@failed_pushes = []
|
|
32
|
+
@published_files = []
|
|
33
|
+
build_and_push(gem_file)
|
|
34
|
+
print_warnings
|
|
35
|
+
end
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
def check_all_repositories(gem_name)
|
|
38
|
+
@failed_repositories = []
|
|
39
|
+
repositories.each do |repo|
|
|
40
|
+
versions_on_repository(gem_name, repo)
|
|
41
|
+
end
|
|
40
42
|
end
|
|
41
|
-
end
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
44
|
+
def versions_on_repository(gem_name, repository)
|
|
45
|
+
url = "#{repository[:url]}/api/v1/gems/#{gem_name}.json"
|
|
46
|
+
uri = URI.parse(url)
|
|
47
|
+
response = uri.read(accept: "application/json")
|
|
48
|
+
data = JSON.parse(response)
|
|
49
|
+
data["versions"].map { |v| v["number"] }
|
|
50
|
+
rescue StandardError => e
|
|
51
|
+
@failed_repositories << repository[:name]
|
|
52
|
+
@warnings << { repository: repository[:name], error: "Cannot fetch versions: #{e.message}" }
|
|
53
|
+
[]
|
|
54
|
+
end
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
def next_version(gem_name, current_version)
|
|
57
|
+
return current_version unless current_version
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
ver = ::Gem::Version.new(current_version)
|
|
60
|
+
loop do
|
|
61
|
+
return ver.to_s unless version_exists_on_all_repos?(gem_name, ver)
|
|
61
62
|
|
|
62
|
-
|
|
63
|
+
ver = ver.bump
|
|
64
|
+
end
|
|
63
65
|
end
|
|
64
|
-
end
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
def available_repositories
|
|
68
|
+
repositories.map { |r| r[:name] } - @failed_repositories
|
|
69
|
+
end
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
def any_available?
|
|
72
|
+
@failed_repositories.size < repositories.size
|
|
73
|
+
end
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
private
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
def build_and_push(gem_file)
|
|
78
|
+
repositories.each do |repo|
|
|
79
|
+
push(gem_file, repository: repo)
|
|
80
|
+
end
|
|
79
81
|
end
|
|
80
|
-
end
|
|
81
82
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
def push(gem_file, repository:)
|
|
84
|
+
result = GemPush.new(gem_file, repository, @otp_provider).attempt
|
|
85
|
+
return record_success(gem_file, repository) if result.success
|
|
85
86
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
record_push_failure(repository, result.error)
|
|
88
|
+
rescue StandardError => e
|
|
89
|
+
@failed_pushes << { repository: repository[:name], error: e.message }
|
|
90
|
+
end
|
|
90
91
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
def record_success(gem_file, repository)
|
|
93
|
+
@published_files << gem_file
|
|
94
|
+
@successful_repos << repository[:name]
|
|
95
|
+
end
|
|
95
96
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
def record_push_failure(repository, message)
|
|
98
|
+
@failed_pushes << { repository: repository[:name], error: message.strip }
|
|
99
|
+
end
|
|
99
100
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
def version_exists_on_all_repos?(gem_name, version)
|
|
102
|
+
repositories.all? do |repo|
|
|
103
|
+
versions_on_repository(gem_name, repo).include?(version.to_s)
|
|
104
|
+
end
|
|
103
105
|
end
|
|
104
|
-
end
|
|
105
106
|
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
def print_warnings
|
|
108
|
+
return if @failed_pushes.empty?
|
|
108
109
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
puts "[WARN] The following repositories were unavailable:"
|
|
111
|
+
@failed_pushes.each do |failure|
|
|
112
|
+
puts " - #{failure[:repository]}: #{failure[:error]}"
|
|
113
|
+
end
|
|
114
|
+
puts "[WARN] Warning: Published to #{@published_files.size} of #{repositories.size} repositories"
|
|
112
115
|
end
|
|
113
|
-
puts "[WARN] Warning: Published to #{@published_files.size} of #{repositories.size} repositories"
|
|
114
116
|
end
|
|
115
117
|
end
|
|
116
118
|
end
|
|
@@ -3,58 +3,60 @@
|
|
|
3
3
|
require "open3"
|
|
4
4
|
|
|
5
5
|
module Rake
|
|
6
|
-
module
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def initialize(gem_file, repository, otp_provider)
|
|
12
|
-
@gem_file = gem_file
|
|
13
|
-
@repository = repository
|
|
14
|
-
@otp_provider = otp_provider
|
|
15
|
-
end
|
|
6
|
+
module Gem
|
|
7
|
+
module Maintenance
|
|
8
|
+
# Pushes a single gem file to one repository, retrying with a renewed API key on auth failure.
|
|
9
|
+
class GemPush
|
|
10
|
+
Result = Struct.new(:success, :error)
|
|
16
11
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
12
|
+
def initialize(gem_file, repository, otp_provider)
|
|
13
|
+
@gem_file = gem_file
|
|
14
|
+
@repository = repository
|
|
15
|
+
@otp_provider = otp_provider
|
|
16
|
+
end
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
def attempt
|
|
19
|
+
out_err, status = Open3.capture2e(env, command)
|
|
20
|
+
return Result.new(true, nil) if status.success?
|
|
21
|
+
return retry_with_renewed_key if auth_failure?(out_err)
|
|
24
22
|
|
|
25
|
-
|
|
23
|
+
Result.new(false, out_err)
|
|
24
|
+
end
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
cmd = "gem push #{@gem_file} --host #{@repository[:url]}"
|
|
29
|
-
otp = @otp_provider.otp_for(@repository[:name], otp_seed_env_var: @repository[:otp_seed_env_var])
|
|
30
|
-
cmd += " --otp #{otp}" if otp
|
|
31
|
-
cmd
|
|
32
|
-
end
|
|
26
|
+
private
|
|
33
27
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
def command
|
|
29
|
+
cmd = "gem push #{@gem_file} --host #{@repository[:url]}"
|
|
30
|
+
otp = @otp_provider.otp_for(@repository[:name], otp_seed_env_var: @repository[:otp_seed_env_var])
|
|
31
|
+
cmd += " --otp #{otp}" if otp
|
|
32
|
+
cmd
|
|
33
|
+
end
|
|
37
34
|
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
def env
|
|
36
|
+
env_var = @repository[:api_key_env_var]
|
|
37
|
+
return {} unless env_var
|
|
40
38
|
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
key = ENV.fetch(env_var, nil)
|
|
40
|
+
return {} if key.nil? || key.empty?
|
|
43
41
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
{ "GEM_HOST_API_KEY" => key }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def auth_failure?(output)
|
|
46
|
+
output.match?(/unauthorized|api.key|forbidden/i) ||
|
|
47
|
+
output.include?("401") || output.include?("403")
|
|
48
|
+
end
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
def retry_with_renewed_key
|
|
51
|
+
new_key = ApiKeyRenewer.new(otp_provider: @otp_provider).renew(@repository)
|
|
52
|
+
return Result.new(false, "Auth failed and renewal credentials unavailable.") unless new_key
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
_out_err, status = Open3.capture2e(env.merge("GEM_HOST_API_KEY" => new_key), command)
|
|
55
|
+
if status.success?
|
|
56
|
+
Result.new(true, nil)
|
|
57
|
+
else
|
|
58
|
+
Result.new(false, "Push failed after key renewal.")
|
|
59
|
+
end
|
|
58
60
|
end
|
|
59
61
|
end
|
|
60
62
|
end
|