Add atomic runtime tuple promotion gate

This commit is contained in:
2026-07-18 14:14:41 -07:00
parent b84432f989
commit a640f44972
7 changed files with 995 additions and 5 deletions

View File

@@ -20,7 +20,7 @@ jobs:
- uses: ruby/setup-ruby@v1 - uses: ruby/setup-ruby@v1
with: with:
ruby-version: "3.4" ruby-version: "3.4"
- run: ruby test/repository_test.rb - run: ruby test/repository_test.rb && ruby test/runtime_tuple_promoter_test.rb
prepare: prepare:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@@ -41,6 +41,7 @@ Prerequisites are Ruby 3.2+, Python 3, `jq`, and Docker.
```sh ```sh
ruby test/repository_test.rb ruby test/repository_test.rb
ruby test/runtime_tuple_promoter_test.rb
OPENCODE_RUBY_PATH=/data/projects/opencode-ruby-alpha6 \ OPENCODE_RUBY_PATH=/data/projects/opencode-ruby-alpha6 \
ruby ruby/opencode_ruby_fixture_contract.rb ruby ruby/opencode_ruby_fixture_contract.rb
OPENCODE_RUBY_PATH=/data/projects/opencode-ruby-alpha6 \ OPENCODE_RUBY_PATH=/data/projects/opencode-ruby-alpha6 \

View File

@@ -25,6 +25,63 @@ Promotion is a reviewed manifest change that moves the old `current` tuple to
merged and deployed separately. Workflows in this repository have no deploy merged and deployed separately. Workflows in this repository have no deploy
credentials or deploy steps. credentials or deploy steps.
Use the repository promotion command; do not hand-edit the three tuple slots.
It binds evidence to a canonical SHA-256 of the complete tuple, requires full
consumer and gem commits, rejects mutable runtime image coordinates, and writes
the manifest with a same-filesystem atomic rename. The command only changes
`manifests/runtime-tuples.json`; it cannot deploy a consumer.
First inspect the fingerprints for both the candidate and rollback tuple:
```sh
ruby scripts/promote_runtime_tuple.rb fingerprint \
--consumer travelwolf \
--consumer-commit FULL_40_CHARACTER_CONSUMER_COMMIT
```
Commit one or more JSON evidence documents under `evidence/`. Each document
must explicitly contain values matching the promotion:
```json
{
"schema_version": 1,
"consumer": "travelwolf",
"profile": "rails-persisted-turn",
"consumer_commit": "FULL_40_CHARACTER_CONSUMER_COMMIT",
"status": "pass",
"certified_at": "2026-07-18T12:00:00Z",
"tuple_sha256": "sha256:FULL_64_CHARACTER_FINGERPRINT"
}
```
Preview the exact manifest transition with `--dry-run`, then repeat without it
to write the manifest:
```sh
ruby scripts/promote_runtime_tuple.rb promote \
--consumer travelwolf \
--consumer-commit FULL_40_CHARACTER_CONSUMER_COMMIT \
--status pass \
--certified-at 2026-07-18T12:00:00Z \
--evidence evidence/travelwolf-candidate.json \
--previous-status pass \
--previous-certified-at 2026-07-17T12:00:00Z \
--previous-evidence evidence/travelwolf-rollback.json \
--dry-run
```
The `--previous-*` arguments are required while the old `current` tuple lacks a
valid certification record. Later promotions reuse and revalidate that record.
Both candidate and previous evidence must match the printed tuple fingerprint,
consumer, full consumer commit, timestamp, and `pass` status. A tag may be kept
only in a `tag_provenance` field; `image`, `registry_ref`, and base image fields
must use `image@sha256:...`, while a private local artifact may use an exact
`docker_image_id`.
The command clears `candidate` after promotion. It sets the repository-wide
`migration_state` to `certified` only after every consumer has certified
`current` and `previous` tuples and no pending candidate.
## Rollback ## Rollback
Rollback restores the whole `previous` tuple. Do not roll back only the gem or Rollback restores the whole `previous` tuple. Do not roll back only the gem or

View File

@@ -0,0 +1,495 @@
# frozen_string_literal: true
require "digest"
require "json"
require "pathname"
require "tempfile"
require "time"
module OpenCodeCompat
class PromotionError < StandardError; end
class RuntimeTuplePromoter
FULL_COMMIT = /\A[0-9a-f]{40}\z/
DIGEST = /\Asha256:[0-9a-f]{64}\z/
IMMUTABLE_IMAGE = /\A[^@\s]+@sha256:[0-9a-f]{64}\z/
UTC_TIMESTAMP = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z\z/
CERTIFICATION_STATUS = "pass"
TUPLE_METADATA_KEYS = %w[
certification
certified_at
compatibility_certified_at
promoted_at
status
].freeze
EXACT_IMAGE_REFERENCE_KEYS = %w[
base_image
image
registry_ref
source_image
upstream_base
].freeze
def initialize(root:, manifest_path: File.join(root, "manifests/runtime-tuples.json"))
@root = File.realpath(root)
@manifest_path = File.expand_path(manifest_path)
end
def fingerprints(consumer:, consumer_commit:)
validate_full_commit!(consumer_commit, "consumer commit")
manifest = read_manifest
consumer_entry = fetch_consumer!(manifest, consumer)
profile = fetch_profile!(consumer_entry, consumer)
candidate = prepare_candidate!(consumer_entry, consumer_commit)
current = consumer_entry.fetch("current") do
raise PromotionError, "#{consumer} has no current tuple to preserve as previous"
end
validate_tuple!(current, "#{consumer} current")
{
"consumer" => consumer,
"profile" => profile,
"candidate_tuple_sha256" => tuple_fingerprint(candidate, consumer: consumer, profile: profile),
"current_tuple_sha256" => tuple_fingerprint(current, consumer: consumer, profile: profile)
}
end
def promote(consumer:, consumer_commit:, certification:, previous_certification: nil, dry_run: false)
validate_full_commit!(consumer_commit, "consumer commit")
if dry_run
manifest = read_manifest
return promote_manifest(
manifest,
consumer: consumer,
consumer_commit: consumer_commit,
certification: certification,
previous_certification: previous_certification
)
end
with_current_manifest_lock do |manifest|
promoted = promote_manifest(
manifest,
consumer: consumer,
consumer_commit: consumer_commit,
certification: certification,
previous_certification: previous_certification
)
atomic_write(promoted)
promoted
end
end
private
def read_manifest
parse_json(File.read(@manifest_path), @manifest_path)
rescue Errno::ENOENT
raise PromotionError, "runtime tuple manifest does not exist: #{@manifest_path}"
end
def promote_manifest(manifest, consumer:, consumer_commit:, certification:, previous_certification:)
consumer_entry = fetch_consumer!(manifest, consumer)
profile = fetch_profile!(consumer_entry, consumer)
candidate = prepare_candidate!(consumer_entry, consumer_commit)
current = consumer_entry.fetch("current") do
raise PromotionError, "#{consumer} has no current tuple to preserve as previous"
end
validate_tuple!(current, "#{consumer} current")
candidate_fingerprint = tuple_fingerprint(candidate, consumer: consumer, profile: profile)
current_fingerprint = tuple_fingerprint(current, consumer: consumer, profile: profile)
if candidate_fingerprint == current_fingerprint
raise PromotionError, "#{consumer} candidate is identical to its current tuple"
end
certified_candidate = certify_tuple!(
candidate,
consumer: consumer,
profile: profile,
supplied: certification,
expected_fingerprint: candidate_fingerprint,
label: "candidate"
)
certified_previous = certify_previous!(
current,
consumer: consumer,
profile: profile,
supplied: previous_certification,
expected_fingerprint: current_fingerprint
)
consumer_entry["previous"] = certified_previous
consumer_entry["current"] = certified_candidate
consumer_entry["candidate"] = nil
manifest["migration_state"] = all_consumers_certified?(manifest) ? "certified" : "candidate"
manifest
end
def fetch_consumer!(manifest, consumer)
unless manifest.is_a?(Hash) && manifest["schema_version"] == 1 && manifest["consumers"].is_a?(Hash)
raise PromotionError, "runtime tuple manifest must use schema_version 1 and contain consumers"
end
manifest.fetch("consumers").fetch(consumer) do
raise PromotionError, "unknown consumer #{consumer.inspect}"
end
end
def prepare_candidate!(consumer_entry, consumer_commit)
candidate = consumer_entry["candidate"]
unless candidate.is_a?(Hash)
raise PromotionError, "consumer has no candidate tuple to promote"
end
unless candidate["status"] == "compatibility-certified"
raise PromotionError, "candidate status must be compatibility-certified"
end
validate_timestamp!(candidate["certified_at"], "candidate compatibility certification timestamp")
if candidate.key?("consumer_commit") && candidate["consumer_commit"] != consumer_commit
raise PromotionError, "explicit consumer commit does not match the candidate"
end
prepared = deep_copy(candidate)
prepared["consumer_commit"] = consumer_commit
validate_tuple!(prepared, "candidate")
prepared
end
def fetch_profile!(consumer_entry, consumer)
profile = consumer_entry["profile"]
unless profile.is_a?(String) && profile.match?(/\A[a-z0-9][a-z0-9-]*\z/)
raise PromotionError, "#{consumer} must declare a valid compatibility profile"
end
profile_path = File.join(@root, "profiles", "#{profile}.json")
unless File.file?(profile_path)
raise PromotionError, "#{consumer} compatibility profile does not exist: profiles/#{profile}.json"
end
profile
end
def certify_previous!(tuple, consumer:, profile:, supplied:, expected_fingerprint:)
if tuple["status"] == "certified"
validate_recorded_certification!(
tuple,
consumer: consumer,
profile: profile,
expected_fingerprint: expected_fingerprint,
label: "current rollback"
)
return deep_copy(tuple)
end
unless supplied
raise PromotionError,
"current tuple is not certified; explicit previous certification evidence, timestamp, and status are required"
end
certify_tuple!(
tuple,
consumer: consumer,
profile: profile,
supplied: supplied,
expected_fingerprint: expected_fingerprint,
label: "previous"
)
end
def certify_tuple!(tuple, consumer:, profile:, supplied:, expected_fingerprint:, label:)
certification = validate_supplied_certification!(
supplied,
consumer: consumer,
profile: profile,
tuple: tuple,
expected_fingerprint: expected_fingerprint,
label: label
)
certified = deep_copy(tuple)
if certified.key?("certified_at")
certified["compatibility_certified_at"] = certified.delete("certified_at")
end
certified["status"] = "certified"
certified["certification"] = certification
certified
end
def validate_supplied_certification!(supplied, consumer:, profile:, tuple:, expected_fingerprint:, label:)
unless supplied.is_a?(Hash)
raise PromotionError, "#{label} certification evidence, timestamp, and status are required"
end
status = supplied["status"]
certified_at = supplied["certified_at"]
evidence = Array(supplied["evidence"])
unless status == CERTIFICATION_STATUS
raise PromotionError, "#{label} certification status must be #{CERTIFICATION_STATUS.inspect}"
end
validate_timestamp!(certified_at, "#{label} certification timestamp")
raise PromotionError, "#{label} certification requires at least one evidence file" if evidence.empty?
raise PromotionError, "#{label} certification evidence files must be unique" unless evidence.uniq == evidence
consumer_commit = tuple.fetch("consumer_commit")
normalized_evidence = evidence.map do |reference|
validate_evidence!(
reference,
consumer: consumer,
profile: profile,
consumer_commit: consumer_commit,
status: status,
certified_at: certified_at,
tuple_fingerprint: expected_fingerprint,
label: label
)
end
{
"status" => status,
"certified_at" => certified_at,
"tuple_sha256" => expected_fingerprint,
"evidence" => normalized_evidence
}
end
def validate_recorded_certification!(tuple, consumer:, profile:, expected_fingerprint:, label:)
certification = tuple["certification"]
unless certification.is_a?(Hash) && certification["tuple_sha256"] == expected_fingerprint
raise PromotionError, "#{label} tuple has invalid or stale certification metadata"
end
validate_supplied_certification!(
certification,
consumer: consumer,
profile: profile,
tuple: tuple,
expected_fingerprint: expected_fingerprint,
label: label
)
end
def validate_evidence!(
reference,
consumer:,
profile:,
consumer_commit:,
status:,
certified_at:,
tuple_fingerprint:,
label:
)
unless reference.is_a?(String) && !reference.empty?
raise PromotionError, "#{label} evidence references must be non-empty strings"
end
relative = Pathname.new(reference).cleanpath
if relative.absolute? || relative.each_filename.first != "evidence"
raise PromotionError, "#{label} evidence must be a repository-relative path under evidence/"
end
full_path = File.join(@root, relative.to_s)
evidence_root = File.realpath(File.join(@root, "evidence"))
real_path = File.realpath(full_path)
unless real_path.start_with?("#{evidence_root}#{File::SEPARATOR}")
raise PromotionError, "#{label} evidence resolves outside evidence/"
end
document = parse_json(File.read(real_path), relative.to_s)
unless document.is_a?(Hash) && document["schema_version"] == 1
raise PromotionError, "#{label} evidence #{relative} must use schema_version 1"
end
expected = {
"consumer" => consumer,
"profile" => profile,
"consumer_commit" => consumer_commit,
"status" => status,
"certified_at" => certified_at,
"tuple_sha256" => tuple_fingerprint
}
expected.each do |key, value|
next if document[key] == value
raise PromotionError,
"#{label} evidence #{relative} has #{key}=#{document[key].inspect}; expected #{value.inspect}"
end
relative.to_s
rescue Errno::ENOENT
raise PromotionError, "#{label} evidence does not exist: #{reference}"
end
def validate_tuple!(tuple, label)
raise PromotionError, "#{label} tuple must be an object" unless tuple.is_a?(Hash)
validate_full_commit!(tuple["consumer_commit"], "#{label} consumer commit")
validate_client!(tuple["opencode_ruby"], "#{label} opencode_ruby", required: true)
validate_client!(tuple["opencode_rails"], "#{label} opencode_rails", required: false)
validate_runtime!(tuple["runtime"], "#{label} runtime")
end
def validate_client!(client, label, required:)
if client.nil?
raise PromotionError, "#{label} is required" if required
return
end
unless client.is_a?(Hash) && client["version"].is_a?(String) && !client["version"].empty?
raise PromotionError, "#{label} must include a non-empty version"
end
validate_full_commit!(client["git_commit"], "#{label} git commit")
end
def validate_runtime!(runtime, label)
raise PromotionError, "#{label} must be an object" unless runtime.is_a?(Hash)
unless runtime["reported_version"].is_a?(String) && !runtime["reported_version"].empty?
raise PromotionError, "#{label} must include the reported OpenCode server version"
end
exact_execution_coordinate = false
runtime.each do |key, value|
if key == "docker_image_id" || key.end_with?("_image_id")
unless value.is_a?(String) && value.match?(DIGEST)
raise PromotionError, "#{label} #{key} must be an exact sha256 image ID"
end
exact_execution_coordinate = true if key == "docker_image_id"
elsif image_reference_key?(key)
unless value.is_a?(String) && value.match?(IMMUTABLE_IMAGE)
raise PromotionError, "#{label} #{key} must be an immutable image@sha256 digest, not a tag"
end
exact_execution_coordinate = true if %w[image registry_ref].include?(key)
elsif key == "source_commit"
validate_full_commit!(value, "#{label} source commit")
end
end
unless exact_execution_coordinate
raise PromotionError, "#{label} must include an immutable image, registry_ref, or docker_image_id"
end
end
def image_reference_key?(key)
EXACT_IMAGE_REFERENCE_KEYS.include?(key) ||
(key.end_with?("_image") && key != "tag_provenance") ||
(key.end_with?("_ref") && key != "tag_provenance")
end
def validate_full_commit!(value, label)
return if value.is_a?(String) && value.match?(FULL_COMMIT)
raise PromotionError, "#{label} must be a full 40-character lowercase Git commit"
end
def validate_timestamp!(value, label)
unless value.is_a?(String) && value.match?(UTC_TIMESTAMP)
raise PromotionError, "#{label} must be an explicit RFC 3339 UTC timestamp"
end
Time.iso8601(value)
rescue ArgumentError
raise PromotionError, "#{label} is not a valid timestamp"
end
def tuple_fingerprint(tuple, consumer:, profile:)
payload = deep_copy(tuple)
TUPLE_METADATA_KEYS.each { |key| payload.delete(key) }
envelope = {
"consumer" => consumer,
"profile" => profile,
"tuple" => payload
}
"sha256:#{Digest::SHA256.hexdigest(JSON.generate(canonicalize(envelope)))}"
end
def canonicalize(value)
case value
when Hash
value.keys.sort.to_h { |key| [key, canonicalize(value.fetch(key))] }
when Array
value.map { |item| canonicalize(item) }
else
value
end
end
def deep_copy(value)
JSON.parse(JSON.generate(value))
end
def parse_json(contents, label)
JSON.parse(contents)
rescue JSON::ParserError => e
raise PromotionError, "invalid JSON in #{label}: #{e.message}"
end
def all_consumers_certified?(manifest)
manifest.fetch("consumers").all? do |consumer, entry|
next false unless entry["candidate"].nil?
profile = fetch_profile!(entry, consumer)
%w[current previous].all? do |slot|
tuple = entry[slot]
next false unless tuple.is_a?(Hash) && tuple["status"] == "certified"
validate_tuple!(tuple, "#{consumer} #{slot}")
validate_recorded_certification!(
tuple,
consumer: consumer,
profile: profile,
expected_fingerprint: tuple_fingerprint(tuple, consumer: consumer, profile: profile),
label: "#{consumer} #{slot}"
)
true
rescue PromotionError
false
end
end
end
def with_current_manifest_lock
loop do
retry_with_new_inode = false
result = nil
File.open(@manifest_path, File::RDONLY) do |locked_file|
locked_file.flock(File::LOCK_EX)
unless File.identical?(locked_file, @manifest_path)
retry_with_new_inode = true
next
end
result = yield parse_json(locked_file.read, @manifest_path)
end
return result unless retry_with_new_inode
end
rescue Errno::ENOENT
raise PromotionError, "runtime tuple manifest does not exist: #{@manifest_path}"
end
def atomic_write(manifest)
directory = File.dirname(@manifest_path)
mode = File.stat(@manifest_path).mode & 0o777
payload = JSON.pretty_generate(manifest) + "\n"
Tempfile.create([".runtime-tuples-", ".tmp"], directory) do |temporary|
temporary.chmod(mode)
temporary.write(payload)
temporary.flush
temporary.fsync
temporary.close
File.rename(temporary.path, @manifest_path)
fsync_directory(directory)
end
end
def fsync_directory(directory)
File.open(directory, File::RDONLY, &:fsync)
rescue Errno::EINVAL, Errno::ENOTSUP
# Some filesystems do not support directory fsync; rename is still atomic.
end
end
end

104
scripts/promote_runtime_tuple.rb Executable file
View File

@@ -0,0 +1,104 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require "json"
require "optparse"
require_relative "../lib/opencode_compat/runtime_tuple_promoter"
root = File.expand_path("..", __dir__)
command = ARGV.shift
options = {
"evidence" => [],
"previous_evidence" => []
}
parser = OptionParser.new do |opts|
opts.banner = <<~USAGE
Usage:
ruby scripts/promote_runtime_tuple.rb fingerprint --consumer NAME --consumer-commit SHA
ruby scripts/promote_runtime_tuple.rb promote --consumer NAME --consumer-commit SHA \\
--status pass --certified-at TIMESTAMP --evidence evidence/FILE.json \\
[--previous-status pass --previous-certified-at TIMESTAMP \\
--previous-evidence evidence/FILE.json] [--dry-run]
USAGE
opts.on("--consumer NAME") { |value| options["consumer"] = value }
opts.on("--consumer-commit SHA") { |value| options["consumer_commit"] = value }
opts.on("--status STATUS") { |value| options["status"] = value }
opts.on("--certified-at TIMESTAMP") { |value| options["certified_at"] = value }
opts.on("--evidence PATH") { |value| options["evidence"] << value }
opts.on("--previous-status STATUS") { |value| options["previous_status"] = value }
opts.on("--previous-certified-at TIMESTAMP") { |value| options["previous_certified_at"] = value }
opts.on("--previous-evidence PATH") { |value| options["previous_evidence"] << value }
opts.on("--dry-run") { options["dry_run"] = true }
end
def required!(options, *keys)
missing = keys.reject { |key| options[key] && (!options[key].respond_to?(:empty?) || !options[key].empty?) }
raise OptionParser::MissingArgument, missing.join(", ") unless missing.empty?
end
begin
parser.parse!(ARGV)
raise OptionParser::InvalidArgument, "unexpected arguments: #{ARGV.join(' ')}" unless ARGV.empty?
promoter = OpenCodeCompat::RuntimeTuplePromoter.new(root: root)
case command
when "fingerprint"
required!(options, "consumer", "consumer_commit")
puts JSON.pretty_generate(
promoter.fingerprints(
consumer: options.fetch("consumer"),
consumer_commit: options.fetch("consumer_commit")
)
)
when "promote"
required!(options, "consumer", "consumer_commit", "status", "certified_at", "evidence")
previous_fields = %w[previous_status previous_certified_at previous_evidence]
supplied_previous_fields = previous_fields.select do |key|
value = options[key]
value && (!value.respond_to?(:empty?) || !value.empty?)
end
if supplied_previous_fields.any? && supplied_previous_fields.length != previous_fields.length
raise OptionParser::MissingArgument, (previous_fields - supplied_previous_fields).join(", ")
end
previous_certification = if supplied_previous_fields.empty?
nil
else
{
"status" => options.fetch("previous_status"),
"certified_at" => options.fetch("previous_certified_at"),
"evidence" => options.fetch("previous_evidence")
}
end
promoted = promoter.promote(
consumer: options.fetch("consumer"),
consumer_commit: options.fetch("consumer_commit"),
certification: {
"status" => options.fetch("status"),
"certified_at" => options.fetch("certified_at"),
"evidence" => options.fetch("evidence")
},
previous_certification: previous_certification,
dry_run: options.fetch("dry_run", false)
)
if options.fetch("dry_run", false)
puts JSON.pretty_generate(promoted)
else
current = promoted.fetch("consumers").fetch(options.fetch("consumer")).fetch("current")
puts JSON.pretty_generate(
"consumer" => options.fetch("consumer"),
"current_consumer_commit" => current.fetch("consumer_commit"),
"current_tuple_sha256" => current.dig("certification", "tuple_sha256"),
"migration_state" => promoted.fetch("migration_state")
)
end
else
raise OptionParser::InvalidArgument, "command must be fingerprint or promote"
end
rescue OpenCodeCompat::PromotionError, OptionParser::ParseError => e
warn "error: #{e.message}"
warn parser
exit 1
end

View File

@@ -11,7 +11,7 @@ class RepositoryTest < Minitest::Test
end end
def test_every_json_document_parses def test_every_json_document_parses
paths = Dir.glob(File.join(ROOT, "{fixtures,manifests,profiles}/**/*.json")) paths = Dir.glob(File.join(ROOT, "{evidence,fixtures,manifests,profiles}/**/*.json"))
refute_empty paths refute_empty paths
paths.each { |path| JSON.parse(File.read(path)) } paths.each { |path| JSON.parse(File.read(path)) }
end end
@@ -50,9 +50,21 @@ class RepositoryTest < Minitest::Test
return unless tuples.fetch("migration_state") == "certified" return unless tuples.fetch("migration_state") == "certified"
tuples.fetch("consumers").each_value do |consumer| tuples.fetch("consumers").each_value do |consumer|
refute_nil consumer["current"] %w[current previous].each do |slot|
refute_nil consumer["previous"] tuple = consumer.fetch(slot)
assert_equal "certified", consumer.fetch("current").fetch("status") assert_equal "certified", tuple.fetch("status")
assert_equal "pass", tuple.fetch("certification").fetch("status")
assert_match(/\Asha256:[0-9a-f]{64}\z/, tuple.fetch("certification").fetch("tuple_sha256"))
refute_empty tuple.fetch("certification").fetch("evidence")
end
end
end
def test_runtime_tuple_profiles_exist
tuples = json("manifests/runtime-tuples.json")
tuples.fetch("consumers").each_value do |consumer|
profile = consumer.fetch("profile")
assert_path_exists File.join(ROOT, "profiles", "#{profile}.json")
end end
end end
@@ -60,4 +72,16 @@ class RepositoryTest < Minitest::Test
workflow = File.read(File.join(ROOT, ".github/workflows/watch-upstream.yml")) workflow = File.read(File.join(ROOT, ".github/workflows/watch-upstream.yml"))
refute_match(/\b(kamal|kubectl|helm|nomad|docker\s+service)\b/i, workflow) refute_match(/\b(kamal|kubectl|helm|nomad|docker\s+service)\b/i, workflow)
end end
def test_tuple_promotion_has_no_command_execution_or_deployment_client
paths = %w[
lib/opencode_compat/runtime_tuple_promoter.rb
scripts/promote_runtime_tuple.rb
]
implementation = paths.map { |path| File.read(File.join(ROOT, path)) }.join("\n")
refute_match(/\b(system|exec|spawn|popen|Open3)\b/, implementation)
refute_match(/`[^`]+`/, implementation)
refute_match(/\b(kamal|kubectl|helm|nomad|docker\s+service)\b/i, implementation)
end
end end

View File

@@ -0,0 +1,309 @@
# frozen_string_literal: true
require "fileutils"
require "json"
require "minitest/autorun"
require "tmpdir"
require_relative "../lib/opencode_compat/runtime_tuple_promoter"
class RuntimeTuplePromoterTest < Minitest::Test
CONSUMER = "example"
CURRENT_COMMIT = "1" * 40
CANDIDATE_COMMIT = "2" * 40
RUBY_CURRENT = "3" * 40
RUBY_CANDIDATE = "4" * 40
RAILS_CURRENT = "5" * 40
RAILS_CANDIDATE = "6" * 40
CURRENT_IMAGE = "ghcr.io/anomalyco/opencode@sha256:#{'a' * 64}"
CANDIDATE_IMAGE = "ghcr.io/anomalyco/opencode@sha256:#{'b' * 64}"
CURRENT_TIME = "2026-07-17T12:00:00Z"
CANDIDATE_TIME = "2026-07-18T12:00:00Z"
def setup
@root = Dir.mktmpdir("opencode-compat-promotion")
FileUtils.mkdir_p(File.join(@root, "manifests"))
FileUtils.mkdir_p(File.join(@root, "evidence"))
FileUtils.mkdir_p(File.join(@root, "profiles"))
File.write(File.join(@root, "profiles", "rails-persisted-turn.json"), "{}\n")
write_manifest(valid_manifest)
@promoter = OpenCodeCompat::RuntimeTuplePromoter.new(root: @root)
end
def teardown
FileUtils.remove_entry(@root)
end
def test_promotion_atomically_moves_certified_tuples_and_clears_candidate
candidate, previous = write_matching_evidence
promoted = @promoter.promote(
consumer: CONSUMER,
consumer_commit: CANDIDATE_COMMIT,
certification: certification(CANDIDATE_TIME, candidate),
previous_certification: certification(CURRENT_TIME, previous)
)
consumer = promoted.fetch("consumers").fetch(CONSUMER)
assert_nil consumer["candidate"]
assert_equal CANDIDATE_COMMIT, consumer.dig("current", "consumer_commit")
assert_equal RUBY_CANDIDATE, consumer.dig("current", "opencode_ruby", "git_commit")
assert_equal "certified", consumer.dig("current", "status")
assert_equal candidate.fetch("tuple_sha256"), consumer.dig("current", "certification", "tuple_sha256")
assert_equal CURRENT_COMMIT, consumer.dig("previous", "consumer_commit")
assert_equal RUBY_CURRENT, consumer.dig("previous", "opencode_ruby", "git_commit")
assert_equal "certified", consumer.dig("previous", "status")
assert_equal previous.fetch("tuple_sha256"), consumer.dig("previous", "certification", "tuple_sha256")
assert_equal "certified", promoted.fetch("migration_state")
assert_equal promoted, JSON.parse(File.read(manifest_path))
end
def test_dry_run_returns_promotion_without_changing_manifest
candidate, previous = write_matching_evidence
before = File.binread(manifest_path)
promoted = @promoter.promote(
consumer: CONSUMER,
consumer_commit: CANDIDATE_COMMIT,
certification: certification(CANDIDATE_TIME, candidate),
previous_certification: certification(CURRENT_TIME, previous),
dry_run: true
)
assert_equal "certified", promoted.dig("consumers", CONSUMER, "current", "status")
assert_equal before, File.binread(manifest_path)
end
def test_rejects_missing_candidate_without_touching_manifest
manifest = valid_manifest
manifest.fetch("consumers").fetch(CONSUMER)["candidate"] = nil
write_manifest(manifest)
before = File.binread(manifest_path)
error = assert_raises(OpenCodeCompat::PromotionError) do
@promoter.promote(
consumer: CONSUMER,
consumer_commit: CANDIDATE_COMMIT,
certification: certification(CANDIDATE_TIME, {"path" => "evidence/missing.json"})
)
end
assert_match(/no candidate/, error.message)
assert_equal before, File.binread(manifest_path)
end
def test_rejects_mutable_candidate_image_even_with_an_exact_image_id
manifest = valid_manifest
runtime = manifest.dig("consumers", CONSUMER, "candidate", "runtime")
runtime["registry_ref"] = "ghcr.io/anomalyco/opencode:latest"
runtime["docker_image_id"] = "sha256:#{'c' * 64}"
write_manifest(manifest)
before = File.binread(manifest_path)
error = assert_raises(OpenCodeCompat::PromotionError) do
@promoter.fingerprints(consumer: CONSUMER, consumer_commit: CANDIDATE_COMMIT)
end
assert_match(/immutable image@sha256 digest/, error.message)
assert_equal before, File.binread(manifest_path)
end
def test_rejects_short_consumer_and_client_commits
assert_raises(OpenCodeCompat::PromotionError) do
@promoter.fingerprints(consumer: CONSUMER, consumer_commit: "abc123")
end
manifest = valid_manifest
manifest.dig("consumers", CONSUMER, "candidate", "opencode_ruby")["git_commit"] = "abc123"
write_manifest(manifest)
error = assert_raises(OpenCodeCompat::PromotionError) do
@promoter.fingerprints(consumer: CONSUMER, consumer_commit: CANDIDATE_COMMIT)
end
assert_match(/full 40-character/, error.message)
end
def test_rejects_missing_server_version_or_unknown_profile
manifest = valid_manifest
manifest.dig("consumers", CONSUMER, "candidate", "runtime").delete("reported_version")
write_manifest(manifest)
error = assert_raises(OpenCodeCompat::PromotionError) do
@promoter.fingerprints(consumer: CONSUMER, consumer_commit: CANDIDATE_COMMIT)
end
assert_match(/reported OpenCode server version/, error.message)
manifest = valid_manifest
manifest.dig("consumers", CONSUMER)["profile"] = "untracked-profile"
write_manifest(manifest)
error = assert_raises(OpenCodeCompat::PromotionError) do
@promoter.fingerprints(consumer: CONSUMER, consumer_commit: CANDIDATE_COMMIT)
end
assert_match(/profile does not exist/, error.message)
end
def test_rejects_missing_previous_certification_for_an_uncertified_baseline
candidate, = write_matching_evidence
before = File.binread(manifest_path)
error = assert_raises(OpenCodeCompat::PromotionError) do
@promoter.promote(
consumer: CONSUMER,
consumer_commit: CANDIDATE_COMMIT,
certification: certification(CANDIDATE_TIME, candidate)
)
end
assert_match(/current tuple is not certified/, error.message)
assert_equal before, File.binread(manifest_path)
end
def test_rejects_evidence_that_does_not_match_the_complete_tuple
candidate, previous = write_matching_evidence
manifest = JSON.parse(File.read(manifest_path))
manifest.dig("consumers", CONSUMER, "candidate", "runtime")["image"] =
"ghcr.io/anomalyco/opencode@sha256:#{'f' * 64}"
write_manifest(manifest)
before = File.binread(manifest_path)
error = assert_raises(OpenCodeCompat::PromotionError) do
@promoter.promote(
consumer: CONSUMER,
consumer_commit: CANDIDATE_COMMIT,
certification: certification(CANDIDATE_TIME, candidate),
previous_certification: certification(CURRENT_TIME, previous)
)
end
assert_match(/tuple_sha256/, error.message)
assert_equal before, File.binread(manifest_path)
end
def test_rejects_non_passing_or_implicit_certification_metadata
candidate, previous = write_matching_evidence
assert_raises(OpenCodeCompat::PromotionError) do
@promoter.promote(
consumer: CONSUMER,
consumer_commit: CANDIDATE_COMMIT,
certification: certification(CANDIDATE_TIME, candidate).merge("status" => "pending"),
previous_certification: certification(CURRENT_TIME, previous),
dry_run: true
)
end
assert_raises(OpenCodeCompat::PromotionError) do
@promoter.promote(
consumer: CONSUMER,
consumer_commit: CANDIDATE_COMMIT,
certification: certification("today", candidate),
previous_certification: certification(CURRENT_TIME, previous),
dry_run: true
)
end
end
def test_rejects_duplicate_or_unversioned_evidence
candidate, previous = write_matching_evidence
assert_raises(OpenCodeCompat::PromotionError) do
@promoter.promote(
consumer: CONSUMER,
consumer_commit: CANDIDATE_COMMIT,
certification: certification(CANDIDATE_TIME, candidate).merge(
"evidence" => [candidate.fetch("path"), candidate.fetch("path")]
),
previous_certification: certification(CURRENT_TIME, previous),
dry_run: true
)
end
path = File.join(@root, candidate.fetch("path"))
document = JSON.parse(File.read(path))
document.delete("schema_version")
File.write(path, JSON.pretty_generate(document) + "\n")
assert_raises(OpenCodeCompat::PromotionError) do
@promoter.promote(
consumer: CONSUMER,
consumer_commit: CANDIDATE_COMMIT,
certification: certification(CANDIDATE_TIME, candidate),
previous_certification: certification(CURRENT_TIME, previous),
dry_run: true
)
end
end
private
def valid_manifest
{
"schema_version" => 1,
"migration_state" => "candidate",
"consumers" => {
CONSUMER => {
"profile" => "rails-persisted-turn",
"current" => {
"status" => "observed-production",
"consumer_commit" => CURRENT_COMMIT,
"opencode_ruby" => {"version" => "0.0.1.alpha2", "git_commit" => RUBY_CURRENT},
"opencode_rails" => {"version" => "0.0.1.alpha2", "git_commit" => RAILS_CURRENT},
"runtime" => {"image" => CURRENT_IMAGE, "reported_version" => "1.16.1"}
},
"candidate" => {
"status" => "compatibility-certified",
"certified_at" => "2026-07-18T10:00:00Z",
"opencode_ruby" => {"version" => "0.0.1.alpha6", "git_commit" => RUBY_CANDIDATE},
"opencode_rails" => {"version" => "0.0.1.alpha6", "git_commit" => RAILS_CANDIDATE},
"runtime" => {"image" => CANDIDATE_IMAGE, "reported_version" => "1.18.3"}
},
"previous" => nil
}
}
}
end
def write_matching_evidence
fingerprints = @promoter.fingerprints(consumer: CONSUMER, consumer_commit: CANDIDATE_COMMIT)
candidate = write_evidence(
"candidate.json",
commit: CANDIDATE_COMMIT,
timestamp: CANDIDATE_TIME,
fingerprint: fingerprints.fetch("candidate_tuple_sha256")
)
previous = write_evidence(
"previous.json",
commit: CURRENT_COMMIT,
timestamp: CURRENT_TIME,
fingerprint: fingerprints.fetch("current_tuple_sha256")
)
[candidate, previous]
end
def write_evidence(name, commit:, timestamp:, fingerprint:)
path = File.join("evidence", name)
document = {
"schema_version" => 1,
"consumer" => CONSUMER,
"profile" => "rails-persisted-turn",
"consumer_commit" => commit,
"status" => "pass",
"certified_at" => timestamp,
"tuple_sha256" => fingerprint
}
File.write(File.join(@root, path), JSON.pretty_generate(document) + "\n")
{"path" => path, "tuple_sha256" => fingerprint}
end
def certification(timestamp, evidence)
{
"status" => "pass",
"certified_at" => timestamp,
"evidence" => [evidence.fetch("path")]
}
end
def manifest_path
File.join(@root, "manifests", "runtime-tuples.json")
end
def write_manifest(manifest)
File.write(manifest_path, JSON.pretty_generate(manifest) + "\n")
end
end