Ptah compatibility
The operator does not contain Ptah. It runs one, as a job whose container image and version identity the installation binds, so “which Ptah works with this operator” is a question about that executor and not about the CLI a person used to build the OCI artifact.
support/ptah.json is the machine-readable answer, and it is canonical: the
lifecycle suite takes the image it runs as the executor out of this file, and
the table below is generated from it.
The table
Section titled “The table”Each operator version has a row in the summary and a section with the detail. A declared range, a verified build and the absence of both mean different things, and the next section says what.
Axis: Operator version to the Ptah build the operator executes, identified by the digest-pinned executorImage and the ptahVersion bound beside it. It is not the Ptah CLI a person used to build the OCI artifact. The runner is not a third axis: it is built from the operator source this row names, and the exact protocol both halves speak is in runnerProtocolVersion.
Last measured: 2026-11-07.
| Operator | Declared range | Verified builds | Guide |
|---|---|---|---|
edge |
No range | v0.23.0 |
edge |
v0.2.9 |
No range | v0.17.0 |
v0.2.9 |
v0.2.8 |
No range | v0.16.0 |
v0.2.8 |
v0.2.7 |
No range | v0.15.0 |
v0.2.7 |
v0.2.6 |
No range | v0.14.0 |
v0.2.6 |
v0.2.5 |
No range | v0.13.0 |
v0.2.5 |
v0.2.4 |
No range | v0.13.0 |
v0.2.4 |
v0.2.3 |
No range | v0.13.0 |
v0.2.3 |
v0.2.2 |
No range | v0.13.0 |
v0.2.2 |
v0.2.1 |
No range | v0.13.0 |
v0.2.1 |
v0.2.0 |
No range | v0.13.0 |
v0.2.0 |
The development state. Its guide is at https://operator.ptah.run/edge/.
Declared: No range is claimed. The chart in the source tree and the chart asset ship no executor default, and installing either requires both a digest-pinned executorImage and the ptahVersion verified from that image’s provenance. The chart a release publishes to its OCI registry carries that release’s pair, and refuses half an override. The supported build is exactly the one the lifecycle matrix exercised.
Verified:
- Ptah
v0.23.0. Commitf47b2bfa3b696c93730e7d25397987ed0a4a2562, runner protocol version 1, evidencekubernetes-e2e. The complete OCI resolution and verification, observation, planning, approval, apply, failure-recovery and convergence lifecycle against PostgreSQL and MySQL, on every supported Kubernetes minor, for both artifact formats this operator reads: application/vnd.stokaro.ptah.schema.v1 for a declared schema, whose declared rows converge on both engines, and application/vnd.stokaro.ptah.migrations.v1 for a versioned migration directory, whose history read, approval gate and applied sequence run against PostgreSQL and MySQL.
Limitations:
- The verified build is the release image of the Ptah version under Verified, pinned by digest and run as Ptah published it: the operator builds no executor of its own. It contains stokaro/ptah#3777 and #3794, the complete
--jsondocuments ofschema planandschema applythat the operator reads on every declared-schema Plan and Apply. An executor without #3777, v0.9.0 included, rejects--jsonwith exit code 2. - The verified build contains stokaro/ptah#5173, which names in every document why a command could not open its target database: plan contract 3, apply 4, migration status and run 2, and a drift report of version 1. The operator reads those versions and no others, so an executor without #5173, v0.17.0 included, fails every Observe, Plan, Apply and migration read on a document it refuses.
- The verified build contains stokaro/ptah#5499, which reads a PostgreSQL schema through pg_catalog for every role, so the login the databases page documents reports a table another role created, as a superuser does. An executor without #5499, v0.21.0 included, leaves that table out of the report under that login, and the data-plane row that creates one fails.
- The verified build contains stokaro/ptah#5545: a MySQL read that cannot see every trigger, routine or view definition in the database fails with permission_denied and names what it could not see, where an executor without #5545, v0.22.0 included, reported those objects as absent. The login the databases page documents holds TRIGGER, EXECUTE and SHOW VIEW on the database for that reason, and the narrower login it documented before fails every Observe and Plan at this build.
- The migration machine contracts –
migrations status --jsoncarrying contract_version and a per-migration record, andmigrations up --jsoncarrying an outcome – reached Ptah after v0.3.0-201, andmigrations up --expect-sequence, which the operator passes on every migration Apply, reached it in v0.9.0. A build older than v0.9.0 resolves and verifies a migration artifact and then fails at its own history document or at the flag every migration Apply passes, so the versioned workflow needs at least v0.9.0 rather than any build that has the commands. - The verified scope is the operator’s database support window, the release lines on that version’s databases page. Other engines Ptah addresses are unverified here, whatever the Ptah build.
- PostgreSQL 18.x is evidenced by the Added database lines workflow rather than by the lifecycle matrix every push runs: run 42440618274 passed on every supported Kubernetes minor at this build.
- The published verified set is a repository document. It is not compiled into the manager, and nothing compares a running executor’s ptahVersion against it, so a combination outside this set is outside what the matrix measured rather than something the operator refuses. What is refused before a database is mutated is a change of any bound execution component between the plan and the apply – Ptah, the executor image, the runner protocol version, the controller-state version – checked before the Job is created. The manager’s image and revision and the runner image are recorded and not bound: a manager release that changes only them applies the plans and approvals its predecessor left. What is refused after a Job has run is a runner speaking another protocol version, which is a rejected result frame rather than a prevented mutation. Pinning both the executor image and its ptahVersion at install is a requirement on whoever installs, not a check at runtime.
v0.2.9
Section titled “v0.2.9”A released version. Its guide is at https://operator.ptah.run/v0.2.9/.
Declared: No range is claimed. The chart ships no executor default, installation requires both a digest-pinned executorImage and the ptahVersion verified from that image’s provenance, and the supported build is exactly the one the lifecycle matrix exercised.
Verified:
- Ptah
v0.17.0. Commit38bd6e91cfe3a70543046da9c3ada05a0460a103, runner protocol version 1, evidencekubernetes-e2e. The scope is the same as foredge.
Limitations: the same as edge without its second, third, fourth and seventh limitations.
v0.2.8
Section titled “v0.2.8”A released version. Its guide is at https://operator.ptah.run/v0.2.8/.
Declared: the same as v0.2.9.
Verified:
- Ptah
v0.16.0. Commit91e28d4331950e949590fd9ac404373bc17b572d, runner protocol version 1, evidencekubernetes-e2e. The scope is the same as foredge.
Limitations: the same as v0.2.9.
v0.2.7
Section titled “v0.2.7”A released version. Its guide is at https://operator.ptah.run/v0.2.7/.
Declared: the same as v0.2.9.
Verified:
- Ptah
v0.15.0. Commit6ba0a93a44d3decd6782d88e57b88151edd4c788, runner protocol version 1, evidencekubernetes-e2e. The scope is the same as foredge.
Limitations: the same as v0.2.9.
v0.2.6
Section titled “v0.2.6”A released version. Its guide is at https://operator.ptah.run/v0.2.6/.
Declared: the same as v0.2.9.
Verified:
- Ptah
v0.14.0. Commite046527f79c58651d2082dce65b5ceb48dbc2606, runner protocol version 1, evidencekubernetes-e2e. The scope is the same as foredge.
Limitations: the same as v0.2.9.
v0.2.5
Section titled “v0.2.5”A released version. Its guide is at https://operator.ptah.run/v0.2.5/.
Declared: the same as v0.2.9.
Verified:
- Ptah
v0.13.0. Commiteb69f8c4435643c5c9f3fbd1c9953b9ba0f44b6f, runner protocol version 1, evidencekubernetes-e2e. The scope is the same as foredge.
Limitations: the same as v0.2.9.
v0.2.4
Section titled “v0.2.4”A released version. Its guide is at https://operator.ptah.run/v0.2.4/.
Declared: the same as v0.2.9.
Verified: the same as v0.2.5.
Limitations: the same as v0.2.9.
v0.2.3
Section titled “v0.2.3”A released version. Its guide is at https://operator.ptah.run/v0.2.3/.
Declared: the same as v0.2.9.
Verified: the same as v0.2.5.
Limitations: the same as v0.2.9.
v0.2.2
Section titled “v0.2.2”A released version. Its guide is at https://operator.ptah.run/v0.2.2/.
Declared: the same as v0.2.9.
Verified: the same as v0.2.5.
Limitations: the same as v0.2.9.
v0.2.1
Section titled “v0.2.1”A released version. Its guide is at https://operator.ptah.run/v0.2.1/.
Declared: the same as v0.2.9.
Verified: the same as v0.2.5.
Limitations: the same as v0.2.9.
v0.2.0
Section titled “v0.2.0”A released version. Its guide is at https://operator.ptah.run/v0.2.0/.
Declared: the same as v0.2.9.
Verified: the same as v0.2.5.
Limitations: the same as v0.2.9.
Evidence
Section titled “Evidence”kubernetes-e2e: The full lifecycle suite in test/e2e, run by the kubernetes-e2e job of .github/workflows/ci.yml on every pull request and on the weekly default-branch run. hack/e2e-kind.sh and the workflow both take the image from this file, so the Ptah build the suite runs and the claim published here are one declaration.
Three states, kept apart
Section titled “Three states, kept apart”A compatibility table usually blurs three different claims. This one keeps them in separate fields, because the difference is what a reader is actually asking about.
Declared is a promise. declared.range names the Ptah versions an operator
version is meant to work with. Where it is null, nothing is promised, and
declared.statement says why.
Verified is a measurement. Each verified entry names a Ptah build the
operator was actually run against, the evidence behind it, and what ran.
ptahRelease is null when the build is not a released Ptah, and the table
then names the build by ptahDescribe. ptahImage is the Ptah release image
that ran, pinned by digest: the suite copies it unchanged and builds no
executor, so what the measurement covers is exactly that image. An operator
release names the same image, and
Releases and provenance says how it is checked
against this row.
Absent is neither. A combination no row mentions is untested, and untested
is not incompatible. A version with nothing verified carries
unverifiedReason naming the check that has not run; the verifier refuses an
empty list that says nothing, because an empty list reads as “works with
everything”.
What each field means
Section titled “What each field means”| Field | Meaning |
|---|---|
operator |
edge for the development state, or an exact vMAJOR.MINOR.PATCH release. |
stage |
development or released. |
documentation.published |
Whether a guide exists for this operator version. The table links to it only when it does. |
documentation.source |
The revision that guide is built from: master for the development state, and its own tag for a release. |
documentation.fixRevision |
An exact commit that replaces the tag as the build revision for one release. |
documentation.fixReason |
What that fix corrects. Required beside a fix revision. |
declared.range |
The promised Ptah versions, or null. |
declared.statement |
Why there is no range, when there is none. |
verified[].ptahRelease |
The released Ptah version, or null when the verified build is not a release. |
verified[].ptahCommit |
The exact Ptah commit the suite built its executor from. |
verified[].ptahDescribe |
What git describe --tags --always calls that commit in a complete checkout, so a reader sees something other than forty hex characters. A tag name carries no commit to compare, so the verifier accepts one only when it repeats this row’s ptahRelease. |
verified[].runnerProtocolVersion |
The frame version the executor and the operator spoke in that run. Not a third axis — the runner is built from the operator source the row names — and checked against the constant rather than trusted. |
verified[].evidence |
A key into evidence, which says what ran and how the tested build and this claim stay one declaration. |
verified[].scope |
What the run covered. |
limitations |
Known constraints on the rows above. |
One pin, not two
Section titled “One pin, not two”The image in verified[].ptahImage is the executor the lifecycle suite runs,
and verified[].ptahCommit is the commit it was built from. hack/e2e-kind.sh
reads both from this file, and the kubernetes-e2e job takes the commit from
the same place through the support job’s output. The release workflow reads
them here too, through hack/releaseverify, when it names the executor in its
manifest. Nothing else may write them down:
make verify-ptah-supportThe guard refuses a catalog that is malformed or self-contradictory, and it refuses the lifecycle script, the CI or release workflow, or the executor recipe carrying the commit a second time. A literal in any of them would keep working on the day it and the catalog disagreed, and the table on this page would then name a build nothing ran.
The command performs no network requests.
Changing the claim
Section titled “Changing the claim”Changing which Ptah the suite verifies is one edit to verified[].ptahCommit
plus its ptahDescribe, and the matrix jobs then run against that build. Do not
raise lastVerified without a run behind it; the field records when the claim
was last measured, not when the file was last touched.
The drift finding categories the pinned build can emit are recorded in
support/ptah-drift-categories.json, with the digests of the Ptah files that
produce them: the drift command, the schema classifier and its feature
findings, the row summary and the undecided note. The record lists the fixed
categories and, separately, the qualified ones Ptah writes bare or followed by
:<kind>. The job that builds the shared task images compares the record
with the pinned source. A new pin that changed any of those files fails there
until the record is rewritten:
go run ./hack/ptahdriftcategories -ptah <Ptah checkout> -writeA unit test holds the operator’s vocabulary to the recorded lists, because the
operator refuses a drift report that names a category it does not know. A
category the new build adds therefore joins the vocabulary, the PtahSchema
status pattern and the CRD schema version in the same change. The pattern keeps
every category an earlier release stored, so a summary written before an
upgrade still validates. A pin that leaves those files alone needs no edit.
Any change to the file changes the table, so regenerate it in the same commit:
cd docs/sitenpm run compatibility:writeAdding an operator release adds a row with its own verified list. A release
that has not been run against any Ptah build gets unverifiedReason rather than
an empty list, and the table then shows a version nobody has measured instead of
a version that works with everything.
Repinning Ptah
Section titled “Repinning Ptah”A repin moves verified[].ptahImage and the fields beside it to another Ptah
release. Do these in one change, in this order.
Make the catalog edit Changing the claim describes, since everything below reads the pin from it. Then regenerate the captures from the new image, and read the diff:
DOCKER_CONTEXT=<context> make runner-harness-writeThe target builds the runner from this commit and runs it inside the pinned
image, in a container with the shape of an operation Pod, against PostgreSQL,
MySQL, an OCI registry and the end-to-end Git server. It runs every runner
operation from OCI and Git sources, and the Git fetch before an operation, on
schemas that carry an added column, an extension, a policy on a new and on an
existing table, declared rows, a source it cannot load, and every target fault the
failure kinds name. It rewrites what changed under internal/dataplane/testdata: a document, the
arguments, an exit status, a line of standard error, a result frame. On a pin
that moved nothing it changes nothing, so the diff is what the release changed. A
document the operator’s decoders refuse fails the run instead of being written:
fix the decoder first, and move any contract counter the
API compatibility page asks for.
A verification’s standard error is compared and never forwarded. On a refusal
ptah oci verify writes its report to standard output and the same findings to
standard error, and exits 2. The runner reads that as the policy’s refusal, and
passes on the requirement codes alone, only when the text is what
dataplane.VerifyRefusalDiagnostics makes of the report; anything else reads as
invalid_verification_output. A release that rewords that sentence moves the
schema-verify-refused rows, and the capture fails until the function agrees.
Read the differs-* rows first. They run a database that differs from its
declaration, in the ways differingRows in test/runnerharness/differing_test.go
lists, because a comparison a release adds never fires on a database the operator
converged. Each records whatever its steps answer, so a moved outcome reaches the
diff. go test ./test/runnerharness then fails on a row whose reading no longer
holds the shape it is named for, or whose plan the manager now rates differently.
Decide whether the move is wanted, then change want beside the row. A plan the
manager reads as neither destructive nor a change of privileges runs unattended
under apply: Always, so read any row that moves there as a finding.
make runner-harness runs the same rows and compares. A change of shape is any
difference left once the volatile values are masked: a field added, removed or
retyped, an element added or removed, a contract version, an outcome, an exit
status, a refusal line, an argument. The volatile values are declared in
test/runnerharness/shape.go, and each is what a second run on one pin moved:
the times and durations a migration records, an address, the digest of an
artifact, whose manifest records when it was pushed, the correlation id on a log
line, and the random part of a name the runner makes under TMPDIR. A plan’s
digest, a fingerprint and a Git content digest are held. CI runs it on every pull
request and every push to master, and a commit that moved the pin without new
captures fails in go test before that job starts.
On CI’s own Docker a run takes a few minutes. Over SSH from a loaded machine it
has taken over twenty, and RUNNER_HARNESS_TIMEOUT sets the bound, an hour by
default. A run that passes the bound or is killed leaves nothing on the daemon:
a process it started removes everything the run made.
Recapture the SQL readings under testdata/e2e/readings, which record every
statement each schema and migration command sent to the database. Run the probe
once with --compare and the executor the readings were taken with, to show the
procedure reproduces them, then with the new one:
python3 support/qualification/probes/schema_sql_readings.py \ --context <context> --executor-image <image> --out testdata/e2e/readingsRecalibrate support/capacity/input-calibration.json, since a release that
changes what a plan holds changes how much input fits a limit. Run the verifier
once per engine with the release’s binary, on the Docker host, because copying the
binary over a client’s link runs past the probe’s step limit. Write the commit and
the report digests into the file, and any repeated characters a band now needs,
and add the measurement to support/qualification/0.2.0-capacity-inputs.md:
python3 support/qualification/probes/capacity_fixture_verify.py \ --engine <engine> --docker-context <context> \ --ptah-binary <binary> --ptah-sha256 <digest> --output <new directory>Search the non-test files in test/e2e for literal Ptah output, and recapture
every reading their unit tests hold them to. A predicate that matches a header a
new release no longer writes stays green offline as long as its fixture came from
the old one:
grep -rnE 'ptah |-- (POSTGRES|MYSQL)|contract_version' test/e2e --include='*.go' --exclude='*_test.go'Then let the matrix run against the new build. The harness does not replace kind: admission, RBAC, the Job and Pod lifecycle, NetworkPolicy and durable result delivery are proven there.
Correcting a release guide
Section titled “Correcting a release guide”A release’s pages are built from that release’s own tag, so a correction cannot arrive by editing master. Assign the corrected revision to the release instead:
"documentation": { "published": true, "source": "v0.1.0", "fixRevision": "<exact 40-character commit>", "fixReason": "the install command named a chart path that never shipped"}The Git tag does not move and no new binary is released. The revision is a commit rather than a branch because the published pages must stay the ones somebody named, and the reason is required because a fix with no stated correction is indistinguishable from a rebuild.
What a fix may carry is a correction to a command or an explanation. What it may not carry is a feature from development into an older guide. The offline guard holds the shape; the publish workflow holds the property it cannot see, that the commit descends from the release it is assigned to.
Most releases need no entry. This is the explicit exception rather than a documentation branch per release.
Where it is published
Section titled “Where it is published”The table on this page is the published one. The documentation gate renders it
from support/ptah.json again and refuses a page that says something else, so
the catalog and the table change in one commit or not at all.
Each guide renders the file at the revision it was built from, so a release guide shows the catalog as it stood at that release’s tag. The development guide is rebuilt from master whenever the file changes, and operator.ptah.run/support/ptah/ always opens its copy. That is the address to link to from elsewhere.
Database engines are a separate axis and live in database support; Kubernetes versions live in Kubernetes support.