Skip to content

Commit 8c4d299

Browse files
committed
Add update script for github releases
Signed-off-by: Tim Meusel <tim@bastelfreak.de>
1 parent b599558 commit 8c4d299

3 files changed

Lines changed: 339 additions & 0 deletions

File tree

Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ gem 'vanagon', *location_for(ENV['VANAGON_LOCATION'] || 'https://github.com/open
2121
# https://www.rubyonmac.dev/certificate-verify-failed-unable-to-get-certificate-crl-openssl-ssl-sslerror
2222
gem 'openssl' unless `uname -o`.chomp == 'Cygwin'
2323

24+
gem 'octokit', '< 11'
25+
2426
group(:development, optional: true) do
2527
gem 'hashdiff', require: false
2628
gem 'highline', require: false

README.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,98 @@ end
107107
### End automated maintenance section ###
108108
```
109109
The rake task will leave any lines it doesn't know about alone (in this case, the if/else/end logic) and update both checksums, with the default without the `# GEM TYPE` decorator being the `ruby` uncompiled gem. Try not to get too fancy with logic in here.
110+
111+
## Updating (GitHub) releases
112+
113+
We provide two rake tasks, `vox:print_outdated_components` and `vox:update_outdated_components`.
114+
The first one inspects all non-rubygem components:
115+
116+
```
117+
$ bundle exec rake vox:print_outdated_components
118+
Checking 14 component(s) for updates...
119+
120+
augeas... up to date (1.14.1)
121+
curl... error (Could not parse upstream version 'curl-8_20_0')
122+
dmidecode... skipped (No GitHub URL detected)
123+
libedit... skipped (No GitHub URL detected)
124+
libffi... up to date (3.5.2)
125+
libxml2... skipped (No GitHub URL detected)
126+
libyaml... up to date (0.2.5)
127+
openssl-3.0... OUTDATED: 3.0.20 -> 4.0.0
128+
puppet-ca-bundle... up to date (1.1.0)
129+
readline... skipped (No GitHub URL detected)
130+
ruby-3.2... skipped (No GitHub URL detected)
131+
ruby-augeas... up to date (0.6.0)
132+
ruby-shadow... up to date (2.5.1)
133+
virt-what... skipped (No GitHub URL detected)
134+
135+
=== Components with available updates ===
136+
openssl-3.0: 3.0.20 -> 4.0.0 (upstream tag: openssl-4.0.0)
137+
138+
=== Errors encountered ===
139+
curl: Could not parse upstream version 'curl-8_20_0'
140+
141+
=== Skipped (no checkable upstream) ===
142+
dmidecode: No GitHub URL detected
143+
libedit: No GitHub URL detected
144+
libxml2: No GitHub URL detected
145+
readline: No GitHub URL detected
146+
ruby-3.2: No GitHub URL detected
147+
virt-what: No GitHub URL detected
148+
```
149+
150+
It will search for new releases upstream.
151+
Right now only github.com is supported, but most of our components come from rubygems.org or github.com anyways, so this catches 85% of our components.
152+
The second rake task checks the GitHub API for new releases and updates the json file with the version & URL.
153+
154+
```
155+
$ bundle exec rake vox:update_outdated_components
156+
Checking 14 component(s) for updates...
157+
158+
augeas... up to date (1.14.1)
159+
curl... error (Could not parse upstream version 'curl-8_20_0')
160+
dmidecode... skipped (No GitHub URL detected)
161+
libedit... skipped (No GitHub URL detected)
162+
libffi... up to date (3.5.2)
163+
libxml2... skipped (No GitHub URL detected)
164+
libyaml... up to date (0.2.5)
165+
openssl-3.0... OUTDATED: 3.0.20 -> 4.0.0
166+
puppet-ca-bundle... up to date (1.1.0)
167+
readline... skipped (No GitHub URL detected)
168+
ruby-3.2... skipped (No GitHub URL detected)
169+
ruby-augeas... up to date (0.6.0)
170+
ruby-shadow... up to date (2.5.1)
171+
virt-what... skipped (No GitHub URL detected)
172+
173+
=== Components with available updates ===
174+
openssl-3.0: 3.0.20 -> 4.0.0 (upstream tag: openssl-4.0.0)
175+
176+
=== Errors encountered ===
177+
curl: Could not parse upstream version 'curl-8_20_0'
178+
179+
=== Skipped (no checkable upstream) ===
180+
dmidecode: No GitHub URL detected
181+
libedit: No GitHub URL detected
182+
libxml2: No GitHub URL detected
183+
readline: No GitHub URL detected
184+
ruby-3.2: No GitHub URL detected
185+
virt-what: No GitHub URL detected
186+
187+
Updated openssl-3.0 to 4.0.0
188+
One or more components could not be checked.
189+
$ git diff configs/components/openssl-3.0.json
190+
diff --git a/configs/components/openssl-3.0.json b/configs/components/openssl-3.0.json
191+
index 0f11c8a..10f1a32 100644
192+
--- a/configs/components/openssl-3.0.json
193+
+++ b/configs/components/openssl-3.0.json
194+
@@ -1,5 +1,5 @@
195+
{
196+
- "version": "3.0.20",
197+
- "url": "https://github.com/openssl/openssl/releases/download/openssl-3.0.20/openssl-3.0.20.tar.gz",
198+
- "sha256sum": "c80a01dfc70ece4dc21168932c37739042d404d46ccc81a5986dd75314ecda6f"
199+
+ "version": "4.0.0",
200+
+ "url": "https://github.com/openssl/openssl/releases/download/openssl-4.0.0/openssl-4.0.0.tar.gz",
201+
+ "sha256sum": "c32cf49a959c4f345f9606982dd36e7d28f7c58b19c2e25d75624d2b3d2f79ac"
202+
}
203+
$
204+
```

tasks/update_components.rake

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
# frozen_string_literal: true
2+
3+
require 'digest'
4+
require 'json'
5+
require 'octokit'
6+
require 'open-uri'
7+
require 'rake'
8+
require 'rubygems/version'
9+
require 'uri'
10+
11+
COMPONENTS_JSON_GLOB = File.join(File.expand_path('..', __dir__), 'configs', 'components', '*.json')
12+
13+
def github_client
14+
@github_client ||= Octokit::Client.new(access_token: ENV['GITHUB_TOKEN']).tap do |client|
15+
client.auto_paginate = true
16+
end
17+
end
18+
19+
# Extract GitHub owner and repo from a URL string.
20+
# Returns [owner, repo] or nil if not a GitHub URL.
21+
def github_owner_repo(url)
22+
return nil unless url.to_s =~ %r{github\.com/([^/]+)/([^/\s?#]+)}
23+
24+
[Regexp.last_match(1), Regexp.last_match(2).sub(/\.git$/, '')]
25+
end
26+
27+
# Normalize a version string by stripping common tag prefixes.
28+
def normalize_version(tag)
29+
tag.sub(/\Av(?=\d)/, '')
30+
.sub(/\Arefs\/tags\/v?/, '')
31+
.sub(/\Arelease-/, '')
32+
.sub(/\Aopenssl-/, '')
33+
end
34+
35+
# Try to parse a version from a normalized string, returning nil if unparseable.
36+
def try_version(str)
37+
Gem::Version.new(str)
38+
rescue ArgumentError
39+
nil
40+
end
41+
42+
def latest_github_release(owner, repo)
43+
github_client.latest_release("#{owner}/#{repo}")
44+
rescue StandardError => e
45+
warn " Warning: could not fetch latest release for #{owner}/#{repo}: #{e}"
46+
nil
47+
end
48+
49+
# *sighs* this looks a bit complicated. some repos have weird tags like release-$ver or $major_$minor_$patch
50+
# and somme people, like openssl, maintain multiple streams. So the latest tag might not be the highest version
51+
# We do some version comparison to find the actual highest version
52+
def latest_github_tag(owner, repo)
53+
github_client.tags("#{owner}/#{repo}", per_page: 100)
54+
.map { |tag| [tag.name, try_version(normalize_version(tag.name))] }
55+
.reject { |_, version| version.nil? || version.prerelease? }
56+
.max_by { |_, version| version }
57+
.first
58+
end
59+
60+
def current_version(data)
61+
if data['ref'].to_s =~ %r{refs/tags/(.*)}
62+
normalize_version(Regexp.last_match(1))
63+
else
64+
normalize_version(data['version'])
65+
end
66+
end
67+
68+
def latest_upstream_tag(owner, repo, ref)
69+
if ref.nil?
70+
latest_github_release(owner, repo)&.tag_name || latest_github_tag(owner, repo)
71+
else
72+
latest_github_tag(owner, repo)
73+
end
74+
end
75+
76+
def download_digest(url)
77+
digest = Digest::SHA256.new
78+
79+
OpenURI.open_uri(url, 'rb') do |io|
80+
digest << io.read(1024 * 16) until io.eof?
81+
end
82+
83+
digest.hexdigest
84+
end
85+
86+
def updated_release_url(current_url:, current_version:, latest_version:, latest_tag:, release:)
87+
uri = URI(current_url)
88+
updated_path = uri.path.sub(%r{(/releases/download/)[^/]+(/)}, "\\1#{latest_tag}\\2")
89+
updated_path = updated_path.gsub(current_version, latest_version) unless current_version.empty?
90+
asset_name = File.basename(updated_path)
91+
release_asset = release&.assets&.find { |asset| asset.name == asset_name }
92+
93+
return release_asset.browser_download_url if release_asset
94+
95+
uri.path = updated_path
96+
uri.to_s
97+
end
98+
99+
def update_component_file(path, latest_tag:, latest_version:)
100+
data = JSON.parse(File.read(path))
101+
owner, repo = github_owner_repo(data['url'])
102+
release = latest_github_release(owner, repo)
103+
current_ver = current_version(data)
104+
updated = false
105+
106+
if data.key?('version') && data['version'] != latest_version
107+
data['version'] = latest_version
108+
updated = true
109+
end
110+
111+
desired_ref = "refs/tags/#{latest_tag}"
112+
if data.key?('ref') && data['ref'] != desired_ref
113+
data['ref'] = desired_ref
114+
updated = true
115+
end
116+
117+
if data['url'].include?('github.com') && data['url'].include?('/releases/download/')
118+
new_url = updated_release_url(
119+
current_url: data['url'],
120+
current_version: current_ver,
121+
latest_version: latest_version,
122+
latest_tag: latest_tag,
123+
release: release
124+
)
125+
126+
if new_url != data['url']
127+
data['url'] = new_url
128+
updated = true
129+
end
130+
end
131+
132+
digest = download_digest(data['url'])
133+
data['sha256sum'] = digest
134+
135+
File.write(path, "#{JSON.pretty_generate(data)}\n") if updated
136+
137+
{ updated: updated, url: data['url'] }
138+
end
139+
140+
def check_component(path)
141+
name = File.basename(path, '.json')
142+
data = JSON.parse(File.read(path))
143+
144+
owner, repo = github_owner_repo(data['url'])
145+
146+
return { name: name, status: :skip, reason: 'No GitHub URL detected' } unless owner
147+
148+
current_ver_str = current_version(data)
149+
current_ver = try_version(current_ver_str)
150+
latest_tag = latest_upstream_tag(owner, repo, data['ref'])
151+
152+
return { name: name, status: :error, reason: 'Could not determine latest upstream version' } if latest_tag.nil?
153+
154+
latest_ver_str = normalize_version(latest_tag)
155+
latest_ver = try_version(latest_ver_str)
156+
157+
return { name: name, status: :error, reason: "Could not parse upstream version '#{latest_ver_str}'" } if latest_ver.nil?
158+
return { name: name, status: :error, reason: "Could not parse current version '#{current_ver_str}'" } if current_ver.nil?
159+
160+
if latest_ver > current_ver
161+
{ name: name, path: path, status: :outdated, current: current_ver_str, latest: latest_ver_str, tag: latest_tag }
162+
else
163+
{ name: name, path: path, status: :up_to_date, current: current_ver_str }
164+
end
165+
end
166+
167+
def component_results
168+
Dir[COMPONENTS_JSON_GLOB].map { |path| check_component(path) }
169+
end
170+
171+
def print_component_results(results)
172+
puts "Checking #{results.length} component(s) for updates...\n\n"
173+
174+
outdated = []
175+
errors = []
176+
skipped = []
177+
178+
results.each do |result|
179+
print " #{result[:name]}... "
180+
$stdout.flush
181+
182+
case result[:status]
183+
when :up_to_date
184+
puts "up to date (#{result[:current]})"
185+
when :outdated
186+
puts "OUTDATED: #{result[:current]} -> #{result[:latest]}"
187+
outdated << result
188+
when :skip
189+
puts "skipped (#{result[:reason]})"
190+
skipped << result
191+
when :error
192+
puts "error (#{result[:reason]})"
193+
errors << result
194+
end
195+
end
196+
197+
puts "\n"
198+
199+
unless outdated.empty?
200+
puts '=== Components with available updates ==='
201+
outdated.each do |result|
202+
puts " #{result[:name]}: #{result[:current]} -> #{result[:latest]} (upstream tag: #{result[:tag]})"
203+
end
204+
puts ''
205+
end
206+
207+
unless errors.empty?
208+
puts '=== Errors encountered ==='
209+
errors.each { |result| puts " #{result[:name]}: #{result[:reason]}" }
210+
puts ''
211+
end
212+
213+
unless skipped.empty?
214+
puts '=== Skipped (no checkable upstream) ==='
215+
skipped.each { |result| puts " #{result[:name]}: #{result[:reason]}" }
216+
puts ''
217+
end
218+
219+
puts 'All components are up to date.' if outdated.empty? && errors.empty?
220+
221+
{ outdated: outdated, errors: errors, skipped: skipped }
222+
end
223+
224+
namespace :vox do
225+
desc 'Print non-rubygem components with upstream GitHub updates'
226+
task :print_outdated_components do
227+
print_component_results(component_results)
228+
end
229+
230+
desc 'Update outdated non-rubygem components backed by GitHub releases or tags'
231+
task :update_outdated_components do
232+
summary = print_component_results(component_results)
233+
234+
summary[:outdated].each do |result|
235+
update_component_file(result[:path], latest_tag: result[:tag], latest_version: result[:latest])
236+
puts "Updated #{result[:name]} to #{result[:latest]}"
237+
end
238+
239+
abort 'One or more components could not be checked.' unless summary[:errors].empty?
240+
puts 'No component files needed changes.' if summary[:outdated].empty?
241+
end
242+
end

0 commit comments

Comments
 (0)