# Registry publishing: DuckDB Community Extensions + PGXN Research report (verified 2025‑12, against the actual upstream repos). Goal: automate, on a `v*` tag push, publishing `mentat_duckdb` to the DuckDB Community Extensions registry and `pg_mentat` to PGXN — on top of the existing `.github/workflows/release.yml` (which builds per‑PG tarballs + CLI and makes a GitHub Release). Repo: `codeberg.org/gregburd/mentat` (push‑mirrors to `github.com/gburd/mentat`). CI runs on the GitHub side. Two truths up front: - **DuckDB registry:** publishing = **a PR to `duckdb/community-extensions`** that edits one `description.yml`. Their CI builds+signs centrally for all platforms. First submission is a human‑reviewed PR; version bumps can be a CI‑opened PR, but every publish still passes through their PR merge (a human gate). - **PGXN:** publishing = `pgxn-bundle` + `pgxn-release` inside the `pgxn/pgxn-tools` container with `PGXN_USERNAME`/`PGXN_PASSWORD`. Fully automatable, **but** our `META.json` is currently wrong (bad `file` path, stale version) and the pgrx layout is non‑standard for classic PGXN install. Bundle will *upload* but the distribution is not source‑installable the classic way. Details below. --- ## 1. DuckDB Community Extensions — `mentat_duckdb` Upstream verified: - Repo: `https://github.com/duckdb/community-extensions` - Submission model doc: `UPDATING.md` in that repo (Community Extension Update Guide). - Rust extension template: `https://github.com/duckdb/extension-template-rs` - CI‑tools (reusable build workflow + Makefiles): `https://github.com/duckdb/extension-ci-tools` ### 1.1 The submission model — one `description.yml` per extension Register by adding **`extensions//description.yml`** (note: `.yml`, not `.yaml`) to `duckdb/community-extensions` via PR. There is exactly one descriptor file per extension; it points at *your* repo + a git ref. Their CI reads it, clones your repo at that ref, builds, signs, and deploys. Fields, verified against `scripts/build.py` and real descriptors: ```yaml extension: name: mentat # INSTALL/LOAD name; must match Makefile EXTENSION_NAME description: # required version: # your extension version (free-form; e.g. 1.8.0) language: Rust # display language build: cargo # build system: cmake | cargo (cargo => Rust template path) license: Apache-2.0 maintainers: - gregburd # GitHub usernames requires_toolchains: "rust;python3" # extra toolchains their CI installs before build # optional: # excluded_platforms: "wasm_mvp;wasm_eh;wasm_threads;windows_amd64_mingw;linux_amd64_musl" # vcpkg_url / vcpkg_commit # only for C/C++ vcpkg deps; N/A for us repo: github: gburd/mentat # owner/repo that CI clones (the GitHub mirror) ref: # the source ref they build (SHA, not tag — see 1.2) # ref_next: # only used when validating against next DuckDB (see 1.2) docs: hello_world: | # optional, shown on the website LOAD mentat; SELECT * FROM mentat_query('...'); extended_description: | # optional ... ``` Note what the descriptor **does not** contain: no `duckdb` version and no `extension-ci-tools` version. Those are **owned by community‑extensions' own CI**, not by you (see 1.3). You only declare `build: cargo` + `requires_toolchains: rust` and point at your repo. **Real pure‑Rust example** (verified live) — `extensions/quackformers/description.yml`: ```yaml extension: name: quackformers description: Bert-based embedding extension. version: 0.1.5.5 language: Rust build: cargo license: MIT excluded_platforms: "wasm_mvp;wasm_eh;wasm_threads;windows_amd64_mingw;linux_amd64_musl" requires_toolchains: "rust;python3" maintainers: - martin-conur repo: github: martin-conur/quackformers ref: 4741f1a317837cd110e5801825ad7af1bbc3bd87 docs: hello_world: | SELECT embed('this is an embeddable sentence'); ``` `quackformers` is built from `duckdb/extension-template-rs` — the same base our `crates/duckdb` Makefile mirrors. **That's our template.** (Most `query-farm` Rust extensions such as `crypto`, `lindel`, `evalexpr_rhai` use `build: cmake` + `requires_toolchains: rust` because they wrap Rust inside a C++ shell; the pure duckdb‑rs path is `build: cargo`, which is what we want.) ### 1.2 How new versions publish From `UPDATING.md` and `scripts/build.py`: - The descriptor points at **one active source ref** via `repo.ref`. - **Updating `repo.ref` (and `extension.version`) and merging that PR** makes the new ref the source; community CI rebuilds and re‑deploys for every platform + DuckDB version. **Every release = a PR to `duckdb/community-extensions`.** There is no webhook/API that pulls your new tag automatically. - `repo.ref` is a **commit SHA**, not a tag, in practice (all examples use SHAs). You can resolve `v1.8.0` → its SHA in CI and write the SHA. - Community extensions are **also** rebuilt automatically whenever DuckDB itself releases a new version — you don't PR for that; it just re‑runs against the ref you already have. `ref_next` exists only to let you supply a *different* commit for validating against the upcoming (not-yet-stable) DuckDB. So "automate future releases update the registry" concretely means: **CI opens a PR to `duckdb/community-extensions` that changes exactly `extensions/mentat/description.yml` — bumping `extension.version` and `repo.ref` to the new tag's commit.** ### 1.3 Build & signing are central — version‑lock is a lucky match Their CI does the build + sign for all platforms and all supported DuckDB versions. You provide only a buildable repo at `repo.ref`. Verified from `community-extensions/.github/workflows/build.yml`: ``` DUCKDB_LATEST_STABLE: 'v1.5.5' DUCKDB_VERSION: v1.5.5 # default uses: duckdb/extension-ci-tools/.github/workflows/_extension_distribution.yml@v1.5-variegata duckdb_version: v1.5.5 ci_tools_version: v1.5-variegata extra_toolchains: ``` **This is the key alignment:** community‑extensions currently builds against **DuckDB v1.5.5**, which is *exactly* what `mentat_duckdb` is pinned to (`duckdb = "~1.10505.0"`, `TARGET_DUCKDB_VERSION=v1.5.5`, `USE_UNSTABLE_C_API=1`). Our version‑lock is not a blocker right now — it matches the registry's stable target. The official Rust template (`extension-template-rs`) uses the *identical* `USE_UNSTABLE_C_API=1` + `TARGET_DUCKDB_VERSION=v1.5.5`, so our unstable‑C‑API posture is normal for a duckdb‑rs extension, not an exception. Caveat to watch: when DuckDB bumps its stable (v1.6+), community CI will try to rebuild `mentat` against the new version. Because we pin the unstable C API to v1.5.5, that rebuild will fail until we bump the `duckdb` crate + Makefile `TARGET_DUCKDB_VERSION` together and PR a new `repo.ref`. That's the standard Rust‑extension maintenance treadmill (`UPDATING.md` "Upgrading to a new DuckDB version"), not a defect. What our repo must expose for their CI (all already present in `crates/duckdb`): - `Makefile` including `extension-ci-tools/makefiles/c_api_extensions/{base,rust}.Makefile`, with `EXTENSION_NAME=mentat`, `USE_UNSTABLE_C_API=1`, `TARGET_DUCKDB_VERSION=v1.5.5`, and the `configure`/`debug`/`release`/`test` targets. ✅ present. - `EXTENSION_LIB_FILENAME`/`RUST_LIBNAME` override (our cdylib is `libmentat_duckdb.so`). ✅ present. - The `extension-ci-tools` submodule. ✅ present (pinned to `main`; see gotcha below). - Cross‑platform buildability: the extension is pure duckdb‑rs + rusqlite `bundled`, so it should build on linux/macOS/windows. `linux_amd64_musl` is a likely `excluded_platforms` (rusqlite/openssl-ish deps); mirror `quackformers`' exclusions to start, then relax. **Gotcha (local, not registry):** our submodule is pinned to `main` (`20bad04c`), while the community CI uses the tag `v1.5-variegata`. This only affects *local* `make` builds, not the central build. If a local build breaks after an upstream `main` change, pin the submodule to `v1.5-variegata` to match what the registry uses. ### 1.4 Automation recipe (DuckDB) First submission is **manual** (open the PR to `duckdb/community-extensions`, their team reviews it). Automation is for the *subsequent* version bumps, and even then the PR still needs a human merge upstream — you cannot fully auto‑publish; upstream PR review is a hard gate. Recommended: maintainer keeps a fork `gburd/community-extensions`. On a `v*` tag, a job resolves the tag's SHA, edits `extensions/mentat/description.yml` in the fork, and opens a PR to `duckdb/community-extensions` via `peter-evans/create-pull-request`. ```yaml # New job in release.yml, gated the same way as the others (needs: gate). duckdb-registry-pr: name: PR to duckdb/community-extensions needs: gate runs-on: ubuntu-latest steps: - name: Resolve tag commit SHA id: sha run: echo "sha=${GITHUB_SHA}" >> "$GITHUB_OUTPUT" - name: Check out our fork of community-extensions uses: actions/checkout@v4 with: repository: gburd/community-extensions # maintainer's fork token: ${{ secrets.COMMUNITY_EXT_PAT }} # PAT with repo scope on the fork path: community-extensions - name: Update descriptor working-directory: community-extensions run: | VER="${GITHUB_REF_NAME#v}" # 1.8.0 SHA="${{ steps.sha.outputs.sha }}" mkdir -p extensions/mentat cat > extensions/mentat/description.yml <-.zip" \ -H 'X-Requested-With: XMLHttpRequest' https://manager.pgxn.org/upload ``` Secrets: **`PGXN_USERNAME`** and **`PGXN_PASSWORD`** (your PGXN Manager creds). `pgxnclient` (the old Python `pgxn` CLI) still exists but the canonical CI path is the `pgxn/pgxn-tools` image; use it. ### 2.2 What the release ZIP must contain — and our two problems `pgxn-bundle` does two things (verified from the script): 1. `pgxn validate-meta META.json` — **spec validation only** (structure per `meta-spec`), *not* a filesystem check that `provides.*.file` exists. 2. `git archive --prefix "-/" ... HEAD` — zips the **entire HEAD tree** with the dist prefix. So the bundle step will *succeed* even with a broken `file` path. But the resulting distribution is only correct if META matches the tree. Two real issues in the current `META.json` (`~/ws/mentat/META.json`): - **Wrong `provides.pg_mentat.file`.** It says `"pg_mentat/pg_mentat.control"`, but there is **no `pg_mentat/` dir at the repo root** — the control file lives at `crates/pg/pg_mentat/pg_mentat.control`. In the bundled zip the declared path won't resolve. Fix the `file` to the real path, e.g. `"crates/pg/pg_mentat/pg_mentat.control"` (and `docfile` — `README.md` at root is fine). - **Stale `version`.** META is `1.7.0` (both top‑level and `provides.*.version`); releasing v1.8.0 requires bumping both. `Trunk.toml` is likewise `1.7.0`. `pgxn validate-meta` won't catch a version *mismatch with the tag* — you must keep them in sync yourself (or derive META version from the tag in CI). Present & valid in META: `name`, `abstract`, `description`, `maintainer`, `license` (`apache_2_0`), `meta-spec` (1.0.0 + url), `provides`, `resources`, `tags`. Structurally `pgxn validate-meta` should pass once `version` is bumped; the `file` path is a *correctness* bug, not a validation failure. - **pgrx‑vs‑PGXN layout (the honest caveat).** Classic PGXN distributions are PGXS source trees: a consumer downloads the zip and runs `make && make install` driven by `provides.*.file` (a `.control`) + a PGXS `Makefile`. `pg_mentat` is a **pgrx** extension — no PGXS `Makefile`, and the `.control`/SQL are *generated* by `cargo pgrx package`, not shipped as static installable files at the paths PGXN expects. Consequences: - `pgxn-bundle`/`pgxn-release` **will upload** (the Manager only needs a valid META + a zip), so the release *appears* on PGXN and is mirrored/searchable. - But it is **not `make`‑installable from source** by a classic PGXN user, and PGXN's build‑farm won't build a pgrx tree. In practice PGXN becomes a *discovery/metadata* channel for `pg_mentat`, while real installation happens via the GitHub Release tarballs (already built by `release.yml`) or Trunk‑like binary registries. Decide whether a "listed but not source‑installable" PGXN entry is worth it. (Several pgrx extensions do exactly this — publish to PGXN for discoverability and ship binaries elsewhere.) ### 2.3 Trunk (`pgt.dev` / `Trunk.toml`) — skip it, the registry is defunct `Trunk.toml` targets the Tembo "Trunk" PG binary registry (`pgt.dev`, `trunk publish`). Verified 2025‑12: - `pgt.dev` **no longer resolves in DNS** (dead host). - The `tembo-io` GitHub org / `tembo-io/trunk` repo returns **Not Found** (gone). - The `pg-trunk` crate on crates.io hasn't been updated since **April 2025**. Tembo wound down the hosted Trunk registry. **Do not automate Trunk.** Leave `Trunk.toml` in place as harmless metadata (or delete it); there's nothing live to publish to. Revisit only if a successor registry appears. ### 2.4 Automation recipe (PGXN) Fully automatable (no upstream human gate on publish — the account approval is a one‑time human step). Add a job to `release.yml` gated the same way (`>= v1.7.0`, so use the existing `gate` job). Fix META first (2.2) or the uploaded dist is broken. ```yaml pgxn-release: name: Publish to PGXN needs: gate runs-on: ubuntu-latest container: pgxn/pgxn-tools # canonical image; ships pgxn-bundle + pgxn-release steps: - name: Check out the repo uses: actions/checkout@v4 with: submodules: false # PGXN dist is pg_mentat only; keep the zip lean - name: Sync META.json version to the tag (belt-and-suspenders) run: | VER="${GITHUB_REF_NAME#v}" # 1.8.0 # Fail loudly if META wasn't bumped to match the tag. MJSON_VER="$(perl -MJSON=decode_json -E 'say decode_json(join "", <>)->{version}' META.json)" if [ "$MJSON_VER" != "$VER" ]; then echo "ERROR: META.json version ($MJSON_VER) != tag ($VER). Bump META.json." >&2 exit 1 fi - name: Bundle the release id: bundle run: pgxn-bundle # validates META, writes pg_mentat-.zip - name: Release on PGXN env: PGXN_USERNAME: ${{ secrets.PGXN_USERNAME }} PGXN_PASSWORD: ${{ secrets.PGXN_PASSWORD }} run: pgxn-release ``` Secrets: **`PGXN_USERNAME`**, **`PGXN_PASSWORD`** — add to the GitHub repo settings once the PGXN account is approved. One‑time maintainer steps: 1. Register at `https://manager.pgxn.org/` and get the account approved. 2. Add `PGXN_USERNAME` / `PGXN_PASSWORD` repo secrets. 3. **Fix `META.json`** (`provides.pg_mentat.file` → real path; bump `version` to the release version) — otherwise the uploaded distribution is malformed. 4. Accept the pgrx caveat (2.2): PGXN entry is discovery/metadata, not classic source‑install. --- ## 3. Summary table | | DuckDB Community Extensions | PGXN | |---|---|---| | Publish mechanism | PR editing `extensions/mentat/description.yml` in `duckdb/community-extensions` | `pgxn-bundle` + `pgxn-release` (image `pgxn/pgxn-tools`) | | Build/sign | **Central** (their CI, all platforms, DuckDB v1.5.5, `ci_tools_version v1.5-variegata`) | You bundle a zip; PGXN Manager just stores it | | Our version‑lock | **Matches** (registry stable = v1.5.5 = our pin) ✅ | n/a | | First time | Manual PR + upstream review | Create + get PGXN account approved | | Per release (automatable?) | CI opens PR (needs `COMMUNITY_EXT_PAT`); **merge is a human gate** | Fully automatable (`PGXN_USERNAME`/`PGXN_PASSWORD`) | | Secrets | `COMMUNITY_EXT_PAT` (fork+PR scope) | `PGXN_USERNAME`, `PGXN_PASSWORD` | | Non‑standard for us | Unstable‑C‑API version‑lock (normal for duckdb‑rs; breaks on DuckDB bumps) | `META.json` `file` path wrong + version stale; pgrx tree not classic `make`‑installable | | `repo.github` / source | Must be **GitHub mirror** `gburd/mentat` (their CI clones GitHub) | Bundles whatever is at HEAD of the checked‑out repo | | Trunk (`pgt.dev`) | — | **Defunct — do not automate** (`pgt.dev` dead, `tembo-io/trunk` gone) | ## 4. Verified sources - DuckDB community: `github.com/duckdb/community-extensions` (`README.md`, `UPDATING.md`, `scripts/build.py`, `.github/workflows/build.yml` → `DUCKDB_LATEST_STABLE: v1.5.5`, `_extension_distribution.yml@v1.5-variegata`). - Real descriptors: `extensions/quackformers/description.yml` (pure Rust, `build: cargo`), `extensions/crypto/description.yml`, `extensions/waddle/description.yml` (in `UPDATING.md`). - Rust template: `github.com/duckdb/extension-template-rs` (`Makefile` → `USE_UNSTABLE_C_API=1`, `TARGET_DUCKDB_VERSION=v1.5.5`; `.github/workflows/MainDistributionPipeline.yml`). - PGXN tooling: `github.com/pgxn/docker-pgxn-tools` (`README.md`, `bin/pgxn-bundle`, `bin/pgxn-release`); image `pgxn/pgxn-tools`; upload endpoint `https://manager.pgxn.org/upload`. - Local: `~/ws/mentat/META.json`, `~/ws/mentat/Trunk.toml`, `~/ws/mentat/crates/duckdb/Makefile`, `~/ws/mentat/crates/duckdb/Cargo.toml`, `~/ws/mentat/.github/workflows/release.yml`, `~/ws/mentat/crates/pg/pg_mentat/pg_mentat.control`. - Trunk defunct: `pgt.dev` no DNS; `github.com/tembo-io/trunk` 404; crates.io `pg-trunk` last updated 2025‑04.