kettle-gha-pins 0.1.0 → 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
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +18 -1
- data/README.md +39 -21
- data/exe/kettle-gha-pins +30 -0
- data/lib/kettle/gha/pins/action_resolver.rb +200 -0
- data/lib/kettle/gha/pins/cache_progress.rb +50 -0
- data/lib/kettle/gha/pins/cli.rb +773 -0
- data/lib/kettle/gha/pins/github_client.rb +207 -0
- data/lib/kettle/gha/pins/persistent_action_cache.rb +186 -0
- data/lib/kettle/gha/pins/version.rb +1 -1
- data/lib/kettle/gha/pins.rb +22 -0
- data.tar.gz.sig +0 -0
- metadata +29 -8
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5ee4e694080aa9ce9e860fdd085015793830efc399a8ff295dd3fb6fa9dcf4dc
|
|
4
|
+
data.tar.gz: d9b39c2192b77c942ffcf055abc30efd7a6ae6a78c8baf274776c1473bab179f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 90f1f8ccee7b91f7fb72d4459577f0e145565a8897463ac64afa765213fb90e90af4ba2b945148e61a3a4f67e0c4718fb5a2c751ddbfa8d74d13a8b5b0082f61
|
|
7
|
+
data.tar.gz: a8a087d884e121de676bb70eff44f5524368fe2890afab896ff18fcd775f78887d64b482a3e395cf576e4d39023f7daef93a02412012da8788669492f16c01d9
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,21 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
30
30
|
|
|
31
31
|
### Security
|
|
32
32
|
|
|
33
|
+
## [0.2.0] - 2026-07-22
|
|
34
|
+
|
|
35
|
+
- TAG: [v0.2.0][0.2.0t]
|
|
36
|
+
- COVERAGE: 99.07% -- 849/857 lines in 8 files
|
|
37
|
+
- BRANCH COVERAGE: 92.55% -- 348/376 branches in 8 files
|
|
38
|
+
- 22.22% documented
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- Added the shared `PersistentActionCache`, `GitHubClient`, and action ref
|
|
43
|
+
resolver API so GitHub Actions pin tools can reuse cache, network, and upgrade
|
|
44
|
+
planning behavior without shelling out to each other.
|
|
45
|
+
- Added the `kettle-gha-pins` executable for scanning standard GitHub Actions
|
|
46
|
+
workflow YAML files and updating action refs to immutable SHAs.
|
|
47
|
+
|
|
33
48
|
## [0.1.0] - 2026-07-22
|
|
34
49
|
|
|
35
50
|
- TAG: [v0.1.0][0.1.0t]
|
|
@@ -61,6 +76,8 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
61
76
|
- kettle-jem-template-20260720-005 - Generated README Support & Community rows
|
|
62
77
|
now include a RubyForum help badge.
|
|
63
78
|
|
|
64
|
-
[Unreleased]: https://github.com/kettle-dev/kettle-gha-pins/compare/v0.
|
|
79
|
+
[Unreleased]: https://github.com/kettle-dev/kettle-gha-pins/compare/v0.2.0...HEAD
|
|
80
|
+
[0.2.0]: https://github.com/kettle-dev/kettle-gha-pins/compare/v0.1.0...v0.2.0
|
|
81
|
+
[0.2.0t]: https://github.com/kettle-dev/kettle-gha-pins/releases/tag/v0.2.0
|
|
65
82
|
[0.1.0]: https://github.com/kettle-dev/kettle-gha-pins/compare/c633526495c7db0a8721a94a71c3def0f3cc71bb...v0.1.0
|
|
66
83
|
[0.1.0t]: https://github.com/kettle-dev/kettle-gha-pins/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -21,14 +21,22 @@ I've summarized my thoughts in [this blog post](https://dev.to/galtzo/hostile-ta
|
|
|
21
21
|
|
|
22
22
|
## 🌻 Synopsis <a href="https://discord.gg/3qme4XHNKN"><img alt="Galtzo FLOSS Logo by Aboling0, CC BY-SA 4.0" src="https://logos.galtzo.com/assets/images/galtzo-floss/avatar-128px.svg" width="8%" align="right"/></a> <a href="https://ruby-toolbox.com"><img alt="ruby-lang Logo, Yukihiro Matsumoto, Ruby Visual Identity Team, CC BY-SA 2.5" src="https://logos.galtzo.com/assets/images/ruby-lang/avatar-128px.svg" width="8%" align="right"/></a>
|
|
23
23
|
|
|
24
|
-
`kettle-gha-pins` is the shared
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
`kettle-gha-pins` is the shared GitHub Actions pin maintenance library for
|
|
25
|
+
kettle-dev tooling. It owns the action-release version rubric, persistent action
|
|
26
|
+
metadata cache, GitHub release/tag/commit resolution client, and ref upgrade
|
|
27
|
+
planner used by pin maintenance commands.
|
|
28
28
|
|
|
29
|
-
The gem is intentionally
|
|
29
|
+
The gem is intentionally API-focused. Command-line tools such as
|
|
30
30
|
`kettle-gha-sha-pins` and template maintenance scripts can depend on this gem
|
|
31
|
-
instead of each carrying their own subtly different
|
|
31
|
+
instead of each carrying their own subtly different cache, network, and version
|
|
32
|
+
comparison logic.
|
|
33
|
+
|
|
34
|
+
For ordinary workflow pin maintenance, install this gem and run:
|
|
35
|
+
|
|
36
|
+
```console
|
|
37
|
+
kettle-gha-pins --check
|
|
38
|
+
kettle-gha-pins --write --upgrade patch
|
|
39
|
+
```
|
|
32
40
|
|
|
33
41
|
## 💡 Info you can shake a stick at
|
|
34
42
|
|
|
@@ -129,8 +137,8 @@ gem install kettle-gha-pins
|
|
|
129
137
|
|
|
130
138
|
## ⚙️ Configuration
|
|
131
139
|
|
|
132
|
-
There is no global configuration for the library API. Callers pass
|
|
133
|
-
|
|
140
|
+
There is no global configuration for the library API. Callers pass options to
|
|
141
|
+
the client and cache objects they create.
|
|
134
142
|
|
|
135
143
|
The shared upgrade policies are:
|
|
136
144
|
|
|
@@ -142,27 +150,37 @@ The shared upgrade policies are:
|
|
|
142
150
|
Invalid upgrade levels normalize to `patch`, matching the historical
|
|
143
151
|
`kettle-gha-sha-pins` default.
|
|
144
152
|
|
|
153
|
+
`Kettle::Gha::Pins::PersistentActionCache.default_path` intentionally preserves
|
|
154
|
+
the historical `kettle-gha-sha-pins` cache location so command-line tools can
|
|
155
|
+
share action metadata without coupling to each other.
|
|
156
|
+
|
|
145
157
|
## 🔧 Basic Usage
|
|
146
158
|
|
|
147
159
|
```ruby
|
|
148
160
|
require "kettle/gha/pins"
|
|
149
161
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
162
|
+
cache = Kettle::Gha::Pins::PersistentActionCache.new(
|
|
163
|
+
path: Kettle::Gha::Pins::PersistentActionCache.default_path
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
client = Kettle::Gha::Pins::GitHubClient.new(
|
|
167
|
+
token: ENV["GITHUB_TOKEN"],
|
|
168
|
+
api_base: Kettle::Gha::Pins::API_BASE,
|
|
169
|
+
user_agent: "my-gha-pin-tool",
|
|
170
|
+
persistent_cache: cache
|
|
156
171
|
)
|
|
157
172
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
173
|
+
versions_by_repo = {}
|
|
174
|
+
plan = Kettle::Gha::Pins.resolve_action_plan(
|
|
175
|
+
cache: versions_by_repo,
|
|
176
|
+
client: client,
|
|
177
|
+
repo_ref: "codecov/codecov-action",
|
|
178
|
+
old_ref: "fb8b3582c8e4def4969c97caa2f19720cb33a72f",
|
|
179
|
+
upgrade_level: "major"
|
|
162
180
|
)
|
|
163
181
|
|
|
164
|
-
|
|
165
|
-
# => "
|
|
182
|
+
plan[:updates]
|
|
183
|
+
# => {sha: "...", version: "7.0.0", reason: "upgrade_to_allowed_release"}
|
|
166
184
|
```
|
|
167
185
|
|
|
168
186
|
When two version-equivalent tags point at the same SHA, the rubric keeps the
|
|
@@ -514,7 +532,7 @@ Thanks for RTFM. ☺️
|
|
|
514
532
|
[📌gitmoji]: https://gitmoji.dev
|
|
515
533
|
[📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
|
|
516
534
|
[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
|
517
|
-
[🧮kloc-img]: https://img.shields.io/badge/KLOC-0.
|
|
535
|
+
[🧮kloc-img]: https://img.shields.io/badge/KLOC-0.857-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
|
|
518
536
|
[🔐security]: https://github.com/kettle-dev/kettle-gha-pins/blob/main/SECURITY.md
|
|
519
537
|
[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
|
|
520
538
|
[📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
|
data/exe/kettle-gha-pins
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
$stdout.sync = true
|
|
5
|
+
$stderr.sync = true
|
|
6
|
+
|
|
7
|
+
script_basename = File.basename(__FILE__)
|
|
8
|
+
|
|
9
|
+
begin
|
|
10
|
+
require "kettle/gha/pins"
|
|
11
|
+
rescue LoadError
|
|
12
|
+
repo_lib = File.expand_path("../lib", __dir__)
|
|
13
|
+
$LOAD_PATH.unshift(repo_lib) unless $LOAD_PATH.include?(repo_lib)
|
|
14
|
+
begin
|
|
15
|
+
require "kettle/gha/pins"
|
|
16
|
+
rescue LoadError => e
|
|
17
|
+
warn("#{script_basename}: could not load dependency: #{e.class}: #{e.message}")
|
|
18
|
+
warn("Hint: Install the kettle-gha-pins gem (`gem install kettle-gha-pins`) or add it to your Gemfile.")
|
|
19
|
+
exit(1)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
begin
|
|
24
|
+
status = Kettle::Gha::Pins::CLI.new(ARGV).run!
|
|
25
|
+
exit(status)
|
|
26
|
+
rescue => e
|
|
27
|
+
warn("#{script_basename}: unexpected error: #{e.class}: #{e.message}")
|
|
28
|
+
warn(e.backtrace.join("\n"))
|
|
29
|
+
exit(1)
|
|
30
|
+
end
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kettle
|
|
4
|
+
module Gha
|
|
5
|
+
module Pins
|
|
6
|
+
# Shared planning rules for converting and upgrading GitHub Action refs.
|
|
7
|
+
module ActionResolver
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def resolve_action_plan(cache:, client:, repo_ref:, old_ref:, upgrade_level: DEFAULT_UPGRADE_LEVEL)
|
|
11
|
+
versions = if cache.key?(repo_ref)
|
|
12
|
+
cache.fetch(repo_ref)
|
|
13
|
+
else
|
|
14
|
+
cache[repo_ref] = client.versions_for_repo(repo_ref)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
determine_upgrade_plan(
|
|
18
|
+
old_ref: old_ref,
|
|
19
|
+
repo_ref: repo_ref,
|
|
20
|
+
versions: versions,
|
|
21
|
+
upgrade_level: upgrade_level,
|
|
22
|
+
client: client
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def determine_upgrade_plan(old_ref:, repo_ref:, versions:, upgrade_level:, client:)
|
|
27
|
+
level = VersionRubric.normalize_upgrade_level(upgrade_level)
|
|
28
|
+
|
|
29
|
+
current_ref = old_ref.to_s.strip
|
|
30
|
+
return {is_outdated: false, updates: nil, reason: nil, current_version: nil} if current_ref.empty?
|
|
31
|
+
|
|
32
|
+
available_versions = versions || []
|
|
33
|
+
latest = available_versions.first
|
|
34
|
+
|
|
35
|
+
current_sha = if SHA_RE.match?(current_ref) || WEAK_SHA_RE.match?(current_ref)
|
|
36
|
+
current_ref
|
|
37
|
+
else
|
|
38
|
+
client.commit_sha(repo_ref, current_ref)
|
|
39
|
+
end
|
|
40
|
+
parsed_current_ref = VersionRubric.parse(current_ref)
|
|
41
|
+
version_equivalent_entry = if parsed_current_ref
|
|
42
|
+
available_versions.find { |entry| entry[:version_obj] == parsed_current_ref }
|
|
43
|
+
end
|
|
44
|
+
matched_entry = matching_version_entry(available_versions, current_ref, current_sha, client, repo_ref)
|
|
45
|
+
unresolved_version_ref = false
|
|
46
|
+
if matched_entry.nil? && current_sha.to_s.empty? && version_equivalent_entry && non_sha?(current_ref)
|
|
47
|
+
matched_entry = version_equivalent_entry
|
|
48
|
+
unresolved_version_ref = true
|
|
49
|
+
end
|
|
50
|
+
current_version = matched_entry ? matched_entry[:version] : nil
|
|
51
|
+
|
|
52
|
+
updates = nil
|
|
53
|
+
reason = nil
|
|
54
|
+
is_outdated = false
|
|
55
|
+
latest_outdated = nil
|
|
56
|
+
|
|
57
|
+
if current_version
|
|
58
|
+
latest_outdated = VersionRubric.latest_outdated_target(current_version, available_versions)
|
|
59
|
+
target = VersionRubric.choose_upgrade_target(current_version, available_versions, level)
|
|
60
|
+
target_sha = target ? version_entry_sha(target, client, repo_ref) : nil
|
|
61
|
+
latest_outdated_sha = latest_outdated ? version_entry_sha(latest_outdated, client, repo_ref) : nil
|
|
62
|
+
if latest_outdated && stale_sha?(current_ref, latest_outdated_sha)
|
|
63
|
+
latest_outdated = latest_outdated.merge(sha: latest_outdated_sha)
|
|
64
|
+
is_outdated = true
|
|
65
|
+
reason = UPGRADE_REASON
|
|
66
|
+
end
|
|
67
|
+
if target && stale_sha?(current_ref, target_sha)
|
|
68
|
+
updates = {
|
|
69
|
+
sha: target_sha,
|
|
70
|
+
version: target[:version],
|
|
71
|
+
reason: UPGRADE_REASON
|
|
72
|
+
}
|
|
73
|
+
reason ||= UPGRADE_REASON
|
|
74
|
+
end
|
|
75
|
+
if updates.nil? && unresolved_version_ref
|
|
76
|
+
matched_sha = version_entry_sha(matched_entry, client, repo_ref)
|
|
77
|
+
if stale_sha?(current_ref, matched_sha)
|
|
78
|
+
updates = {
|
|
79
|
+
sha: matched_sha,
|
|
80
|
+
version: nil,
|
|
81
|
+
reason: NON_SHA_REASON
|
|
82
|
+
}
|
|
83
|
+
latest_outdated ||= matched_entry.merge(sha: matched_sha)
|
|
84
|
+
is_outdated = true
|
|
85
|
+
reason ||= NON_SHA_REASON
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
elsif current_sha && non_sha?(current_ref)
|
|
89
|
+
if stale_sha?(current_ref, current_sha)
|
|
90
|
+
updates = {
|
|
91
|
+
sha: current_sha,
|
|
92
|
+
version: nil,
|
|
93
|
+
reason: NON_SHA_REASON
|
|
94
|
+
}
|
|
95
|
+
reason = NON_SHA_REASON
|
|
96
|
+
end
|
|
97
|
+
elsif current_sha
|
|
98
|
+
latest_sha = latest ? version_entry_sha(latest, client, repo_ref) : nil
|
|
99
|
+
if latest && stale_sha?(current_ref, latest_sha)
|
|
100
|
+
latest_outdated = latest.merge(sha: latest_sha)
|
|
101
|
+
updates = {
|
|
102
|
+
sha: latest_sha,
|
|
103
|
+
version: latest[:version],
|
|
104
|
+
reason: STALE_SHA_REASON
|
|
105
|
+
}
|
|
106
|
+
reason = STALE_SHA_REASON
|
|
107
|
+
is_outdated = true
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
{
|
|
112
|
+
is_outdated: is_outdated,
|
|
113
|
+
updates: updates,
|
|
114
|
+
reason: reason,
|
|
115
|
+
current_version: current_version,
|
|
116
|
+
latest_outdated: latest_outdated
|
|
117
|
+
}
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def matching_version_entry(versions, current_ref, current_sha, client, repo_ref)
|
|
121
|
+
parsed = VersionRubric.parse(current_ref)
|
|
122
|
+
if parsed
|
|
123
|
+
direct = versions.find { |entry| entry[:tag] == current_ref }
|
|
124
|
+
return direct if direct
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
return nil unless current_sha
|
|
128
|
+
|
|
129
|
+
prefix = current_sha[0, 40]
|
|
130
|
+
versions.find do |entry|
|
|
131
|
+
sha = version_entry_sha(entry, client, repo_ref)
|
|
132
|
+
sha.to_s.start_with?(prefix)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def version_entry_sha(entry, client, repo_ref)
|
|
137
|
+
return nil unless entry
|
|
138
|
+
return entry[:sha] unless entry[:sha].to_s.empty?
|
|
139
|
+
|
|
140
|
+
sha = client.commit_sha(repo_ref, entry[:tag])
|
|
141
|
+
entry[:sha] = sha
|
|
142
|
+
sha
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def short_sha?(candidate)
|
|
146
|
+
return false unless candidate
|
|
147
|
+
|
|
148
|
+
WEAK_SHA_RE.match?(candidate)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def non_sha?(candidate)
|
|
152
|
+
!SHA_RE.match?(candidate) && !WEAK_SHA_RE.match?(candidate)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def stale_sha?(current, latest)
|
|
156
|
+
return false if current.nil? || latest.nil?
|
|
157
|
+
|
|
158
|
+
current_down = current.downcase
|
|
159
|
+
latest_down = latest.downcase
|
|
160
|
+
|
|
161
|
+
if current_down.length < latest_down.length
|
|
162
|
+
!latest_down.start_with?(current_down)
|
|
163
|
+
else
|
|
164
|
+
current_down != latest_down
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
class << self
|
|
170
|
+
def resolve_action_plan(...)
|
|
171
|
+
ActionResolver.resolve_action_plan(...)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def determine_upgrade_plan(...)
|
|
175
|
+
ActionResolver.determine_upgrade_plan(...)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def matching_version_entry(...)
|
|
179
|
+
ActionResolver.matching_version_entry(...)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def version_entry_sha(...)
|
|
183
|
+
ActionResolver.version_entry_sha(...)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def short_sha?(...)
|
|
187
|
+
ActionResolver.short_sha?(...)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def non_sha?(...)
|
|
191
|
+
ActionResolver.non_sha?(...)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def stale_sha?(...)
|
|
195
|
+
ActionResolver.stale_sha?(...)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby-progressbar"
|
|
4
|
+
|
|
5
|
+
module Kettle
|
|
6
|
+
module Gha
|
|
7
|
+
module Pins
|
|
8
|
+
class CacheProgress
|
|
9
|
+
FORMAT = "%t %b %c/%C"
|
|
10
|
+
LENGTH = 30
|
|
11
|
+
|
|
12
|
+
def initialize(total:, cached_title:, live_title:, output:, enabled: true, skipped_title: nil)
|
|
13
|
+
@total = total.to_i
|
|
14
|
+
@cached_count = 0
|
|
15
|
+
@live_count = 0
|
|
16
|
+
@skipped_count = 0
|
|
17
|
+
@cached_bar = progress_bar(cached_title, output, enabled)
|
|
18
|
+
@live_bar = progress_bar(live_title, output, enabled)
|
|
19
|
+
@skipped_bar = skipped_title ? progress_bar(skipped_title, output, enabled) : nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
attr_reader :cached_count, :live_count, :skipped_count
|
|
23
|
+
|
|
24
|
+
def cached
|
|
25
|
+
@cached_count += 1
|
|
26
|
+
@cached_bar&.increment
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def live
|
|
30
|
+
@live_count += 1
|
|
31
|
+
@live_bar&.increment
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def skipped
|
|
35
|
+
@skipped_count += 1
|
|
36
|
+
@skipped_bar&.increment
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def progress_bar(title, output, enabled)
|
|
42
|
+
return unless enabled
|
|
43
|
+
return unless @total.positive?
|
|
44
|
+
|
|
45
|
+
ProgressBar.create(title: title, total: @total, format: FORMAT, length: LENGTH, output: output)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|