puppetlabs-onceover 5.0.4 → 5.0.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e95581c2065fec671dd17a38351cbb3ea0d0a196c83e814c9f3ec1122ba1cbd9
4
- data.tar.gz: aaf03ff8479d236f38d53e86199d5787136761c2f6e35da34db4665d56d58fa9
3
+ metadata.gz: c991dea08d7eb606b8f13d8cd9c270ef3e7fba1ce7b3adb1761f212cecbc1d5c
4
+ data.tar.gz: ebb53a42b2538e507d1d40db33787efdd33eacf81e4b22ceec03229244c27d40
5
5
  SHA512:
6
- metadata.gz: 43b88d11469774667a11c9e73549497bf94ffae4180e6bcab33b8dd196cfbd25d76f438ed9d09c6bc496dee3dde39f2465d3db027b84eca75ff63748adbd3056
7
- data.tar.gz: c09f20f9eed9c02347dd79857d4ee6018d07370c09f3ba3a4c25c7a0e468cb61863b56d80f929503704f8529ef1a0925ef2f771b16e98f3a0f14672547c67d73
6
+ metadata.gz: fe347612ab83e67a31f1850db126ce95e9da6e6a4e8dd8ec77cb6f605db8c5ddb3a20e7e1edcee5aa998d42b8f544433ee0d5e400fee636ef7e89dc1004dcf84
7
+ data.tar.gz: eca5387e7eebb1b790cdabb9917722b44b6312622bf6fae26e3b3ec0593ba917769b190feca48605cb6d12fc85f5a5c2dabbe716eae0aa94c882f9040e53ef44
data/CHANGELOG.md CHANGED
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
5
5
 
6
6
  The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org).
7
7
 
8
+ ## [v5.0.5](https://github.com/puppetlabs/puppetlabs-onceover/tree/v5.0.5) - 2026-09-15
9
+
10
+ [Full Changelog](https://github.com/puppetlabs/puppetlabs-onceover/compare/v5.0.4...v5.0.5)
11
+
12
+ ### Other
13
+
14
+ - Replace GitHub REST tag lookups with git ls-remote to fix rate-limit CI failures [#28](https://github.com/puppetlabs/puppetlabs-onceover/pull/28) ([actowery](https://github.com/actowery))
15
+ - Pin json < 3.0 to fix r10k module deploy arity error [#27](https://github.com/puppetlabs/puppetlabs-onceover/pull/27) ([actowery](https://github.com/actowery))
16
+ - Fix ERB.new calls broken on Ruby 4.0 (rake_tasks.rb) [#26](https://github.com/puppetlabs/puppetlabs-onceover/pull/26) ([actowery](https://github.com/actowery))
17
+ - Pin facter to Puppetcore, fix PUPPET_FORGE_TOKEN truthiness bug [#25](https://github.com/puppetlabs/puppetlabs-onceover/pull/25) ([actowery](https://github.com/actowery))
18
+
8
19
  ## [v5.0.4](https://github.com/puppetlabs/puppetlabs-onceover/tree/v5.0.4) - 2026-09-03
9
20
 
10
21
  [Full Changelog](https://github.com/puppetlabs/puppetlabs-onceover/compare/v5.0.3...v5.0.4)
data/Gemfile CHANGED
@@ -2,7 +2,7 @@
2
2
  gemsource_default = ENV['GEM_SOURCE'] || 'https://rubygems.org'
3
3
  gemsource_puppetcore = if ENV['GEM_SOURCE']
4
4
  gemsource_default
5
- elsif ENV['PUPPET_FORGE_TOKEN']
5
+ elsif ENV['PUPPET_FORGE_TOKEN'] && !ENV['PUPPET_FORGE_TOKEN'].empty?
6
6
  'https://rubygems-puppetcore.puppet.com'
7
7
  else
8
8
  ENV['GEM_SOURCE_PUPPETCORE'] || gemsource_default
@@ -20,9 +20,28 @@ gem 'pry-coolline', '> 0.0', '< 1.0.0'
20
20
  # and no auth is attempted against Puppetcore.
21
21
  gem 'puppet', ENV['PUPPET_GEM_VERSION'] || '~> 8', source: gemsource_puppetcore
22
22
 
23
+ # facter is a runtime dependency of puppet, but pinning `puppet`'s own source
24
+ # does NOT pin its transitive dependencies to that same source -- bundler
25
+ # still resolves facter from whichever source(s) declare it, so without this
26
+ # explicit pin facter would silently keep resolving from public rubygems.org
27
+ # (built from the public facter repo) even when puppet itself correctly comes
28
+ # from Puppetcore (built from the private facter-private source). Pin it
29
+ # explicitly so both come from the same place.
30
+ gem 'facter', source: gemsource_puppetcore
31
+
23
32
  # Puppet on Ruby 3.3 / 3.4 has some missing dependencies
24
33
  gem 'syslog', '~> 0.3' if RUBY_VERSION >= '3.4'
25
34
 
35
+ # json 3.0 made JSON.parse keyword-only (dropped the legacy positional-hash
36
+ # opts argument). faraday's JSON response middleware
37
+ # (Faraday::Response::Json#parse) still calls `decoder.public_send(method,
38
+ # body, @parser_options || {})` positionally, so with json ~> 3.0 every
39
+ # Forge/HTTP JSON response r10k parses raises
40
+ # `wrong number of arguments (given 2, expected 1)`. Pin to the last
41
+ # compatible major until faraday releases a fix for json 3.0's new
42
+ # signature.
43
+ gem 'json', '< 3.0'
44
+
26
45
  # Windows platform runtime deps. The published puppet/openvox rubygems.org
27
46
  # artefacts are built on Linux and guard `ffi` / `win32ole` with build-host
28
47
  # platform checks, so those deps never make it into the Linux-published
@@ -40,7 +40,7 @@ task :generate_onceover_yaml do
40
40
  repo = PuppetlabsOnceover::Controlrepo.new
41
41
  template_dir = File.expand_path('../../templates', File.dirname(__FILE__))
42
42
  onceover_yaml_template = File.read(File.expand_path('./controlrepo.yaml.erb', template_dir))
43
- puts ERB.new(onceover_yaml_template, nil, '-').result(binding)
43
+ puts ERB.new(onceover_yaml_template, trim_mode: '-').result(binding)
44
44
  end
45
45
 
46
46
  task :generate_nodesets do
@@ -81,7 +81,7 @@ task :generate_nodesets do
81
81
  # Use an ERB template
82
82
  template_dir = File.expand_path('../../templates', File.dirname(__FILE__))
83
83
  fixtures_template = File.read(File.expand_path('./nodeset.yaml.erb', template_dir))
84
- puts ERB.new(fixtures_template, nil, '-').result(binding)
84
+ puts ERB.new(fixtures_template, trim_mode: '-').result(binding)
85
85
  end
86
86
  end
87
87
 
@@ -1,6 +1,5 @@
1
1
  require 'puppet/version'
2
- require 'net/http'
3
- require 'uri'
2
+ require 'git'
4
3
  require 'multi_json'
5
4
  require 'r10k/module_loader/puppetfile'
6
5
  require 'puppetlabs-onceover/logger'
@@ -69,16 +68,18 @@ class PuppetlabsOnceover
69
68
  # any puppet version this gem tests against). Neither is needed: the
70
69
  # module list is stable (see CORE_MODULES) and each module's own repo
71
70
  # is the authoritative source for its own latest version.
72
- # https://docs.github.com/en/rest/repos/repos#list-repository-tags
71
+ #
72
+ # Tags are read via the git protocol (git ls-remote) rather than
73
+ # GitHub's REST API: the REST API's unauthenticated rate limit
74
+ # (60 req/hr) is shared across every GitHub-hosted Actions runner IP,
75
+ # so 10 sequential lookups per test run reliably exhausted it in CI.
76
+ # git's smart-HTTP protocol has no equivalent per-hour cap.
73
77
  @vendored_references = CORE_MODULES.map do |mod_name|
74
- tags = query_or_cache(
75
- "https://api.github.com/repos/puppetlabs/puppetlabs-#{mod_name}/tags",
76
- nil,
77
- component_cache(mod_name),
78
- )
78
+ repo_url = "https://github.com/puppetlabs/puppetlabs-#{mod_name}.git"
79
+ tags = query_or_cache(repo_url, component_cache(mod_name))
79
80
  latest_tag = tags.first['name']
80
81
  {
81
- 'url' => "https://github.com/puppetlabs/puppetlabs-#{mod_name}.git",
82
+ 'url' => repo_url,
82
83
  'ref' => "refs/tags/#{latest_tag}"
83
84
  }
84
85
  end
@@ -153,40 +154,36 @@ class PuppetlabsOnceover
153
154
  end
154
155
  end
155
156
 
156
- # Return json from a query whom caches, or from the cache to avoid spamming github
157
- def query_or_cache(url, params, filepath)
157
+ # Return tag data from a query whom caches, or from the cache to avoid
158
+ # re-querying the remote on every run
159
+ def query_or_cache(repo_url, filepath)
158
160
  if (File.exist? filepath) && (@force_update == false)
159
161
  logger.debug "Using cache: #{filepath}"
160
162
  json = read_json_dump(filepath)
161
163
  else
162
- logger.debug "Making GET request to: #{url}"
163
- json = github_get(url, params)
164
+ logger.debug "Listing tags for: #{repo_url}"
165
+ json = remote_tags(repo_url)
164
166
  logger.debug "Caching response to: #{filepath}"
165
167
  write_json_dump(filepath, json)
166
168
  end
167
169
  json
168
170
  end
169
171
 
170
- # Given a github url and optional query parameters, return the parsed json body
171
- def github_get(url, params)
172
- uri = URI.parse(url)
173
- uri.query = URI.encode_www_form(params) if params
174
- http = Net::HTTP.new(uri.host, uri.port)
175
- http.use_ssl = (uri.scheme == 'https')
176
- request = Net::HTTP::Get.new(uri.request_uri)
177
- request['Accept'] = 'application/vnd.github.raw+json'
178
- request['X-GitHub-Api-Version'] = '2022-11-28'
179
- response = http.request(request)
180
-
181
- case response
182
- when Net::HTTPOK # 200
183
- MultiJson.load(response.body)
184
- else
185
- # Expose the ratelimit response headers
186
- # https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28#checking-the-status-of-your-rate-limit
187
- ratelimit_headers = response.to_hash.select { |k, _v| k =~ /x-ratelimit.*/ }
188
- raise "#{response.code} #{response.message} #{ratelimit_headers}"
172
+ # Given a git remote url, return its tags as [{ 'name' => tag }, ...],
173
+ # sorted newest version first (matches the shape/ordering the old
174
+ # GitHub REST tags API returned, so callers can keep using .first).
175
+ def remote_tags(repo_url)
176
+ refs = Git.ls_remote(repo_url, refs: true)['tags'] || {}
177
+ versions = refs.keys.filter_map do |name|
178
+ next if name.end_with?('^{}') # annotated-tag dereference entries
179
+
180
+ begin
181
+ [Gem::Version.new(name.delete_prefix('v')), name]
182
+ rescue ArgumentError
183
+ nil # skip non-version-shaped tags
184
+ end
189
185
  end
186
+ versions.sort_by(&:first).reverse.map { |_version, name| { 'name' => name } }
190
187
  end
191
188
 
192
189
  # Returns parsed json of file
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class PuppetlabsOnceover
4
- VERSION = '5.0.4'
4
+ VERSION = '5.0.5'
5
5
  end
@@ -87,17 +87,19 @@ describe 'rake_tasks.rb' do
87
87
  end
88
88
 
89
89
  describe 'generate_onceover_yaml' do
90
- it 'reads the controlrepo and attempts to render the controlrepo.yaml.erb template' do
91
- repo = double('repo')
90
+ it 'reads the controlrepo and renders the controlrepo.yaml.erb template' do
91
+ repo = double('repo', roles: %w[role::web role::db], facts_files: ['/some/repo/facts/centos7.json', '/some/repo/facts/Windows_Server-2019.json'])
92
92
  allow(PuppetlabsOnceover::Controlrepo).to receive(:new).and_return(repo)
93
93
 
94
- # NOTE: the bundled erb gem (6.0.7, from Ruby 3.3.12) no longer accepts
95
- # ERB.new's legacy positional (safe_level, trim_mode) arguments, which
96
- # rake_tasks.rb still passes. This is a pre-existing incompatibility in
97
- # production code, not something introduced by this test. We assert the
98
- # task reaches and exercises that ERB.new call (line coverage), rather
99
- # than papering over the ArgumentError it currently raises in this Ruby.
100
- expect { Rake::Task['generate_onceover_yaml'].invoke }.to raise_error(ArgumentError)
94
+ # ERB.new's legacy positional (safe_level, trim_mode) arguments were
95
+ # removed in the bundled erb gem (6.0.7+, Ruby 4.0) -- rake_tasks.rb
96
+ # used to pass them positionally, which raised ArgumentError on that
97
+ # Ruby before this fix. Now that it's fixed (keyword trim_mode:), this
98
+ # asserts the template actually renders real content from the repo
99
+ # double, not just that it "reaches" the call.
100
+ expect { Rake::Task['generate_onceover_yaml'].invoke }.to output(
101
+ a_string_including('role::web', 'role::db', 'centos7', 'Windows_Server-2019', 'Windows_Server-2019')
102
+ ).to_stdout
101
103
  end
102
104
  end
103
105
 
@@ -117,12 +119,16 @@ describe 'rake_tasks.rb' do
117
119
  allow(Net::HTTP).to receive(:new).and_return(http_double)
118
120
  allow(http_double).to receive(:get).and_return(response)
119
121
 
120
- # See the note in the generate_onceover_yaml spec above: the bundled
121
- # erb 6.0.7 no longer accepts ERB.new's legacy positional arguments,
122
- # so this task raises ArgumentError once it reaches its own ERB.new
123
- # call further down. We assert the "HOSTS:" header and 404 branch
124
- # (comment_out = true) execute first, covering those lines.
125
- expect { Rake::Task['generate_nodesets'].invoke }.to raise_error(ArgumentError).and output(/HOSTS:/).to_stdout
122
+ # This deprecated task (Beaker removed) has a second, deeper,
123
+ # pre-existing bug beyond the ERB.new Ruby-4.0 incompatibility fixed
124
+ # elsewhere: nodeset.yaml.erb expects a `hosts_hash` local (and a
125
+ # `prefix` local) that the task body never actually defines -- it
126
+ # builds flat locals (node_name/boxname/platform/url/comment_out)
127
+ # instead. Out of scope to redesign a deprecated task here; asserting
128
+ # the real resulting error (and that execution reaches it, covering
129
+ # the "HOSTS:" header and 404 branch first) rather than the old,
130
+ # now-fixed ArgumentError.
131
+ expect { Rake::Task['generate_nodesets'].invoke }.to raise_error(NameError, /hosts_hash/).and output(/HOSTS:/).to_stdout
126
132
  end
127
133
 
128
134
  it 'writes a nodeset entry using the virtualbox provider url when the box lookup succeeds' do
@@ -150,8 +156,8 @@ describe 'rake_tasks.rb' do
150
156
 
151
157
  # As above: covers the successful (non-404) lookup branch, including
152
158
  # the virtualbox provider url extraction, before hitting the same
153
- # pre-existing ERB.new incompatibility.
154
- expect { Rake::Task['generate_nodesets'].invoke }.to raise_error(ArgumentError).and output(/HOSTS:/).to_stdout
159
+ # pre-existing hosts_hash/prefix bug described above.
160
+ expect { Rake::Task['generate_nodesets'].invoke }.to raise_error(NameError, /hosts_hash/).and output(/HOSTS:/).to_stdout
155
161
  end
156
162
  end
157
163
 
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
  require 'tmpdir'
3
3
  require 'fileutils'
4
4
  require 'multi_json'
5
- require 'net/http'
5
+ require 'git'
6
6
  require 'puppetlabs-onceover/controlrepo' # pulls in `include PuppetlabsOnceover::Logger` at top-level
7
7
  require 'puppetlabs-onceover/vendored_modules'
8
8
 
@@ -50,7 +50,7 @@ describe PuppetlabsOnceover::VendoredModules do
50
50
  it 'creates it via FileUtils.mkdir_p' do
51
51
  cachedir = File.join(tempdir, 'vendored_modules')
52
52
  expect(File.directory?(cachedir)).to be false
53
- allow_any_instance_of(described_class).to receive(:github_get).and_return([{ 'name' => 'v9.9.9' }])
53
+ allow_any_instance_of(described_class).to receive(:remote_tags).and_return([{ 'name' => 'v9.9.9' }])
54
54
  described_class.new(repo: repo_double)
55
55
  expect(File.directory?(cachedir)).to be true
56
56
  end
@@ -67,9 +67,9 @@ describe PuppetlabsOnceover::VendoredModules do
67
67
  end
68
68
 
69
69
  context 'when opts[:force_update] is true' do
70
- it 'always calls github_get, ignoring any existing cache' do
70
+ it 'always calls remote_tags, ignoring any existing cache' do
71
71
  populate_cache(File.join(tempdir, 'vendored_modules'))
72
- expect_any_instance_of(described_class).to receive(:github_get).at_least(:once).and_return([{ 'name' => 'v5.0.0' }])
72
+ expect_any_instance_of(described_class).to receive(:remote_tags).at_least(:once).and_return([{ 'name' => 'v5.0.0' }])
73
73
  described_class.new(repo: repo_double, force_update: true)
74
74
  end
75
75
  end
@@ -223,67 +223,58 @@ describe PuppetlabsOnceover::VendoredModules do
223
223
  it 'reads from the cache file when it already exists' do
224
224
  cache_file = File.join(Dir.mktmpdir, 'cached.json')
225
225
  File.write(cache_file, MultiJson.dump([{ 'name' => 'v1.0.0' }]))
226
- expect(vm).not_to receive(:github_get)
227
- result = vm.query_or_cache('https://api.github.com/repos/puppetlabs/x/tags', nil, cache_file)
226
+ expect(vm).not_to receive(:remote_tags)
227
+ result = vm.query_or_cache('https://github.com/puppetlabs/x.git', cache_file)
228
228
  expect(result).to eq([{ 'name' => 'v1.0.0' }])
229
229
  end
230
230
 
231
- it 'calls github_get and writes the cache when the file does not exist' do
231
+ it 'calls remote_tags and writes the cache when the file does not exist' do
232
232
  cache_file = File.join(Dir.mktmpdir, 'missing.json')
233
- allow(vm).to receive(:github_get).and_return([{ 'name' => 'v2.0.0' }])
234
- result = vm.query_or_cache('https://api.github.com/repos/puppetlabs/x/tags', nil, cache_file)
233
+ allow(vm).to receive(:remote_tags).and_return([{ 'name' => 'v2.0.0' }])
234
+ result = vm.query_or_cache('https://github.com/puppetlabs/x.git', cache_file)
235
235
  expect(result).to eq([{ 'name' => 'v2.0.0' }])
236
236
  expect(File.exist?(cache_file)).to be true
237
237
  expect(MultiJson.load(File.read(cache_file))).to eq([{ 'name' => 'v2.0.0' }])
238
238
  end
239
239
  end
240
240
 
241
- describe '#github_get' do
241
+ describe '#remote_tags' do
242
242
  subject(:vm) do
243
243
  populate_cache(File.join(tempdir, 'vendored_modules'))
244
244
  described_class.new(repo: repo_double)
245
245
  end
246
246
 
247
- it 'returns the parsed json body on a 200 response' do
248
- response = Net::HTTPOK.new('1.1', '200', 'OK')
249
- def response.body
250
- MultiJson.dump([{ 'name' => 'v3.0.0' }])
251
- end
252
- http = instance_double(Net::HTTP)
253
- allow(Net::HTTP).to receive(:new).and_return(http)
254
- allow(http).to receive(:use_ssl=)
255
- allow(http).to receive(:request).and_return(response)
256
-
257
- result = vm.github_get('https://api.github.com/repos/puppetlabs/puppetlabs-augeas_core/tags', nil)
258
- expect(result).to eq([{ 'name' => 'v3.0.0' }])
247
+ it 'returns tags sorted newest version first' do
248
+ allow(Git).to receive(:ls_remote).and_return(
249
+ 'tags' => {
250
+ 'v1.0.0' => { ref: 'refs/tags/v1.0.0', sha: 'a' },
251
+ 'v2.0.1' => { ref: 'refs/tags/v2.0.1', sha: 'b' },
252
+ 'v2.0.1^{}' => { ref: 'refs/tags/v2.0.1^{}', sha: 'c' },
253
+ '1.5.0' => { ref: 'refs/tags/1.5.0', sha: 'd' }
254
+ }
255
+ )
256
+
257
+ result = vm.remote_tags('https://github.com/puppetlabs/puppetlabs-augeas_core.git')
258
+ expect(result).to eq([{ 'name' => 'v2.0.1' }, { 'name' => '1.5.0' }, { 'name' => 'v1.0.0' }])
259
259
  end
260
260
 
261
- it 'passes along query params when given' do
262
- response = Net::HTTPOK.new('1.1', '200', 'OK')
263
- def response.body
264
- MultiJson.dump([{ 'name' => 'v4.0.0' }])
265
- end
266
- http = instance_double(Net::HTTP)
267
- allow(Net::HTTP).to receive(:new).and_return(http)
268
- allow(http).to receive(:use_ssl=)
269
- allow(http).to receive(:request).and_return(response)
261
+ it 'skips non-version-shaped tags' do
262
+ allow(Git).to receive(:ls_remote).and_return(
263
+ 'tags' => {
264
+ 'v1.0.0' => { ref: 'refs/tags/v1.0.0', sha: 'a' },
265
+ 'not-a-version' => { ref: 'refs/tags/not-a-version', sha: 'b' }
266
+ }
267
+ )
270
268
 
271
- result = vm.github_get('https://api.github.com/repos/puppetlabs/puppetlabs-augeas_core/tags', { 'per_page' => '1' })
272
- expect(result).to eq([{ 'name' => 'v4.0.0' }])
269
+ result = vm.remote_tags('https://github.com/puppetlabs/puppetlabs-augeas_core.git')
270
+ expect(result).to eq([{ 'name' => 'v1.0.0' }])
273
271
  end
274
272
 
275
- it 'raises with the ratelimit headers on a non-200 response' do
276
- response = Net::HTTPForbidden.new('1.1', '403', 'Forbidden')
277
- def response.to_hash
278
- { 'x-ratelimit-remaining' => ['0'] }
279
- end
280
- http = instance_double(Net::HTTP)
281
- allow(Net::HTTP).to receive(:new).and_return(http)
282
- allow(http).to receive(:use_ssl=)
283
- allow(http).to receive(:request).and_return(response)
273
+ it 'returns an empty array when the remote has no tags' do
274
+ allow(Git).to receive(:ls_remote).and_return({})
284
275
 
285
- expect { vm.github_get('https://api.github.com/repos/puppetlabs/puppetlabs-augeas_core/tags', nil) }
286
- .to raise_error(/403 Forbidden/)
276
+ result = vm.remote_tags('https://github.com/puppetlabs/puppetlabs-augeas_core.git')
277
+ expect(result).to eq([])
287
278
  end
288
279
  end
289
280
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: puppetlabs-onceover
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.4
4
+ version: 5.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet, Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-03 00:00:00.000000000 Z
11
+ date: 2026-09-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: backticks