Certify alpha8 production and rollback tuples (#7)
Some checks failed
Candidate compatibility / prepare (push) Successful in 12s
Candidate compatibility / repository (push) Successful in 17s
Candidate compatibility / image ${{ matrix.id }} (push) Has been skipped
Candidate compatibility / fixture-contract (push) Successful in 48s
Candidate compatibility / lockstep clients Ruby 4.0 (push) Successful in 1m0s
Candidate compatibility / lockstep clients Ruby 3.4 (push) Successful in 1m1s
Candidate compatibility / lockstep clients Ruby 3.2 (push) Successful in 1m4s
Candidate compatibility / lockstep clients Ruby 3.3 (push) Successful in 1m43s
Candidate compatibility / Gitea full exact image matrix (push) Successful in 1m48s
Watch upstream OpenCode / open-compatibility-pr (push) Failing after 12s

* Add explicit rollback bootstrap and maintenance model

* Certify alpha8 production and rollback tuples
This commit is contained in:
Ajay Krishnan
2026-07-20 01:33:27 -07:00
committed by GitHub
parent 328fa39c55
commit 3bb0877034
16 changed files with 2167 additions and 159 deletions

158
docs/maintenance.md Normal file
View File

@@ -0,0 +1,158 @@
# 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.
Direct consumers still need immutable inputs even though they do not use the
Ruby gems. A system service must execute a clean certified checkout or image,
not whichever files happen to be in a mutable development worktree. Node and
Bun plugins must be pinned through their package lock and tested against the
same OpenCode digest; a globally cached, unversioned plugin is not release
provenance. Generated OpenCode or MCP configuration needs both schema validation
and a candidate-runtime smoke test. Keep OpenCode auto-update disabled in every
certified lane so a process restart cannot silently change the server half of a
previously passing tuple.
## 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.
## Rollback
Rollback restores the whole certified `previous` tuple. Do not roll back only
the gem, only the consumer, or only the runtime image: the wire contract is the
unit of compatibility.
The deployment platform's immediately preceding application image is not
automatically the certified OpenCode rollback. Keep it as emergency service
provenance, but if it contains a client/runtime tuple known to fail the contract
it must not occupy `previous`. In that case rollback means deploying the exact
consumer commit and runtime coordinates recorded in the certified `previous`
tuple, even if that is different from the platform's one-click rollback target.
## 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.
Browser suites must provision the reviewed Chrome/Selenium path explicitly;
an absent browser is a runner failure, not product evidence. Keep a system-test
suite serial when its harness shares an ephemeral server port instead of
mistaking parallel `EADDRINUSE` failures for OpenCode regressions.
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.
For a frozen rollback snapshot that is no longer a merge candidate, provide a
default-branch audit workflow that accepts and checks out an explicit full SHA.
That lets current runner plumbing test the immutable old application tree
without adding a CI-only commit to the rollback coordinate or pretending a
known runner-workflow failure is an application incompatibility.
## Expected breakages
- Prompt submission before event subscription can miss a terminal event and
hang a turn.
- An SSE parser that recognizes only `\n\n` can stall on CRLF, bare-CR,
byte-order-mark, comment, or multiline-data framing that is valid on the
wire.
- 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.
- A Bun/Node server close can wait forever on idle keep-alive sockets unless
idle connections are explicitly reaped after admissions stop; the resulting
orphan can exhaust a service cgroup's PID budget and block a safe upgrade.
- 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.
- `Opencode::Turn` is still an alpha-stage Rails composition seam; constructor,
observer, persistence, or finalization changes can break hosts even when the
lower-level Ruby wire client remains compatible.
- 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.