OCI registry artifacts
Publish, pin, pull, and consume Ptah migrations and desired schemas through an OCI registry.
Ptah can store migration directories and desired schemas as OCI 1.1 artifacts
in GHCR, ECR, GAR, Harbor, Docker Hub, a local registry:2, or another
OCI-compliant registry. Authentication comes from the Docker credential store;
there is no Ptah account or Ptah-specific registry token.
This is a native Ptah workflow. It does not implement Atlas Cloud or the
atlas:// scheme.
Choose a reference
Section titled “Choose a reference”oci://registry.example/team/repositoryoci://registry.example/team/repository:tagoci://registry.example/team/repository@sha256:<64-lowercase-hex-characters>An unqualified reference resolves to :latest. Tags are movable pointers. A
manifest digest is immutable and is the appropriate pin for a reproducible
deployment:
oci://ghcr.io/acme/app-migrations:stableoci://ghcr.io/acme/app-migrations@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdefEvery push writes latest, a collision-resistant generated UTC version tag in
vYYYYMMDDhhmmss-<random-base32> form, the positional reference tag when
present, and each repeatable --tag. --version overrides the generated
version tag.
If a later tag update fails after earlier tags moved, Ptah reports the manifest digest, completed tags, and failed tag.
Ptah also rejects references that contain both a tag and digest, embedded credentials, query strings, fragments, escaped path separators, surrounding whitespace, or uppercase repository components.
Authenticate securely
Section titled “Authenticate securely”Log in with the registry’s normal Docker flow:
docker login ghcr.ioPtah honors DOCKER_CONFIG, Docker’s default config.json, credsStore, and
per-registry credHelpers. Do not include credentials in the oci://
reference.
Ptah also keeps a store of its own, for a machine with no Docker installed:
ptah oci login ghcr.ioptah oci logout ghcr.ioThe credential is checked against the registry before anything is written, so a
typo fails there rather than at the next push. The password is never taken from
the command line — it is read from the terminal, or from standard input when
one is piped in. A platform credential helper is used where one exists;
otherwise the credential goes to a Docker-format file under ~/.ptah, and the
command says which of the two happened.
ptah oci logout removes what Ptah stored and nothing else. A credential
docker login placed is left alone, because taking it away would log you out
of something you did not ask about.
HTTPS is the default. --plain-http disables transport encryption and is only
for an explicitly trusted local registry used for development or tests. Do not
use it with GHCR, ECR, GAR, Harbor, Docker Hub, or a production registry.
Every command that resolves an oci:// source registers the flag, including
ptah oci referrers, ptah schema inspect, and the rest of push, pull, and the
direct consumers.
Registry operations and Docker credential-helper lookups have a two-minute default deadline, with shorter dial, TLS-handshake, and response-header deadlines.
Identity, integrity, and authenticity
Section titled “Identity, integrity, and authenticity”An OCI digest identifies exact bytes, not the publisher. ptah.sum and
atlas.sum detect changes inside a migration directory, but they are not
signatures. Ptah does not currently verify artifact signatures.
Use registry access controls, trusted writers, digest allowlists, and a
registry-native signature policy when publisher authenticity is required.
Deployment reports are deliberately redacted. plan.json contains schema
fingerprints, dialect and capability metadata, object names, risk assessments,
and generated SQL statements, so protect it like schema and migration content.
Publish migrations
Section titled “Publish migrations”Hash and verify the directory before publishing:
ptah migrations hash --dir ./migrations
ptah migrations push \ oci://ghcr.io/acme/app-migrations \ --migrations-dir ./migrations \ --dir-format ptah \ --verify-sum \ --tag stableThe migration artifact contains only the inputs Ptah captures for a migration run:
- SQL files;
ptah.sumoratlas.sum;.ptah-lint.yaml.
Unrelated files are excluded. --dir-format accepts auto, ptah, or atlas.
A mismatched integrity file always fails before the registry upload;
--verify-sum additionally makes a missing integrity file an error.
The command prints the manifest digest. Promote that exact artifact by digest:
MIGRATIONS=oci://ghcr.io/acme/app-migrations@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
ptah migrations status \ --db-url "$DATABASE_URL" \ --migrations-dir "$MIGRATIONS" \ --json
ptah migrations up \ --db-url "$DATABASE_URL" \ --migrations-dir "$MIGRATIONS" \ --verify-sumptah migrations down accepts the same direct source:
ptah migrations down \ --db-url "$DATABASE_URL" \ --migrations-dir "$MIGRATIONS" \ --target 5 \ --confirmup, status, and down pull into an immutable in-memory filesystem and use
the ordinary Ptah migration engine. An explicit --dir-format must match the
artifact metadata.
--verify-sum is available on up, down and status. It verifies the pulled
artifact before opening the database and, unlike the always-on gate, refuses an
artifact that carries no integrity file at all. Read
Identity, integrity, and authenticity
for what that verification establishes over a movable tag, which is less than
the flag’s name suggests: the sum travels inside the artifact, so it proves the
pulled files are internally consistent, not that they are the reviewed ones.
Pin a digest to fix which bytes a later pull gets; that section also explains
why pinning one is not by itself a statement about who published them.
ptah migrations lint --dir oci://... also reads an OCI migration artifact
directly. See Attach lint and plan reports for
the optional referrer output.
ptah migrations validate --dir oci://... answers the integrity question on its
own, without a database and without executing anything:
ptah migrations validate --dir "$MIGRATIONS"It exits 0 when the artifact matches the sum it carries, 1 when a migration was added, removed or edited out of band, and 2 when the artifact carries no sum at all — the same contract it has for a local directory.
When the reference is a tag, a successful run prints on standard error the same
movable-tag qualifier up, down and status print, naming the digest the tag
resolved to. A digest-pinned reference prints nothing extra: there is no movable
pointer left to warn about.
It does not replace --verify-sum on the consuming verbs: a separate call
resolves the tag in its own process, so a movable tag can select different bytes
before up or down resolves it again.
Reconstruct a migration directory
Section titled “Reconstruct a migration directory”ptah migrations pull \ "$MIGRATIONS" \ --out ./pulled-migrationsThe destination path must be absent. Ptah stages the captured files and renames them into place so a failed pull does not leave a partial directory. Any existing file, symlink, or directory, including an empty directory, is rejected without being overwritten.
Understand deployment reporting
Section titled “Understand deployment reporting”After an OCI-backed migrations up applies pending migrations successfully,
Ptah attempts to attach a deployment.json referrer to the exact migration
manifest digest that was pulled.
The report records:
- a generated deployment ID;
- the migration artifact digest;
- the database dialect;
- before and after versions;
- versions actually added to committed revision state;
- UTC start and finish timestamps;
- a
succeededoutcome.
The report schema has no fields for database URLs, hostnames, environment values, local paths, SQL text, or credentials.
No report is attempted for a local migration directory, a dry run, a no-op run, a failed migration, or a failed final status check. Report attachment is best-effort: a registry failure prints a warning but does not turn a successful migration into a failed command.
Use --skip-report when the registry is read-only or another system owns the
deployment record:
ptah migrations up \ --db-url "$DATABASE_URL" \ --migrations-dir "$MIGRATIONS" \ --verify-sum \ --skip-reportAttach lint and plan reports
Section titled “Attach lint and plan reports”Lint directly from an OCI migration artifact:
ptah migrations lint \ --dir "$MIGRATIONS" \ --dialect postgres \ --format jsonAdd --attach to publish the canonical lint.json report as a referrer of the
exact migration manifest digest:
ptah migrations lint \ --dir "$MIGRATIONS" \ --dialect postgres \ --format json \ --attach--attach requires an OCI --dir. --git-base is not supported with an OCI
lint source because the artifact has no local Git history. Ptah publishes the
report before applying the normal lint failure threshold, so findings can be
stored even when lint then returns its negative-result code.
Attach a canonical plan.json report to an OCI desired-schema artifact:
SCHEMA=oci://ghcr.io/acme/app-schema@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
ptah migrations plan \ --schema-file "$SCHEMA" \ --db-url "$DATABASE_URL" \ --report json \ --attachPlan attachment requires exactly one OCI --schema-file. Combining that source
with a local schema file, Go root, or external schema command is rejected
because Ptah cannot choose an unambiguous subject. Planning without --attach
can still include OCI schema artifacts in a composite desired schema.
The plan records the desired artifact digest, a SHA-256 fingerprint of the complete live schema used as current state, the dialect, selected schemas, effective capabilities, and statement assessments.
Lint and plan attachments are not best-effort. If publication fails, the command fails.
Referrer discovery guarantees
Section titled “Referrer discovery guarantees”With a native OCI Referrers API, the registry provides the concurrent index.
Without it, ORAS writes the standard referrers tag-schema index and Ptah also
writes a unique content-derived tag for every attachment. ptah oci referrers
merges and validates both sources, so concurrent Ptah processes do not lose
attachments through one shared fallback tag.
The content-derived tags are a Ptah extension. A different client that reads only the standard tag-schema index can miss an entry after concurrent cross-process publication. Use a registry with the native Referrers API for complete cross-client audit discovery.
List referrer metadata
Section titled “List referrer metadata”List every direct referrer attached to an artifact:
ptah oci referrers "$MIGRATIONS"Filter by Ptah report type and select JSON output when automation needs stable fields:
ptah oci referrers "$MIGRATIONS" --type lint --format jsonptah oci referrers "$SCHEMA" --type plan --format json--type accepts all, lint, plan, or deployment, and --format accepts
text or json. Defaults are all and text. The result contains direct
referrer descriptor metadata. Text output contains digest, artifact type, media
type, and size; JSON output also contains annotations when present.
ptah oci fetch downloads the report payload itself — see
Native commands. It writes the published
bytes and does not interpret them.
A registry that gained the referrers index after Ptah published through its
durable tag holds attachments no other OCI client can find, and nothing about
the artifact says so. ptah oci reindex repairs that by republishing the
manifest: the content is byte-identical, so the digest does not move, and a
registry serving the index builds the entry when it receives a manifest
carrying a subject. The pass ends by asking the registry again, because one
that accepted the manifest and built no entry looks exactly like one that did
until somebody checks — anything still missing is reported as unrepaired rather
than counted as fixed.
The subject follows the ordinary reference rules. An unqualified subject
resolves to :latest, a tag resolves to its current manifest, and a digest
selects an immutable subject. Use a digest to inspect attachments for the exact
artifact used by a deployment or analysis run. The command uses Docker
credentials and HTTPS by default; --plain-http is only for an explicitly
trusted local registry.
Read a repository before you consume it
Section titled “Read a repository before you consume it”Four questions come up before an artifact is applied, and each has a verb.
Which aliases exist? ptah oci tags lists them. The aliases are what a
promotion moves and what a pin replaces, so this is the view that says which of
them a repository currently carries.
ptah oci tags oci://ghcr.io/acme/dbWhich artifact does this alias name today? ptah oci resolve answers with
the pinned reference alone, so a step can capture it:
DIGEST=$(ptah oci resolve oci://ghcr.io/acme/db:latest)ptah migrations up --from "$DIGEST"Pinning is what makes the two commands describe the same artifact. A tag moved between them would otherwise change what runs without changing the pipeline.
What does the artifact declare? ptah oci inspect reads the manifest and
stops there — the descriptor, the artifact type, the subject when the artifact
is itself a referrer, the annotations, and each layer’s name, media type, size
and digest. Nothing is downloaded.
ptah oci inspect "$DIGEST"It reports referrer discovery too, and the source column is the part worth
reading. Ptah writes both the standard referrers index and its own
content-derived durable tag; a referrer listed as durable-tag came back from
the second mechanism only, which means Ptah finds it and another OCI client may
not. --no-referrers skips that lookup.
Will another client find what Ptah published? ptah oci capabilities asks
the registry rather than inferring it:
ptah oci capabilities oci://ghcr.io/acme/dbThe question is put with the client pinned to the referrers API, so it cannot quietly fall back to the tag schema and report a success that came from somewhere else. A refusal naming the API as unsupported is the registry saying no; anything else is a failure to ask, and is reported as an error rather than as a no.
Publish a desired schema
Section titled “Publish a desired schema”Publish one or more SQL, YAML, HCL, or DBML sources:
ptah schema push \ oci://ghcr.io/acme/app-schema \ --schema-file ./schema.sql \ --dialect postgresAnnotated Go models publish the same way, with --root-dir ./models in place
of --schema-file. Ptah merges the selected sources and renders one canonical
schema.hcl layer, plus a managed-data.json layer when the schema declares
reference data.
That second layer carries the declared rows themselves, under
application/vnd.stokaro.ptah.managed-data.v1+json. A data block names a file
in the working copy that declared it, and an artifact travels without that
working copy, so the rows travel instead. Each value keeps the YAML tag it
resolved to and the text that declared it: 007 and "007" are different
values in the same column, and resolving them to Go values loses the
difference. A column absent from a row was not declared, and a column written as
null was declared null.
--checks ./release-checks.sql publishes a third layer, under
application/vnd.stokaro.ptah.checks.v1+sql, carrying the -- +ptah check
release assertions that describe what the change was for. ptah db verify --checks oci://... reads them back, so the assertions a reviewer approved and
the assertions that run are one set of bytes rather than two files that drifted.
Pin the reference by digest to make that binding hold: a tag can be moved, and
a digest names the bytes it named. See Verify a release.
A reader that does not know the managed-data media type refuses the artifact instead of reading the schema layer and deploying a database without the rows its author declared. The media type is what an artifact says its reader must understand: a build too old for a layer refuses the whole artifact rather than reading around it, which is the same rule a new layer added later will be read under.
ptah schema apply and ptah schema plan resolve an artifact before they open
the target, so that refusal lands before a database connection exists. A reader
that verified after connecting would have opened the database with whatever the
credentials allow, for an artifact it then declined.
That canonical HCL includes API export metadata authored in YAML, HCL, or Go:
api_name, openapi_name, graphql_name, and proto_name on tables and
columns, plus column api_type and api_expose. Pulling the artifact and
rendering it again preserves those values and therefore the same OpenAPI,
GraphQL, and Protobuf identities. SQL and DBML inputs have no lossless spelling
for this metadata, so their contracts continue to derive names and types from
storage. A composite source retains metadata from
each complete YAML, HCL, or Go-owned object and rejects conflicting duplicate
definitions.
Schema publication fails closed:
- managed reference data is rejected because it cannot be represented without loss;
- role passwords are rejected so credentials cannot be published in an artifact;
- any lossy HCL renderer diagnostic rejects publication;
- the generated HCL must parse and render back to identical canonical bytes.
Ptah never pushes a partial schema artifact.
Decide which tags a push writes
Section titled “Decide which tags a push writes”A push with no tag flag moves latest onto the new digest and writes nothing
else. Four flags widen that:
| Flag | Writes | Movable |
|---|---|---|
--version v1.0.0 |
that tag, plus latest |
no — write-once |
--generated-version |
v<UTC timestamp>-<id>, plus latest |
no — write-once |
--latest |
latest |
yes |
--tag stable |
that tag, repeatable, plus latest |
yes |
The receipt names every tag the push wrote:
Pushed schema as oci://ghcr.io/acme/app-schema:latestDigest: sha256:20ec84e49b907163744a887dc2126491053bba1f2c82ddf6c73c006babc81512Version: v1.0.0Tags: [v1.0.0 latest]A write-once tag is not reserved, it is pinned to a digest. Pushing the same
content under a version tag that already exists succeeds and changes nothing.
Pushing different content under it refuses, names both digests, and exits 2:
error: push oci://ghcr.io/acme/app-schema:latest: OCI write-once tag already exists: "v1.0.0" resolves to sha256:20ec84e4... instead of sha256:5b238472...Version: is blank on a push that assigned no version tag, which is every push
without --version or --generated-version.
Pull an artifact back to a file
Section titled “Pull an artifact back to a file”Pulling validates the artifact and creates a new canonical HCL file:
ptah schema pull \ oci://ghcr.io/acme/app-schema:stable \ --out ./schema.hclThe output file must not already exist. A second pull to the same path refuses
with error: schema artifact output already exists: <path> and exits 2, so a
pull never overwrites a working copy. A reference the registry does not hold
refuses with error: pull ...: fetch OCI manifest: ...: not found and exits 2
as well.
An artifact that declares reference rows is written as two files: the canonical
HCL at --out, and managed-data.json beside it. Both paths are printed. The
HCL carries the declaration and not the rows – a data block names the row
file of the working copy that published the artifact, and that path exists
nowhere else – so the layer beside the schema is the only account of the rows a
consumer has. Keep the pair together: --schema-file reads the rows from it,
whether you name the HCL or the directory holding it. Neither file is written
when the other cannot be, because a canonical HCL beside a missing row layer
reads exactly like an artifact that declares no rows.
Exit codes
Section titled “Exit codes”Both verbs exit 0 on success and 2 on every refusal: a usage error, an
authentication or transport failure, a lossy schema, a write-once tag collision,
an --out path that exists, or a reference that does not resolve. Neither uses
1. See Exit codes.
Consume a desired schema
Section titled “Consume a desired schema”Every native command that accepts --schema-file resolves oci:// through it
and exposes --plain-http for a trusted local registry. This includes
schema test; the command-surface test keeps the flag and resolver paired as
new consumers are added.
The scheme belongs to that flag rather than to desired state in general.
schema diff takes its sources through --from/--to, does not resolve
oci://, and does not register --plain-http. Pull the artifact with
ptah schema pull and pass the file when using that command.
schema inspect is the one that materializes rather than parses: the artifact
is pulled to a canonical HCL file, that file is materialized on the
destructively reset --dev-url database, and the result is introspected — so
its output is normalized by a real database of the target dialect, exactly as
it is for a local schema file. The output is byte-identical to inspecting the
same artifact after ptah schema pull.
ptah schema inspect \ --schema-file "$SCHEMA" \ --dev-url "$DEV_DATABASE_URL"The Atlas-compatible ptah-compat schema inspect --url deliberately does not
accept oci://, and refuses it with the community binary’s own
sql/sqlclient: unknown driver "oci". That surface’s contract is to match the
pinned community binary, which has no oci:// driver.
Compare and check drift
Section titled “Compare and check drift”Use an OCI schema through the repeatable --schema-file option:
SCHEMA=oci://ghcr.io/acme/app-schema@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
ptah schema compare \ --db-url "$DATABASE_URL" \ --schema-file "$SCHEMA"
ptah schema drift \ --db-url "$DATABASE_URL" \ --schema-file "$SCHEMA" \ --format jsonOCI schema inputs can be combined with other repeatable --schema-file and
--root-dir inputs through Ptah’s composite schema loader.
Map Atlas concepts to OCI
Section titled “Map Atlas concepts to OCI”| Atlas Registry | Ptah OCI |
|---|---|
atlas://app |
oci://registry.example/team/app |
| Unpinned source resolves to latest | Unqualified oci:// resolves to :latest |
Atlas login and ATLAS_TOKEN |
docker login, DOCKER_CONFIG, and credential helpers |
| Registry version | Generated vYYYYMMDDhhmmss-<random-base32> tag or explicit --version |
| Movable tag | Reference tag or repeatable --tag |
| Immutable content pin | OCI manifest @sha256:... digest |
Migration directory plus atlas.sum |
Per-file OCI layers, including atlas.sum or ptah.sum |
| Desired schema | Canonical, lossless schema.hcl artifact |
| Deployment audit | Best-effort deployment-report referrer after native migrations up |
| Lint results and migration plans | migrations lint --attach and migrations plan --attach; list direct descriptor metadata with ptah oci referrers |
| Cloud UI, promotion, policy, and accounts | Not provided; use registry-native controls |
The native workflow does not make the Atlas-compatible migrate push or
schema push implemented commands. Those paths remain Atlas community-edition
boundary stubs in the ptah-compat binary, and the Atlas-compatible apply
command does not gain native OCI transport flags.
Publish to GHCR from GitHub Actions
Section titled “Publish to GHCR from GitHub Actions”Grant the job packages: write. The Docker login action writes credentials that
Ptah reads from Docker’s configuration:
name: Publish migrations
on: push: branches: [main]
permissions: contents: read packages: write
jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7
- uses: actions/setup-go@v6 with: go-version: "1.26.x"
- name: Log in to GHCR uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Install Ptah run: go install ptah.run/cmd/ptah@latest
- name: Publish migration artifact shell: bash run: | ref="oci://ghcr.io/${GITHUB_REPOSITORY,,}-migrations" ptah migrations push "$ref" \ --migrations-dir ./migrations \ --verify-sum \ --tag "$GITHUB_SHA"Pin the Ptah install to a release or commit in production CI. Existing organization packages may also need the repository’s Actions workflow granted write access in the package settings. GitHub documents the current token and package permission model in Publishing and installing a package with GitHub Actions.
Next steps
Section titled “Next steps”- Verifying what you publish and apply: Integrity and safety.
- Linting a published directory and gating destructive statements: Lint and gate unsafe SQL.
- The complete verb and flag inventory for the OCI commands: Native commands.
- Which registry-backed features are supported where: Capabilities.