Files
codex-proxy-rs/.github/scripts/release-metadata.rb
T
ZyyandCodex b9bcb4e086 ci: streamline builds and release validation
Share binary builds and dependency caches, run Windows platform checks in parallel, and reuse verified CI evidence for version-only releases. Reuse prebuilt CI image inputs and add read-only release preflight.

Co-authored-by: Codex <noreply@openai.com>
2026-11-05 13:03:59 +08:00

131 lines
4.8 KiB
Ruby

require 'fileutils'
require 'json'
require 'open3'
require 'rubygems'
require 'time'
require 'yaml'
# 本地入口与发行工作流共用版本、说明和平台合同
module ReleaseMetadata
APP_NAME = 'codex-proxy-rs'
SEMVER = /\A(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(?:alpha|beta|rc|exp)\.[1-9]\d*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?\z/
def self.version(raw)
value = raw.delete_prefix('v')
raise "Invalid release version: #{raw}" unless value.match?(SEMVER)
value
end
def self.prerelease?(value)
version(value).split('+', 2).first.include?('-')
end
def self.sort_key(value)
Gem::Version.new(version(value).split('+', 2).first)
end
def self.validate_notes(value)
tag = "v#{version(value)}"
lines = File.readlines('release/notes.md')
unless lines.first&.strip == "# #{tag}" && !lines.drop(1).join.strip.empty?
raise "release/notes.md: expected non-empty notes starting with '# #{tag}'"
end
end
def self.git(*arguments)
output, status = Open3.capture2('git', *arguments)
raise "git #{arguments.join(' ')} failed" unless status.success?
output.strip
end
def self.platforms
entries = YAML.load_file('release/platforms.yaml').fetch('platforms')
raise 'release/platforms.yaml: platforms must be a non-empty array' unless entries.is_a?(Array) && !entries.empty?
seen = {}
entries.each do |entry|
raise 'Invalid release platform' unless entry.is_a?(Hash)
%w[os arch runner target binary archive].each do |key|
raise "Invalid platform #{key}" unless entry[key].is_a?(String) && !entry[key].strip.empty?
end
key = "#{entry['os']}-#{entry['arch']}"
raise "Duplicate release platform: #{key}" if seen[key]
seen[key] = true
raise "Unsupported archive: #{entry['archive']}" unless entry['archive'] == 'tar.gz'
if entry['docker'] == true
platform = entry['dockerPlatform']
raise "Missing Docker platform: #{key}" unless platform.is_a?(String) && !platform.strip.empty?
entry['dockerPlatform'] = platform.strip
end
entry['artifact'] = "#{APP_NAME}-#{key}"
end
raise 'At least one Docker platform is required' unless entries.any? { |entry| entry['docker'] == true }
entries
end
def self.outputs(tag)
value = version(YAML.load_file('release/version.yaml').fetch('version'))
raise "Release tag #{tag} does not match v#{value}" unless tag == "v#{value}"
validate_notes(value)
entries = platforms
docker, native = entries.partition { |entry| entry['docker'] == true }
previous = git('tag', '--list', 'v*', '--merged', 'HEAD').lines.map(&:strip).filter_map do |candidate|
next unless candidate.delete_prefix('v').match?(SEMVER)
key = sort_key(candidate)
[key, candidate] if key < sort_key(value)
end.max_by(&:first)&.last
{
'tag' => tag,
'version' => value,
'docker_version' => value.tr('+', '-'),
'build_time' => Time.now.utc.iso8601,
'git_sha' => git('rev-parse', 'HEAD'),
'platform_matrix' => JSON.generate('include' => entries),
'docker_matrix' => JSON.generate('include' => docker),
'non_docker_matrix' => JSON.generate('include' => native),
'windows_matrix' => JSON.generate('include' => entries.select { |entry| entry['os'] == 'windows' }),
'docker_platforms' => docker.map { |entry| entry['dockerPlatform'] }.join(','),
'previous_tag' => previous,
'prerelease' => prerelease?(value),
'build_type' => value.split('+', 2).first.include?('-exp.') ? 'experimental' : 'release'
}
end
def self.seal_manifest(metadata)
path = 'backend/crates/gateway-plugin/runtime/plugin-host-compatibility.json'
host = JSON.parse(File.read(path))
raise "Invalid plugin host compatibility declaration: #{path}" unless host['schema_version'] == 2
manifest = {
'schema_version' => 1,
'sealed' => true,
'gateway_version' => metadata.fetch('version'),
'gateway_git_sha' => metadata.fetch('git_sha'),
'plugin_host' => host,
'plugins' => []
}
FileUtils.mkdir_p('dist/plugin-release-assets')
File.write('dist/plugin-release-assets/plugin-release-manifest.json', JSON.pretty_generate(manifest) + "\n")
end
end
if $PROGRAM_NAME == __FILE__
begin
command, value = ARGV
raise 'usage: release-metadata.rb validate VERSION | outputs TAG' unless ARGV.length == 2
case command
when 'validate'
value = ReleaseMetadata.version(value)
ReleaseMetadata.validate_notes(value)
puts value
when 'outputs'
metadata = ReleaseMetadata.outputs(value)
ReleaseMetadata.seal_manifest(metadata)
metadata.each { |key, entry| puts "#{key}=#{entry}" }
else
raise "Unknown command: #{command}"
end
rescue StandardError => error
warn "error: #{error.message}"
exit 1
end
end