Files
opencode-compat/ruby/lockstep_client_contract.rb
Ajay Krishnan 3dfecb72c6
Some checks failed
Candidate compatibility / prepare (push) Failing after 32s
Candidate compatibility / fixture-contract (push) Has been skipped
Candidate compatibility / lockstep clients Ruby 3.2 (push) Has been skipped
Candidate compatibility / lockstep clients Ruby 3.3 (push) Has been skipped
Candidate compatibility / lockstep clients Ruby 3.4 (push) Has been skipped
Candidate compatibility / lockstep clients Ruby 4.0 (push) Has been skipped
Candidate compatibility / image ${{ matrix.id }} (push) Has been skipped
Candidate compatibility / repository (push) Successful in 37s
Watch upstream OpenCode / open-compatibility-pr (push) Failing after 11s
Test alpha8 commit candidates before publication
2026-07-19 22:43:44 -07:00

423 lines
15 KiB
Ruby

# frozen_string_literal: true
require "fileutils"
require "json"
require "open3"
require "rubygems"
module OpenCodeCompat
class LockstepClientContract
class ContractError < StandardError; end
class GitCommandError < ContractError
attr_reader :exitstatus
def initialize(message, exitstatus:)
@exitstatus = exitstatus
super(message)
end
end
CONTRACT_NAME = "opencode-client-lockstep"
OPENCODE_RUBY_GIT_URI = "https://github.com/ajaynomics/opencode-ruby.git"
PUBLICATION_PROVENANCE = {
"published" => "annotated-tag",
"unpublished" => "commit"
}.freeze
REQUIRED_ENV = %w[
OPENCODE_CLIENT_PUBLICATION_STATE
OPENCODE_RUBY_PATH
OPENCODE_RUBY_COMMIT
OPENCODE_RUBY_PROVENANCE_KIND
OPENCODE_RUBY_VERSION
OPENCODE_RAILS_PATH
OPENCODE_RAILS_COMMIT
OPENCODE_RAILS_PROVENANCE_KIND
OPENCODE_RAILS_VERSION
].freeze
def self.from_environment(env: ENV, git_resolver: nil, git_head_resolver: nil)
require "bundler"
require "opencode-rails"
new(
env: env,
git_resolver: git_resolver,
git_head_resolver: git_head_resolver,
loaded_specs: Gem.loaded_specs,
bundle_specs: Bundler.load.specs,
runtime_ruby_version: RUBY_VERSION,
ruby_version: Opencode::VERSION,
rails_version: Opencode::RAILS_VERSION
)
end
def self.canonical_json(value)
JSON.generate(canonicalize(value))
end
def self.canonicalize(value)
case value
when Hash
value.keys.map(&:to_s).sort.to_h do |key|
source_key = value.key?(key) ? key : value.keys.find { |candidate| candidate.to_s == key }
[key, canonicalize(value.fetch(source_key))]
end
when Array
value.map { |entry| canonicalize(entry) }
else
value
end
end
private_class_method :canonicalize
def initialize(
env:,
loaded_specs:,
bundle_specs:,
ruby_version:,
rails_version:,
runtime_ruby_version: RUBY_VERSION,
git_resolver: nil,
git_head_resolver: nil
)
@env = env
@loaded_specs = loaded_specs
@bundle_specs = bundle_specs
@ruby_version = ruby_version.to_s
@rails_version = rails_version.to_s
@runtime_ruby_version = runtime_ruby_version.to_s
@git_resolver = git_resolver || method(:resolve_git)
@git_head_resolver = git_head_resolver
end
def verify
ensure_required_environment!
ruby_path = realpath("OPENCODE_RUBY_PATH")
rails_path = realpath("OPENCODE_RAILS_PATH")
ruby_commit = exact_commit("OPENCODE_RUBY_COMMIT")
rails_commit = exact_commit("OPENCODE_RAILS_COMMIT")
expected_ruby_version = exact_version("OPENCODE_RUBY_VERSION")
expected_rails_version = exact_version("OPENCODE_RAILS_VERSION")
publication_state = exact_publication_state
assert_equal!(ruby_commit, checkout_head(ruby_path, "opencode-ruby"), "opencode-ruby checkout commit")
assert_equal!(rails_commit, checkout_head(rails_path, "opencode-rails"), "opencode-rails checkout commit")
ruby_source_provenance = source_provenance(
path: ruby_path,
label: "opencode-ruby",
prefix: "OPENCODE_RUBY",
publication_state: publication_state,
expected_commit: ruby_commit,
expected_version: expected_ruby_version
)
rails_source_provenance = source_provenance(
path: rails_path,
label: "opencode-rails",
prefix: "OPENCODE_RAILS",
publication_state: publication_state,
expected_commit: rails_commit,
expected_version: expected_rails_version
)
assert_equal!(expected_ruby_version, @ruby_version, "loaded Opencode::VERSION")
assert_equal!(expected_rails_version, @rails_version, "loaded Opencode::RAILS_VERSION")
assert_equal!(expected_ruby_version, expected_rails_version, "lockstep client version")
ruby_spec = loaded_spec!("opencode-ruby")
rails_spec = loaded_spec!("opencode-rails")
assert_equal!(expected_ruby_version, ruby_spec.version.to_s, "loaded opencode-ruby gem version")
assert_equal!(expected_rails_version, rails_spec.version.to_s, "loaded opencode-rails gem version")
assert_equal!(rails_path, File.realpath(rails_spec.full_gem_path), "loaded opencode-rails checkout")
dependency = exact_runtime_dependency!(rails_spec, expected_ruby_version)
bundle_spec = bundled_ruby_spec!
source = bundle_spec.source
unless source && source.respond_to?(:revision) && source.respond_to?(:ref) && source.respond_to?(:uri) &&
source.class.name.match?(/(?:\A|::)Git\z/)
raise ContractError, "Bundler opencode-ruby source must be a Git source"
end
bundler_revision = source.revision.to_s
bundler_ref = source.ref.to_s
bundler_uri = source.uri.to_s
assert_exact_commit!(bundler_revision, "Bundler opencode-ruby revision")
assert_exact_commit!(bundler_ref, "Bundler opencode-ruby ref")
assert_equal!(ruby_commit, bundler_revision, "Bundler opencode-ruby revision")
assert_equal!(ruby_commit, bundler_ref, "Bundler opencode-ruby ref")
assert_equal!(OPENCODE_RUBY_GIT_URI, bundler_uri, "Bundler opencode-ruby URI")
{
"contract" => CONTRACT_NAME,
"publication_state" => publication_state,
"opencode_rails" => {
"checkout_commit" => rails_commit,
"gem_version" => rails_spec.version.to_s,
"loaded_version" => @rails_version,
"source_provenance" => rails_source_provenance,
"runtime_dependency" => {
"name" => dependency.name,
"requirement" => dependency.requirement.to_s
}
},
"opencode_ruby" => {
"bundler_git_ref" => bundler_ref,
"bundler_git_revision" => bundler_revision,
"bundler_git_uri" => bundler_uri,
"bundler_source" => source.class.name,
"checkout_commit" => ruby_commit,
"gem_version" => ruby_spec.version.to_s,
"loaded_version" => @ruby_version,
"source_provenance" => ruby_source_provenance
},
"ruby_runtime_version" => @runtime_ruby_version,
"schema_version" => 2,
"status" => "pass",
"workflow" => {
"head_sha" => @env["OPENCODE_COMPAT_HEAD_SHA"],
"repository" => @env["OPENCODE_COMPAT_REPOSITORY"],
"run_attempt" => @env["OPENCODE_COMPAT_RUN_ATTEMPT"],
"run_id" => @env["OPENCODE_COMPAT_RUN_ID"],
"run_url" => @env["OPENCODE_COMPAT_RUN_URL"]
}
}
rescue Errno::ENOENT, Errno::EACCES => error
raise ContractError, error.message
end
def emit(io: $stdout)
document = verify
json = self.class.canonical_json(document)
evidence_path = @env["OPENCODE_COMPAT_EVIDENCE_PATH"].to_s
unless evidence_path.empty?
expanded_path = File.expand_path(evidence_path)
FileUtils.mkdir_p(File.dirname(expanded_path))
File.binwrite(expanded_path, "#{json}\n")
end
io.puts(json)
document
end
private
def ensure_required_environment!
missing = REQUIRED_ENV.select { |name| @env[name].to_s.empty? }
return if missing.empty?
raise ContractError, "missing required environment: #{missing.join(', ')}"
end
def realpath(name)
File.realpath(@env.fetch(name))
end
def exact_commit(name)
value = @env.fetch(name)
assert_exact_commit!(value, name)
value
end
def exact_publication_state
value = @env.fetch("OPENCODE_CLIENT_PUBLICATION_STATE")
return value if PUBLICATION_PROVENANCE.key?(value)
raise ContractError,
"OPENCODE_CLIENT_PUBLICATION_STATE must be one of #{PUBLICATION_PROVENANCE.keys.join(', ')}, got #{value.inspect}"
end
def exact_tag(name)
exact_tag_value(@env.fetch(name), name)
end
def exact_tag_value(value, label)
if value.match?(/\A[A-Za-z0-9][A-Za-z0-9._-]*\z/) && !value.include?("..") && !value.end_with?(".")
return value
end
raise ContractError, "#{label} must be a safe exact Git tag name, got #{value.inspect}"
end
def exact_tag_object(name)
value = @env.fetch(name)
return value if value.match?(/\A[0-9a-f]{40}\z/)
raise ContractError,
"#{name} must be a full lowercase 40-character Git object ID, got #{value.inspect}"
end
def assert_exact_commit!(value, label)
return if value.match?(/\A[0-9a-f]{40}\z/)
raise ContractError, "#{label} must be a full lowercase 40-character Git commit, got #{value.inspect}"
end
def exact_version(name)
value = @env.fetch(name)
return value if Gem::Version.correct?(value)
raise ContractError, "#{name} must be an exact RubyGems version, got #{value.inspect}"
end
def checkout_head(path, label)
head = if @git_head_resolver
@git_head_resolver.call(path).to_s.strip
else
git_value(path, "#{label} checkout HEAD", "rev-parse", "--verify", "HEAD^{commit}")
end
assert_exact_commit!(head, "#{label} checkout HEAD")
head
rescue ContractError
raise
rescue StandardError => error
raise ContractError, "could not resolve #{label} checkout HEAD: #{error.message}"
end
def source_provenance(path:, label:, prefix:, publication_state:, expected_commit:, expected_version:)
kind_name = "#{prefix}_PROVENANCE_KIND"
kind = @env.fetch(kind_name)
expected_kind = PUBLICATION_PROVENANCE.fetch(publication_state)
assert_equal!(expected_kind, kind, "#{label} provenance kind for #{publication_state} candidate")
case kind
when "commit"
ensure_unpublished_tag_absent!(path, label, expected_version)
supplied_tag_fields = %W[#{prefix}_TAG #{prefix}_TAG_OBJECT].select do |name|
!@env[name].to_s.empty?
end
unless supplied_tag_fields.empty?
raise ContractError,
"#{label} unpublished commit provenance must not supply #{supplied_tag_fields.join(', ')}"
end
{"commit" => expected_commit, "kind" => "commit"}
when "annotated-tag"
annotated_tag_provenance(
path: path,
label: label,
tag_name: "#{prefix}_TAG",
tag_object: "#{prefix}_TAG_OBJECT",
expected_commit: expected_commit
).merge("kind" => "annotated-tag")
else
raise ContractError, "#{kind_name} has unsupported value #{kind.inspect}"
end
end
def ensure_unpublished_tag_absent!(path, label, expected_version)
tag = exact_tag_value("v#{expected_version}", "#{label} expected release tag")
tag_ref = "refs/tags/#{tag}"
resolved = begin
@git_resolver.call(path, "rev-parse", "--verify", "--quiet", tag_ref).to_s.strip
rescue GitCommandError => error
raise ContractError, "could not check #{label} release tag: #{error.message}" unless error.exitstatus == 1
nil
rescue KeyError
nil
end
return if resolved.nil? || resolved.empty?
raise ContractError,
"#{label} release tag #{tag} already exists; publication_state must be published with annotated-tag provenance"
end
def annotated_tag_provenance(path:, label:, tag_name:, tag_object:, expected_commit:)
tag = exact_tag(tag_name)
object = exact_tag_object(tag_object)
tag_ref = "refs/tags/#{tag}"
object_type = git_value(path, "#{label} annotated tag object type", "cat-file", "-t", object)
assert_equal!("tag", object_type, "#{label} annotated tag object type")
resolved_object = git_value(path, "#{label} local tag ref", "rev-parse", "--verify", tag_ref)
assert_exact_commit!(resolved_object, "#{label} local tag ref object")
assert_equal!(object, resolved_object, "#{label} local tag ref object")
object_commit = git_value(path, "#{label} annotated tag peel", "rev-parse", "--verify", "#{object}^{commit}")
assert_exact_commit!(object_commit, "#{label} annotated tag peeled commit")
assert_equal!(expected_commit, object_commit, "#{label} annotated tag peeled commit")
ref_commit = git_value(path, "#{label} local tag ref peel", "rev-parse", "--verify", "#{tag_ref}^{commit}")
assert_exact_commit!(ref_commit, "#{label} local tag ref peeled commit")
assert_equal!(expected_commit, ref_commit, "#{label} local tag ref peeled commit")
{
"annotated_tag_object" => object,
"peeled_commit" => object_commit,
"tag" => tag
}
end
def git_value(path, label, *arguments)
@git_resolver.call(path, *arguments).to_s.strip
rescue StandardError => error
raise ContractError, "could not resolve #{label}: #{error.message}"
end
def resolve_git(path, *arguments)
stdout, stderr, status = Open3.capture3("git", "-C", path, *arguments)
return stdout.strip if status.success?
detail = stderr.strip
detail = "git exited #{status.exitstatus}" if detail.empty?
raise GitCommandError.new(detail, exitstatus: status.exitstatus)
end
def loaded_spec!(name)
@loaded_specs.fetch(name)
rescue KeyError
raise ContractError, "#{name} is not present in Gem.loaded_specs"
end
def exact_runtime_dependency!(rails_spec, ruby_version)
dependencies = rails_spec.runtime_dependencies.select { |dependency| dependency.name == "opencode-ruby" }
unless dependencies.length == 1
raise ContractError, "opencode-rails must declare exactly one runtime dependency on opencode-ruby"
end
dependency = dependencies.first
expected = [["=", Gem::Version.new(ruby_version)]]
unless dependency.requirement.requirements == expected
raise ContractError,
"opencode-rails must require opencode-ruby = #{ruby_version}, got #{dependency.requirement}"
end
dependency
end
def bundled_ruby_spec!
specs = @bundle_specs.select { |spec| spec.name == "opencode-ruby" }
return specs.first if specs.length == 1
raise ContractError, "Bundler must resolve exactly one opencode-ruby spec, found #{specs.length}"
end
def assert_equal!(expected, actual, label)
return if expected == actual
raise ContractError, "#{label} must equal #{expected.inspect}, got #{actual.inspect}"
end
end
end
if $PROGRAM_NAME == __FILE__
begin
OpenCodeCompat::LockstepClientContract.from_environment.emit
rescue StandardError => error
failure = OpenCodeCompat::LockstepClientContract.canonical_json(
"contract" => OpenCodeCompat::LockstepClientContract::CONTRACT_NAME,
"error" => "#{error.class}: #{error.message}",
"schema_version" => 2,
"status" => "fail"
)
if (evidence_path = ENV["OPENCODE_COMPAT_EVIDENCE_PATH"]) && !evidence_path.empty?
expanded_path = File.expand_path(evidence_path)
FileUtils.mkdir_p(File.dirname(expanded_path))
File.binwrite(expanded_path, "#{failure}\n")
end
warn failure
exit 1
end
end