kettle-gha-pins 0.3.9 → 0.3.11
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 +33 -1
- data/README.md +20 -1
- data/lib/kettle/gha/pins/cli.rb +127 -4
- data/lib/kettle/gha/pins/github_client.rb +68 -31
- data/lib/kettle/gha/pins/persistent_action_cache.rb +9 -2
- data/lib/kettle/gha/pins/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +4 -4
- 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: e6eef00386a5fa9f57acdf295331f68f11293cf9d90ef09adbb666aac6a97d0c
|
|
4
|
+
data.tar.gz: 6b433ddcef3d0599c12e8496aa5a80df31f6bcaee358634fb04c167a5c36ac24
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7c6fa524d51f68f4ffb392195d77cb44f782b9ec65e05940915b4ac595e036768a7ed647eca7d7aefbd9da30adebb462fc3e5860f138e783559054257d215982
|
|
7
|
+
data.tar.gz: d7097d6af75ffc23901d1e9fa6208f7704c9f5d12e54aab125438b61088418056ccab1730ad06643945f1fd82052f62d2c315e555769e33535eb7cdd07fbcb50
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,34 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
30
30
|
|
|
31
31
|
### Security
|
|
32
32
|
|
|
33
|
+
## [0.3.11] - 2026-08-10
|
|
34
|
+
|
|
35
|
+
- TAG: [v0.3.11][0.3.11t]
|
|
36
|
+
- COVERAGE: 97.00% -- 1033/1065 lines in 8 files
|
|
37
|
+
- BRANCH COVERAGE: 85.60% -- 416/486 branches in 8 files
|
|
38
|
+
- 25.93% documented
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- Resolve large GitHub Actions annotated-tag inventories through a single Git exchange so stale cache reviews do not time out on repositories such as codecov/codecov-action.
|
|
43
|
+
|
|
44
|
+
- Replace refreshed GitHub Actions cache inventories instead of retaining obsolete versions and forcing repeated refreshes.
|
|
45
|
+
|
|
46
|
+
## [0.3.10] - 2026-08-09
|
|
47
|
+
|
|
48
|
+
- TAG: [v0.3.10][0.3.10t]
|
|
49
|
+
- COVERAGE: 98.08% -- 1022/1042 lines in 8 files
|
|
50
|
+
- BRANCH COVERAGE: 88.03% -- 412/468 branches in 8 files
|
|
51
|
+
- 26.17% documented
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
|
|
55
|
+
- Add JSON action inventory and cache review modes, strict offline pin resolution, and configurable action metadata TTL handling.
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
|
|
59
|
+
- Keep JSON action inventory and review parsing compatible with Ruby 2.5.
|
|
60
|
+
|
|
33
61
|
## [0.3.9] - 2026-08-05
|
|
34
62
|
|
|
35
63
|
- TAG: [v0.3.9][0.3.9t]
|
|
@@ -282,7 +310,11 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
282
310
|
- kettle-jem-template-20260720-005 - Generated README Support & Community rows
|
|
283
311
|
now include a RubyForum help badge.
|
|
284
312
|
|
|
285
|
-
[Unreleased]: https://github.com/kettle-dev/kettle-gha-pins/compare/v0.3.
|
|
313
|
+
[Unreleased]: https://github.com/kettle-dev/kettle-gha-pins/compare/v0.3.11...HEAD
|
|
314
|
+
[0.3.11]: https://github.com/kettle-dev/kettle-gha-pins/compare/v0.3.10...v0.3.11
|
|
315
|
+
[0.3.11t]: https://github.com/kettle-dev/kettle-gha-pins/releases/tag/v0.3.11
|
|
316
|
+
[0.3.10]: https://github.com/kettle-dev/kettle-gha-pins/compare/v0.3.9...v0.3.10
|
|
317
|
+
[0.3.10t]: https://github.com/kettle-dev/kettle-gha-pins/releases/tag/v0.3.10
|
|
286
318
|
[0.3.9]: https://github.com/kettle-dev/kettle-gha-pins/compare/v0.3.8...v0.3.9
|
|
287
319
|
[0.3.9t]: https://github.com/kettle-dev/kettle-gha-pins/releases/tag/v0.3.9
|
|
288
320
|
[0.3.8]: https://github.com/kettle-dev/kettle-gha-pins/compare/v0.3.7...v0.3.8
|
data/README.md
CHANGED
|
@@ -155,6 +155,17 @@ cooldown window. The same default can be supplied with
|
|
|
155
155
|
`KETTLE_GHA_PINS_COOLDOWN_DAYS`. The default is `0`, so checks remain strict
|
|
156
156
|
unless a project opts into a warning window.
|
|
157
157
|
|
|
158
|
+
The action metadata cache uses a one-day TTL by default. Set `--ttl DAYS` to
|
|
159
|
+
change it; `--ttl 0` forces a refresh. `--offline` trusts the cache without
|
|
160
|
+
checking TTL or contacting GitHub and fails if required repository metadata is
|
|
161
|
+
missing.
|
|
162
|
+
|
|
163
|
+
For family orchestration, `kettle-gha-pins --list --json` emits the action
|
|
164
|
+
repositories used by a project without network access. A separate
|
|
165
|
+
`kettle-gha-pins --review --input PATH` refreshes the repositories listed in
|
|
166
|
+
that JSON and updates the shared cache. Review refreshes metadata independently
|
|
167
|
+
of the selected major/minor/patch upgrade policy.
|
|
168
|
+
|
|
158
169
|
`Kettle::Gha::Pins::PersistentActionCache.default_path` intentionally preserves
|
|
159
170
|
the historical `kettle-gha-sha-pins` cache location so command-line tools can
|
|
160
171
|
share action metadata without coupling to each other.
|
|
@@ -188,6 +199,14 @@ plan[:updates]
|
|
|
188
199
|
# => {sha: "...", version: "7.0.0", reason: "upgrade_to_allowed_release"}
|
|
189
200
|
```
|
|
190
201
|
|
|
202
|
+
The family-oriented cache workflow is:
|
|
203
|
+
|
|
204
|
+
```console
|
|
205
|
+
kettle-gha-pins --list --json
|
|
206
|
+
kettle-gha-pins --review --input pins.json
|
|
207
|
+
kettle-gha-pins --offline --write --upgrade major
|
|
208
|
+
```
|
|
209
|
+
|
|
191
210
|
When two version-equivalent tags point at the same SHA, the rubric keeps the
|
|
192
211
|
more explicit release spelling. For example, `v7` and `v7.0.0` pointing at the
|
|
193
212
|
same commit canonicalize to `v7.0.0`; if they point at different SHAs, both
|
|
@@ -563,7 +582,7 @@ Thanks for RTFM. ☺️
|
|
|
563
582
|
[📌gitmoji]: https://gitmoji.dev
|
|
564
583
|
[📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
|
|
565
584
|
[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
|
566
|
-
[🧮kloc-img]: https://img.shields.io/badge/KLOC-
|
|
585
|
+
[🧮kloc-img]: https://img.shields.io/badge/KLOC-1.065-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
|
|
567
586
|
[🔐security]: https://github.com/kettle-dev/kettle-gha-pins/blob/main/SECURITY.md
|
|
568
587
|
[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
|
|
569
588
|
[📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
|
data/lib/kettle/gha/pins/cli.rb
CHANGED
|
@@ -67,9 +67,13 @@ module Kettle
|
|
|
67
67
|
user_agent: "kettle-gha-pins",
|
|
68
68
|
upgrade: DEFAULT_UPGRADE_LEVEL,
|
|
69
69
|
cache_path: ENV["KETTLE_GHA_SHA_PINS_CACHE"] || PersistentActionCache.default_path,
|
|
70
|
+
ttl_days: DEFAULT_CACHE_TTL_SECONDS / 86_400.0,
|
|
70
71
|
cooldown_days: cooldown_days,
|
|
71
72
|
clock: clock,
|
|
72
73
|
refresh_cache: false,
|
|
74
|
+
offline: false,
|
|
75
|
+
mode: :project,
|
|
76
|
+
input: nil,
|
|
73
77
|
reject_patterns: Set.new,
|
|
74
78
|
progress: nil
|
|
75
79
|
}
|
|
@@ -78,20 +82,43 @@ module Kettle
|
|
|
78
82
|
def run!
|
|
79
83
|
parse!
|
|
80
84
|
|
|
81
|
-
|
|
85
|
+
return list_workflows! if @options[:mode] == :list
|
|
86
|
+
|
|
87
|
+
client, persistent_cache = build_client
|
|
88
|
+
return review_cache!(client: client, persistent_cache: persistent_cache) if @options[:mode] == :review
|
|
89
|
+
|
|
90
|
+
run_project!(client: client)
|
|
91
|
+
rescue Error, GitHubClient::CacheMissError, GitHubClient::RefreshError => error
|
|
92
|
+
@err.puts("kettle-gha-pins: #{error.message}")
|
|
93
|
+
2
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def build_client
|
|
99
|
+
@options[:token] ||= gh_auth_token if @options[:api_base] == API_BASE && !@options[:offline]
|
|
82
100
|
persistent_cache = if @options[:cache_path].to_s.empty?
|
|
83
101
|
nil
|
|
84
102
|
else
|
|
85
|
-
PersistentActionCache.new(
|
|
103
|
+
PersistentActionCache.new(
|
|
104
|
+
path: @options[:cache_path],
|
|
105
|
+
ttl_seconds: (@options[:ttl_days] * 86_400).to_i,
|
|
106
|
+
clock: @options[:clock]
|
|
107
|
+
)
|
|
86
108
|
end
|
|
87
109
|
client = GitHubClient.new(
|
|
88
110
|
token: @options[:token],
|
|
89
111
|
api_base: @options[:api_base],
|
|
90
112
|
user_agent: @options[:user_agent],
|
|
91
113
|
persistent_cache: persistent_cache,
|
|
92
|
-
refresh_cache: @options[:refresh_cache]
|
|
114
|
+
refresh_cache: @options[:refresh_cache],
|
|
115
|
+
offline: @options[:offline],
|
|
116
|
+
fail_on_refresh_error: @options[:mode] == :review
|
|
93
117
|
)
|
|
118
|
+
[client, persistent_cache]
|
|
119
|
+
end
|
|
94
120
|
|
|
121
|
+
def run_project!(client:)
|
|
95
122
|
state = {
|
|
96
123
|
files_scanned: 0,
|
|
97
124
|
files_with_changes: 0,
|
|
@@ -293,7 +320,79 @@ module Kettle
|
|
|
293
320
|
0
|
|
294
321
|
end
|
|
295
322
|
|
|
296
|
-
|
|
323
|
+
def list_workflows!
|
|
324
|
+
state = {files_scanned: 0, failures: 0, errors: []}
|
|
325
|
+
workflow_files = discover_workflow_files(@options[:root], @options[:reject_patterns])
|
|
326
|
+
workflows = load_workflows(workflow_files, state)
|
|
327
|
+
pins = workflows.flat_map do |workflow|
|
|
328
|
+
workflow.fetch(:uses_nodes).each_with_object([]) do |node, entries|
|
|
329
|
+
parsed = classify_action_ref(node[:value].to_s)
|
|
330
|
+
next unless parsed
|
|
331
|
+
|
|
332
|
+
action = parsed.fetch(:action)
|
|
333
|
+
entries << {
|
|
334
|
+
"repository" => "#{action[:owner]}/#{action[:repo]}",
|
|
335
|
+
"ref" => action[:ref],
|
|
336
|
+
"value" => parsed[:value],
|
|
337
|
+
"path" => workflow[:path],
|
|
338
|
+
"line" => node[:line].to_i + 1
|
|
339
|
+
}
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
repositories = pins.map { |pin| pin.fetch("repository") }.uniq.sort
|
|
343
|
+
puts JSON.pretty_generate(
|
|
344
|
+
"schema_version" => 1,
|
|
345
|
+
"root" => @options[:root],
|
|
346
|
+
"repositories" => repositories,
|
|
347
|
+
"pins" => pins
|
|
348
|
+
)
|
|
349
|
+
state[:failures].zero? ? 0 : 2
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def review_cache!(client:, persistent_cache:)
|
|
353
|
+
raise Error, "--review requires a writable cache; do not use --cache-path \"\"" unless persistent_cache
|
|
354
|
+
|
|
355
|
+
repositories = review_repositories
|
|
356
|
+
refreshed = repositories.map do |repository|
|
|
357
|
+
versions = client.versions_for_repo(repository)
|
|
358
|
+
{
|
|
359
|
+
"repository" => repository,
|
|
360
|
+
"versions" => versions.length,
|
|
361
|
+
"cache" => client.last_versions_cache_hit ? "hit" : "refreshed"
|
|
362
|
+
}
|
|
363
|
+
end
|
|
364
|
+
puts JSON.pretty_generate(
|
|
365
|
+
"schema_version" => 1,
|
|
366
|
+
"mode" => "review",
|
|
367
|
+
"ttl_days" => @options[:ttl_days],
|
|
368
|
+
"repositories" => refreshed
|
|
369
|
+
)
|
|
370
|
+
0
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def review_repositories
|
|
374
|
+
source = @options[:input].to_s
|
|
375
|
+
source = if source.empty? || source == "-"
|
|
376
|
+
$stdin.read
|
|
377
|
+
else
|
|
378
|
+
File.read(source)
|
|
379
|
+
end
|
|
380
|
+
payload = JSON.parse(source)
|
|
381
|
+
entries = if payload.is_a?(Array)
|
|
382
|
+
payload
|
|
383
|
+
elsif payload.is_a?(Hash)
|
|
384
|
+
payload["repositories"] || payload["actions"] || payload["pins"] || []
|
|
385
|
+
else
|
|
386
|
+
[]
|
|
387
|
+
end
|
|
388
|
+
entries.each_with_object([]) do |entry, repositories|
|
|
389
|
+
value = entry.is_a?(Hash) ? (entry["repository"] || entry["action"] || entry["value"]) : entry
|
|
390
|
+
repository = value.to_s.split("@", 2).first
|
|
391
|
+
repositories << repository if repository.match?(%r{\A[^/\s]+/[^/\s]+\z})
|
|
392
|
+
end.uniq.sort
|
|
393
|
+
rescue JSON::ParserError => error
|
|
394
|
+
raise Error, "invalid review JSON: #{error.message}"
|
|
395
|
+
end
|
|
297
396
|
|
|
298
397
|
def parse!
|
|
299
398
|
parser = OptionParser.new do |opt|
|
|
@@ -328,6 +427,27 @@ module Kettle
|
|
|
328
427
|
opt.on("--refresh-cache", "Bypass cached action release data and refresh discovered actions") do
|
|
329
428
|
@options[:refresh_cache] = true
|
|
330
429
|
end
|
|
430
|
+
opt.on("--ttl DAYS", Float, "Cache TTL in days (default: #{@options[:ttl_days]})") do |days|
|
|
431
|
+
Kernel.abort("Invalid --ttl value #{days.inspect}; use a non-negative number") if days.negative?
|
|
432
|
+
|
|
433
|
+
@options[:ttl_days] = days
|
|
434
|
+
end
|
|
435
|
+
opt.on("--offline", "Use only the persistent cache; fail on a cache miss") do
|
|
436
|
+
@options[:offline] = true
|
|
437
|
+
end
|
|
438
|
+
opt.on("--list", "List project action pins as JSON without network access") do
|
|
439
|
+
raise OptionParser::InvalidOption, "--list cannot be combined with --review" if @options[:mode] == :review
|
|
440
|
+
|
|
441
|
+
@options[:mode] = :list
|
|
442
|
+
end
|
|
443
|
+
opt.on("--review", "Refresh action metadata from JSON input and update the cache") do
|
|
444
|
+
raise OptionParser::InvalidOption, "--review cannot be combined with --list" if @options[:mode] == :list
|
|
445
|
+
|
|
446
|
+
@options[:mode] = :review
|
|
447
|
+
end
|
|
448
|
+
opt.on("--input PATH", "Review JSON input path, or - for stdin") do |path|
|
|
449
|
+
@options[:input] = path
|
|
450
|
+
end
|
|
331
451
|
opt.on("--cache-path PATH", "Action release cache path (default: #{@options[:cache_path]})") do |path|
|
|
332
452
|
@options[:cache_path] = path
|
|
333
453
|
end
|
|
@@ -356,6 +476,9 @@ module Kettle
|
|
|
356
476
|
end
|
|
357
477
|
end
|
|
358
478
|
parser.parse!(@argv)
|
|
479
|
+
if @options[:mode] == :review && @options[:offline]
|
|
480
|
+
raise OptionParser::InvalidOption, "--review cannot be combined with --offline"
|
|
481
|
+
end
|
|
359
482
|
end
|
|
360
483
|
|
|
361
484
|
def display_path(path)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "net/http"
|
|
5
|
+
require "open3"
|
|
5
6
|
require "timeout"
|
|
6
7
|
require "uri"
|
|
7
8
|
|
|
@@ -10,12 +11,18 @@ module Kettle
|
|
|
10
11
|
module Pins
|
|
11
12
|
# Lightweight GitHub API client for commit and release SHA resolution.
|
|
12
13
|
class GitHubClient
|
|
13
|
-
|
|
14
|
+
ANNOTATED_TAG_GIT_FALLBACK_THRESHOLD = 4
|
|
15
|
+
class CacheMissError < Error; end
|
|
16
|
+
class RefreshError < Error; end
|
|
17
|
+
|
|
18
|
+
def initialize(token:, api_base:, user_agent:, persistent_cache: nil, refresh_cache: false, offline: false, fail_on_refresh_error: false, open_timeout: DEFAULT_HTTP_OPEN_TIMEOUT_SECONDS, read_timeout: DEFAULT_HTTP_READ_TIMEOUT_SECONDS, refresh_timeout: DEFAULT_HTTP_REFRESH_TIMEOUT_SECONDS)
|
|
14
19
|
@token = token
|
|
15
20
|
@api_base = api_base
|
|
16
21
|
@user_agent = user_agent
|
|
17
22
|
@persistent_cache = persistent_cache
|
|
18
23
|
@refresh_cache = refresh_cache
|
|
24
|
+
@offline = offline
|
|
25
|
+
@fail_on_refresh_error = fail_on_refresh_error
|
|
19
26
|
@open_timeout = open_timeout
|
|
20
27
|
@read_timeout = read_timeout
|
|
21
28
|
@refresh_timeout = refresh_timeout
|
|
@@ -34,15 +41,22 @@ module Kettle
|
|
|
34
41
|
return @release_cache[repo_ref]
|
|
35
42
|
end
|
|
36
43
|
|
|
44
|
+
if @offline
|
|
45
|
+
cached = @persistent_cache&.versions_for_repo(repo_ref, fresh: false)
|
|
46
|
+
raise CacheMissError, "offline cache miss for #{repo_ref}" if cached.nil?
|
|
47
|
+
|
|
48
|
+
@last_versions_cache_hit = true
|
|
49
|
+
@release_cache[repo_ref] = cached
|
|
50
|
+
return cached
|
|
51
|
+
end
|
|
52
|
+
|
|
37
53
|
stale = nil
|
|
38
54
|
unless @refresh_cache
|
|
39
55
|
cached = @persistent_cache&.versions_for_repo(repo_ref, fresh: true)
|
|
40
56
|
if cached
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return cached
|
|
45
|
-
end
|
|
57
|
+
@last_versions_cache_hit = true
|
|
58
|
+
@release_cache[repo_ref] = cached
|
|
59
|
+
return cached
|
|
46
60
|
end
|
|
47
61
|
stale = @persistent_cache&.versions_for_repo(repo_ref, fresh: false)
|
|
48
62
|
end
|
|
@@ -50,10 +64,10 @@ module Kettle
|
|
|
50
64
|
releases = nil
|
|
51
65
|
Timeout.timeout(@refresh_timeout) do
|
|
52
66
|
data = request_json("/repos/#{repo_ref}/releases?per_page=100")
|
|
53
|
-
return
|
|
67
|
+
return refresh_failed!(repo_ref, stale, "invalid releases response") unless data.is_a?(Array)
|
|
54
68
|
|
|
55
69
|
tag_shas = tag_ref_shas(repo_ref)
|
|
56
|
-
return
|
|
70
|
+
return refresh_failed!(repo_ref, stale, "invalid tag response") unless tag_shas
|
|
57
71
|
|
|
58
72
|
releases = build_release_versions(data, tag_shas)
|
|
59
73
|
end
|
|
@@ -61,7 +75,7 @@ module Kettle
|
|
|
61
75
|
@release_cache[repo_ref] = releases
|
|
62
76
|
releases
|
|
63
77
|
rescue Timeout::Error
|
|
64
|
-
|
|
78
|
+
refresh_failed!(repo_ref, stale, "GitHub refresh timed out")
|
|
65
79
|
end
|
|
66
80
|
|
|
67
81
|
def commit_sha(repo_ref, ref, refresh: false)
|
|
@@ -71,13 +85,15 @@ module Kettle
|
|
|
71
85
|
return @commit_cache[cache_key] if @commit_cache.key?(cache_key) && !refresh
|
|
72
86
|
|
|
73
87
|
unless @refresh_cache || refresh
|
|
74
|
-
cached = @persistent_cache&.ref_sha(repo_ref, ref, fresh:
|
|
88
|
+
cached = @persistent_cache&.ref_sha(repo_ref, ref, fresh: !@offline)
|
|
75
89
|
if cached
|
|
76
90
|
@commit_cache[cache_key] = cached
|
|
77
91
|
return cached
|
|
78
92
|
end
|
|
79
93
|
end
|
|
80
94
|
|
|
95
|
+
raise CacheMissError, "offline cache miss for #{repo_ref}@#{ref}" if @offline
|
|
96
|
+
|
|
81
97
|
data = request_json("/repos/#{repo_ref}/commits/#{uri_encode(ref)}")
|
|
82
98
|
sha = if data.is_a?(Hash)
|
|
83
99
|
data.fetch("sha", "")[0, 40]
|
|
@@ -106,24 +122,10 @@ module Kettle
|
|
|
106
122
|
versions
|
|
107
123
|
end
|
|
108
124
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
# only when that probe identifies a tag or target the cache lacks.
|
|
112
|
-
def cache_requires_refresh?(repo_ref, cached)
|
|
113
|
-
latest = request_json("/repos/#{repo_ref}/releases/latest")
|
|
114
|
-
return false unless latest.is_a?(Hash)
|
|
115
|
-
|
|
116
|
-
tag = latest["tag_name"].to_s
|
|
117
|
-
return false unless VersionRubric.parse(tag)
|
|
118
|
-
|
|
119
|
-
cached_entry = cached.find { |entry| entry[:tag].to_s == tag }
|
|
120
|
-
return true unless cached_entry
|
|
125
|
+
def refresh_failed!(repo_ref, stale, message)
|
|
126
|
+
return cached_versions(repo_ref, stale) unless @fail_on_refresh_error
|
|
121
127
|
|
|
122
|
-
|
|
123
|
-
cached_sha = cached_entry[:sha].to_s
|
|
124
|
-
return !latest_sha.to_s.empty? if cached_sha.empty?
|
|
125
|
-
|
|
126
|
-
!latest_sha.to_s.empty? && latest_sha != cached_sha
|
|
128
|
+
raise RefreshError, "#{message} for #{repo_ref}"
|
|
127
129
|
end
|
|
128
130
|
|
|
129
131
|
def build_release_versions(data, tag_shas)
|
|
@@ -150,7 +152,7 @@ module Kettle
|
|
|
150
152
|
data = request_json("/repos/#{repo_ref}/git/matching-refs/tags/")
|
|
151
153
|
return nil unless data.is_a?(Array)
|
|
152
154
|
|
|
153
|
-
data.each_with_object(
|
|
155
|
+
entries = data.each_with_object([]) do |entry, memo|
|
|
154
156
|
ref = entry["ref"].to_s
|
|
155
157
|
next unless ref.start_with?("refs/tags/")
|
|
156
158
|
|
|
@@ -160,16 +162,51 @@ module Kettle
|
|
|
160
162
|
object = entry["object"]
|
|
161
163
|
next unless object.is_a?(Hash)
|
|
162
164
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
+
memo << [tag, object["type"].to_s, object["sha"].to_s[0, 40]]
|
|
166
|
+
end
|
|
167
|
+
annotated_tag_shas = git_tag_ref_shas(repo_ref) if entries.count { |_, type, _| type == "tag" } > ANNOTATED_TAG_GIT_FALLBACK_THRESHOLD
|
|
168
|
+
|
|
169
|
+
entries.each_with_object({}) do |(tag, type, sha), memo|
|
|
170
|
+
case type
|
|
165
171
|
when "commit"
|
|
166
172
|
memo[tag] = sha
|
|
167
173
|
when "tag"
|
|
168
|
-
memo[tag] = annotated_tag_commit_sha(repo_ref, sha)
|
|
174
|
+
memo[tag] = annotated_tag_shas&.fetch(tag, nil) || annotated_tag_commit_sha(repo_ref, sha)
|
|
169
175
|
end
|
|
170
176
|
end
|
|
171
177
|
end
|
|
172
178
|
|
|
179
|
+
# The REST API exposes annotated tags as tag objects, requiring one
|
|
180
|
+
# additional request per tag to reach the commit. A single Git
|
|
181
|
+
# protocol exchange returns both tag objects and peeled commit refs,
|
|
182
|
+
# avoiding a refresh timeout for repositories with large tag histories.
|
|
183
|
+
def git_tag_ref_shas(repo_ref)
|
|
184
|
+
return unless @api_base.to_s.sub(%r{/\z}, "") == API_BASE
|
|
185
|
+
|
|
186
|
+
stdout, _stderr, status = Open3.capture3(
|
|
187
|
+
{"GIT_TERMINAL_PROMPT" => "0"},
|
|
188
|
+
"git",
|
|
189
|
+
"ls-remote",
|
|
190
|
+
"--tags",
|
|
191
|
+
"https://github.com/#{repo_ref}.git"
|
|
192
|
+
)
|
|
193
|
+
return unless status.success?
|
|
194
|
+
|
|
195
|
+
stdout.each_line.each_with_object({}) do |line, memo|
|
|
196
|
+
sha, ref = line.split
|
|
197
|
+
next unless SHA_RE.match?(sha.to_s)
|
|
198
|
+
next unless ref.to_s.start_with?("refs/tags/")
|
|
199
|
+
|
|
200
|
+
peeled = ref.end_with?("^{}")
|
|
201
|
+
tag = ref.sub(%r{\Arefs/tags/}, "").sub(/\^\{\}\z/, "")
|
|
202
|
+
next unless VersionRubric.parse(tag)
|
|
203
|
+
|
|
204
|
+
memo[tag] = sha if peeled || !memo.key?(tag)
|
|
205
|
+
end
|
|
206
|
+
rescue IOError, SystemCallError
|
|
207
|
+
nil
|
|
208
|
+
end
|
|
209
|
+
|
|
173
210
|
def annotated_tag_commit_sha(repo_ref, tag_sha)
|
|
174
211
|
return nil if tag_sha.to_s.empty?
|
|
175
212
|
|
|
@@ -54,8 +54,15 @@ module Kettle
|
|
|
54
54
|
return if repo_ref.to_s.empty?
|
|
55
55
|
|
|
56
56
|
action = data.fetch("actions")[repo_ref] ||= {}
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
previous_versions = action["versions"] || {}
|
|
58
|
+
previous_tags = previous_versions.values.each_with_object([]) do |entry, tags|
|
|
59
|
+
tag = entry["tag"].to_s
|
|
60
|
+
tags << tag unless tag.empty?
|
|
61
|
+
end
|
|
62
|
+
stored_versions = {}
|
|
63
|
+
refs = (action["refs"] || {}).reject { |ref, _entry| previous_tags.include?(ref) }
|
|
64
|
+
action["versions"] = stored_versions
|
|
65
|
+
action["refs"] = refs
|
|
59
66
|
timestamp = @clock.call.utc.iso8601
|
|
60
67
|
|
|
61
68
|
versions.each do |entry|
|
|
@@ -6,7 +6,7 @@ module Kettle
|
|
|
6
6
|
# Version namespace for this gem.
|
|
7
7
|
module Version
|
|
8
8
|
# Current gem version.
|
|
9
|
-
VERSION = "0.3.
|
|
9
|
+
VERSION = "0.3.11"
|
|
10
10
|
end
|
|
11
11
|
# Current gem version exposed at the traditional constant location.
|
|
12
12
|
VERSION = Version::VERSION # Traditional Constant Location
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kettle-gha-pins
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.11
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter H. Boling
|
|
@@ -301,10 +301,10 @@ licenses:
|
|
|
301
301
|
- MIT
|
|
302
302
|
metadata:
|
|
303
303
|
homepage_uri: https://github.com/kettle-dev/kettle-gha-pins
|
|
304
|
-
source_code_uri: https://github.com/kettle-dev/kettle-gha-pins/tree/v0.3.
|
|
305
|
-
changelog_uri: https://github.com/kettle-dev/kettle-gha-pins/blob/v0.3.
|
|
304
|
+
source_code_uri: https://github.com/kettle-dev/kettle-gha-pins/tree/v0.3.11
|
|
305
|
+
changelog_uri: https://github.com/kettle-dev/kettle-gha-pins/blob/v0.3.11/CHANGELOG.md
|
|
306
306
|
bug_tracker_uri: https://github.com/kettle-dev/kettle-gha-pins/issues
|
|
307
|
-
documentation_uri: https://www.rubydoc.info/gems/kettle-gha-pins/0.3.
|
|
307
|
+
documentation_uri: https://www.rubydoc.info/gems/kettle-gha-pins/0.3.11
|
|
308
308
|
funding_uri: https://github.com/sponsors/pboling
|
|
309
309
|
wiki_uri: https://github.com/kettle-dev/kettle-gha-pins/wiki
|
|
310
310
|
news_uri: https://www.railsbling.com/tags/kettle-gha-pins
|
metadata.gz.sig
CHANGED
|
Binary file
|