Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/friendly-dogs-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@emdash-cms/plugin-cli": minor
---

Adds Changesets-aware automated plugin releases. `emdash-plugin release setup` detects a root `.changeset/config.json` and offers to follow Changesets releases, `<slug>@<version>` tags, or manual runs. Use `--trigger auto|changesets|tags|manual` in non-interactive setup.

The Changesets variant accepts the official Changesets Action published-package JSON through a reusable workflow. It supports mixed monorepos where npm package names differ from EmDash plugin IDs, ignores ordinary npm packages, verifies every reported plugin version, and publishes matching plugins as a matrix. Private EmDash-only packages produce a setup warning unless Changesets versions and tags them.
54 changes: 52 additions & 2 deletions apps/release-action/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,59 @@ emdash-plugin release setup

Before writing `.github/workflows/emdash-release.yml`, the command creates a missing package profile or adds delegated-release settings to an existing valid profile. Profile setup binds the package to the canonical GitHub repository, uses the signed-in [Atmosphere account](https://docs.emdashcms.com/plugins/creating-plugins/publishing/#your-atmosphere-account) as the initial approver, and asks whether approval is required for permission increases or every release. Run `emdash-plugin profile setup` to perform this step without changing the workflow file.

The generated root workflow uses pinned third-party Actions, resolves `<slug>@<version>` tags to one plugin package, builds one bundle, creates GitHub provenance for the exact bundle, and calls this Action. Every plugin package in the repository reuses the workflow. It does not push the workflow. The generated workflow currently supports public repositories because the verifier trusts GitHub's public Sigstore root.
The generated root workflow uses pinned third-party Actions, accepts packages released by Changesets, `<slug>@<version>` tags, or manual selections, builds one bundle for each release, creates GitHub provenance for the exact bundle, and calls this Action. Every plugin package in the repository reuses the workflow. It does not push the workflow. The generated workflow currently supports public repositories because the verifier trusts GitHub's public Sigstore root.

## Follow Changesets releases

Choose **Follow Changesets releases** during `release setup` to generate a reusable EmDash workflow. Add its caller after the existing Changesets publish job. The caller passes Changesets' published-package JSON; the EmDash workflow ignores ordinary packages and publishes matching `emdash-plugin.jsonc` packages at the reported versions.

For Changesets Action v2 with Changesets CLI v3, expose the kebab-case output:

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[needs fixing] The v2 caller example reads the Changesets step output as steps.changesets.outputs['published-packages'], but @changesets/action exposes the published-package list as publishedPackages (camelCase) — the same output this repository already uses in .github/workflows/release.yml. Copying this snippet will pass an undefined value into the EmDash reusable workflow. Keep the published-packages job output name, but read from the upstream camelCase output.

Suggested change
published-packages: ${{ steps.changesets.outputs['published-packages'] }}
published-packages: ${{ steps.changesets.outputs.publishedPackages }}


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

Changesets Action v1 with Changesets CLI v2 uses `steps.changesets.outputs.publishedPackages` instead. Keep the normalized job output and caller unchanged:

```yaml
jobs:
release:
# Keep the existing runner, permissions, and steps.
outputs:
published: ${{ steps.changesets.outputs.published }}
published-packages: ${{ steps.changesets.outputs.publishedPackages }}

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

Private EmDash-only packages require `privatePackages.version: true` and `privatePackages.tag: true` in `.changeset/config.json`. Add unrelated private packages to `ignore`.

Start the workflow by pushing a package tag such as `gallery@1.2.3`. The service checks that the signed package profile names the GitHub repository before creating a connection request. The Action writes an approval link to the job summary and waits. Open the link, sign in to the release service, and check the repository, workflow file, branch or tag, and environment reported by GitHub. After confirmation, the same Action run requests a fresh OIDC token and submits the release. Later packages reuse approved tag and branch scopes when their signed profiles name the same repository.
Start the release using the source selected during setup: let Changesets publish the package, push a package tag such as `gallery@1.2.3`, or run the workflow manually. The service checks that the signed package profile names the GitHub repository before creating a connection request. The Action writes an approval link to the job summary and waits. Open the link, sign in to the release service, and check the repository, workflow file, branch or tag, and environment reported by GitHub. After confirmation, the same Action run requests a fresh OIDC token and submits the release. Later packages reuse approved tag and branch scopes when their signed profiles name the same repository.

For tag-triggered releases, choose whether the workflow may publish all package version tags or only the current tag. The approval never grants authority by itself: the publisher's Atmosphere session must confirm the signed GitHub identity before the service creates a publishing policy.

Expand Down
2 changes: 1 addition & 1 deletion apps/release-service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ The publisher and approver interfaces use the same Atmosphere account identity a
The service processes an automated release in this order:

1. The publisher authorises the exact create-only release and blob OAuth scope.
2. A GitHub Actions job presents a GitHub OIDC token and the package selected by its `<slug>@<version>` tag.
2. A GitHub Actions job presents a GitHub OIDC token and the package selected by a Changesets version update, `<slug>@<version>` tag, or manual run.
3. The service verifies the signed package profile and its canonical repository before creating a pending connection request.
4. The publisher checks the repository, workflow file, ref, and environment before confirming the repository connection.
5. Every workflow run presents a fresh GitHub OIDC token. The service compares its repository, owner, workflow, ref, environment, commit, run, and runner claims with the stored policy. Packages whose signed profiles name the same repository reuse approved tag and branch scopes.
Expand Down
2 changes: 1 addition & 1 deletion apps/release-service/src/ui/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ describe("release-service web surfaces", () => {
expect(screen.getByText("pnpm exec emdash-plugin release setup")).toBeTruthy();
expect(
screen.getByText(
"Review and commit .github/workflows/emdash-release.yml, then push a package tag such as gallery@1.2.3 or start it from GitHub Actions.",
"Review and commit .github/workflows/emdash-release.yml. EmDash can follow packages released by Changesets, package tags, or manual GitHub Actions runs.",
),
).toBeTruthy();
expect(screen.getAllByText("@publisher.example.com")).toHaveLength(1);
Expand Down
2 changes: 1 addition & 1 deletion apps/release-service/src/ui/PublisherPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -641,7 +641,7 @@ export function PublisherPage() {
<p className="text-kumo-subtle">
{t(
"publisher.workload.setupResult",
"Review and commit .github/workflows/emdash-release.yml, then push a package tag such as gallery@1.2.3 or start it from GitHub Actions.",
"Review and commit .github/workflows/emdash-release.yml. EmDash can follow packages released by Changesets, package tags, or manual GitHub Actions runs.",
)}
</p>
<p className="text-kumo-subtle">
Expand Down
12 changes: 11 additions & 1 deletion docs/src/content/docs/plugins/creating-plugins/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ emdash-plugin publish Build, upload, and publish a releas
emdash-plugin update-package [--yes] Preview or apply package-profile changes
emdash-plugin profile setup Prepare the signed package profile for delegated releases
emdash-plugin release setup Create the delegated-release GitHub Actions workflow
emdash-plugin release plan Plan repository releases for GitHub Actions
emdash-plugin release prepare <slug[@ver]> Prepare one repository package for GitHub Actions
emdash-plugin login <handle-or-did> Sign in with your Atmosphere account
emdash-plugin logout [--did <did>] Revoke the active session
Expand Down Expand Up @@ -178,9 +179,18 @@ It accepts the `profile setup` flags plus the following workflow options:
| --- | --- | --- |
| `--service-url <origin>` | `https://releases.emdashcms.com` | HTTPS origin used by the generated Action. |
| `--action-ref <ref>` | `main` | EmDash repository ref containing the release Action. |
| `--trigger <mode>` | `auto` | Release source: `changesets`, `tags`, or `manual`. `auto` offers Changesets when `.changeset/config.json` exists. |
| `--force` | `false` | Replace an existing generated workflow. Without it, setup leaves the existing file unchanged. |

The command never pushes the generated workflow. The first `slug@version` package tag creates a repository connection request using GitHub OpenID Connect; no Actions secret is required. Follow [Automated plugin releases](/plugins/creating-plugins/delegated-releases/) to review the workflow, authorise the release service, connect the repository, and publish the first release.
When setup detects Changesets in an interactive terminal, it asks how EmDash plugins should be released. **Follow Changesets releases** publishes the same versions for packages containing `emdash-plugin.jsonc`. The other choices follow `<slug>@<version>` tags or allow manual runs only. In non-interactive use, `auto` selects Changesets when a valid root configuration exists and package tags otherwise.

The Changesets variant is a reusable workflow. Add one caller job after the existing Changesets publish job and pass its official published-package JSON output. Private EmDash-only packages require `privatePackages.version: true` and `privatePackages.tag: true`; setup warns when either option is missing.

The command never pushes the generated workflow. The first automated run creates a repository connection request using GitHub OpenID Connect; no Actions secret is required. Follow [Automated plugin releases](/plugins/creating-plugins/delegated-releases/) to review the workflow, authorise the release service, connect the repository, and publish the first release.

## `release plan`

`release plan` is used by the generated workflow. With `--published-packages <json>`, it maps the Changesets Action output to packages containing `emdash-plugin.jsonc`, verifies their versions, and writes a JSON selector matrix to `GITHUB_OUTPUT`. With `--package <slug[@version]>`, it validates one manual selector. The command does not build or publish packages.

## `release prepare`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ pnpm exec emdash-plugin validate

The command creates `.github/workflows/emdash-release.yml`. It does not push the file and does not replace an existing workflow unless you pass `--force`.

The generated workflow runs for package tags matching `<slug>@<version>` and through `workflow_dispatch`. It grants `contents: read`, `id-token: write`, and `attestations: write`; pins third-party Actions to full commit identifiers; runs the exact plugin CLI version that generated the file; resolves one package from its manifest; builds one plugin bundle; creates GitHub build provenance for those exact bytes; and passes both files to the EmDash release Action.
If the repository contains `.changeset/config.json`, interactive setup offers **Follow Changesets releases**. When Changesets releases a package containing `emdash-plugin.jsonc`, the reusable EmDash workflow publishes the same version. Connect it to the existing Changesets workflow as described below. Otherwise, the generated workflow runs for package tags matching `<slug>@<version>`. Both variants support manual runs and can be selected explicitly with `--trigger changesets|tags|manual`.

The workflow grants each job only its required `contents`, `id-token`, and `attestations` permissions; pins third-party Actions to full commit identifiers; runs the exact plugin CLI version that generated the file; resolves each package from its manifest; builds one plugin bundle; creates GitHub build provenance for those exact bytes; and passes both files to the EmDash release Action.

The workflow lives at the repository root and is shared by every plugin package in that repository. Running `release setup` from a nested package still writes `.github/workflows/emdash-release.yml` at the root.

Expand All @@ -81,7 +83,9 @@ pnpm exec emdash-plugin validate

5. Start the release workflow.

Update the package version before creating the version tag. The following commands start a `1.2.3` release:
With Changesets, merge the version pull request and let its publish job complete. The Changesets Action passes the packages it released to the reusable EmDash workflow. Ordinary npm packages are ignored; packages containing `emdash-plugin.jsonc` publish the same version to EmDash.

With the package-tag trigger, update the package version before creating the version tag. The following commands start a `1.2.3` release:

```sh
git tag gallery@1.2.3
Expand All @@ -106,6 +110,56 @@ pnpm exec emdash-plugin validate

</Steps>

## Connect a Changesets workflow

The generated `.github/workflows/emdash-release.yml` accepts the Changesets Action published-package JSON through `workflow_call`. Add an output to the existing Changesets job, then call the EmDash workflow from a dependent job. Replace `release` and `changesets` when the existing job or step uses another ID.

Changesets Action v2 uses the `published-packages` output. Add the following job output and caller to a workflow using Changesets CLI v3:

```yaml title=".github/workflows/release.yml"
jobs:
release:
# Keep the existing runner, permissions, and steps.
outputs:
published: ${{ steps.changesets.outputs.published }}
published-packages: ${{ steps.changesets.outputs['published-packages'] }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[needs fixing] The canonical docs repeat the incorrect v2 step-output name. The Changesets Action output is publishedPackages in both v1 and v2, so this expression evaluates to undefined. Normalize it to a published-packages job output, but read from publishedPackages.

Suggested change
published-packages: ${{ steps.changesets.outputs['published-packages'] }}
published-packages: ${{ steps.changesets.outputs.publishedPackages }}


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

Changesets Action v1 uses the camel-case `publishedPackages` step output. Use this expression for a workflow using Changesets CLI v2:

```yaml title=".github/workflows/release.yml"
jobs:
release:
# Keep the existing runner, permissions, and steps.
outputs:
published: ${{ steps.changesets.outputs.published }}
published-packages: ${{ steps.changesets.outputs.publishedPackages }}

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

Keep Changesets responsible for its version pull request and package publication. The EmDash caller runs only when Changesets reports `published: true`. For private EmDash-only packages, set both `privatePackages.version` and `privatePackages.tag` to `true` in `.changeset/config.json`. Add unrelated private applications and test fixtures to `ignore`.

## Add another package

Prepare the package profile from its source directory. The existing root workflow and repository connection are reused:
Expand All @@ -114,14 +168,14 @@ Prepare the package profile from its source directory. The existing root workflo
pnpm exec emdash-plugin profile setup --dir packages/comments
```

Update the package version, then push its package tag:
With Changesets, add the package to a changeset and merge its version pull request. With the package-tag trigger, update the package version and push its tag:

```sh
git tag comments@1.0.0
git push origin comments@1.0.0
```

The workflow resolves `comments` to one `emdash-plugin.jsonc`, checks that the manifest version is `1.0.0`, and verifies that the signed profile names the connected repository before accepting artifact uploads. Duplicate package IDs and tag-version mismatches fail before attestation.
The workflow resolves `comments` to one `emdash-plugin.jsonc`, checks the selected version, and verifies that the signed profile names the connected repository before accepting artifact uploads. Duplicate package IDs and version mismatches fail before attestation.

## What the release service verifies

Expand Down
2 changes: 2 additions & 0 deletions docs/technical-specs/delegated-release-service.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ The service normalizes omitted values to the protocol defaults. It validates the

`emdash-plugin release setup` creates a missing package profile or adds the extension to an existing valid profile through the publisher's local CLI session. It asks for confirmation in an interactive terminal and preserves existing package metadata. The service checks the signed extension before creating a workflow connection request and again before accepting that workflow's artifact uploads. A missing profile, missing extension, or repository mismatch returns `PACKAGE_PROFILE_REQUIRED` with the local setup command.

Setup writes one repository workflow. With Changesets, the workflow is called after the existing Changesets publish job and receives its official published-package JSON. It maps package names to plugin manifests, verifies the reported versions, and emits one matrix entry per matching plugin. Without Changesets, `<slug>@<version>` tags select a package directly. Manual selection remains available in every generated workflow. Trigger selection does not change the service's workload identity, repository connection, provenance, or package-profile checks.

The delegated path always requires supported provenance, even when `requireProvenance` is absent. The profile field communicates the publisher's requirement to every installer and non-delegated publisher. A supplied unsupported predicate is present-but-unverifiable and fails delegated publication.

### Signed release provenance
Expand Down
Loading
Loading