-
Notifications
You must be signed in to change notification settings - Fork 55
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
368 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
[ | ||
{ | ||
"puppet": 7.24, | ||
"ruby": 2.7 | ||
}, | ||
{ | ||
"puppet": 8.0, | ||
"ruby": 3.2 | ||
} | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,270 @@ | ||
#!/usr/bin/env ruby | ||
# frozen_string_literal: true | ||
|
||
require 'optparse' | ||
require 'ostruct' | ||
require 'json' | ||
require 'English' | ||
|
||
# wrap up running in a Github Action | ||
module Action | ||
class << self | ||
def init(to = 'auto') | ||
@notice = true | ||
@type = if to.eql? 'auto' | ||
ENV['GITHUB_ACTIONS'] ? 'github' : 'stdout' | ||
else | ||
to | ||
end | ||
$stderr = $stdout if @type == 'github' | ||
end | ||
|
||
def config(**args) | ||
error("invalid Action.config: #{args}") unless args.is_a?(Hash) | ||
args.each do |arg| | ||
instance_variable_set(:"@#{arg[0]}", arg[1]) | ||
end | ||
end | ||
|
||
def debug(msg) | ||
output(msg, '::debug::') if @debug | ||
end | ||
|
||
def notice(msg) | ||
output(msg, '::notice::') if @notice | ||
end | ||
|
||
def error(msg) | ||
output(msg, '::error::') | ||
exit 1 | ||
end | ||
|
||
def warning(msg) | ||
output(msg, '::warning::') | ||
end | ||
|
||
def group(message, data) | ||
$stderr.puts "::group::#{message}" | ||
$stderr.puts JSON.pretty_generate(data) | ||
$stderr.puts '::endgroup::' | ||
end | ||
|
||
def set_output(key, value) | ||
@output ||= @type == 'github' ? ENV.fetch('GITHUB_OUTPUT', nil) : '/dev/stdout' | ||
error('GITHUB_OUTPUT environment is not set?') if @output.nil? | ||
|
||
File.open(@output, 'a') { |f| f.puts("#{key}=#{JSON.generate(value)}") } | ||
end | ||
|
||
private | ||
|
||
def output(msg, prefix = nil) | ||
$stderr.puts "#{prefix}#{msg}" | ||
end | ||
end | ||
end | ||
|
||
options = OpenStruct.new( | ||
puppet_exclude: [], | ||
puppet_include: [], | ||
platform_exclude: [], | ||
platform_include: [], | ||
arch_include: [], | ||
arch_exclude: [], | ||
provision_prefer: [], | ||
provision_include: [], | ||
provision_exclude: [] | ||
) | ||
|
||
begin | ||
Action.init | ||
|
||
ARGV.unshift('--provision-prefer', 'docker') unless ARGV.include? '--provision-prefer' | ||
ARGV.unshift('--metadata', 'metadata.json') unless ARGV.include? '--metadata' | ||
ARGV.unshift('--output', 'auto') unless ARGV.include? '--output' | ||
ARGV.unshift('--collections', File.join(__dir__, 'collections.json')) unless ARGV.include? '--collections' | ||
ARGV.unshift('--provisioners', File.join(__dir__, 'provisioners.json')) unless ARGV.include? '--provisioners' | ||
|
||
OptionParser.accept(JSON) do |v| | ||
begin | ||
x = JSON.parse(File.read(v)) if v | ||
raise "nothing parsed from file #{v}" if x.empty? | ||
|
||
x | ||
rescue JSON::ParserError | ||
raise "error parsing file #{v}" | ||
end | ||
rescue RuntimeError, Errno::ENOENT | ||
raise OptionParser::InvalidArgument, $ERROR_INFO | ||
end | ||
|
||
OutputType = ->(value) {} | ||
OptionParser.accept(OutputType) do |v| | ||
raise OptionParser::InvalidArgument, v \ | ||
unless %w[auto github stdout].include?(v) | ||
|
||
Action.init(v) | ||
end | ||
|
||
ArchType = ->(value) {} | ||
OptionParser.accept(ArchType) do |v| | ||
raise OptionParser::InvalidArgument, v \ | ||
unless %w[x86_64 arm64].include?(v) | ||
|
||
v | ||
end | ||
|
||
OptionParser.new do |opt| | ||
opt.separator "Generate Github Actions Matrices from Puppet metadata.json\n\nOptions:" | ||
opt.on('--puppet-include MAJOR', Integer, 'Select puppet major version') { |o| options.puppet_include << o } | ||
opt.on('--puppet-exclude MAJOR', Integer, 'Filter puppet major version') { |o| options.puppet_exclude << o } | ||
# TODO | ||
# opt.on('--platform-include MATCH', String, 'Select matching platforms') { |o| options.platform_include << o } | ||
# opt.on('--platform-exclude MATCH', String, 'Filter matching platforms') { |o| options.platform_exclude << o } | ||
opt.on('--arch-include ARCH', ArchType, 'Select architecture') { |o| options.arch_include << o } | ||
opt.on('--arch-exclude ARCH', ArchType, 'Filter architecture') { |o| options.arch_exclude << o } | ||
opt.on('--provision-prefer NAME', String, 'Prefer provisioner (default: docker)') { |o| options.provision_prefer << o } | ||
opt.on('--provision-include NAME', String, 'Select provisioner (default: all)') { |o| options.provision_include << o } | ||
opt.on('--provision-exclude NAME', String, 'Filter provisioner') { |o| options.provision_exclude << o } | ||
opt.on('--collections FILE', JSON, 'File containing puppet to ruby version map (default: built-in)') { |o| options.collections = o } | ||
opt.on('--provisioners FILE', JSON, 'File containing platforms by provisioner (default: built-in)') { |o| options.provisioners = o } | ||
opt.on('--metadata FILE', JSON, 'File containing module metadata json (default: metadata.json)') { |o| options.metadata = o } | ||
opt.on('--debug', TrueClass, 'Enable debug messages') { |o| options.debug = o } | ||
opt.on('--quiet', TrueClass, 'Disable notice messages') { |o| options.quiet = o } | ||
opt.on('--output TYPE', OutputType, 'Type of output to generate; auto, github or stdout (default: auto)') { |o| options.metadata = o } | ||
end.parse! | ||
|
||
Action.config(debug: true) if options[:debug] | ||
Action.config(notice: false) if options[:quiet] | ||
|
||
# validate provisioners | ||
options[:provision_include].select! do |p| | ||
options[:provisioners].key?(p) or raise OptionParser::InvalidArgument, "--provision-include '#{p}' not found in provisioners" | ||
end | ||
|
||
# filter provisioners | ||
unless options[:provision_include].empty? | ||
options[:provisioners].delete_if do |k, _| | ||
!options[:provision_include].include?(k.to_s) and Action.notice("provision include filtered #{k}") | ||
end | ||
end | ||
options[:provisioners].delete_if do |k, _| | ||
if options[:provision_exclude].include?(k.to_s) | ||
Action.notice("provision exclude filtered #{k}") | ||
true | ||
end | ||
end | ||
|
||
# sort provisioners | ||
options[:provisioners] = options[:provisioners].sort_by { |key, _| options[:provision_prefer].index(key.to_s) || options[:provision_prefer].length }.to_h \ | ||
unless options[:provision_prefer].empty? | ||
|
||
raise OptionParser::ParseError, 'no provisioners left after filters applied' if options[:provisioners].empty? | ||
rescue OptionParser::ParseError => e | ||
Action.error(e.message) | ||
end | ||
|
||
matrix = { platforms: [], collection: [] } | ||
spec_matrix = { include: [] } | ||
|
||
# collection matrix | ||
version_re = /([>=<]{1,2})\s*([\d.]+)/ | ||
options[:metadata]['requirements']&.each do |req| | ||
next unless req['name'] == 'puppet' && req['version_requirement'] | ||
|
||
puppet_version_reqs = req['version_requirement'].scan(version_re).map(&:join) | ||
if puppet_version_reqs.empty? | ||
Action.warning("Didn't recognize version_requirement '#{req['version_requirement']}'") | ||
break | ||
end | ||
|
||
options[:collections].each do |collection| | ||
next unless options[:puppet_include].each do |major| | ||
break if major != collection['puppet'].to_i | ||
|
||
Action.debug("including collection #{collection.inspect}") | ||
end | ||
|
||
next unless options[:puppet_exclude].each do |major| | ||
if major.eql? collection['puppet'].to_i | ||
Action.debug("excluding collection #{collection.inspect}") | ||
break | ||
end | ||
end | ||
|
||
# Test against the "largest" puppet version in a collection, e.g. `7.9999` to allow puppet requirements with a non-zero lower bound on minor/patch versions. | ||
# This assumes that such a boundary will always allow the latest actually existing puppet version of a release stream, trading off simplicity vs accuracy here. | ||
gem_req = Gem::Requirement.create(puppet_version_reqs) | ||
next unless gem_req.satisfied_by?(Gem::Version.new("#{collection['puppet'].to_i}.9999")) | ||
|
||
matrix[:collection] << "puppet#{collection['puppet'].to_i}-nightly" | ||
|
||
spec_matrix[:include] << { | ||
puppet_version: "~> #{collection['puppet']}", | ||
ruby_version: collection['ruby'] | ||
} | ||
end | ||
end | ||
|
||
# Set platforms based on declared operating system support | ||
options[:metadata]['operatingsystem_support'].each do |os_sup| | ||
os_sup['operatingsystemrelease'].sort_by(&:to_i).each do |os_ver| | ||
os_ver_platforms = [] | ||
image_key = [os_sup['operatingsystem'], os_ver] | ||
|
||
# next unless options[:platform_exclude].each do |match| | ||
# break if major != collection['puppet'].to_i | ||
|
||
# Action.debug("including collection #{collection.inspect}") | ||
# end | ||
|
||
options[:provisioners].each do |provisioner, platforms| | ||
images = platforms.dig(*image_key) | ||
next if images.nil? | ||
|
||
unless options[:arch_include].empty? | ||
images.delete_if do |arch, _| | ||
unless options[:arch_include].include?(arch) | ||
Action.notice("arch include filtered #{image_key.join('-')}-#{arch} from #{provisioner}") | ||
true | ||
end | ||
end | ||
end | ||
|
||
images.delete_if do |arch, _| | ||
if options[:arch_exclude].include?(arch) | ||
Action.notice("arch exclude filtered #{image_key.join('-')}-#{arch} from #{provisioner}") | ||
true | ||
end | ||
end | ||
|
||
next if images.empty? | ||
|
||
images.each do |arch, image| | ||
label = (arch.eql?('x86_64') ? image_key : image_key + [arch]).join('-') | ||
next if os_ver_platforms.any? { |h| h[:label] == label } | ||
|
||
os_ver_platforms << { | ||
label: label, | ||
provider: provisioner, | ||
arch: arch, | ||
image: image | ||
} | ||
end | ||
end | ||
|
||
if os_ver_platforms.empty? | ||
Action.warning("#{image_key.join('-')} no provisioner found") | ||
else | ||
matrix[:platforms].push(*os_ver_platforms) | ||
end | ||
end | ||
end | ||
|
||
Action.group('matrix', matrix) if options[:debug] | ||
Action.group('spec_matrix', spec_matrix) if options[:debug] | ||
|
||
Action.error('no collections after filters applied') if matrix[:collection].empty? | ||
|
||
Action.set_output('matrix', matrix) | ||
Action.set_output('spec_matrix', spec_matrix) |
Oops, something went wrong.