rake-gem-maintenance 0.1.5 → 0.1.6
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/.woodpecker/publish.yml +52 -0
- data/.woodpecker/renew_api_key.yml +47 -0
- data/.woodpecker/verify.yml +38 -0
- data/CLAUDE.md +14 -0
- data/Gemfile.lock +33 -28
- data/README.md +75 -0
- data/lib/rake/gem/maintenance/api_key_renewer.rb +48 -0
- data/lib/rake/gem/maintenance/ci_environment.rb +12 -0
- data/lib/rake/gem/maintenance/credential_store.rb +65 -0
- data/lib/rake/gem/maintenance/gem_publisher.rb +18 -7
- data/lib/rake/gem/maintenance/gem_push.rb +62 -0
- data/lib/rake/gem/maintenance/install_tasks.rb +8 -0
- data/lib/rake/gem/maintenance/otp_provider.rb +45 -0
- data/lib/rake/gem/maintenance/renew_api_key_task.rb +139 -0
- data/lib/rake/gem/maintenance/repos.rb +46 -7
- data/lib/rake/gem/maintenance/ruby_gems_api_key_creator.rb +51 -0
- data/lib/rake/gem/maintenance/upgrade_task.rb +23 -0
- data/lib/rake/gem/maintenance/version.rb +1 -1
- data/lib/rake/gem/maintenance/woodpecker_secret_store.rb +89 -0
- data/lib/rake/gem/maintenance.rb +8 -0
- data/scripts/ci_publish_rubygems.rb +29 -0
- metadata +27 -2
- data/rake-gem-maintenance.gemspec +0 -32
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6dc9985cb348ee5f79a3170eb6ef534b5fe8bc1aad6361be15178f883ba22767
|
|
4
|
+
data.tar.gz: e819c2d3f58a936c59fb8caf620f84e92dc68bf59cee8f592bb852c03d02de62
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 43b4d7dec762bcefd6cd816f4c7a8c05a5e091929937f6e329801a6d4ff211fabdac4631f449d39501807b408f0f1411dc97637519857da49911cafa5bd108f6
|
|
7
|
+
data.tar.gz: 81d529473f1404940f0cc45d1011af5d1930506f972b2485cbe190f184cd53a820cbfaa683201ec0b8ef85e63f2d50714cf337780087a94704071a5ef82d735c
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# publish.yml — build and publish rake-gem-maintenance to rubygems.org
|
|
2
|
+
#
|
|
3
|
+
# Triggers:
|
|
4
|
+
# - push to main (auto-publish on every merge)
|
|
5
|
+
# - manual (ad-hoc publish on demand)
|
|
6
|
+
#
|
|
7
|
+
# Required Woodpecker secrets (org-level cbp-org):
|
|
8
|
+
# rubygems_api_key — rubygems.org API key (set at https://ci.cbp-org.internal)
|
|
9
|
+
# rubygems_otp_seed — base32 TOTP seed for rubygems.org 2FA (already registered)
|
|
10
|
+
# badge_service_token — write token for badge.cbp-org.internal
|
|
11
|
+
|
|
12
|
+
when:
|
|
13
|
+
event: [push, manual]
|
|
14
|
+
branch: main
|
|
15
|
+
|
|
16
|
+
labels:
|
|
17
|
+
platform: linux
|
|
18
|
+
|
|
19
|
+
clone:
|
|
20
|
+
git:
|
|
21
|
+
image: woodpeckerci/plugin-git:2.8.0
|
|
22
|
+
settings:
|
|
23
|
+
skip_verify: true
|
|
24
|
+
|
|
25
|
+
steps:
|
|
26
|
+
- name: publish-gem
|
|
27
|
+
image: ruby:4.0.2-alpine
|
|
28
|
+
backend_options:
|
|
29
|
+
docker:
|
|
30
|
+
network_mode: host
|
|
31
|
+
environment:
|
|
32
|
+
GEM_HOST_API_KEY:
|
|
33
|
+
from_secret: rubygems_api_key
|
|
34
|
+
RUBYGEMS_OTP_SEED:
|
|
35
|
+
from_secret: rubygems_otp_seed
|
|
36
|
+
commands:
|
|
37
|
+
- apk add --no-cache build-base
|
|
38
|
+
- bundle install --jobs 4 --retry 3
|
|
39
|
+
- ruby scripts/ci_publish_rubygems.rb
|
|
40
|
+
|
|
41
|
+
- name: badge
|
|
42
|
+
image: docker-registry.cbp-org.internal/badge-reporter:latest
|
|
43
|
+
when:
|
|
44
|
+
status: [success, failure]
|
|
45
|
+
backend_options:
|
|
46
|
+
docker:
|
|
47
|
+
network_mode: host
|
|
48
|
+
environment:
|
|
49
|
+
BADGE_SECRET:
|
|
50
|
+
from_secret: badge_service_token
|
|
51
|
+
commands:
|
|
52
|
+
- badge report publish
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
when:
|
|
2
|
+
- event: cron
|
|
3
|
+
cron: monthly-renew-api-key
|
|
4
|
+
- event: manual
|
|
5
|
+
|
|
6
|
+
labels:
|
|
7
|
+
platform: linux
|
|
8
|
+
|
|
9
|
+
clone:
|
|
10
|
+
git:
|
|
11
|
+
image: woodpeckerci/plugin-git:2.8.0
|
|
12
|
+
settings:
|
|
13
|
+
skip_verify: true
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- name: renew-api-key
|
|
17
|
+
image: ruby:4.0.2-alpine
|
|
18
|
+
backend_options:
|
|
19
|
+
docker:
|
|
20
|
+
network_mode: host
|
|
21
|
+
environment:
|
|
22
|
+
RUBYGEMS_USERNAME:
|
|
23
|
+
from_secret: rubygems_username
|
|
24
|
+
RUBYGEMS_PASSWORD:
|
|
25
|
+
from_secret: rubygems_password
|
|
26
|
+
RUBYGEMS_OTP_SEED:
|
|
27
|
+
from_secret: rubygems_otp_seed
|
|
28
|
+
WOODPECKER_TOKEN:
|
|
29
|
+
from_secret: woodpecker_api_token
|
|
30
|
+
WOODPECKER_SERVER: "https://ci.cbp-org.internal"
|
|
31
|
+
commands:
|
|
32
|
+
- apk add --no-cache build-base
|
|
33
|
+
- bundle install --jobs 4 --retry 3
|
|
34
|
+
- bundle exec rake upgrade:renew_api_key
|
|
35
|
+
|
|
36
|
+
- name: badge
|
|
37
|
+
image: docker-registry.cbp-org.internal/badge-reporter:latest
|
|
38
|
+
when:
|
|
39
|
+
status: [success, failure]
|
|
40
|
+
backend_options:
|
|
41
|
+
docker:
|
|
42
|
+
network_mode: host
|
|
43
|
+
environment:
|
|
44
|
+
BADGE_SECRET:
|
|
45
|
+
from_secret: badge_service_token
|
|
46
|
+
commands:
|
|
47
|
+
- badge report renew-api-key
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
when:
|
|
2
|
+
event: [push, pull_request, manual]
|
|
3
|
+
|
|
4
|
+
labels:
|
|
5
|
+
platform: linux
|
|
6
|
+
|
|
7
|
+
clone:
|
|
8
|
+
git:
|
|
9
|
+
image: woodpeckerci/plugin-git:2.8.0
|
|
10
|
+
settings:
|
|
11
|
+
skip_verify: true
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- name: verify
|
|
15
|
+
image: ruby:4.0.2-alpine
|
|
16
|
+
backend_options:
|
|
17
|
+
docker:
|
|
18
|
+
network_mode: host
|
|
19
|
+
environment:
|
|
20
|
+
CUCUMBER_PUBLISH_QUIET: "true"
|
|
21
|
+
commands:
|
|
22
|
+
- apk add --no-cache build-base
|
|
23
|
+
- mkdir -p /root/.local/share/ruby-advisory-db/gems
|
|
24
|
+
- bundle install --jobs 4 --retry 3
|
|
25
|
+
- bundle exec rake verify
|
|
26
|
+
|
|
27
|
+
- name: badge
|
|
28
|
+
image: docker-registry.cbp-org.internal/badge-reporter:latest
|
|
29
|
+
when:
|
|
30
|
+
status: [success, failure]
|
|
31
|
+
backend_options:
|
|
32
|
+
docker:
|
|
33
|
+
network_mode: host
|
|
34
|
+
environment:
|
|
35
|
+
BADGE_SECRET:
|
|
36
|
+
from_secret: badge_service_token
|
|
37
|
+
commands:
|
|
38
|
+
- badge report verify
|
data/CLAUDE.md
CHANGED
|
@@ -54,6 +54,20 @@ Rake::GemMaintenance::UpgradeTask.new do |t|
|
|
|
54
54
|
end
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
## Repository & CI Workflow
|
|
58
|
+
|
|
59
|
+
**GitHub** (`github.com/cbroult/rake-gem-maintenance`) is the canonical source.
|
|
60
|
+
**Forgejo** (`git.cbp-org.internal/forgejo-admin/rake-gem-maintenance`) is a read-only pull mirror.
|
|
61
|
+
|
|
62
|
+
- All pull requests go on **GitHub only**. Never open PRs on Forgejo.
|
|
63
|
+
- Forgejo mirrors GitHub automatically every 10 minutes — never push to the `forgejo` remote manually.
|
|
64
|
+
- **Woodpecker CI** (`ci.cbp-org.internal`) watches Forgejo and runs verify/publish/renew pipelines.
|
|
65
|
+
- Local clone only needs the `origin` (GitHub) remote. Remove `forgejo` if present: `git remote remove forgejo`.
|
|
66
|
+
|
|
67
|
+
## Code Style
|
|
68
|
+
|
|
69
|
+
- Never add `# rubocop:disable` comments without explicit user permission.
|
|
70
|
+
|
|
57
71
|
## Commit Conventions
|
|
58
72
|
|
|
59
73
|
- Use conventional commit format: `type(scope): subject`
|
data/Gemfile.lock
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
rake-gem-maintenance (0.1.
|
|
4
|
+
rake-gem-maintenance (0.1.6)
|
|
5
5
|
bundler-audit
|
|
6
6
|
gem-release
|
|
7
7
|
rake
|
|
8
|
+
rotp
|
|
8
9
|
|
|
9
10
|
GEM
|
|
10
11
|
remote: https://rubygems.org/
|
|
11
12
|
specs:
|
|
12
|
-
aruba (2.
|
|
13
|
+
aruba (2.4.0)
|
|
13
14
|
bundler (>= 1.17)
|
|
14
15
|
contracts (>= 0.16.0, < 0.18.0)
|
|
15
|
-
cucumber (>= 8.0, <
|
|
16
|
+
cucumber (>= 8.0, < 12.0)
|
|
17
|
+
irb (~> 1.16)
|
|
16
18
|
rspec-expectations (>= 3.4, < 5.0)
|
|
17
19
|
thor (~> 1.0)
|
|
18
20
|
ast (2.4.3)
|
|
@@ -24,18 +26,18 @@ GEM
|
|
|
24
26
|
thor (~> 1.0)
|
|
25
27
|
coderay (1.1.3)
|
|
26
28
|
contracts (0.17.3)
|
|
27
|
-
cucumber (
|
|
29
|
+
cucumber (11.0.0)
|
|
28
30
|
base64 (~> 0.2)
|
|
29
31
|
builder (~> 3.2)
|
|
30
32
|
cucumber-ci-environment (> 9, < 12)
|
|
31
|
-
cucumber-core (
|
|
33
|
+
cucumber-core (>= 16.2.0, < 17)
|
|
32
34
|
cucumber-cucumber-expressions (> 17, < 20)
|
|
33
|
-
cucumber-html-formatter (> 21, <
|
|
35
|
+
cucumber-html-formatter (> 21, < 24)
|
|
34
36
|
diff-lcs (~> 1.5)
|
|
35
37
|
logger (~> 1.6)
|
|
36
38
|
mini_mime (~> 1.1)
|
|
37
39
|
multi_test (~> 1.1)
|
|
38
|
-
sys-uname (~> 1.
|
|
40
|
+
sys-uname (~> 1.5)
|
|
39
41
|
cucumber-ci-environment (11.0.0)
|
|
40
42
|
cucumber-core (16.2.0)
|
|
41
43
|
cucumber-gherkin (> 36, < 40)
|
|
@@ -43,16 +45,15 @@ GEM
|
|
|
43
45
|
cucumber-tag-expressions (> 6, < 9)
|
|
44
46
|
cucumber-cucumber-expressions (19.0.0)
|
|
45
47
|
bigdecimal
|
|
46
|
-
cucumber-gherkin (39.
|
|
48
|
+
cucumber-gherkin (39.1.0)
|
|
47
49
|
cucumber-messages (>= 31, < 33)
|
|
48
|
-
cucumber-html-formatter (
|
|
50
|
+
cucumber-html-formatter (23.1.0)
|
|
49
51
|
cucumber-messages (> 23, < 33)
|
|
50
52
|
cucumber-messages (32.3.1)
|
|
51
53
|
cucumber-tag-expressions (8.1.0)
|
|
52
54
|
date (3.5.1)
|
|
53
55
|
diff-lcs (1.6.2)
|
|
54
56
|
erb (6.0.4)
|
|
55
|
-
ffi (1.17.4-x64-mingw-ucrt)
|
|
56
57
|
ffi (1.17.4-x86_64-linux-gnu)
|
|
57
58
|
formatador (1.2.3)
|
|
58
59
|
reline
|
|
@@ -83,7 +84,12 @@ GEM
|
|
|
83
84
|
guard (~> 2.0)
|
|
84
85
|
rubocop (< 2.0)
|
|
85
86
|
io-console (0.8.2)
|
|
86
|
-
|
|
87
|
+
irb (1.18.0)
|
|
88
|
+
pp (>= 0.6.0)
|
|
89
|
+
prism (>= 1.3.0)
|
|
90
|
+
rdoc (>= 4.0.0)
|
|
91
|
+
reline (>= 0.4.2)
|
|
92
|
+
json (2.19.5)
|
|
87
93
|
language_server-protocol (3.17.0.5)
|
|
88
94
|
lint_roller (1.1.0)
|
|
89
95
|
listen (3.10.0)
|
|
@@ -105,6 +111,9 @@ GEM
|
|
|
105
111
|
parser (3.3.11.1)
|
|
106
112
|
ast (~> 2.4.1)
|
|
107
113
|
racc
|
|
114
|
+
pp (0.6.3)
|
|
115
|
+
prettyprint
|
|
116
|
+
prettyprint (0.2.0)
|
|
108
117
|
prism (1.9.0)
|
|
109
118
|
pry (0.16.0)
|
|
110
119
|
coderay (~> 1.1)
|
|
@@ -126,6 +135,7 @@ GEM
|
|
|
126
135
|
regexp_parser (2.12.0)
|
|
127
136
|
reline (0.6.3)
|
|
128
137
|
io-console (~> 0.5)
|
|
138
|
+
rotp (6.3.0)
|
|
129
139
|
rspec (3.13.2)
|
|
130
140
|
rspec-core (~> 3.13.0)
|
|
131
141
|
rspec-expectations (~> 3.13.0)
|
|
@@ -139,7 +149,7 @@ GEM
|
|
|
139
149
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
140
150
|
rspec-support (~> 3.13.0)
|
|
141
151
|
rspec-support (3.13.7)
|
|
142
|
-
rubocop (1.86.
|
|
152
|
+
rubocop (1.86.2)
|
|
143
153
|
json (~> 2.3)
|
|
144
154
|
language_server-protocol (~> 3.17.0.2)
|
|
145
155
|
lint_roller (~> 1.1.0)
|
|
@@ -165,19 +175,13 @@ GEM
|
|
|
165
175
|
sys-uname (1.5.1)
|
|
166
176
|
ffi (~> 1.1)
|
|
167
177
|
memoist3 (~> 1.0.0)
|
|
168
|
-
sys-uname (1.5.1-universal-mingw32)
|
|
169
|
-
ffi (~> 1.1)
|
|
170
|
-
memoist3 (~> 1.0.0)
|
|
171
|
-
win32ole
|
|
172
178
|
thor (1.5.0)
|
|
173
179
|
tsort (0.2.0)
|
|
174
180
|
unicode-display_width (3.2.0)
|
|
175
181
|
unicode-emoji (~> 4.1)
|
|
176
182
|
unicode-emoji (4.2.0)
|
|
177
|
-
win32ole (1.9.3)
|
|
178
183
|
|
|
179
184
|
PLATFORMS
|
|
180
|
-
x64-mingw-ucrt
|
|
181
185
|
x86_64-linux
|
|
182
186
|
|
|
183
187
|
DEPENDENCIES
|
|
@@ -197,7 +201,7 @@ DEPENDENCIES
|
|
|
197
201
|
rubocop-rspec
|
|
198
202
|
|
|
199
203
|
CHECKSUMS
|
|
200
|
-
aruba (2.
|
|
204
|
+
aruba (2.4.0) sha256=92bb696efb06b1aa1dc3ff8b13ca71cd727e59d27572f9264216b9722b558299
|
|
201
205
|
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
|
|
202
206
|
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
|
|
203
207
|
bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
|
|
@@ -205,18 +209,17 @@ CHECKSUMS
|
|
|
205
209
|
bundler-audit (0.9.3) sha256=81c8766c71e47d0d28a0f98c7eed028539f21a6ea3cd8f685eb6f42333c9b4e9
|
|
206
210
|
coderay (1.1.3) sha256=dc530018a4684512f8f38143cd2a096c9f02a1fc2459edcfe534787a7fc77d4b
|
|
207
211
|
contracts (0.17.3) sha256=e72e626413ea47099becb7b5683beb1c2ea902c69f5bad55c9258fe2b48314d7
|
|
208
|
-
cucumber (
|
|
212
|
+
cucumber (11.0.0) sha256=14bb964cc172999e9010fece2fad9f104044b055b3199230091894637a2a784c
|
|
209
213
|
cucumber-ci-environment (11.0.0) sha256=0df79a9e1d0b015b3d9def680f989200d96fef206f4d19ccf86a338c4f71d1e2
|
|
210
214
|
cucumber-core (16.2.0) sha256=592b58a95cf42feef8e5a349f68e363784ba3b6568ffbcf6776e38e136cf970b
|
|
211
215
|
cucumber-cucumber-expressions (19.0.0) sha256=33208ff204732ac9bed42b46993a0a243054f71ece08579d57e53df6a1c9d93a
|
|
212
|
-
cucumber-gherkin (39.
|
|
213
|
-
cucumber-html-formatter (
|
|
216
|
+
cucumber-gherkin (39.1.0) sha256=aed12a0c955d8563d80a012633c1a72075525f4d64d4cc983001df2181b379ed
|
|
217
|
+
cucumber-html-formatter (23.1.0) sha256=7789b4a792c876394b9604aeb66aa5cf4c61514473b7e712c76d5eaedcdd8cdf
|
|
214
218
|
cucumber-messages (32.3.1) sha256=ddc88e4c1cf7afb96c06005b92a4a6f221a2fa435a8b4ca04677d215fd82771c
|
|
215
219
|
cucumber-tag-expressions (8.1.0) sha256=9bd8c4b6654f8e5bf2a9c99329b6f32136a75e50cd39d4cfb3927d0fa9f52e21
|
|
216
220
|
date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
|
|
217
221
|
diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
|
|
218
222
|
erb (6.0.4) sha256=38e3803694be357fe2bfe312487c74beaf9fb4e5beb3e22498952fe1645b95d9
|
|
219
|
-
ffi (1.17.4-x64-mingw-ucrt) sha256=f6ff9618cfccc494138bddade27aa06c74c6c7bc367a1ea1103d80c2fcb9ed35
|
|
220
223
|
ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d
|
|
221
224
|
formatador (1.2.3) sha256=19fa898133c2c26cdbb5d09f6998c1e137ad9427a046663e55adfe18b950d894
|
|
222
225
|
gem-release (2.2.4) sha256=2f11124c1580c811507c3b47e875e420cf3ed792a98105b49df11971e6e94db3
|
|
@@ -227,7 +230,8 @@ CHECKSUMS
|
|
|
227
230
|
guard-rspec (4.7.3) sha256=a47ba03cbd1e3c71e6ae8645cea97e203098a248aede507461a43e906e2f75ca
|
|
228
231
|
guard-rubocop (1.5.0) sha256=3041d796dcb5ee31e352de74732250826f5f235b4ff48df9dbf424a6dc736251
|
|
229
232
|
io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
|
|
230
|
-
|
|
233
|
+
irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3
|
|
234
|
+
json (2.19.5) sha256=218a18553e4801d579ca7e0f5bc72bafd776d7397238a1fb4e74db5b0a812c59
|
|
231
235
|
language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
|
|
232
236
|
lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
|
|
233
237
|
listen (3.10.0) sha256=c6e182db62143aeccc2e1960033bebe7445309c7272061979bb098d03760c9d2
|
|
@@ -242,24 +246,27 @@ CHECKSUMS
|
|
|
242
246
|
ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912
|
|
243
247
|
parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356
|
|
244
248
|
parser (3.3.11.1) sha256=d17ace7aabe3e72c3cc94043714be27cc6f852f104d81aa284c2281aecc65d54
|
|
249
|
+
pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6
|
|
250
|
+
prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
|
|
245
251
|
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
|
|
246
252
|
pry (0.16.0) sha256=d76c69065698ed1f85e717bd33d7942c38a50868f6b0673c636192b3d1b6054e
|
|
247
253
|
psych (5.3.1) sha256=eb7a57cef10c9d70173ff74e739d843ac3b2c019a003de48447b2963d81b1974
|
|
248
254
|
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
|
|
249
255
|
rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
|
|
250
256
|
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
|
|
251
|
-
rake-gem-maintenance (0.1.
|
|
257
|
+
rake-gem-maintenance (0.1.6)
|
|
252
258
|
rb-fsevent (0.11.2) sha256=43900b972e7301d6570f64b850a5aa67833ee7d87b458ee92805d56b7318aefe
|
|
253
259
|
rb-inotify (0.11.1) sha256=a0a700441239b0ff18eb65e3866236cd78613d6b9f78fea1f9ac47a85e47be6e
|
|
254
260
|
rdoc (7.2.0) sha256=8650f76cd4009c3b54955eb5d7e3a075c60a57276766ebf36f9085e8c9f23192
|
|
255
261
|
regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
|
|
256
262
|
reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
|
|
263
|
+
rotp (6.3.0) sha256=75d40087e65ed0d8022c33055a6306c1c400d1c12261932533b5d6cbcd868854
|
|
257
264
|
rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
|
|
258
265
|
rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
|
|
259
266
|
rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
|
|
260
267
|
rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47
|
|
261
268
|
rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c
|
|
262
|
-
rubocop (1.86.
|
|
269
|
+
rubocop (1.86.2) sha256=bb2e97f635eda42c448f2588f4a6ff78f221b8bdfdf65b1e9b07fbd57521b45d
|
|
263
270
|
rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035
|
|
264
271
|
rubocop-rake (0.7.1) sha256=3797f2b6810c3e9df7376c26d5f44f3475eda59eb1adc38e6f62ecf027cbae4d
|
|
265
272
|
rubocop-rspec (3.9.0) sha256=8fa70a3619408237d789aeecfb9beef40576acc855173e60939d63332fdb55e2
|
|
@@ -267,12 +274,10 @@ CHECKSUMS
|
|
|
267
274
|
shellany (0.0.1) sha256=0e127a9132698766d7e752e82cdac8250b6adbd09e6c0a7fbbb6f61964fedee7
|
|
268
275
|
stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1
|
|
269
276
|
sys-uname (1.5.1) sha256=784d7e6491b0393c25cbbe5ac38324ac7be9fda083a6094832648af669386d7b
|
|
270
|
-
sys-uname (1.5.1-universal-mingw32) sha256=aceb618e3276da5eae0ce368e9f6fae8c1f3e9ef23a0595cb88db7b6ecd45f62
|
|
271
277
|
thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73
|
|
272
278
|
tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
|
|
273
279
|
unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
|
|
274
280
|
unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
|
|
275
|
-
win32ole (1.9.3) sha256=01f43dc5dc13806e6e58204f538b4a28f3d85968ea89074abc9a3cd118e94d96
|
|
276
281
|
|
|
277
282
|
BUNDLED WITH
|
|
278
283
|
4.0.4
|
data/README.md
CHANGED
|
@@ -46,6 +46,81 @@ Rake::GemMaintenance::VersionBumpTask.new do |t|
|
|
|
46
46
|
end
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
## Automated Publishing to rubygems.org
|
|
50
|
+
|
|
51
|
+
Set two environment variables and `gem push` runs fully unattended — including TOTP 2FA code
|
|
52
|
+
generation if your rubygems.org account has MFA enabled.
|
|
53
|
+
|
|
54
|
+
| Env var | Purpose |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `GEM_HOST_API_KEY` | rubygems.org API key (scoped to push) |
|
|
57
|
+
| `RUBYGEMS_OTP_SEED` | Base32 TOTP seed — auto-generates the 2FA code; omit if MFA is disabled |
|
|
58
|
+
|
|
59
|
+
### Quick setup
|
|
60
|
+
|
|
61
|
+
`require "rake/gem_maintenance/install_tasks"` pre-configures both env var names automatically —
|
|
62
|
+
no extra Ruby needed. See [features/install_tasks.feature](features/install_tasks.feature) for
|
|
63
|
+
the full workflow.
|
|
64
|
+
|
|
65
|
+
### Custom env var names
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
require "rake/gem/maintenance"
|
|
69
|
+
|
|
70
|
+
Rake::GemMaintenance::Repos.rubygems_api_key_env_var = "MY_RUBYGEMS_KEY"
|
|
71
|
+
Rake::GemMaintenance::Repos.rubygems_otp_seed_env_var = "MY_OTP_SEED"
|
|
72
|
+
|
|
73
|
+
Rake::GemMaintenance::UpgradeTask.new
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
See [features/upgrade_task/repos_configuration.feature](features/upgrade_task/repos_configuration.feature)
|
|
77
|
+
for all configuration options including geminabox and dual publishing.
|
|
78
|
+
|
|
79
|
+
### Local credential store
|
|
80
|
+
|
|
81
|
+
After the first successful `upgrade:renew_api_key` run, the API key and OTP seed are saved to:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
~/.config/rake-gem-maintenance/credentials.yml # Linux / Mac (respects $XDG_CONFIG_HOME)
|
|
85
|
+
%APPDATA%\rake-gem-maintenance\credentials.yml # Windows
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The file is created with `0600` permissions (owner-read-only on Unix). It stores `username`,
|
|
89
|
+
`gem_host_api_key`, and `rubygems_otp_seed` — **never the password**. Any project using
|
|
90
|
+
`require "rake/gem_maintenance/install_tasks"` automatically loads the key and OTP seed from
|
|
91
|
+
this file at startup, so `gem push` works without any manual env-var setup.
|
|
92
|
+
|
|
93
|
+
See [features/upgrade_task/credential_store.feature](features/upgrade_task/credential_store.feature)
|
|
94
|
+
for the full behaviour specification.
|
|
95
|
+
|
|
96
|
+
### API key renewal
|
|
97
|
+
|
|
98
|
+
API keys can be rotated in two ways:
|
|
99
|
+
|
|
100
|
+
**Automatic** — when `gem push` returns a 401/403, the publisher transparently obtains a new
|
|
101
|
+
key using `RUBYGEMS_USERNAME` + `RUBYGEMS_PASSWORD` (+ TOTP from `RUBYGEMS_OTP_SEED` if MFA is
|
|
102
|
+
enabled), then retries the push once. No intervention needed.
|
|
103
|
+
|
|
104
|
+
**On-demand** — run the task explicitly to rotate ahead of expiry:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
rake upgrade:renew_api_key
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Locally this prompts for credentials interactively. In CI, supply all three env vars for
|
|
111
|
+
unattended operation:
|
|
112
|
+
|
|
113
|
+
| Env var | Purpose |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `RUBYGEMS_USERNAME` | rubygems.org account username or email |
|
|
116
|
+
| `RUBYGEMS_PASSWORD` | rubygems.org account password |
|
|
117
|
+
| `RUBYGEMS_OTP_SEED` | Same TOTP seed as above — reused here to authenticate the key-creation request |
|
|
118
|
+
|
|
119
|
+
The new key is written back to the `GEM_HOST_API_KEY` CI secret automatically (requires
|
|
120
|
+
`WOODPECKER_TOKEN` and `WOODPECKER_SERVER` when running under Woodpecker CI).
|
|
121
|
+
|
|
122
|
+
See [features/upgrade_task/renew_api_key.feature](features/upgrade_task/renew_api_key.feature).
|
|
123
|
+
|
|
49
124
|
## License
|
|
50
125
|
|
|
51
126
|
The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rake
|
|
4
|
+
module GemMaintenance
|
|
5
|
+
# Renews a rubygems.org API key using credentials from env vars and
|
|
6
|
+
# persists the new key to Woodpecker CI when server details are available.
|
|
7
|
+
class ApiKeyRenewer
|
|
8
|
+
def initialize(otp_provider:,
|
|
9
|
+
username_env_var: "RUBYGEMS_USERNAME",
|
|
10
|
+
password_env_var: "RUBYGEMS_PASSWORD")
|
|
11
|
+
@otp_provider = otp_provider
|
|
12
|
+
@username_env_var = username_env_var
|
|
13
|
+
@password_env_var = password_env_var
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def renew(repository)
|
|
17
|
+
username = env_credential(@username_env_var)
|
|
18
|
+
password = env_credential(@password_env_var)
|
|
19
|
+
return nil if username.nil? || password.nil?
|
|
20
|
+
|
|
21
|
+
otp = @otp_provider.otp_for(repository[:name], otp_seed_env_var: repository[:otp_seed_env_var])
|
|
22
|
+
new_key = RubyGemsApiKeyCreator.new(host: repository.fetch(:url, "https://rubygems.org"))
|
|
23
|
+
.create(username, password, otp: otp)
|
|
24
|
+
persist_to_woodpecker(new_key)
|
|
25
|
+
new_key
|
|
26
|
+
rescue StandardError
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def env_credential(var)
|
|
33
|
+
value = ENV.fetch(var, nil)
|
|
34
|
+
value&.empty? ? nil : value
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def persist_to_woodpecker(new_key)
|
|
38
|
+
server = ENV.fetch("WOODPECKER_SERVER", nil)
|
|
39
|
+
token = ENV.fetch("WOODPECKER_TOKEN", nil)
|
|
40
|
+
return unless server && token
|
|
41
|
+
|
|
42
|
+
org = ENV.fetch("WOODPECKER_ORG", "cbp-org")
|
|
43
|
+
WoodpeckerSecretStore.new(server: server, org: org, token: token)
|
|
44
|
+
.store("rubygems_api_key", new_key)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "rubygems"
|
|
6
|
+
|
|
7
|
+
module Rake
|
|
8
|
+
module GemMaintenance
|
|
9
|
+
# Persists rubygems.org credentials (username, API key, OTP seed) to a local config file.
|
|
10
|
+
class CredentialStore
|
|
11
|
+
def self.default_path
|
|
12
|
+
base = if Gem.win_platform?
|
|
13
|
+
ENV.fetch("APPDATA", File.expand_path("~"))
|
|
14
|
+
else
|
|
15
|
+
ENV.fetch("XDG_CONFIG_HOME", File.join(Dir.home, ".config"))
|
|
16
|
+
end
|
|
17
|
+
File.join(base, "rake-gem-maintenance", "credentials.yml")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(path: self.class.default_path)
|
|
21
|
+
@path = path
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
attr_reader :path
|
|
25
|
+
|
|
26
|
+
def read
|
|
27
|
+
return {} unless File.exist?(@path)
|
|
28
|
+
|
|
29
|
+
YAML.safe_load_file(@path, symbolize_names: true) || {}
|
|
30
|
+
rescue StandardError
|
|
31
|
+
{}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def write(credentials)
|
|
35
|
+
FileUtils.mkdir_p(File.dirname(@path))
|
|
36
|
+
File.write(@path, credentials.transform_keys(&:to_s).to_yaml)
|
|
37
|
+
File.chmod(0o600, @path) unless Gem.win_platform?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def apply_to_env(username_env_var:, api_key_env_var:)
|
|
41
|
+
creds = read
|
|
42
|
+
set_env_if_absent(username_env_var, creds[:username])
|
|
43
|
+
set_env_if_absent("RUBYGEMS_OTP_SEED", creds[:rubygems_otp_seed])
|
|
44
|
+
set_env_if_absent(api_key_env_var, creds[:gem_host_api_key])
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def update(username:, api_key:, api_key_env_var:)
|
|
48
|
+
otp_seed = ENV.fetch("RUBYGEMS_OTP_SEED", nil)
|
|
49
|
+
updated = read.merge(username: username, gem_host_api_key: api_key)
|
|
50
|
+
updated[:rubygems_otp_seed] = otp_seed if otp_seed && !otp_seed.empty?
|
|
51
|
+
write(updated)
|
|
52
|
+
ENV[api_key_env_var] = api_key
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def set_env_if_absent(env_var, value)
|
|
58
|
+
return unless value && !value.empty?
|
|
59
|
+
return if (existing = ENV.fetch(env_var, nil)) && !existing.empty?
|
|
60
|
+
|
|
61
|
+
ENV[env_var] = value
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -9,8 +9,12 @@ module Rake
|
|
|
9
9
|
class GemPublisher
|
|
10
10
|
attr_reader :repositories, :warnings, :failed_repositories, :successful_repos
|
|
11
11
|
|
|
12
|
-
def initialize(repositories = default_repositories
|
|
12
|
+
def initialize(repositories = default_repositories,
|
|
13
|
+
otp_provider: OtpProvider.new,
|
|
14
|
+
ci_environment: CIEnvironment)
|
|
13
15
|
@repositories = repositories
|
|
16
|
+
@otp_provider = otp_provider
|
|
17
|
+
@ci_environment = ci_environment
|
|
14
18
|
@warnings = []
|
|
15
19
|
@failed_pushes = []
|
|
16
20
|
@failed_repositories = []
|
|
@@ -76,16 +80,23 @@ module Rake
|
|
|
76
80
|
end
|
|
77
81
|
|
|
78
82
|
def push(gem_file, repository:)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
@successful_repos << repository[:name]
|
|
84
|
-
end
|
|
83
|
+
result = GemPush.new(gem_file, repository, @otp_provider).attempt
|
|
84
|
+
return record_success(gem_file, repository) if result.success
|
|
85
|
+
|
|
86
|
+
record_push_failure(repository, result.error)
|
|
85
87
|
rescue StandardError => e
|
|
86
88
|
@failed_pushes << { repository: repository[:name], error: e.message }
|
|
87
89
|
end
|
|
88
90
|
|
|
91
|
+
def record_success(gem_file, repository)
|
|
92
|
+
@published_files << gem_file
|
|
93
|
+
@successful_repos << repository[:name]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def record_push_failure(repository, message)
|
|
97
|
+
@failed_pushes << { repository: repository[:name], error: message.strip }
|
|
98
|
+
end
|
|
99
|
+
|
|
89
100
|
def version_exists_on_all_repos?(gem_name, version)
|
|
90
101
|
repositories.all? do |repo|
|
|
91
102
|
versions_on_repository(gem_name, repo).include?(version.to_s)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module Rake
|
|
6
|
+
module GemMaintenance
|
|
7
|
+
# Pushes a single gem file to one repository, retrying with a renewed API key on auth failure.
|
|
8
|
+
class GemPush
|
|
9
|
+
Result = Struct.new(:success, :error)
|
|
10
|
+
|
|
11
|
+
def initialize(gem_file, repository, otp_provider)
|
|
12
|
+
@gem_file = gem_file
|
|
13
|
+
@repository = repository
|
|
14
|
+
@otp_provider = otp_provider
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def attempt
|
|
18
|
+
out_err, status = Open3.capture2e(env, command)
|
|
19
|
+
return Result.new(true, nil) if status.success?
|
|
20
|
+
return retry_with_renewed_key if auth_failure?(out_err)
|
|
21
|
+
|
|
22
|
+
Result.new(false, out_err)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def command
|
|
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
|
|
33
|
+
|
|
34
|
+
def env
|
|
35
|
+
env_var = @repository[:api_key_env_var]
|
|
36
|
+
return {} unless env_var
|
|
37
|
+
|
|
38
|
+
key = ENV.fetch(env_var, nil)
|
|
39
|
+
return {} if key.nil? || key.empty?
|
|
40
|
+
|
|
41
|
+
{ "GEM_HOST_API_KEY" => key }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def auth_failure?(output)
|
|
45
|
+
output.match?(/unauthorized|api.key|forbidden/i) ||
|
|
46
|
+
output.include?("401") || output.include?("403")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def retry_with_renewed_key
|
|
50
|
+
new_key = ApiKeyRenewer.new(otp_provider: @otp_provider).renew(@repository)
|
|
51
|
+
return Result.new(false, "Auth failed and renewal credentials unavailable.") unless new_key
|
|
52
|
+
|
|
53
|
+
_out_err, status = Open3.capture2e(env.merge("GEM_HOST_API_KEY" => new_key), command)
|
|
54
|
+
if status.success?
|
|
55
|
+
Result.new(true, nil)
|
|
56
|
+
else
|
|
57
|
+
Result.new(false, "Push failed after key renewal.")
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -2,5 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../maintenance"
|
|
4
4
|
|
|
5
|
+
Rake::GemMaintenance::Repos.rubygems_api_key_env_var = "GEM_HOST_API_KEY"
|
|
6
|
+
Rake::GemMaintenance::Repos.rubygems_otp_seed_env_var = "RUBYGEMS_OTP_SEED"
|
|
7
|
+
|
|
5
8
|
Rake::GemMaintenance::UpgradeTask.new
|
|
6
9
|
Rake::GemMaintenance::VersionBumpTask.new
|
|
10
|
+
|
|
11
|
+
Rake::GemMaintenance::CredentialStore.new.apply_to_env(
|
|
12
|
+
username_env_var: "RUBYGEMS_USERNAME",
|
|
13
|
+
api_key_env_var: "GEM_HOST_API_KEY"
|
|
14
|
+
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rake
|
|
4
|
+
module GemMaintenance
|
|
5
|
+
# Resolves a 2FA OTP code for gem push, either from the environment or interactively.
|
|
6
|
+
# Resolution order for otp_for:
|
|
7
|
+
# 1. RUBYGEMS_OTP env var set → use raw code (works in CI and locally)
|
|
8
|
+
# 2. otp_seed_env_var provided and env var set → generate TOTP code (works in CI and locally)
|
|
9
|
+
# 3. CI environment → nil (gate only interactive prompt)
|
|
10
|
+
# 4. Interactive prompt
|
|
11
|
+
class OtpProvider
|
|
12
|
+
def initialize(ci_environment: CIEnvironment, input: $stdin)
|
|
13
|
+
@ci_environment = ci_environment
|
|
14
|
+
@input = input
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def otp_for(repository_name, otp_seed_env_var: nil)
|
|
18
|
+
env_otp = ENV.fetch("RUBYGEMS_OTP", nil)
|
|
19
|
+
return env_otp if env_otp && !env_otp.empty?
|
|
20
|
+
|
|
21
|
+
if otp_seed_env_var
|
|
22
|
+
seed = ENV.fetch(otp_seed_env_var, nil)
|
|
23
|
+
return generate_totp(seed) if seed && !seed.empty?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
return nil if @ci_environment.ci?
|
|
27
|
+
|
|
28
|
+
prompt_for_otp(repository_name)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def generate_totp(seed)
|
|
34
|
+
require "rotp"
|
|
35
|
+
::ROTP::TOTP.new(seed).now
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def prompt_for_otp(repository_name)
|
|
39
|
+
print "Enter OTP for #{repository_name} (blank to skip): "
|
|
40
|
+
value = @input.gets&.chomp
|
|
41
|
+
value&.empty? ? nil : value
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rake"
|
|
4
|
+
require "rake/tasklib"
|
|
5
|
+
require_relative "credential_store"
|
|
6
|
+
|
|
7
|
+
module Rake
|
|
8
|
+
module GemMaintenance
|
|
9
|
+
# Generates a new rubygems.org API key via the rubygems.org API and stores
|
|
10
|
+
# it in a Woodpecker CI org-level secret. Intended for local developer use only.
|
|
11
|
+
#
|
|
12
|
+
# Creates: <namespace>:renew_api_key
|
|
13
|
+
#
|
|
14
|
+
# Reads WOODPECKER_SERVER and WOODPECKER_TOKEN (or ~/.config/woodpecker/token)
|
|
15
|
+
# from the environment.
|
|
16
|
+
class RenewApiKeyTask < ::Rake::TaskLib
|
|
17
|
+
attr_accessor :namespace_name, :host, :api_key_env_var, :ci_environment,
|
|
18
|
+
:woodpecker_server, :woodpecker_org, :woodpecker_secret_name,
|
|
19
|
+
:username_env_var, :password_env_var, :credential_store
|
|
20
|
+
|
|
21
|
+
def initialize(namespace_name = :upgrade)
|
|
22
|
+
super()
|
|
23
|
+
apply_defaults(namespace_name)
|
|
24
|
+
define_tasks
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def apply_defaults(namespace_name)
|
|
30
|
+
@namespace_name = namespace_name
|
|
31
|
+
@host = "https://rubygems.org"
|
|
32
|
+
@api_key_env_var = "GEM_HOST_API_KEY"
|
|
33
|
+
@ci_environment = CIEnvironment
|
|
34
|
+
@woodpecker_server = ENV.fetch("WOODPECKER_SERVER", nil)
|
|
35
|
+
@woodpecker_org = ENV.fetch("WOODPECKER_ORG", "cbp-org")
|
|
36
|
+
@woodpecker_secret_name = "rubygems_api_key"
|
|
37
|
+
@username_env_var = "RUBYGEMS_USERNAME"
|
|
38
|
+
@password_env_var = "RUBYGEMS_PASSWORD"
|
|
39
|
+
@credential_store = CredentialStore.new
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def define_tasks
|
|
43
|
+
task_instance = self
|
|
44
|
+
namespace namespace_name do
|
|
45
|
+
desc "Generate a new rubygems.org API key and store it in Woodpecker CI"
|
|
46
|
+
task(:renew_api_key) { task_instance.send(:run_renewal) }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def run_renewal
|
|
51
|
+
credential_store.apply_to_env(username_env_var: username_env_var, api_key_env_var: api_key_env_var)
|
|
52
|
+
abort_if_ci
|
|
53
|
+
username, password = prompt_credentials
|
|
54
|
+
prompt_otp_seed_if_missing
|
|
55
|
+
api_key = generate_api_key(username, password)
|
|
56
|
+
save_and_distribute(username, api_key)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def generate_api_key(username, password)
|
|
60
|
+
otp = OtpProvider.new.otp_for("rubygems")
|
|
61
|
+
RubyGemsApiKeyCreator.new(host: host).create(username, password, otp: otp)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def save_and_distribute(username, api_key)
|
|
65
|
+
puts "\n[INFO] New API key generated."
|
|
66
|
+
credential_store.update(username: username, api_key: api_key, api_key_env_var: api_key_env_var)
|
|
67
|
+
store_in_woodpecker(api_key)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def abort_if_ci
|
|
71
|
+
return unless ci_environment.ci?
|
|
72
|
+
|
|
73
|
+
missing = [username_env_var, password_env_var].select { |v| env_credential(v).nil? }
|
|
74
|
+
return if missing.empty?
|
|
75
|
+
|
|
76
|
+
abort "[ERROR] Set #{missing.join(' and ')} CI secrets to run renewal unattended."
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def prompt_otp_seed_if_missing
|
|
80
|
+
return if (seed = ENV.fetch("RUBYGEMS_OTP_SEED", nil)) && !seed.empty?
|
|
81
|
+
|
|
82
|
+
print "rubygems.org OTP seed (TOTP secret, not a code): "
|
|
83
|
+
seed = $stdin.gets&.chomp
|
|
84
|
+
ENV["RUBYGEMS_OTP_SEED"] = seed if seed && !seed.empty?
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def prompt_credentials
|
|
88
|
+
username = env_credential(username_env_var) || prompt_username
|
|
89
|
+
password = env_credential(password_env_var) || read_password("rubygems.org password: ")
|
|
90
|
+
[username, password]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def env_credential(env_var)
|
|
94
|
+
value = ENV.fetch(env_var, nil)
|
|
95
|
+
value&.empty? ? nil : value
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def prompt_username
|
|
99
|
+
print "rubygems.org username: "
|
|
100
|
+
value = $stdin.gets&.chomp
|
|
101
|
+
abort "[ERROR] No username provided." if value.nil? || value.empty?
|
|
102
|
+
value
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def read_password(prompt)
|
|
106
|
+
require "io/console"
|
|
107
|
+
print prompt
|
|
108
|
+
password = $stdin.noecho(&:gets)&.chomp
|
|
109
|
+
puts
|
|
110
|
+
password
|
|
111
|
+
rescue LoadError
|
|
112
|
+
print prompt
|
|
113
|
+
$stdin.gets&.chomp
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def store_in_woodpecker(api_key)
|
|
117
|
+
unless woodpecker_server
|
|
118
|
+
puts "[INFO] Set WOODPECKER_SERVER to auto-store the key in Woodpecker CI."
|
|
119
|
+
puts "[INFO] New API key (store as secret '#{woodpecker_secret_name}'): #{api_key}"
|
|
120
|
+
return
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
token = read_woodpecker_token
|
|
124
|
+
abort "[ERROR] No Woodpecker token. Set WOODPECKER_TOKEN or run woodpecker-cli setup." unless token
|
|
125
|
+
|
|
126
|
+
WoodpeckerSecretStore.new(server: woodpecker_server, org: woodpecker_org, token: token)
|
|
127
|
+
.store(woodpecker_secret_name, api_key)
|
|
128
|
+
puts "[SUCCESS] API key stored in Woodpecker secret '#{woodpecker_secret_name}'."
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def read_woodpecker_token
|
|
132
|
+
ENV.fetch("WOODPECKER_TOKEN", nil) ||
|
|
133
|
+
File.read(File.expand_path("~/.config/woodpecker/token")).strip
|
|
134
|
+
rescue Errno::ENOENT
|
|
135
|
+
nil
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -14,35 +14,74 @@ module Rake
|
|
|
14
14
|
# t.gem_repositories = Rake::GemMaintenance::Repos.all
|
|
15
15
|
# end
|
|
16
16
|
#
|
|
17
|
+
# @example Use local geminabox only
|
|
18
|
+
# Rake::GemMaintenance::GeminaboxUpgradeTask.new
|
|
19
|
+
#
|
|
20
|
+
# @example Dual publishing: geminabox + rubygems.org
|
|
21
|
+
# Rake::GemMaintenance::UpgradeTask.new do |t|
|
|
22
|
+
# t.gem_repositories = Rake::GemMaintenance::Repos.geminabox +
|
|
23
|
+
# Rake::GemMaintenance::Repos.rubygems
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
17
26
|
# @example Reconfigure internal URL
|
|
18
27
|
# Rake::GemMaintenance::Repos.internal_url = "https://my-internal-gem.example.com"
|
|
28
|
+
#
|
|
29
|
+
# @example Configure API key and TOTP seed env vars
|
|
30
|
+
# Rake::GemMaintenance::Repos.rubygems_api_key_env_var = "GEM_HOST_API_KEY"
|
|
31
|
+
# Rake::GemMaintenance::Repos.rubygems_otp_seed_env_var = "RUBYGEMS_OTP_SEED"
|
|
32
|
+
# Rake::GemMaintenance::Repos.geminabox_url = "http://localhost:9292"
|
|
19
33
|
module Repos
|
|
20
34
|
@internal_url = "https://gems.cbp-org.internal"
|
|
21
35
|
@rubygems_url = "https://rubygems.org"
|
|
36
|
+
@geminabox_url = "http://localhost:9292"
|
|
37
|
+
|
|
38
|
+
@rubygems_api_key_env_var = nil
|
|
39
|
+
@internal_api_key_env_var = nil
|
|
40
|
+
@geminabox_api_key_env_var = nil
|
|
41
|
+
|
|
42
|
+
@rubygems_otp_seed_env_var = nil
|
|
43
|
+
@internal_otp_seed_env_var = nil
|
|
44
|
+
@geminabox_otp_seed_env_var = nil
|
|
22
45
|
|
|
23
46
|
class << self
|
|
24
|
-
attr_accessor :internal_url, :rubygems_url
|
|
47
|
+
attr_accessor :internal_url, :rubygems_url, :geminabox_url,
|
|
48
|
+
:rubygems_api_key_env_var, :internal_api_key_env_var,
|
|
49
|
+
:geminabox_api_key_env_var,
|
|
50
|
+
:rubygems_otp_seed_env_var, :internal_otp_seed_env_var,
|
|
51
|
+
:geminabox_otp_seed_env_var
|
|
25
52
|
end
|
|
26
53
|
|
|
27
54
|
# Publish only to internal repository
|
|
28
55
|
# @return [Array<Hash>] repository configuration
|
|
29
56
|
def self.internal
|
|
30
|
-
|
|
57
|
+
base = { name: "cbp-org", url: internal_url }
|
|
58
|
+
base[:api_key_env_var] = internal_api_key_env_var if internal_api_key_env_var
|
|
59
|
+
base[:otp_seed_env_var] = internal_otp_seed_env_var if internal_otp_seed_env_var
|
|
60
|
+
[base]
|
|
31
61
|
end
|
|
32
62
|
|
|
33
63
|
# Publish to both rubygems.org and internal repository
|
|
34
64
|
# @return [Array<Hash>] repository configuration
|
|
35
65
|
def self.all
|
|
36
|
-
|
|
37
|
-
{ name: "rubygems", url: rubygems_url },
|
|
38
|
-
{ name: "cbp-org", url: internal_url }
|
|
39
|
-
]
|
|
66
|
+
rubygems + internal
|
|
40
67
|
end
|
|
41
68
|
|
|
42
69
|
# Publish only to rubygems.org (the default)
|
|
43
70
|
# @return [Array<Hash>] repository configuration
|
|
44
71
|
def self.rubygems
|
|
45
|
-
|
|
72
|
+
base = { name: "rubygems", url: rubygems_url }
|
|
73
|
+
base[:api_key_env_var] = rubygems_api_key_env_var if rubygems_api_key_env_var
|
|
74
|
+
base[:otp_seed_env_var] = rubygems_otp_seed_env_var if rubygems_otp_seed_env_var
|
|
75
|
+
[base]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Publish only to a local geminabox instance
|
|
79
|
+
# @return [Array<Hash>] repository configuration
|
|
80
|
+
def self.geminabox
|
|
81
|
+
base = { name: "geminabox", url: geminabox_url }
|
|
82
|
+
base[:api_key_env_var] = geminabox_api_key_env_var if geminabox_api_key_env_var
|
|
83
|
+
base[:otp_seed_env_var] = geminabox_otp_seed_env_var if geminabox_otp_seed_env_var
|
|
84
|
+
[base]
|
|
46
85
|
end
|
|
47
86
|
|
|
48
87
|
# Default configuration: rubygems.org only
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rake
|
|
4
|
+
module GemMaintenance
|
|
5
|
+
# Creates a new scoped API key on rubygems.org via the v2 API.
|
|
6
|
+
# Handles OTP header injection and maps HTTP error codes to actionable messages.
|
|
7
|
+
class RubyGemsApiKeyCreator
|
|
8
|
+
def initialize(host: "https://rubygems.org")
|
|
9
|
+
@host = host
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def create(username, password, otp: nil)
|
|
13
|
+
require "net/http"
|
|
14
|
+
|
|
15
|
+
request = build_request(username, password, otp)
|
|
16
|
+
response = http_client.request(request)
|
|
17
|
+
parse_response(response)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def build_request(username, password, otp)
|
|
23
|
+
uri = URI("#{@host}/api/v1/api_key")
|
|
24
|
+
key_name = "rake-gem-maintenance-ci-#{Time.now.strftime('%Y%m%d')}"
|
|
25
|
+
req = Net::HTTP::Post.new(uri)
|
|
26
|
+
req.basic_auth(username, password)
|
|
27
|
+
req["OTP"] = otp if otp
|
|
28
|
+
req["Content-Type"] = "application/x-www-form-urlencoded"
|
|
29
|
+
req.body = "name=#{key_name}&push_rubygem=true"
|
|
30
|
+
req
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def http_client
|
|
34
|
+
uri = URI(@host)
|
|
35
|
+
http = Net::HTTP.new(uri.hostname, uri.port)
|
|
36
|
+
http.use_ssl = true
|
|
37
|
+
http
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def parse_response(response)
|
|
41
|
+
case response.code.to_i
|
|
42
|
+
when 200, 201 then response.body.strip
|
|
43
|
+
when 401 then abort "[ERROR] Invalid credentials for #{@host}."
|
|
44
|
+
when 403 then abort "[ERROR] Forbidden. Check your MFA settings on #{@host}."
|
|
45
|
+
when 449 then abort "[ERROR] OTP required by #{@host}. Set RUBYGEMS_OTP_SEED and retry."
|
|
46
|
+
else abort "[ERROR] #{@host} returned #{response.code}: #{response.body.strip}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
require "net/http"
|
|
4
4
|
require "rake"
|
|
5
5
|
require "rake/tasklib"
|
|
6
|
+
require_relative "ci_environment"
|
|
7
|
+
require_relative "otp_provider"
|
|
8
|
+
require_relative "renew_api_key_task"
|
|
6
9
|
require_relative "gem_publisher"
|
|
7
10
|
require_relative "repos"
|
|
8
11
|
|
|
@@ -20,6 +23,12 @@ module Rake
|
|
|
20
23
|
:run_bundle_audit, :auto_pipeline, :gem_repositories,
|
|
21
24
|
:gem_publisher_class, :gem_name, :gem_version
|
|
22
25
|
|
|
26
|
+
attr_writer :renew_api_key_task_class
|
|
27
|
+
|
|
28
|
+
def renew_api_key_task_class
|
|
29
|
+
@renew_api_key_task_class || RenewApiKeyTask
|
|
30
|
+
end
|
|
31
|
+
|
|
23
32
|
def initialize(name = :upgrade)
|
|
24
33
|
super()
|
|
25
34
|
apply_default_configuration(name)
|
|
@@ -85,6 +94,7 @@ module Rake
|
|
|
85
94
|
define_gems_task
|
|
86
95
|
define_commit_task
|
|
87
96
|
define_push_task
|
|
97
|
+
define_renew_api_key_task
|
|
88
98
|
end
|
|
89
99
|
|
|
90
100
|
def define_info_tasks
|
|
@@ -186,6 +196,10 @@ module Rake
|
|
|
186
196
|
end
|
|
187
197
|
end
|
|
188
198
|
|
|
199
|
+
def define_renew_api_key_task
|
|
200
|
+
renew_api_key_task_class.new(name)
|
|
201
|
+
end
|
|
202
|
+
|
|
189
203
|
def pipeline_tasks
|
|
190
204
|
return auto_pipeline if auto_pipeline
|
|
191
205
|
|
|
@@ -294,5 +308,14 @@ module Rake
|
|
|
294
308
|
@gem_repositories = Repos.all
|
|
295
309
|
end
|
|
296
310
|
end
|
|
311
|
+
|
|
312
|
+
# Upgrades gems and publishes to a local geminabox instance only.
|
|
313
|
+
# Uses Repos.geminabox as default repositories.
|
|
314
|
+
class GeminaboxUpgradeTask < UpgradeTask
|
|
315
|
+
def apply_default_configuration(name)
|
|
316
|
+
super
|
|
317
|
+
@gem_repositories = Repos.geminabox
|
|
318
|
+
end
|
|
319
|
+
end
|
|
297
320
|
end
|
|
298
321
|
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rake
|
|
4
|
+
module GemMaintenance
|
|
5
|
+
# Creates or updates an org-level secret in a Woodpecker CI server.
|
|
6
|
+
# SSL verification is disabled because Woodpecker is typically served
|
|
7
|
+
# on an internal network with a private CA.
|
|
8
|
+
class WoodpeckerSecretStore
|
|
9
|
+
def initialize(server:, org:, token:)
|
|
10
|
+
@server = server
|
|
11
|
+
@org = org
|
|
12
|
+
@token = token
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def store(secret_name, value, events: %w[push tag manual])
|
|
16
|
+
org_id = find_org_id
|
|
17
|
+
abort "[ERROR] Woodpecker org '#{@org}' not found on #{@server}." unless org_id
|
|
18
|
+
|
|
19
|
+
if secret_exists?(org_id, secret_name)
|
|
20
|
+
patch("/api/orgs/#{org_id}/secrets/#{secret_name}",
|
|
21
|
+
{ value: value, events: events, images: [] })
|
|
22
|
+
else
|
|
23
|
+
post("/api/orgs/#{org_id}/secrets",
|
|
24
|
+
{ name: secret_name, value: value, events: events, images: [] })
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def find_org_id
|
|
31
|
+
orgs = get("/api/orgs")
|
|
32
|
+
orgs&.find { |o| o["name"] == @org }&.fetch("id", nil)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def secret_exists?(org_id, secret_name)
|
|
36
|
+
secrets = get("/api/orgs/#{org_id}/secrets")
|
|
37
|
+
secrets&.any? { |s| s["name"] == secret_name }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def get(path)
|
|
41
|
+
require "json"
|
|
42
|
+
response = request(Net::HTTP::Get, path)
|
|
43
|
+
JSON.parse(response.body) if response.is_a?(Net::HTTPSuccess)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def post(path, body)
|
|
47
|
+
request(Net::HTTP::Post, path, body)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def patch(path, body)
|
|
51
|
+
request(Net::HTTP::Patch, path, body)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def request(klass, path, body = nil)
|
|
55
|
+
require "net/http"
|
|
56
|
+
require "json"
|
|
57
|
+
require "openssl"
|
|
58
|
+
|
|
59
|
+
uri = URI("#{@server}#{path}")
|
|
60
|
+
http.request(build_req(klass, uri, body))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def http
|
|
64
|
+
uri = URI(@server)
|
|
65
|
+
h = Net::HTTP.new(uri.hostname, uri.port)
|
|
66
|
+
h.use_ssl = (uri.scheme == "https")
|
|
67
|
+
h.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
68
|
+
h
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def build_req(klass, uri, body)
|
|
72
|
+
req = klass.new(uri)
|
|
73
|
+
req["Authorization"] = "Bearer #{@token}"
|
|
74
|
+
if body
|
|
75
|
+
req["Content-Type"] = "application/json"
|
|
76
|
+
req.body = JSON.generate(body)
|
|
77
|
+
end
|
|
78
|
+
req
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def build_http(uri)
|
|
82
|
+
http = Net::HTTP.new(uri.hostname, uri.port)
|
|
83
|
+
http.use_ssl = (uri.scheme == "https")
|
|
84
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
85
|
+
http
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
data/lib/rake/gem/maintenance.rb
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "maintenance/version"
|
|
4
4
|
require_relative "maintenance/version_bump_task"
|
|
5
|
+
require_relative "maintenance/ci_environment"
|
|
6
|
+
require_relative "maintenance/credential_store"
|
|
7
|
+
require_relative "maintenance/otp_provider"
|
|
8
|
+
require_relative "maintenance/ruby_gems_api_key_creator"
|
|
9
|
+
require_relative "maintenance/woodpecker_secret_store"
|
|
10
|
+
require_relative "maintenance/api_key_renewer"
|
|
11
|
+
require_relative "maintenance/renew_api_key_task"
|
|
12
|
+
require_relative "maintenance/gem_push"
|
|
5
13
|
require_relative "maintenance/gem_publisher"
|
|
6
14
|
require_relative "maintenance/repos"
|
|
7
15
|
require_relative "maintenance/upgrade_task"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# ci-publish-rubygems.rb — build and push this gem to rubygems.org
|
|
4
|
+
#
|
|
5
|
+
# Required env:
|
|
6
|
+
# GEM_HOST_API_KEY rubygems.org API key (from Woodpecker secret rubygems_api_key)
|
|
7
|
+
# RUBYGEMS_OTP_SEED base32 TOTP seed (from Woodpecker secret rubygems_otp_seed)
|
|
8
|
+
|
|
9
|
+
$LOAD_PATH.unshift File.join(__dir__, "..", "lib")
|
|
10
|
+
require "rake/gem/maintenance"
|
|
11
|
+
|
|
12
|
+
Rake::GemMaintenance::Repos.rubygems_api_key_env_var = "GEM_HOST_API_KEY"
|
|
13
|
+
Rake::GemMaintenance::Repos.rubygems_otp_seed_env_var = "RUBYGEMS_OTP_SEED"
|
|
14
|
+
|
|
15
|
+
gemspec_file = Dir["*.gemspec"].first
|
|
16
|
+
abort "ERROR: No gemspec found in #{Dir.pwd}" unless gemspec_file
|
|
17
|
+
|
|
18
|
+
system("gem build #{gemspec_file}") or abort "ERROR: gem build failed"
|
|
19
|
+
|
|
20
|
+
gem_file = Dir["*.gem"].max_by { |f| File.mtime(f) }
|
|
21
|
+
abort "ERROR: No .gem file found after build" unless gem_file
|
|
22
|
+
|
|
23
|
+
puts "Publishing #{gem_file} to rubygems.org..."
|
|
24
|
+
publisher = Rake::GemMaintenance::GemPublisher.new(Rake::GemMaintenance::Repos.rubygems)
|
|
25
|
+
publisher.publish(gem_file)
|
|
26
|
+
|
|
27
|
+
abort "ERROR: Failed to publish #{gem_file} to rubygems.org" if publisher.successful_repos.empty?
|
|
28
|
+
|
|
29
|
+
puts "Published #{gem_file} successfully to: #{publisher.successful_repos.join(', ')}"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rake-gem-maintenance
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Christophe Broult
|
|
@@ -51,6 +51,20 @@ dependencies:
|
|
|
51
51
|
- - ">="
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rotp
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
54
68
|
description: Provides reusable Rake::TaskLib subclasses for upgrading gem dependencies
|
|
55
69
|
and bumping versions.
|
|
56
70
|
email:
|
|
@@ -61,6 +75,9 @@ extra_rdoc_files: []
|
|
|
61
75
|
files:
|
|
62
76
|
- ".rspec"
|
|
63
77
|
- ".rubocop.yml"
|
|
78
|
+
- ".woodpecker/publish.yml"
|
|
79
|
+
- ".woodpecker/renew_api_key.yml"
|
|
80
|
+
- ".woodpecker/verify.yml"
|
|
64
81
|
- CLAUDE.md
|
|
65
82
|
- Gemfile
|
|
66
83
|
- Gemfile.lock
|
|
@@ -71,13 +88,21 @@ files:
|
|
|
71
88
|
- TODO.md
|
|
72
89
|
- cucumber.yml
|
|
73
90
|
- lib/rake/gem/maintenance.rb
|
|
91
|
+
- lib/rake/gem/maintenance/api_key_renewer.rb
|
|
92
|
+
- lib/rake/gem/maintenance/ci_environment.rb
|
|
93
|
+
- lib/rake/gem/maintenance/credential_store.rb
|
|
74
94
|
- lib/rake/gem/maintenance/gem_publisher.rb
|
|
95
|
+
- lib/rake/gem/maintenance/gem_push.rb
|
|
75
96
|
- lib/rake/gem/maintenance/install_tasks.rb
|
|
97
|
+
- lib/rake/gem/maintenance/otp_provider.rb
|
|
98
|
+
- lib/rake/gem/maintenance/renew_api_key_task.rb
|
|
76
99
|
- lib/rake/gem/maintenance/repos.rb
|
|
100
|
+
- lib/rake/gem/maintenance/ruby_gems_api_key_creator.rb
|
|
77
101
|
- lib/rake/gem/maintenance/upgrade_task.rb
|
|
78
102
|
- lib/rake/gem/maintenance/version.rb
|
|
79
103
|
- lib/rake/gem/maintenance/version_bump_task.rb
|
|
80
|
-
- rake
|
|
104
|
+
- lib/rake/gem/maintenance/woodpecker_secret_store.rb
|
|
105
|
+
- scripts/ci_publish_rubygems.rb
|
|
81
106
|
homepage: https://github.com/cbroult/rake-gem-maintenance
|
|
82
107
|
licenses:
|
|
83
108
|
- MIT
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "lib/rake/gem/maintenance/version"
|
|
4
|
-
|
|
5
|
-
Gem::Specification.new do |spec|
|
|
6
|
-
spec.name = "rake-gem-maintenance"
|
|
7
|
-
spec.version = Rake::GemMaintenance::VERSION
|
|
8
|
-
spec.authors = ["Christophe Broult"]
|
|
9
|
-
spec.email = ["cbroult@yahoo.com"]
|
|
10
|
-
|
|
11
|
-
spec.summary = "Rake tasks for gem maintenance: dependency upgrades and version bumps."
|
|
12
|
-
spec.description = "Provides reusable Rake::TaskLib subclasses for upgrading gem dependencies and bumping versions."
|
|
13
|
-
spec.homepage = "https://github.com/cbroult/rake-gem-maintenance"
|
|
14
|
-
spec.license = "MIT"
|
|
15
|
-
spec.required_ruby_version = ">= 3.3.7"
|
|
16
|
-
|
|
17
|
-
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
|
18
|
-
spec.metadata["homepage_uri"] = spec.homepage
|
|
19
|
-
spec.metadata["changelog_uri"] = File.join(spec.homepage, "Changelog")
|
|
20
|
-
spec.metadata["rubygems_mfa_required"] = "true"
|
|
21
|
-
|
|
22
|
-
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
23
|
-
`git ls-files -z`.split("\x0").reject do |f|
|
|
24
|
-
(f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
spec.require_paths = ["lib"]
|
|
28
|
-
|
|
29
|
-
spec.add_dependency "bundler-audit"
|
|
30
|
-
spec.add_dependency "gem-release"
|
|
31
|
-
spec.add_dependency "rake"
|
|
32
|
-
end
|