Files
captoys/potluck/.agents/skills/creating-plugins/references/publishing.md
T
dckc 4b4ef8fea6 capsite: potluck, a fresh EmDash site (cloudflare:blog out of the box)
pnpm create emdash@latest potluck --pm pnpm --install --yes, per
https://docs.emdashcms.com/ ("Create your first EmDash site"); the
--yes defaults are cloudflare + blog. Replaces the old madmode/
starter (never committed).

Generated-by: create-emdash 1.2.0, run by Claude Opus 5.5
2026-10-07 19:57:43 -05:00

5.7 KiB

Publishing sandboxed plugins

Use @emdash-cms/plugin-cli for registry publishing. The package profile and release records belong to the Atmosphere account named by publisher in emdash-plugin.jsonc.

Required package metadata

Before publishing, require:

  • @emdash-cms/plugin-cli installed in the plugin package;
  • a unique plugin slug;
  • a publisher DID or Atmosphere handle;
  • a version in package.json or emdash-plugin.jsonc;
  • a license, at least one author, and at least one security contact;
  • declared capabilities and allowed hosts that match the plugin implementation;
  • a canonical public GitHub repo for delegated releases.

Run validation from the plugin directory:

pnpm exec emdash-plugin validate

Local publishing

Use local publishing when the maintainer starts the release from their computer:

pnpm exec emdash-plugin login alice.example.com
pnpm exec emdash-plugin publish

publish builds and validates the bundle, uploads its files to the publisher's personal data server, and creates the package release record. Use emdash-plugin bundle to inspect a tarball without publishing it.

The success output uses the registry identifier @<publisher-handle>/<slug>, prints the eventual plugin-page URL, and provides emdash-plugin info <handle> <slug> --version <version> --watch. Before approval, info reads the labeler's current checks and does not return unapproved package metadata from the aggregator.

Automated repository releases

Generate the shared workflow from one plugin package, not the monorepo root. Pass --dir <plugin-directory> when running the command from elsewhere:

pnpm exec emdash-plugin release setup

The command prepares the current signed package profile and writes .github/workflows/emdash-release.yml at the Git repository root. If the manifest omits repo, setup detects a GitHub origin remote and pre-fills the repository prompt. It does not push the file. The workflow is shared by all plugin packages in that repository and requires no Actions secret.

When .changeset/config.json exists at the repository root, interactive setup offers Follow Changesets releases. The generated workflow accepts the Changesets Action published-package JSON and publishes packages that also contain emdash-plugin.jsonc.

Connect Changesets Action v2 by exposing its outputs from the existing release job and calling the generated workflow:

jobs:
  release:
    # Keep the existing runner, permissions, and steps.
    outputs:
      published: ${{ steps.changesets.outputs.published }}
      published-packages: ${{ steps.changesets.outputs['published-packages'] }}

  publish-emdash-plugins:
    needs: release
    if: needs.release.outputs.published == 'true'
    uses: ./.github/workflows/emdash-release.yml
    with:
      published-packages: ${{ needs.release.outputs['published-packages'] }}
    permissions:
      contents: read
      id-token: write
      attestations: write

For Changesets Action v1, set the normalized published-packages job output from ${{ steps.changesets.outputs.publishedPackages }} instead. Private EmDash-only packages require both privatePackages.version: true and privatePackages.tag: true; add unrelated private packages to ignore.

Without Changesets, the generated workflow publishes tags in <slug>@<version> form:

git tag gallery@1.2.3
git push origin gallery@1.2.3

The workflow runs release prepare through the exact plugin CLI version that generated it. The command finds exactly one emdash-plugin.jsonc whose slug matches the tag, requires the manifest version to match, builds that package, and writes its outputs for the provenance and release Actions. Duplicate slugs, missing packages, and version mismatches stop before attestation.

The first run uses GitHub OpenID Connect to request a repository connection. The service checks that the initiating package's signed profile names the same repository before creating the request. The publisher approves the repository, workflow file, ref scope, and environment in the release dashboard. A manual run requests approval the first time its branch is used; confirmation adds that scope without removing approved tags or branches.

Prepare another package without changing the workflow. Changesets users add it to a changeset; package-tag users push its tag:

pnpm exec emdash-plugin profile setup --dir packages/comments
git tag comments@1.0.0
git push origin comments@1.0.0

profile setup confirms that it published the profile, then prints emdash-plugin publish for a manual release and emdash-plugin release setup for GitHub Actions.

The later package reuses an approved repository scope only when its signed profile names that repository. Package approvals created by older workflows remain package-scoped until an unmatched package or ref is explicitly approved as a repository connection. Release confirmation remains package-specific: escalation-only requires approval when declared access increases, while always requires approval for every release.

Verification boundaries

The release service verifies the GitHub repository and owner IDs, workflow, ref, environment, commit, run, runner, package profile, bundle checksum, manifest identity, declared access, and raw Sigstore provenance. It accepts artifacts only after the repository workflow and signed package profile both authorize the run.

Metadata moderation and release verification are separate. Registry moderation covers displayed package metadata and media. Installation independently verifies release records, bundle checksums, and declared access.

Use the public Automated plugin releases guide for the complete publisher journey and troubleshooting.