+++ title = "Publish Command" description = "Publish every lexicon in the workspace to the configured PDS + DNS" weight = 11 +++ `mlf publish` is the one-shot publish orchestrator. It computes what would change on the PDS, validates it locally, ensures the required `_lexicon` TXT records are in place via the configured DNS plugin, and then applies the minimum set of `putRecord` / `deleteRecord` calls to make the remote match the workspace. A per-publish `lol.mlf.package` manifest record is written alongside (toggleable via `[publish].manifest`). ## Usage ```bash mlf publish # normal publish mlf publish --dry-run # compute + validate the plan, no network writes mlf publish --force # override breaking-change aborts + DNS-mismatch aborts mlf publish --non-interactive # fail rather than prompt for anything ``` Ephemeral credential overrides (not stored) can be passed after the flags: ```bash mlf publish \ --pds-app-password ${{ secrets.ATPROTO_APP_PASSWORD }} \ --dns-cloudflare-api-token ${{ secrets.CLOUDFLARE_TOKEN }} ``` Field names come from the plugin's options schema. For the PDS: `--pds-handle`, `--pds-app-password`. For a DNS plugin: `--dns--`. ## What it does, in order 1. **Load workspace** — parse every `.mlf`, generate Lexicon JSON, wrap each as a `com.atproto.lexicon.schema` record, and compute the record's CID (DAG-CBOR + SHA-256). 2. **Fetch remote state** — resolve each `_lexicon.` TXT to a DID, then `listRecords` the `com.atproto.lexicon.schema` collection on that repo. Every NSID + CID already on the PDS is collected. 3. **Validate**: - **Scope** — every local NSID must be a descendant of `[package].name`. - **Meta-schema** — every generated record must have `$type` / `lexicon` / `id` / `defs` in the expected shape. Bundled; no network. - **Breaking-change** — for each NSID that exists on both sides, detect removed fields, changed types, optional-to-required transitions, and removed defs. Under `[publish].breaking_changes = "deny"` (default), any finding aborts the publish unless `--force` is passed. - **Single-DID gate** — every authority must resolve to the same DID. 4. **Plan** — produce the minimal set of `put` / `update` / `delete` actions. Records outside `[package].name.*` on the same repo are left alone. 5. **Dry-run check** — if `--dry-run`, print the plan and stop. 6. **Reconcile DNS** — for each authority: TXT missing → plugin creates it; TXT matches session DID → no-op; TXT points elsewhere → abort (unless `--force`, in which case we overwrite). 7. **Authenticate + apply** — `createSession` against the PDS with the stored handle + app password, then push each action. 8. **Write manifest** — `putRecord` the `lol.mlf.package` record with `published: [{nsid, cid}, ...]` sorted lexicographically. Skipped when `[publish].manifest = false`. ## Configuration `mlf publish` reads the `[publish]` section from `mlf.toml`: ```toml [publish] enabled = true # default true; set false to block temporarily dns = "cloudflare" # REQUIRED — which DNS plugin to use manifest = true # emit lol.mlf.package (default true) breaking_changes = "deny" # "deny" | "warn" | "allow" ``` If `[publish]` is absent entirely, the workspace is *not publishable* and `mlf publish` refuses. ## Manifest record Published at `lol.mlf.package` in the same repo: ```json { "$type": "com.atproto.lexicon.schema", "lexicon": 1, "id": "lol.mlf.package", "publishedAt": "2026-04-17T…", "tool": "mlf@0.1.0", "published": [ {"nsid": "com.example.forum.post", "cid": "bafy…"}, {"nsid": "com.example.forum.thread", "cid": "bafy…"} ], "resolvedDependencies": [] } ``` The record's own CID deterministically identifies this publish event. ## CI/CD pattern ```yaml - run: mlf login pds --project --handle matt.example.com --app-password ${{ secrets.ATPROTO_APP_PASSWORD }} - run: mlf login dns cloudflare --project --api-token ${{ secrets.CLOUDFLARE_TOKEN }} - run: mlf publish --non-interactive ``` Or as a single invocation with ephemeral credentials (no login step): ```yaml - run: | mlf publish --non-interactive \ --pds-handle matt.example.com \ --pds-app-password ${{ secrets.ATPROTO_APP_PASSWORD }} \ --dns-cloudflare-api-token ${{ secrets.CLOUDFLARE_TOKEN }} ``` ## Exit codes - `0` — plan applied successfully (or dry-run completed without findings). - Non-zero — validation failure, breaking-change abort, DNS-mismatch abort, missing credentials, network error, or any `putRecord` failure. The relevant miette diagnostic names the exact issue. ## See also - [`mlf status`](../08-status/) — see the same plan without running the validators. - [`mlf diff`](../09-diff/) — inspect one record's proposed change. - [`mlf login`](../10-login/) — set up the PDS and DNS credentials `publish` needs.