Add explicit rollback bootstrap and maintenance model
This commit is contained in:
@@ -122,7 +122,9 @@ durable certification documents and Git history are the archive. This keeps a
|
||||
fast-moving upstream from making every future run retest every old release.
|
||||
|
||||
See [docs/certification.md](docs/certification.md) for promotion, canary, and
|
||||
rollback evidence requirements.
|
||||
rollback evidence requirements, and [docs/maintenance.md](docs/maintenance.md)
|
||||
for consumer ownership, release order, runner boundaries, and expected failure
|
||||
modes.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -155,14 +155,31 @@ have real passing evidence can the manifest honestly contain certified
|
||||
`current` and `previous` tuples. Until then, `promotion_readiness` remains
|
||||
blocked and the candidate PR must not be treated as a deploy authorization.
|
||||
|
||||
The schema-v1 promotion command deliberately cannot perform the first degraded
|
||||
bootstrap transition when `current` is a known-failing baseline. Do not bypass
|
||||
that guard by hand-editing the manifest or relabeling alpha2 evidence. A
|
||||
separate reviewed state-machine change must first add an explicit
|
||||
`bootstrap-current-only` state that preserves the failed baseline as
|
||||
uncertified emergency provenance and leaves `previous` null. Itemized rollback
|
||||
certification is complete only after a later, materially distinct passing tuple
|
||||
can move the first certified current into `previous`.
|
||||
Use the explicit degraded-bootstrap transition for the first passing rollback
|
||||
tuple. It requires passing evidence bound to the complete candidate fingerprint
|
||||
and a literal acknowledgement; it preserves the failed baseline as uncertified
|
||||
`emergency_provenance`, leaves `previous` null, and records the temporary
|
||||
`bootstrap-current-only` state:
|
||||
|
||||
```sh
|
||||
ruby scripts/promote_runtime_tuple.rb bootstrap-current \
|
||||
--consumer travelwolf \
|
||||
--consumer-commit FULL_40_CHARACTER_ROLLBACK_COMMIT \
|
||||
--status pass \
|
||||
--certified-at 2026-07-20T12:00:00Z \
|
||||
--evidence evidence/travelwolf-rollback.json \
|
||||
--acknowledge-degraded-rollback \
|
||||
accept-degraded-rollback-with-failed-emergency-provenance \
|
||||
--dry-run
|
||||
```
|
||||
|
||||
Review the dry run, repeat without `--dry-run`, and then treat that passing
|
||||
tuple as the base for a normal promotion. Normal promotion moves it into
|
||||
`previous`, removes the degraded rollback marker, and keeps the original failed
|
||||
baseline only as historical emergency provenance. The repository becomes
|
||||
`certified` only when every consumer has exact passing `current` and `previous`
|
||||
tuples. Never use bootstrap to replace an existing previous tuple or to certify
|
||||
a baseline already known to fail.
|
||||
|
||||
## Rollback
|
||||
|
||||
|
||||
117
docs/maintenance.md
Normal file
117
docs/maintenance.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# Maintaining OpenCode consumers
|
||||
|
||||
OpenCode compatibility is an executable tuple, not a gem version. The tuple is
|
||||
the exact client commit, Rails adapter commit where used, OpenCode image digest,
|
||||
consumer commit, compatibility profile, and passing evidence. A change to any
|
||||
coordinate creates a new candidate.
|
||||
|
||||
## Dependency ownership
|
||||
|
||||
The adjacent projects intentionally do not all use the same gems:
|
||||
|
||||
| Consumer | OpenCode boundary | Owned dependency |
|
||||
| --- | --- | --- |
|
||||
| Ajent Rails | Ruby REST/SSE plus persisted Rails turns | `opencode-ruby` and `opencode-rails` |
|
||||
| Travelwolf | Ruby REST/SSE plus persisted Rails turns and Sprite lifecycle | `opencode-ruby` and `opencode-rails` |
|
||||
| Mushu | Ruby REST/SSE with application-owned conversation, claim, recovery, Telegram scope, and idempotency | `opencode-ruby` only |
|
||||
| Greenroom | Direct voice-stream worker | No Ruby adapter gem |
|
||||
| Leela | Custom strict-v2 server, security, and toolchain lane | No Ruby adapter gem |
|
||||
| opencode-ajent | Native CLI and plugin hook/event lane | No Ruby adapter gem |
|
||||
| inference | Provider configuration, hooks, routing, and migration lane | No Ruby adapter gem |
|
||||
| Context Kit | MCP and OpenCode configuration producer | No Ruby adapter gem |
|
||||
|
||||
Do not add `opencode-rails` to an application that owns different persistence
|
||||
semantics, and do not route plugin, provider, voice, or strict-v2 behavior
|
||||
through the Ruby REST/SSE adapter merely to make versions look uniform. Share
|
||||
fixtures, provenance, and promotion policy across those lanes instead.
|
||||
|
||||
Generated release snapshots, detached operational copies, editor locks, caches,
|
||||
and linked task worktrees are evidence or tooling. They are not additional
|
||||
consumers and must not be bulk-upgraded.
|
||||
|
||||
## Supported window
|
||||
|
||||
Support the exact current and previous certified runtime tuples. Do not claim
|
||||
compatibility with arbitrary future OpenCode versions. Additive fields and
|
||||
unknown events should remain tolerant, while terminal text, request count,
|
||||
ownership, persistence, and cleanup invariants remain strict.
|
||||
|
||||
The Ruby/SSE profile covers the endpoints and events the client actually uses,
|
||||
including session creation and deletion, asynchronous prompts, event
|
||||
subscription, status, terminal idle/status events, part deltas and updates,
|
||||
authoritative assistant messages, questions, and permissions. A passing shared
|
||||
profile does not certify Rails persistence, voice streaming, plugin hooks,
|
||||
provider migrations, or generated MCP configuration.
|
||||
|
||||
## Release and promotion order
|
||||
|
||||
1. The watcher records a new upstream release tag and resolved OCI digest in a
|
||||
PR. It never merges or deploys.
|
||||
2. Update fixtures for any observed protocol change before changing the client.
|
||||
3. Build `opencode-ruby` and `opencode-rails` as one release train. Rails must
|
||||
resolve the exact Ruby version and commit being tested.
|
||||
4. Run the shared fixture corpus, Ruby 3.2 through 4.0 lockstep matrix, and every
|
||||
exact public image still used by a current, previous, or candidate tuple.
|
||||
5. Run only the application-owned profiles for each consumer, in its own PR and
|
||||
isolated environment. Health checks alone do not certify a tuple.
|
||||
6. Promote and deploy one consumer at a time. Record the exact production
|
||||
commit, image/base digest, loaded client commits, and live result.
|
||||
7. Commit reviewed evidence and move the old passing current tuple to previous.
|
||||
8. Publish annotated gem tags only after the exact commit candidate is green and
|
||||
the trusted publisher is configured. Publication never implies deployment.
|
||||
|
||||
Commit pins are valid for an unpublished candidate, but the durable published
|
||||
state must record the annotated tag object and peeled commit. Tags or `latest`
|
||||
may be kept as human-readable provenance only; execution coordinates use full
|
||||
Git commits and `image@sha256:...` references.
|
||||
|
||||
## Custom images
|
||||
|
||||
Record each provenance layer separately:
|
||||
|
||||
- consumer commit;
|
||||
- exact output registry digest or Docker image ID;
|
||||
- exact base image digest;
|
||||
- custom OpenCode source commit, when the base is a fork;
|
||||
- build-source commit when it differs from the deployment commit.
|
||||
|
||||
The tuple fingerprint binds these values. On the next rebuild of an older
|
||||
unlabelled private image, add OCI labels for the custom OpenCode source, reported
|
||||
version, consumer build revision, and base digest, then make preflight compare
|
||||
the labels. Do not relabel an already certified image: that changes its digest.
|
||||
|
||||
## Runner and forge contract
|
||||
|
||||
Runner upgrades are a separate compatibility surface from OpenCode upgrades.
|
||||
Workflows must install their required Ruby/toolchain explicitly, declare Bash
|
||||
for scripts that use Bash syntax, and avoid relying on ambient runner packages.
|
||||
|
||||
GitHub and Gitea do not implement every Actions feature identically. Keep the
|
||||
same tests on both forges, but use forge-specific execution where necessary:
|
||||
GitHub retains review artifacts and parallel dynamic image jobs; Gitea runs the
|
||||
same manifest image set sequentially and makes no artifact-retention claim.
|
||||
Neither path may mutate a runner or deploy a consumer.
|
||||
|
||||
A runner-only workflow repair needs exact-head CI, not an application canary.
|
||||
A client, runtime image, event, persistence, or toolchain change needs the
|
||||
profile and consumer canaries described above.
|
||||
|
||||
## Expected breakages
|
||||
|
||||
- Prompt submission before event subscription can miss a terminal event and
|
||||
hang a turn.
|
||||
- Changes to terminal or message-part events can duplicate or lose final text.
|
||||
- Usage events can undercount multi-step requests if totals are overwritten.
|
||||
- Reconnect logic can replay a prompt and create duplicate model requests.
|
||||
- Asset builds can fail when runtime configuration is evaluated without the
|
||||
image variables available only at deploy time.
|
||||
- A workflow can pass locally but fail under `sh` when it uses Bash arrays or
|
||||
`mapfile`.
|
||||
- Plugin hook names, provider schema, config/MCP schema, or CLI flags can break
|
||||
direct consumers even when the Ruby profile stays green.
|
||||
- A custom fork can silently lose its required ordering or permission patch if
|
||||
only an opaque output digest is retained.
|
||||
|
||||
When one of these changes, update the owning profile and consumer evidence. Do
|
||||
not weaken a strict invariant to make a new upstream release pass.
|
||||
|
||||
@@ -15,6 +15,8 @@ module OpenCodeCompat
|
||||
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"
|
||||
DEGRADED_BOOTSTRAP_ACKNOWLEDGEMENT =
|
||||
"accept-degraded-rollback-with-failed-emergency-provenance"
|
||||
NON_CERTIFIABLE_TUPLE_STATUSES = %w[observed-production-contract-failed].freeze
|
||||
TUPLE_METADATA_KEYS = %w[
|
||||
certification
|
||||
@@ -82,6 +84,37 @@ module OpenCodeCompat
|
||||
end
|
||||
end
|
||||
|
||||
def bootstrap_current(consumer:, consumer_commit:, certification:, acknowledgement:, dry_run: false)
|
||||
validate_full_commit!(consumer_commit, "consumer commit")
|
||||
unless acknowledgement == DEGRADED_BOOTSTRAP_ACKNOWLEDGEMENT
|
||||
raise PromotionError,
|
||||
"bootstrap requires explicit acknowledgement #{DEGRADED_BOOTSTRAP_ACKNOWLEDGEMENT.inspect}"
|
||||
end
|
||||
|
||||
if dry_run
|
||||
manifest = read_manifest
|
||||
return bootstrap_current_manifest(
|
||||
manifest,
|
||||
consumer: consumer,
|
||||
consumer_commit: consumer_commit,
|
||||
certification: certification,
|
||||
acknowledgement: acknowledgement
|
||||
)
|
||||
end
|
||||
|
||||
with_current_manifest_lock do |manifest|
|
||||
bootstrapped = bootstrap_current_manifest(
|
||||
manifest,
|
||||
consumer: consumer,
|
||||
consumer_commit: consumer_commit,
|
||||
certification: certification,
|
||||
acknowledgement: acknowledgement
|
||||
)
|
||||
atomic_write(bootstrapped)
|
||||
bootstrapped
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def read_manifest
|
||||
@@ -124,7 +157,55 @@ module OpenCodeCompat
|
||||
consumer_entry["previous"] = certified_previous
|
||||
consumer_entry["current"] = certified_candidate
|
||||
consumer_entry["candidate"] = nil
|
||||
manifest["migration_state"] = all_consumers_certified?(manifest) ? "certified" : "candidate"
|
||||
consumer_entry.delete("rollback_state")
|
||||
refresh_promotion_state!(manifest)
|
||||
manifest
|
||||
end
|
||||
|
||||
def bootstrap_current_manifest(manifest, consumer:, consumer_commit:, certification:, acknowledgement:)
|
||||
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 retain as emergency provenance"
|
||||
end
|
||||
validate_tuple!(current, "#{consumer} current")
|
||||
unless NON_CERTIFIABLE_TUPLE_STATUSES.include?(current["status"])
|
||||
raise PromotionError, "degraded bootstrap is only for a current tuple known to fail the contract"
|
||||
end
|
||||
unless consumer_entry["previous"].nil?
|
||||
raise PromotionError, "degraded bootstrap cannot replace an existing previous tuple"
|
||||
end
|
||||
if consumer_entry.key?("emergency_provenance") || consumer_entry.key?("rollback_state")
|
||||
raise PromotionError, "degraded bootstrap has already been recorded for this consumer"
|
||||
end
|
||||
|
||||
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 failed current tuple"
|
||||
end
|
||||
|
||||
certified_candidate = certify_tuple!(
|
||||
candidate,
|
||||
consumer: consumer,
|
||||
profile: profile,
|
||||
supplied: certification,
|
||||
expected_fingerprint: candidate_fingerprint,
|
||||
label: "bootstrap candidate"
|
||||
)
|
||||
|
||||
consumer_entry["emergency_provenance"] = deep_copy(current)
|
||||
consumer_entry["current"] = certified_candidate
|
||||
consumer_entry["candidate"] = nil
|
||||
consumer_entry["previous"] = nil
|
||||
consumer_entry["rollback_state"] = {
|
||||
"status" => "degraded-no-certified-previous",
|
||||
"acknowledgement" => acknowledgement,
|
||||
"recorded_at" => certification.fetch("certified_at"),
|
||||
"emergency_provenance_status" => current.fetch("status")
|
||||
}
|
||||
refresh_promotion_state!(manifest)
|
||||
manifest
|
||||
end
|
||||
|
||||
@@ -455,6 +536,61 @@ module OpenCodeCompat
|
||||
end
|
||||
end
|
||||
|
||||
def all_consumers_bootstrapped?(manifest)
|
||||
manifest.fetch("consumers").all? do |consumer, entry|
|
||||
next false unless entry["candidate"].nil? && entry["previous"].nil?
|
||||
next false unless entry.dig("rollback_state", "status") == "degraded-no-certified-previous"
|
||||
next false unless NON_CERTIFIABLE_TUPLE_STATUSES.include?(entry.dig("emergency_provenance", "status"))
|
||||
|
||||
profile = fetch_profile!(entry, consumer)
|
||||
tuple = entry["current"]
|
||||
next false unless tuple.is_a?(Hash) && tuple["status"] == "certified"
|
||||
|
||||
validate_tuple!(tuple, "#{consumer} current")
|
||||
validate_recorded_certification!(
|
||||
tuple,
|
||||
consumer: consumer,
|
||||
profile: profile,
|
||||
expected_fingerprint: tuple_fingerprint(tuple, consumer: consumer, profile: profile),
|
||||
label: "#{consumer} current"
|
||||
)
|
||||
true
|
||||
rescue PromotionError
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def migration_state_for(manifest)
|
||||
return "certified" if all_consumers_certified?(manifest)
|
||||
return "bootstrap-current-only" if all_consumers_bootstrapped?(manifest)
|
||||
|
||||
"candidate"
|
||||
end
|
||||
|
||||
def refresh_promotion_state!(manifest)
|
||||
manifest["migration_state"] = migration_state_for(manifest)
|
||||
manifest["promotion_readiness"] = case manifest.fetch("migration_state")
|
||||
when "certified"
|
||||
{
|
||||
"status" => "certified",
|
||||
"reason" => "Every consumer has exact current and previous passing tuples.",
|
||||
"required_action" => "Promote only a newly certified, materially changed tuple."
|
||||
}
|
||||
when "bootstrap-current-only"
|
||||
{
|
||||
"status" => "bootstrap-current-only",
|
||||
"reason" => "Every current tuple is certified, but no independently passing previous tuple exists yet.",
|
||||
"required_action" => "Certify the next meaningful release so normal promotion retains the current tuple as previous."
|
||||
}
|
||||
else
|
||||
{
|
||||
"status" => "candidate",
|
||||
"reason" => "At least one consumer transition remains incomplete.",
|
||||
"required_action" => "Finish exact tuple certification without treating failed emergency provenance as rollback evidence."
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def with_current_manifest_lock
|
||||
loop do
|
||||
retry_with_new_inode = false
|
||||
|
||||
@@ -20,6 +20,10 @@ parser = OptionParser.new do |opts|
|
||||
--status pass --certified-at TIMESTAMP --evidence evidence/FILE.json \\
|
||||
[--previous-status pass --previous-certified-at TIMESTAMP \\
|
||||
--previous-evidence evidence/FILE.json] [--dry-run]
|
||||
ruby scripts/promote_runtime_tuple.rb bootstrap-current --consumer NAME --consumer-commit SHA \\
|
||||
--status pass --certified-at TIMESTAMP --evidence evidence/FILE.json \\
|
||||
--acknowledge-degraded-rollback \\
|
||||
accept-degraded-rollback-with-failed-emergency-provenance [--dry-run]
|
||||
USAGE
|
||||
|
||||
opts.on("--consumer NAME") { |value| options["consumer"] = value }
|
||||
@@ -30,6 +34,9 @@ parser = OptionParser.new do |opts|
|
||||
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("--acknowledge-degraded-rollback VALUE") do |value|
|
||||
options["degraded_rollback_acknowledgement"] = value
|
||||
end
|
||||
opts.on("--dry-run") { options["dry_run"] = true }
|
||||
end
|
||||
|
||||
@@ -94,8 +101,42 @@ begin
|
||||
"migration_state" => promoted.fetch("migration_state")
|
||||
)
|
||||
end
|
||||
when "bootstrap-current"
|
||||
required!(
|
||||
options,
|
||||
"consumer",
|
||||
"consumer_commit",
|
||||
"status",
|
||||
"certified_at",
|
||||
"evidence",
|
||||
"degraded_rollback_acknowledgement"
|
||||
)
|
||||
bootstrapped = promoter.bootstrap_current(
|
||||
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")
|
||||
},
|
||||
acknowledgement: options.fetch("degraded_rollback_acknowledgement"),
|
||||
dry_run: options.fetch("dry_run", false)
|
||||
)
|
||||
if options.fetch("dry_run", false)
|
||||
puts JSON.pretty_generate(bootstrapped)
|
||||
else
|
||||
raise OptionParser::InvalidArgument, "command must be fingerprint or promote"
|
||||
consumer = bootstrapped.fetch("consumers").fetch(options.fetch("consumer"))
|
||||
puts JSON.pretty_generate(
|
||||
"consumer" => options.fetch("consumer"),
|
||||
"current_consumer_commit" => consumer.dig("current", "consumer_commit"),
|
||||
"current_tuple_sha256" => consumer.dig("current", "certification", "tuple_sha256"),
|
||||
"previous" => consumer.fetch("previous"),
|
||||
"rollback_state" => consumer.fetch("rollback_state"),
|
||||
"migration_state" => bootstrapped.fetch("migration_state")
|
||||
)
|
||||
end
|
||||
else
|
||||
raise OptionParser::InvalidArgument, "command must be fingerprint, promote, or bootstrap-current"
|
||||
end
|
||||
rescue OpenCodeCompat::PromotionError, OptionParser::ParseError => e
|
||||
warn "error: #{e.message}"
|
||||
|
||||
@@ -10,14 +10,19 @@ class RuntimeTuplePromoterTest < Minitest::Test
|
||||
CONSUMER = "example"
|
||||
CURRENT_COMMIT = "1" * 40
|
||||
CANDIDATE_COMMIT = "2" * 40
|
||||
NEXT_COMMIT = "7" * 40
|
||||
RUBY_CURRENT = "3" * 40
|
||||
RUBY_CANDIDATE = "4" * 40
|
||||
RUBY_NEXT = "8" * 40
|
||||
RAILS_CURRENT = "5" * 40
|
||||
RAILS_CANDIDATE = "6" * 40
|
||||
RAILS_NEXT = "9" * 40
|
||||
CURRENT_IMAGE = "ghcr.io/anomalyco/opencode@sha256:#{'a' * 64}"
|
||||
CANDIDATE_IMAGE = "ghcr.io/anomalyco/opencode@sha256:#{'b' * 64}"
|
||||
NEXT_IMAGE = "ghcr.io/anomalyco/opencode@sha256:#{'c' * 64}"
|
||||
CURRENT_TIME = "2026-07-17T12:00:00Z"
|
||||
CANDIDATE_TIME = "2026-07-18T12:00:00Z"
|
||||
NEXT_TIME = "2026-07-19T12:00:00Z"
|
||||
|
||||
def setup
|
||||
@root = Dir.mktmpdir("opencode-compat-promotion")
|
||||
@@ -175,6 +180,163 @@ class RuntimeTuplePromoterTest < Minitest::Test
|
||||
assert_equal before, File.binread(manifest_path)
|
||||
end
|
||||
|
||||
def test_explicit_degraded_bootstrap_preserves_failed_emergency_provenance
|
||||
manifest = valid_manifest
|
||||
manifest.dig("consumers", CONSUMER, "current")["status"] = "observed-production-contract-failed"
|
||||
write_manifest(manifest)
|
||||
candidate_fingerprint = @promoter.fingerprints(
|
||||
consumer: CONSUMER,
|
||||
consumer_commit: CANDIDATE_COMMIT
|
||||
).fetch("candidate_tuple_sha256")
|
||||
evidence = write_evidence(
|
||||
"bootstrap-candidate.json",
|
||||
commit: CANDIDATE_COMMIT,
|
||||
timestamp: CANDIDATE_TIME,
|
||||
fingerprint: candidate_fingerprint
|
||||
)
|
||||
before = File.binread(manifest_path)
|
||||
|
||||
preview = @promoter.bootstrap_current(
|
||||
consumer: CONSUMER,
|
||||
consumer_commit: CANDIDATE_COMMIT,
|
||||
certification: certification(CANDIDATE_TIME, evidence),
|
||||
acknowledgement: OpenCodeCompat::RuntimeTuplePromoter::DEGRADED_BOOTSTRAP_ACKNOWLEDGEMENT,
|
||||
dry_run: true
|
||||
)
|
||||
|
||||
assert_equal before, File.binread(manifest_path)
|
||||
assert_equal "bootstrap-current-only", preview.fetch("migration_state")
|
||||
|
||||
bootstrapped = @promoter.bootstrap_current(
|
||||
consumer: CONSUMER,
|
||||
consumer_commit: CANDIDATE_COMMIT,
|
||||
certification: certification(CANDIDATE_TIME, evidence),
|
||||
acknowledgement: OpenCodeCompat::RuntimeTuplePromoter::DEGRADED_BOOTSTRAP_ACKNOWLEDGEMENT
|
||||
)
|
||||
consumer = bootstrapped.dig("consumers", CONSUMER)
|
||||
|
||||
assert_equal "certified", consumer.dig("current", "status")
|
||||
assert_equal CANDIDATE_COMMIT, consumer.dig("current", "consumer_commit")
|
||||
assert_nil consumer["candidate"]
|
||||
assert_nil consumer["previous"]
|
||||
assert_equal "observed-production-contract-failed", consumer.dig("emergency_provenance", "status")
|
||||
assert_nil consumer.dig("emergency_provenance", "certification")
|
||||
assert_equal "degraded-no-certified-previous", consumer.dig("rollback_state", "status")
|
||||
assert_equal "bootstrap-current-only", bootstrapped.dig("promotion_readiness", "status")
|
||||
assert_equal bootstrapped, JSON.parse(File.read(manifest_path))
|
||||
end
|
||||
|
||||
def test_bootstrap_then_normal_promotion_retains_the_first_passing_tuple_as_previous
|
||||
manifest = valid_manifest
|
||||
manifest.dig("consumers", CONSUMER, "current")["status"] = "observed-production-contract-failed"
|
||||
write_manifest(manifest)
|
||||
bootstrap_fingerprint = @promoter.fingerprints(
|
||||
consumer: CONSUMER,
|
||||
consumer_commit: CANDIDATE_COMMIT
|
||||
).fetch("candidate_tuple_sha256")
|
||||
bootstrap_evidence = write_evidence(
|
||||
"bootstrap-rollback.json",
|
||||
commit: CANDIDATE_COMMIT,
|
||||
timestamp: CANDIDATE_TIME,
|
||||
fingerprint: bootstrap_fingerprint
|
||||
)
|
||||
@promoter.bootstrap_current(
|
||||
consumer: CONSUMER,
|
||||
consumer_commit: CANDIDATE_COMMIT,
|
||||
certification: certification(CANDIDATE_TIME, bootstrap_evidence),
|
||||
acknowledgement: OpenCodeCompat::RuntimeTuplePromoter::DEGRADED_BOOTSTRAP_ACKNOWLEDGEMENT
|
||||
)
|
||||
|
||||
manifest = JSON.parse(File.read(manifest_path))
|
||||
manifest.dig("consumers", CONSUMER)["candidate"] = {
|
||||
"status" => "compatibility-certified",
|
||||
"certified_at" => NEXT_TIME,
|
||||
"consumer_commit" => NEXT_COMMIT,
|
||||
"opencode_ruby" => {"version" => "0.0.1.alpha8", "git_commit" => RUBY_NEXT},
|
||||
"opencode_rails" => {"version" => "0.0.1.alpha8", "git_commit" => RAILS_NEXT},
|
||||
"runtime" => {"image" => NEXT_IMAGE, "reported_version" => "1.19.0"}
|
||||
}
|
||||
write_manifest(manifest)
|
||||
next_fingerprint = @promoter.fingerprints(
|
||||
consumer: CONSUMER,
|
||||
consumer_commit: NEXT_COMMIT
|
||||
).fetch("candidate_tuple_sha256")
|
||||
next_evidence = write_evidence(
|
||||
"next-current.json",
|
||||
commit: NEXT_COMMIT,
|
||||
timestamp: NEXT_TIME,
|
||||
fingerprint: next_fingerprint
|
||||
)
|
||||
|
||||
promoted = @promoter.promote(
|
||||
consumer: CONSUMER,
|
||||
consumer_commit: NEXT_COMMIT,
|
||||
certification: certification(NEXT_TIME, next_evidence)
|
||||
)
|
||||
consumer = promoted.dig("consumers", CONSUMER)
|
||||
|
||||
assert_equal NEXT_COMMIT, consumer.dig("current", "consumer_commit")
|
||||
assert_equal CANDIDATE_COMMIT, consumer.dig("previous", "consumer_commit")
|
||||
assert_equal "certified", consumer.dig("previous", "status")
|
||||
assert_equal "observed-production-contract-failed", consumer.dig("emergency_provenance", "status")
|
||||
refute consumer.key?("rollback_state")
|
||||
assert_equal "certified", promoted.fetch("migration_state")
|
||||
assert_equal "certified", promoted.dig("promotion_readiness", "status")
|
||||
end
|
||||
|
||||
def test_degraded_bootstrap_requires_exact_acknowledgement
|
||||
error = assert_raises(OpenCodeCompat::PromotionError) do
|
||||
@promoter.bootstrap_current(
|
||||
consumer: CONSUMER,
|
||||
consumer_commit: CANDIDATE_COMMIT,
|
||||
certification: {},
|
||||
acknowledgement: "yes",
|
||||
dry_run: true
|
||||
)
|
||||
end
|
||||
|
||||
assert_match(/explicit acknowledgement/, error.message)
|
||||
end
|
||||
|
||||
def test_degraded_bootstrap_rejects_a_passing_baseline_or_existing_previous_tuple
|
||||
candidate, = write_matching_evidence
|
||||
error = assert_raises(OpenCodeCompat::PromotionError) do
|
||||
@promoter.bootstrap_current(
|
||||
consumer: CONSUMER,
|
||||
consumer_commit: CANDIDATE_COMMIT,
|
||||
certification: certification(CANDIDATE_TIME, candidate),
|
||||
acknowledgement: OpenCodeCompat::RuntimeTuplePromoter::DEGRADED_BOOTSTRAP_ACKNOWLEDGEMENT,
|
||||
dry_run: true
|
||||
)
|
||||
end
|
||||
assert_match(/only for a current tuple known to fail/, error.message)
|
||||
|
||||
manifest = valid_manifest
|
||||
manifest.dig("consumers", CONSUMER, "current")["status"] = "observed-production-contract-failed"
|
||||
manifest.dig("consumers", CONSUMER)["previous"] = manifest.dig("consumers", CONSUMER, "current").dup
|
||||
write_manifest(manifest)
|
||||
candidate_fingerprint = @promoter.fingerprints(
|
||||
consumer: CONSUMER,
|
||||
consumer_commit: CANDIDATE_COMMIT
|
||||
).fetch("candidate_tuple_sha256")
|
||||
evidence = write_evidence(
|
||||
"bootstrap-existing-previous.json",
|
||||
commit: CANDIDATE_COMMIT,
|
||||
timestamp: CANDIDATE_TIME,
|
||||
fingerprint: candidate_fingerprint
|
||||
)
|
||||
error = assert_raises(OpenCodeCompat::PromotionError) do
|
||||
@promoter.bootstrap_current(
|
||||
consumer: CONSUMER,
|
||||
consumer_commit: CANDIDATE_COMMIT,
|
||||
certification: certification(CANDIDATE_TIME, evidence),
|
||||
acknowledgement: OpenCodeCompat::RuntimeTuplePromoter::DEGRADED_BOOTSTRAP_ACKNOWLEDGEMENT,
|
||||
dry_run: true
|
||||
)
|
||||
end
|
||||
assert_match(/cannot replace an existing previous/, error.message)
|
||||
end
|
||||
|
||||
def test_rejects_evidence_that_does_not_match_the_complete_tuple
|
||||
candidate, previous = write_matching_evidence
|
||||
manifest = JSON.parse(File.read(manifest_path))
|
||||
|
||||
Reference in New Issue
Block a user