mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
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>
194 lines
9.2 KiB
Ruby
194 lines
9.2 KiB
Ruby
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'
|