Skip to content
PtahPtah

OCI registry artifacts

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.

oci://registry.example/team/repository
oci://registry.example/team/repository:tag
oci://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:stable
oci://ghcr.io/acme/app-migrations@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef

Every 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.

Log in with the registry’s normal Docker flow:

Terminal window
docker login ghcr.io

Ptah honors DOCKER_CONFIG, Docker’s default config.json, credsStore, and per-registry credHelpers. Do not include credentials in the oci:// reference.

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.

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.

Hash and verify the directory before publishing:

Terminal window
ptah migrations hash --dir ./migrations
ptah migrations push \
oci://ghcr.io/acme/app-migrations \
--migrations-dir ./migrations \
--dir-format ptah \
--verify-sum \
--tag stable

The migration artifact contains only the inputs Ptah captures for a migration run:

  • SQL files;
  • ptah.sum or atlas.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:

Terminal window
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-sum

ptah migrations down accepts the same direct source:

Terminal window
ptah migrations down \
--db-url "$DATABASE_URL" \
--migrations-dir "$MIGRATIONS" \
--target 5 \
--confirm

up, 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:

Terminal window
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.

Terminal window
ptah migrations pull \
"$MIGRATIONS" \
--out ./pulled-migrations

The 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.

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 succeeded outcome.

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:

Terminal window
ptah migrations up \
--db-url "$DATABASE_URL" \
--migrations-dir "$MIGRATIONS" \
--verify-sum \
--skip-report

Lint directly from an OCI migration artifact:

Terminal window
ptah migrations lint \
--dir "$MIGRATIONS" \
--dialect postgres \
--format json

Add --attach to publish the canonical lint.json report as a referrer of the exact migration manifest digest:

Terminal window
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:

Terminal window
SCHEMA=oci://ghcr.io/acme/app-schema@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
ptah migrations plan \
--schema-file "$SCHEMA" \
--db-url "$DATABASE_URL" \
--report json \
--attach

Plan 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.

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 every direct referrer attached to an artifact:

Terminal window
ptah oci referrers "$MIGRATIONS"

Filter by Ptah report type and select JSON output when automation needs stable fields:

Terminal window
ptah oci referrers "$MIGRATIONS" --type lint --format json
ptah 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.

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.

Publish one or more SQL, YAML, HCL, or DBML sources:

Terminal window
ptah schema push \
oci://ghcr.io/acme/app-schema \
--schema-file ./schema.sql \
--dialect postgres

Annotated 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.

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.

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:latest
Digest: sha256:20ec84e49b907163744a887dc2126491053bba1f2c82ddf6c73c006babc81512
Version: v1.0.0
Tags: [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.

Pulling validates the artifact and creates a new canonical HCL file:

Terminal window
ptah schema pull \
oci://ghcr.io/acme/app-schema:stable \
--out ./schema.hcl

The 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.

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.

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.

Terminal window
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.

Use an OCI schema through the repeatable --schema-file option:

Terminal window
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 json

OCI schema inputs can be combined with other repeatable --schema-file and --root-dir inputs through Ptah’s composite schema loader.

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.

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 go.5x5.cz/ptah/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.