mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
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>
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
require 'fileutils'
|
||||
require 'json'
|
||||
require 'open3'
|
||||
require 'tmpdir'
|
||||
require_relative 'release-quality'
|
||||
|
||||
# 使用临时 Git 仓库验证远端状态和只读边界,不访问真实 GitHub 或修改当前仓库
|
||||
ROOT = File.expand_path('../..', __dir__)
|
||||
|
||||
def assert(condition, message)
|
||||
raise message unless condition
|
||||
end
|
||||
|
||||
def git(*arguments)
|
||||
output, error, status = Open3.capture3('git', *arguments)
|
||||
raise "git #{arguments.first}: #{error}" unless status.success?
|
||||
output.strip
|
||||
end
|
||||
|
||||
def commit(message)
|
||||
git('add', '.')
|
||||
git('commit', '-qm', message)
|
||||
end
|
||||
|
||||
Dir.mktmpdir('release-checks-') do |directory|
|
||||
remote = File.join(directory, 'remote.git')
|
||||
system('git', 'init', '--bare', '-q', remote, exception: true)
|
||||
checkout = File.join(directory, 'checkout')
|
||||
FileUtils.mkdir_p(checkout)
|
||||
Dir.chdir(checkout) do
|
||||
git('init', '-q', '-b', 'main')
|
||||
git('config', 'user.email', 'test@example.invalid')
|
||||
git('config', 'user.name', 'Release test')
|
||||
files = %w[
|
||||
release/publish release/version.yaml release/notes.md release/platforms.yaml
|
||||
.github/scripts/toolchains.rb .github/scripts/release-metadata.rb
|
||||
backend/rust-toolchain.toml deploy/Dockerfile
|
||||
backend/crates/gateway-plugin/runtime/plugin-api-lifecycle.rb
|
||||
backend/crates/gateway-plugin/runtime/plugin-host-compatibility.json
|
||||
backend/crates/gateway-plugin/runtime/plugin-api-deprecations.json
|
||||
]
|
||||
files.each do |path|
|
||||
FileUtils.mkdir_p(File.dirname(path))
|
||||
FileUtils.cp(File.join(ROOT, path), path)
|
||||
end
|
||||
commit('initial source')
|
||||
assert(ReleaseQuality.evidence('owner/repo').nil?, 'root commit should require full checks')
|
||||
git('remote', 'add', 'origin', remote)
|
||||
git('push', '-qu', 'origin', 'main')
|
||||
File.write('release/notes.md', "# v99.0.0\n\nFixture notes\n")
|
||||
|
||||
bin = File.join(directory, 'bin')
|
||||
FileUtils.mkdir_p(bin)
|
||||
File.write(File.join(bin, 'gh'), <<~RUBY)
|
||||
#!/usr/bin/env ruby
|
||||
case ARGV
|
||||
when ['auth', 'status'] then exit 0
|
||||
when ['repo', 'view', #{remote.inspect}, '--json', 'nameWithOwner', '--jq', '.nameWithOwner']
|
||||
puts 'owner/repo'
|
||||
else
|
||||
abort "unexpected gh mutation: \#{ARGV.inspect}"
|
||||
end
|
||||
RUBY
|
||||
File.chmod(0o755, File.join(bin, 'gh'))
|
||||
environment = { 'PATH' => "#{bin}:#{ENV.fetch('PATH')}" }
|
||||
snapshot = lambda do
|
||||
[git('show-ref'), git('status', '--porcelain'), File.binread('.git/index'),
|
||||
files.to_h { |path| [path, File.binread(path)] },
|
||||
git('ls-remote', 'origin'), File.exist?('.git/FETCH_HEAD')]
|
||||
end
|
||||
check = lambda do |success, message = nil|
|
||||
before = snapshot.call
|
||||
output, error, status = Open3.capture3(environment, 'bash', 'release/publish', '--check', '99.0.0')
|
||||
assert(status.success? == success, "preflight outcome: #{output}#{error}")
|
||||
assert("#{output}#{error}".include?(message), "missing error: #{message}; #{error}") if message
|
||||
assert(snapshot.call == before, 'preflight modified files, refs, index, or FETCH_HEAD')
|
||||
end
|
||||
check.call(true, 'Preflight passed')
|
||||
File.write('deploy/Dockerfile', File.read('deploy/Dockerfile') + "\n# fixture\n")
|
||||
check.call(false, 'unstaged changes')
|
||||
git('checkout', '--', 'deploy/Dockerfile')
|
||||
git('tag', 'v99.0.0')
|
||||
check.call(false, 'local tag already exists')
|
||||
git('push', '-q', 'origin', 'refs/tags/v99.0.0')
|
||||
git('tag', '-d', 'v99.0.0')
|
||||
check.call(false, 'remote tag already exists')
|
||||
git('push', '-q', 'origin', ':refs/tags/v99.0.0')
|
||||
|
||||
# 未履行的兼容窗口必须在只读预检中失败,不能等待提交版本后再发现
|
||||
deprecations = 'backend/crates/gateway-plugin/runtime/plugin-api-deprecations.json'
|
||||
previous = File.read(deprecations)
|
||||
File.write(deprecations, JSON.generate([{
|
||||
capability: 'removed-fixture', version: 1, replacement_version: 2, remaining_releases: 7
|
||||
}]))
|
||||
git('add', deprecations)
|
||||
git('commit', '-qm', 'invalid lifecycle fixture')
|
||||
check.call(false, 'replacement v2 must be available')
|
||||
File.write(deprecations, previous)
|
||||
commit('restore lifecycle and notes')
|
||||
File.write('release/notes.md', "# v99.0.0\n\nUpdated fixture notes\n")
|
||||
|
||||
# 远端前进但没有本地对象时,提示同步而不隐式 fetch
|
||||
other = File.join(directory, 'other')
|
||||
git('clone', '-q', '-b', 'main', remote, other)
|
||||
Dir.chdir(other) do
|
||||
git('config', 'user.email', 'test@example.invalid')
|
||||
git('config', 'user.name', 'Release test')
|
||||
File.write('remote-only', 'change')
|
||||
commit('remote advance')
|
||||
git('push', '-q')
|
||||
end
|
||||
check.call(false, 'unseen commits')
|
||||
git('fetch', '-q', 'origin')
|
||||
check.call(false, 'diverged')
|
||||
FileUtils.mv(remote, "#{remote}.hidden")
|
||||
# 读取远端失败不能当作 tag 不存在,仍须保持本地不变
|
||||
before = [git('show-ref'), git('status', '--porcelain'), File.binread('.git/index')]
|
||||
_, error, status = Open3.capture3(environment, 'bash', 'release/publish', '--check', '99.0.0')
|
||||
assert(!status.success? && error.include?('failed to read remote refs'), 'remote failure was ignored')
|
||||
assert(before == [git('show-ref'), git('status', '--porcelain'), File.binread('.git/index')], 'remote failure changed local state')
|
||||
FileUtils.mv("#{remote}.hidden", remote)
|
||||
|
||||
File.write('release/version.yaml', "version: \"99.0.0\"\n")
|
||||
commit('release metadata')
|
||||
parent = git('rev-parse', 'HEAD^')
|
||||
run = { 'head_sha' => parent, 'conclusion' => 'success', 'head_repository' => { 'full_name' => 'owner/repo' }, 'id' => 42 }
|
||||
job = { 'name' => 'quality / backend', 'conclusion' => 'success', 'html_url' => 'https://example.invalid/42' }
|
||||
runs = [run]
|
||||
jobs = [job]
|
||||
original_capture = ReleaseQuality.method(:capture)
|
||||
ReleaseQuality.define_singleton_method(:capture) do |*arguments|
|
||||
if arguments.first == 'gh'
|
||||
endpoint = arguments.find { |argument| argument.start_with?('repos/') }
|
||||
if endpoint.include?('/workflows/ci.yml/runs?')
|
||||
assert(endpoint.include?("event=push&branch=main&head_sha=#{parent}"), 'wrong CI evidence scope')
|
||||
JSON.generate('workflow_runs' => runs)
|
||||
else
|
||||
assert(endpoint == 'repos/owner/repo/actions/runs/42/jobs?per_page=100', 'wrong run jobs')
|
||||
jobs.map { |entry| JSON.generate(entry) }.join("\n")
|
||||
end
|
||||
else
|
||||
original_capture.call(*arguments)
|
||||
end
|
||||
end
|
||||
assert(ReleaseQuality.evidence('owner/repo') == job['html_url'], 'valid CI evidence not reused')
|
||||
%w[skipped failure cancelled].each do |conclusion|
|
||||
job['conclusion'] = conclusion
|
||||
assert(ReleaseQuality.evidence('owner/repo').nil?, "reused #{conclusion} backend")
|
||||
end
|
||||
job['conclusion'] = 'success'
|
||||
jobs = []
|
||||
assert(ReleaseQuality.evidence('owner/repo').nil?, 'reused missing backend job')
|
||||
jobs = [job]
|
||||
run['conclusion'] = 'failure'
|
||||
assert(ReleaseQuality.evidence('owner/repo').nil?, 'reused failed CI run')
|
||||
run['conclusion'] = nil
|
||||
assert(ReleaseQuality.evidence('owner/repo').nil?, 'reused unfinished CI run')
|
||||
run['conclusion'] = 'success'
|
||||
run['head_repository']['full_name'] = 'fork/repo'
|
||||
assert(ReleaseQuality.evidence('owner/repo').nil?, 'reused foreign repository CI')
|
||||
run['head_repository']['full_name'] = 'owner/repo'
|
||||
runs = []
|
||||
assert(ReleaseQuality.evidence('owner/repo').nil?, 'reused absent CI run')
|
||||
runs = [run]
|
||||
output, _, status = Open3.capture3(environment.merge('GITHUB_REPOSITORY' => 'owner/repo'), 'ruby', File.join(ROOT, '.github/scripts/release-quality.rb'))
|
||||
assert(status.success? && output.strip == 'run_backend=true', 'API failure did not fall back to full checks')
|
||||
File.write('backend/source.rs', '// changed source')
|
||||
commit('source changed')
|
||||
assert(ReleaseQuality.evidence('owner/repo').nil?, 'reused CI across source changes')
|
||||
merge = git('commit-tree', 'HEAD^{tree}', '-p', 'HEAD', '-p', 'HEAD^', '-m', 'merge fixture')
|
||||
git('update-ref', 'HEAD', merge)
|
||||
assert(ReleaseQuality.evidence('owner/repo').nil?, 'merge commit should require full checks')
|
||||
|
||||
# 在同一个隔离仓库走实际提交与原子推送路径,dispatch 仍由本地替身接收
|
||||
git('merge', '--no-edit', 'origin/main')
|
||||
File.write('release/notes.md', "# v100.0.0\n\nPublish fixture notes\n")
|
||||
dispatch = File.join(directory, 'dispatch.json')
|
||||
mock = File.join(bin, 'gh')
|
||||
File.write(mock, File.read(mock).sub("else\n", <<~RUBY))
|
||||
when ['workflow', 'run', 'release.yml', '--repo', 'owner/repo', '--ref', 'main', '--raw-field', 'tag=v100.0.0']
|
||||
File.write(#{dispatch.inspect}, ARGV.join(' '))
|
||||
else
|
||||
RUBY
|
||||
output, error, status = Open3.capture3(environment, 'bash', 'release/publish', '100.0.0')
|
||||
assert(status.success?, "publish failed: #{output}#{error}")
|
||||
assert(File.exist?(dispatch), 'workflow was not dispatched')
|
||||
assert(git('log', '-1', '--format=%s') == 'chore(release): v100.0.0', 'wrong release commit')
|
||||
assert(git('status', '--porcelain').empty?, 'release left tracked changes')
|
||||
assert(git('rev-parse', 'HEAD') == git('rev-parse', 'v100.0.0^{commit}'), 'tag differs from release commit')
|
||||
assert(git('ls-remote', 'origin', 'refs/tags/v100.0.0').split.first == git('rev-parse', 'v100.0.0'), 'tag not pushed')
|
||||
end
|
||||
end
|
||||
puts 'Release preflight and CI evidence checks passed'
|
||||
@@ -82,6 +82,7 @@ module ReleaseMetadata
|
||||
'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),
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
require 'json'
|
||||
require 'open3'
|
||||
|
||||
# 仅复用同一源码的主分支 CI,缺少证据时完整检查发行提交
|
||||
module ReleaseQuality
|
||||
def self.capture(*command)
|
||||
output, error, status = Open3.capture3(*command)
|
||||
raise "#{command.first} failed: #{error.strip}" unless status.success?
|
||||
output
|
||||
end
|
||||
|
||||
def self.evidence(repository)
|
||||
parents = capture('git', 'rev-list', '--parents', '-n', '1', 'HEAD').split
|
||||
return nil unless parents.length == 2
|
||||
parent = parents.last
|
||||
changes = capture('git', 'diff', '--name-only', '-z', parent, 'HEAD').split("\0")
|
||||
return nil if changes.empty? || (changes - %w[release/version.yaml release/notes.md]).any?
|
||||
|
||||
# 按最新一次运行判断,失败或仍在执行时不回退到旧的成功记录
|
||||
endpoint = "repos/#{repository}/actions/workflows/ci.yml/runs?event=push&branch=main&head_sha=#{parent}&per_page=1"
|
||||
run = JSON.parse(capture('gh', 'api', endpoint)).fetch('workflow_runs').first
|
||||
return nil unless run && run.fetch('head_sha') == parent && run.fetch('conclusion') == 'success'
|
||||
return nil unless run.fetch('head_repository').fetch('full_name') == repository
|
||||
|
||||
rows = capture('gh', 'api', '--paginate', "repos/#{repository}/actions/runs/#{run.fetch('id')}/jobs?per_page=100", '--jq', '.jobs[] | @json')
|
||||
job = rows.lines.map { |line| JSON.parse(line) }.find { |entry| entry.fetch('name') == 'quality / backend' }
|
||||
return nil unless job && job.fetch('conclusion') == 'success'
|
||||
job.fetch('html_url')
|
||||
end
|
||||
end
|
||||
|
||||
if $PROGRAM_NAME == __FILE__
|
||||
begin
|
||||
evidence = ReleaseQuality.evidence(ENV.fetch('GITHUB_REPOSITORY'))
|
||||
rescue StandardError => error
|
||||
warn "CI evidence unavailable; running full release checks: #{error.message}"
|
||||
evidence = nil
|
||||
end
|
||||
puts "run_backend=#{evidence.nil?}"
|
||||
if ENV['GITHUB_STEP_SUMMARY']
|
||||
message = evidence ? "后端质量检查复用版本提交父提交的 [CI 结果](#{evidence});仅版本与发布说明不同" : '未找到可复用的后端 CI 结果,完整检查发行提交'
|
||||
File.open(ENV.fetch('GITHUB_STEP_SUMMARY'), 'a') { |file| file.puts(message) }
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user