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