puppet_metadata 5.3.0 → 6.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd19aa83607d45feded94b42519f1d8b31e19ca4806d36b5adf6031e74f7a709
4
- data.tar.gz: c88d61173b62fc78a4f40f85d84add61933071b5f7d66000b702dd2ac3b4e6c1
3
+ metadata.gz: 9ac5190193748a7ec1f0d61f9c7ef2bac509bb78e1e854b373362e421cf4a656
4
+ data.tar.gz: dcafc939d26360e09cd7525b72ca2e4268802d2e4103d4c61b14570a4700a402
5
5
  SHA512:
6
- metadata.gz: 725d91a9e780c4cb4249f62e76beb055c64847bf11352e5c8be776139ec8cb266cca6fc3ec2927b81628f2ee8e758a3985e80d2487b6da5e53946478f2e56ac0
7
- data.tar.gz: 50ecf56b078348a0b60356b8eebc0b645bb945ae7cab4c09601d5e42e5a544ffaba7d8b84cc0214567297c73254a77a9485f90e5dbf4b8d3c610bc8fbfc43500
6
+ metadata.gz: 7e18f077b5298afce2a703c1c1c66d941f447a72740dc29105b57d9cd1cb929ed4c4a537a021950ca4ed2c1cda06eaf40d3f1e8c927f7b05212b1f241e0db331
7
+ data.tar.gz: b16239f27e4a1c74174816fe1b59bd28f1503f4839fc52bfc952f3c6ce1fe3d70bdccacb138f136e1f75a1423ef1537d574c07b2625bc044a8a12094ade701a6
data/README.md CHANGED
@@ -10,6 +10,120 @@
10
10
 
11
11
  The gem intends to provide an abstraction over Puppet's metadata.json file. Its API allow easy iteration over its illogical data structures.
12
12
 
13
+ - [puppet\_metadata](#puppet_metadata)
14
+ - [New CLI interface in 6.0.0](#new-cli-interface-in-600)
15
+ - [Manage OS versions in metadata.json](#manage-os-versions-in-metadatajson)
16
+ - [List supported OS versions](#list-supported-os-versions)
17
+ - [Add missing supported OS versions](#add-missing-supported-os-versions)
18
+ - [Remove EOL OS versions](#remove-eol-os-versions)
19
+ - [Generating Github Actions outputs](#generating-github-actions-outputs)
20
+ - [Work with the API](#work-with-the-api)
21
+ - [List all supported operating systems](#list-all-supported-operating-systems)
22
+ - [List supported major puppet versions](#list-supported-major-puppet-versions)
23
+ - [Check if an operating systems is supported](#check-if-an-operating-systems-is-supported)
24
+ - [Get all versions for an Operating System that are not EoL](#get-all-versions-for-an-operating-system-that-are-not-eol)
25
+ - [Get all versions for an Operating System that are not EoL after a certain date](#get-all-versions-for-an-operating-system-that-are-not-eol-after-a-certain-date)
26
+ - [Updating OS EOL dates](#updating-os-eol-dates)
27
+ - [Adding new operating systems](#adding-new-operating-systems)
28
+ - [List supported setfiles](#list-supported-setfiles)
29
+ - [Transfer Notice](#transfer-notice)
30
+ - [License](#license)
31
+ - [Release information](#release-information)
32
+
33
+ ## New CLI interface in 6.0.0
34
+
35
+ Version 6.0.0 introduces a new CLI interface, in `bin/puppet-metadata`.
36
+ It provides a new way of handling default CLI options, like the path to the metadata.json.
37
+
38
+ ```
39
+ $ bundle exec bin/puppet-metadata --help
40
+ Usage: puppet-metadata [options] <action> [options]
41
+ --filename METADATA Metadata filename
42
+
43
+ ACTIONS
44
+ os-versions Manage operating system versions in metadata.json
45
+ setfiles Show the various setfiles supported by the metadata
46
+
47
+ See 'puppet-metadata ACTION --help' for more information on a specific action.
48
+ ```
49
+
50
+ `--filename ` is optional.
51
+ If ommitted, a metadata.json in the current directory will be parsed.
52
+
53
+ Each action is implemented as a file in `lib/puppet_metadata/command/*rb` and automatically loaded via `lib/puppet_metadata/command.rb`.
54
+
55
+ ## Manage OS versions in metadata.json
56
+
57
+ The `os-versions` command provides a unified interface to view, add, and remove operating system versions in the metadata.json.
58
+
59
+ ### List supported OS versions
60
+
61
+ By default, `os-versions` shows which OS versions in your metadata.json are still supported and which are EOL:
62
+
63
+ ```
64
+ $ bundle exec puppet-metadata os-versions
65
+ module-name supports these non-EOL operating system versions:
66
+ AlmaLinux: 8, 9
67
+ CentOS: 9
68
+ Debian: 11, 12
69
+ OracleLinux: 8, 9, 10
70
+ RedHat: 8, 9
71
+ Rocky: 8, 9
72
+ Ubuntu: 22.04, 24.04
73
+
74
+ module-name supports these EOL operating system versions:
75
+ Fedora: 40
76
+ Ubuntu: 20.04
77
+ ```
78
+
79
+ You can filter to a specific OS:
80
+
81
+ ```
82
+ $ bundle exec puppet-metadata os-versions --os Ubuntu
83
+ module-name supports these non-EOL operating system versions:
84
+ Ubuntu: 22.04, 24.04
85
+
86
+ module-name supports these EOL operating system versions:
87
+ Ubuntu: 20.04
88
+ ```
89
+
90
+ ### Add missing supported OS versions
91
+
92
+ Use `--add-missing` to automatically add all non-EOL OS versions to metadata.json:
93
+
94
+ ```
95
+ $ bundle exec puppet-metadata os-versions --add-missing
96
+ Added support:
97
+ CentOS => 10
98
+ Debian => 13
99
+ ```
100
+
101
+ These OSes are exceptions (to align with [beaker-hostgenerator](https://github.com/voxpupuli/beaker-hostgenerator) support):
102
+
103
+ - For SLES, only major versions are added.
104
+ - For Ubuntu, only LTS versions are added.
105
+
106
+ ### Remove EOL OS versions
107
+
108
+ Use `--remove-eol` to automatically remove all EOL OS versions from metadata.json:
109
+
110
+ ```
111
+ $ bundle exec puppet-metadata os-versions --remove-eol
112
+ Removed EOL operating systems:
113
+ CentOS => 7, 8
114
+ Debian => 9, 10
115
+ Ubuntu => 20.04
116
+ ```
117
+
118
+ You can preview changes without modifying metadata.json using `--noop`:
119
+
120
+ ```
121
+ $ bundle exec puppet-metadata os-versions --add-missing --noop
122
+ [NOOP] Would add support:
123
+ CentOS => 10
124
+ Debian => 13
125
+ ```
126
+
13
127
  ## Generating Github Actions outputs
14
128
 
15
129
  To get outputs [usable in Github Actions](https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions), there is the `metadata2gha` command available. This generates based on metadata.json, such as [Beaker](https://github.com/voxpupuli/beaker) setfiles, Puppet major versions and a Puppet unit test matrix.
@@ -71,39 +185,6 @@ Beaker test matrix formatted for readability
71
185
  ]
72
186
  ```
73
187
 
74
- It is possible to specify the path to metadata.json and customize the setfiles. For example, to ensure the setfiles use FQDNs and apply the [systemd PIDFile workaround under docker](https://github.com/docker/for-linux/issues/835). This either means either using an older image (CentOS 7, Ubuntu 16.04) or skipping (CentOS 8).
75
-
76
- ```console
77
- $ metadata2gha --use-fqdn --pidfile-workaround true /path/to/metadata.json
78
- ```
79
-
80
- This results in the following JSON data
81
- ```json
82
- [
83
- {
84
- "name": "Puppet 7 - CentOS 7",
85
- "env": {
86
- "BEAKER_PUPPET_COLLECTION": "puppet7",
87
- "BEAKER_SETFILE": "centos7-64{hostname=centos7-64-puppet7.example.com,image=centos:7.6.1810}"
88
- }
89
- },
90
- {
91
- "name": "Puppet 7 - Debian 12",
92
- "env": {
93
- "BEAKER_PUPPET_COLLECTION": "puppet7",
94
- "BEAKER_SETFILE": "debian12-64{hostname=debian12-64-puppet7.example.com}"
95
- }
96
- },
97
- {
98
- "name": "Puppet 7 - Ubuntu 22.04",
99
- "env": {
100
- "BEAKER_PUPPET_COLLECTION": "puppet7",
101
- "BEAKER_SETFILE": "ubuntu2204-64{hostname=ubuntu2204-64-puppet7.example.com}"
102
- }
103
- }
104
- ]
105
- ```
106
-
107
188
  If you need custom hostname or multiple hosts in your integration tests this could be achived by using the --beaker-hosts option
108
189
 
109
190
  Option argument is 'HOSTNAME:ROLES;HOSTNAME:..;..' where
@@ -212,6 +293,60 @@ The metadata object has several different methods that we can call
212
293
  [7] pry(main)>
213
294
  ```
214
295
 
296
+ ### Get all versions for an Operating System that are not EoL
297
+
298
+ ```
299
+ [1] pry(main)> require 'puppet_metadata'
300
+ => true
301
+ [2] pry(main)> PuppetMetadata::OperatingSystem.supported_releases('RedHat')
302
+ => ["8", "9", "10"]
303
+ [3] pry(main)> PuppetMetadata::OperatingSystem.supported_releases('windows')
304
+ => []
305
+ [4] pry(main)>
306
+ ```
307
+
308
+ **For Operating systems without any known releases, an empty array is returned.**
309
+
310
+ ### Get all versions for an Operating System that are not EoL after a certain date
311
+
312
+ ```
313
+ [1] pry(main)> require 'puppet_metadata'
314
+ => true
315
+ [2] pry(main)> PuppetMetadata::OperatingSystem.supported_releases('CentOS', Date.parse('2025-04-15'))
316
+ => ["9", "10"]
317
+ [3] pry(main)>
318
+ ```
319
+
320
+ CentOS 8 and older aren't listed.
321
+ 8 is EoL since 2024-05-31.
322
+
323
+ ## Updating OS EOL dates
324
+
325
+ The EOL dates for operating systems are stored in `data/eol_dates.json` and are automatically updated weekly via GitHub Actions using data from [endoflife.date](https://endoflife.date/).
326
+
327
+ - For Amazon Linux, this is security support, not standard support.
328
+ - For Debian, this is extended life cycle, not standard support.
329
+ - For CentOS, this is security support, not active support.
330
+ - For OracleLinux, this is basic support, not extended support.
331
+ - For RedHat, this is maintenance support, not extended life cycle.
332
+ - For Rocky, this is security support, not active support.
333
+ - For SLES, this is general support, not long term service pack support.
334
+
335
+ To manually update the EOL dates:
336
+
337
+ ```bash
338
+ ./bin/update_eol_dates
339
+ ```
340
+
341
+ ### Adding new operating systems
342
+
343
+ To add a new operating system to the EOL tracking:
344
+
345
+ 1. Add an entry to the `OS_MAPPING` hash in `bin/update_eol_dates`
346
+ 2. Map it to the corresponding [endoflife.date product identifier](https://endoflife.date/docs/api/)
347
+ 3. Run `./bin/update_eol_dates` to fetch the data
348
+ 4. If the OS requires special handling (like Amazon Linux which uses multiple API endpoints), add a custom handler function
349
+
215
350
  ## List supported setfiles
216
351
 
217
352
  When running beaker on the CLI, you can specify a specific setfile. `puppet_metadata` provides `bin/setfiles` to list all setfiles:
data/bin/metadata2gha CHANGED
@@ -3,10 +3,7 @@ require 'optparse'
3
3
  require 'json'
4
4
  require 'puppet_metadata'
5
5
 
6
- PidfileWorkaround = Object.new
7
-
8
6
  options = {
9
- beaker_pidfile_workaround: false,
10
7
  domain: nil,
11
8
  minimum_major_puppet_version: nil,
12
9
  beaker_fact: nil,
@@ -14,20 +11,8 @@ options = {
14
11
  }
15
12
 
16
13
  OptionParser.new do |opts|
17
- opts.accept(PidfileWorkaround) do |value|
18
- case value
19
- when 'true'
20
- true
21
- when 'false'
22
- false
23
- else
24
- value.split(',')
25
- end
26
- end
27
-
28
14
  opts.banner = "Usage: #{$0} [options] metadata"
29
15
 
30
- opts.on('--pidfile-workaround VALUE', 'Generate the systemd PIDFile workaround to work around a docker bug', PidfileWorkaround) { |opt| options[:beaker_pidfile_workaround] = opt }
31
16
  opts.on('-d', '--domain VALUE', 'the domain for the box, only used when --use-fqdn is set to true') { |opt| options[:domain] = opt }
32
17
  opts.on('--minimum-major-puppet-version VERSION', "Don't create actions for Puppet versions less than this major version") { |opt| options[:minimum_major_puppet_version] = opt }
33
18
  opts.on('--beaker-facter FACT:LABEL:VALUES', 'Expand the matrix based on a fact. Separate values using commas') do |opt|
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # optparse subcommands inspired by https://gist.github.com/rkumar/445735
5
+
6
+ require 'optparse'
7
+ require 'optparse/date'
8
+
9
+ require 'puppet_metadata'
10
+
11
+ def main
12
+ subcommands = PuppetMetadata::BaseCommand.commands
13
+
14
+ options = {}
15
+
16
+ parsers = subcommands.transform_values { |cls| cls.parser(options) }
17
+
18
+ global = OptionParser.new do |opts|
19
+ opts.banner = "Usage: #{opts.program_name} [options] <action> [options]"
20
+ opts.on('--filename METADATA', 'Metadata filename') do |value|
21
+ options[:filename] = value
22
+ end
23
+
24
+ opts.separator ''
25
+ opts.separator 'ACTIONS'
26
+ width = subcommands.keys.max { |command, _parser| command.length }.length
27
+ parsers.each do |command, parser|
28
+ # TODO: positional argument
29
+ parser.banner = "Usage: #{opts.program_name} #{command} [options]"
30
+ opts.separator " #{command.ljust(width + 4)}#{parser.program_name}"
31
+ end
32
+ opts.separator ''
33
+ opts.separator "See '#{opts.program_name} ACTION --help' for more information on a specific action."
34
+ end
35
+
36
+ begin
37
+ global.order!
38
+ rescue OptionParser::InvalidArgument, OptionParser::InvalidOption => e
39
+ warn e.cause ? "#{e}: #{e.cause}" : e
40
+ warn ''
41
+ global.help_exit
42
+ end
43
+ unless (command = ARGV.shift)
44
+ puts global
45
+ exit 1
46
+ end
47
+ if (parser = parsers[command])
48
+ begin
49
+ arguments = parser.parse!
50
+ rescue OptionParser::InvalidArgument, OptionParser::InvalidOption => e
51
+ warn e.cause ? "#{e}: #{e.cause}" : e
52
+ warn ''
53
+ parser.help_exit
54
+ end
55
+
56
+ command = subcommands[command].new(arguments, options)
57
+ command.run
58
+ else
59
+ puts global
60
+ exit 1
61
+ end
62
+ end
63
+
64
+ main
data/bin/setfiles CHANGED
@@ -16,13 +16,14 @@ rescue StandardError => e
16
16
  end
17
17
 
18
18
  options = {
19
- beaker_pidfile_workaround: false,
20
19
  domain: 'example.com',
21
20
  minimum_major_puppet_version: nil,
22
21
  beaker_fact: nil,
23
22
  beaker_hosts: nil,
24
23
  }
25
24
 
25
+ warn 'Command deprecated - call puppet-metadata setfiles instead'
26
+
26
27
  metadata.github_actions(options).outputs[:puppet_beaker_test_matrix].each do |os|
27
28
  puts "BEAKER_SETFILE=\"#{os[:env]['BEAKER_SETFILE']}\""
28
29
  end
@@ -0,0 +1,267 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # This script updates the EOL dates data file using the endoflife.date API
5
+ # It can be run manually or via GitHub Actions automation
6
+
7
+ require 'json'
8
+ require 'net/http'
9
+ require 'uri'
10
+ require 'date'
11
+
12
+ # Mapping between internal OS names and endoflife.date identifiers; only OSes listed here will be updated
13
+ OS_MAPPING = {
14
+ 'AlmaLinux' => 'almalinux',
15
+ 'Amazon' => 'amazon-linux',
16
+ 'CentOS' => 'centos',
17
+ 'Debian' => 'debian',
18
+ 'OracleLinux' => 'oracle-linux',
19
+ 'Fedora' => 'fedora',
20
+ 'FreeBSD' => 'freebsd',
21
+ 'RedHat' => 'rhel',
22
+ 'Rocky' => 'rocky-linux',
23
+ 'Scientific' => nil, # Not in endoflife.date
24
+ 'SLES' => 'sles',
25
+ 'Ubuntu' => 'ubuntu',
26
+ }.freeze
27
+
28
+ def fetch_eol_data(product)
29
+ uri = URI("https://endoflife.date/api/#{product}.json")
30
+ response = Net::HTTP.get_response(uri)
31
+
32
+ unless response.is_a?(Net::HTTPSuccess)
33
+ warn "Failed to fetch data for #{product}: #{response.code} #{response.message}"
34
+ return nil
35
+ end
36
+
37
+ JSON.parse(response.body)
38
+ rescue StandardError => e
39
+ warn "Error fetching data for #{product}: #{e.message}"
40
+ nil
41
+ end
42
+
43
+ def parse_eol_date(eol_value)
44
+ # endoflife.date returns different formats:
45
+ # - Date string: "2024-06-30"
46
+ # - Boolean false: not yet EOL
47
+ # - Boolean true: EOL date unknown
48
+ case eol_value
49
+ when String
50
+ # Validate it's a proper date
51
+ Date.parse(eol_value)
52
+ eol_value
53
+ when false, 'false', true, 'true'
54
+ # false: Not yet EOL
55
+ # true: EOL date unknown - keep existing data or skip
56
+ nil
57
+ end
58
+ rescue Date::Error
59
+ nil
60
+ end
61
+
62
+ def update_os_data(os_name, product_id, current_data)
63
+ return current_data unless product_id
64
+
65
+ puts "Fetching data for #{os_name} (#{product_id})..."
66
+ api_data = fetch_eol_data(product_id)
67
+ return current_data unless api_data
68
+
69
+ updated_data = {}
70
+
71
+ api_data.each do |cycle|
72
+ # The API returns an array of cycles, each with:
73
+ # - cycle: version number
74
+ # - eol: end of life date or boolean
75
+ version = cycle['cycle'].to_s
76
+ eol_value = if os_name == 'Debian' && cycle.key?('extendedSupport')
77
+ cycle['extendedSupport']
78
+ else
79
+ cycle['eol']
80
+ end
81
+
82
+ eol = parse_eol_date(eol_value)
83
+
84
+ if eol
85
+ updated_data[version] = eol
86
+ puts " #{version}: #{eol}"
87
+ elsif eol_value == false || cycle['eol'] == false
88
+ # Track versions that aren't EOL yet
89
+ updated_data[version] = nil
90
+ puts " #{version}: not yet EOL"
91
+ end
92
+ end
93
+
94
+ current_data[os_name]&.each do |version, date|
95
+ unless updated_data.key?(version)
96
+ puts " Preserving #{version}: #{date || 'nil'} (not in API)"
97
+ updated_data[version] = date
98
+ end
99
+ end
100
+
101
+ updated_data
102
+ end
103
+
104
+ def handle_amazon_linux(current_data)
105
+ puts 'Fetching data for Amazon Linux...'
106
+
107
+ updated_data = {}
108
+
109
+ al_data = fetch_eol_data('amazon-linux')
110
+ al_data&.each do |cycle|
111
+ version = cycle['cycle'].to_s
112
+ eol = parse_eol_date(cycle['eol'])
113
+
114
+ # Map version "2" to "2.0" for compatibility with existing metadata entries
115
+ version = '2.0' if version == '2'
116
+
117
+ if eol
118
+ updated_data[version] = eol
119
+ puts " #{version}: #{eol}"
120
+ elsif cycle['eol'] == false
121
+ updated_data[version] = nil
122
+ puts " #{version}: not yet EOL"
123
+ end
124
+ end
125
+
126
+ current_data['Amazon']&.each do |version, date|
127
+ unless updated_data.key?(version)
128
+ puts " Preserving #{version}: #{date || 'nil'} (not in API)"
129
+ updated_data[version] = date
130
+ end
131
+ end
132
+
133
+ updated_data
134
+ end
135
+
136
+ def handle_centos(current_data)
137
+ puts 'Fetching data for CentOS...'
138
+
139
+ updated_data = {}
140
+
141
+ centos_data = fetch_eol_data('centos')
142
+ centos_data&.each do |cycle|
143
+ version = cycle['cycle'].to_s
144
+ eol = parse_eol_date(cycle['eol'])
145
+
146
+ if eol
147
+ updated_data[version] = eol
148
+ puts " #{version}: #{eol}"
149
+ elsif cycle['eol'] == false
150
+ updated_data[version] = nil
151
+ puts " #{version}: not yet EOL"
152
+ end
153
+ end
154
+
155
+ # Fetch CentOS Stream versions after CentOS. Stream takes precedence for overlapping versions.
156
+ stream_data = fetch_eol_data('centos-stream')
157
+ stream_data&.each do |cycle|
158
+ version = cycle['cycle'].to_s
159
+ eol = parse_eol_date(cycle['eol'])
160
+
161
+ if eol
162
+ updated_data[version] = eol
163
+ puts " #{version}: #{eol}"
164
+ elsif cycle['eol'] == false
165
+ updated_data[version] = nil
166
+ puts " #{version}: not yet EOL"
167
+ end
168
+ end
169
+
170
+ current_data['CentOS']&.each do |version, date|
171
+ unless updated_data.key?(version)
172
+ puts " Preserving #{version}: #{date || 'nil'} (not in API)"
173
+ updated_data[version] = date
174
+ end
175
+ end
176
+
177
+ updated_data
178
+ end
179
+
180
+ def handle_ubuntu(current_data)
181
+ puts 'Fetching data for Ubuntu...'
182
+
183
+ updated_data = {}
184
+
185
+ ubuntu_data = fetch_eol_data('ubuntu')
186
+ ubuntu_data&.each do |cycle|
187
+ version = cycle['cycle'].to_s
188
+ eol = parse_eol_date(cycle['eol'])
189
+
190
+ if eol
191
+ updated_data[version] = eol
192
+ puts " #{version}: #{eol}"
193
+ elsif cycle['eol'] == false
194
+ updated_data[version] = nil
195
+ puts " #{version}: not yet EOL"
196
+ end
197
+ end
198
+
199
+ current_data['Ubuntu']&.each do |version, date|
200
+ next if updated_data.key?(version)
201
+
202
+ puts " Preserving #{version}: #{date || 'nil'} (not in API)"
203
+ updated_data[version] = date
204
+ end
205
+
206
+ updated_data
207
+ end
208
+
209
+ def main
210
+ data_file = File.expand_path('../data/eol_dates.json', __dir__)
211
+
212
+ # Load current data
213
+ current_data = JSON.parse(File.read(data_file))
214
+
215
+ # Update each OS
216
+ updated_data = {}
217
+
218
+ OS_MAPPING.each do |os_name, product_id|
219
+ if os_name == 'Amazon'
220
+ updated_data[os_name] = handle_amazon_linux(current_data)
221
+ elsif os_name == 'CentOS'
222
+ updated_data[os_name] = handle_centos(current_data)
223
+ elsif os_name == 'Ubuntu'
224
+ updated_data[os_name] = handle_ubuntu(current_data)
225
+ elsif product_id
226
+ updated_data[os_name] = update_os_data(os_name, product_id, current_data)
227
+ else
228
+ puts "Preserving #{os_name} (not in endoflife.date API)"
229
+ updated_data[os_name] = current_data[os_name]
230
+ end
231
+ end
232
+
233
+ # Sort each OS's versions by EOL date (latest EOL first, then nulls)
234
+ sorted_data = {}
235
+ updated_data.each do |os_name, versions|
236
+ sorted_versions = versions.sort do |a, b|
237
+ version_a, eol_a = a
238
+ version_b, eol_b = b
239
+
240
+ # Handle nil values (not yet EOL) - they go first
241
+ return -1 if eol_a.nil? && !eol_b.nil?
242
+ return 1 if !eol_a.nil? && eol_b.nil?
243
+
244
+ if eol_a == eol_b
245
+ # Same EOL date (or both nil) - sort by version number descending
246
+ Gem::Version.new(version_b) <=> Gem::Version.new(version_a)
247
+ else
248
+ # Different EOL dates - sort by date descending (later date first)
249
+ (eol_b || '0000-00-00') <=> (eol_a || '0000-00-00')
250
+ end
251
+ end.to_h
252
+ sorted_data[os_name] = sorted_versions
253
+ end
254
+
255
+ File.write(data_file, "#{JSON.pretty_generate(sorted_data)}\n")
256
+ puts "\nUpdated #{data_file}"
257
+
258
+ if current_data == updated_data
259
+ puts 'No changes detected.'
260
+ exit 0
261
+ else
262
+ puts 'Changes detected!'
263
+ exit 1 # Exit with 1 to signal changes (useful for CI)
264
+ end
265
+ end
266
+
267
+ main if __FILE__ == $0