Skip to content

Commit ea2ccd5

Browse files
authored
feat(plugin-cli): integrate delegated releases with Changesets (#3093)
* feat: integrate plugin releases with changesets * fix: follow changesets publication output * docs: align release trigger guidance
1 parent e13fa01 commit ea2ccd5

17 files changed

Lines changed: 822 additions & 30 deletions

File tree

.changeset/friendly-dogs-plan.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@emdash-cms/plugin-cli": minor
3+
---
4+
5+
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.
6+
7+
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.

apps/release-action/README.md

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,59 @@ emdash-plugin release setup
1515

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

18-
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.
18+
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.
19+
20+
## Follow Changesets releases
21+
22+
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.
23+
24+
For Changesets Action v2 with Changesets CLI v3, expose the kebab-case output:
25+
26+
```yaml
27+
jobs:
28+
release:
29+
# Keep the existing runner, permissions, and steps.
30+
outputs:
31+
published: ${{ steps.changesets.outputs.published }}
32+
published-packages: ${{ steps.changesets.outputs['published-packages'] }}
33+
34+
publish-emdash-plugins:
35+
needs: release
36+
if: needs.release.outputs.published == 'true'
37+
uses: ./.github/workflows/emdash-release.yml
38+
with:
39+
published-packages: ${{ needs.release.outputs['published-packages'] }}
40+
permissions:
41+
contents: read
42+
id-token: write
43+
attestations: write
44+
```
45+
46+
Changesets Action v1 with Changesets CLI v2 uses `steps.changesets.outputs.publishedPackages` instead. Keep the normalized job output and caller unchanged:
47+
48+
```yaml
49+
jobs:
50+
release:
51+
# Keep the existing runner, permissions, and steps.
52+
outputs:
53+
published: ${{ steps.changesets.outputs.published }}
54+
published-packages: ${{ steps.changesets.outputs.publishedPackages }}
55+
56+
publish-emdash-plugins:
57+
needs: release
58+
if: needs.release.outputs.published == 'true'
59+
uses: ./.github/workflows/emdash-release.yml
60+
with:
61+
published-packages: ${{ needs.release.outputs['published-packages'] }}
62+
permissions:
63+
contents: read
64+
id-token: write
65+
attestations: write
66+
```
67+
68+
Private EmDash-only packages require `privatePackages.version: true` and `privatePackages.tag: true` in `.changeset/config.json`. Add unrelated private packages to `ignore`.
1969

20-
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.
70+
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.
2171

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

apps/release-service/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ The publisher and approver interfaces use the same Atmosphere account identity a
2121
The service processes an automated release in this order:
2222

2323
1. The publisher authorises the exact create-only release and blob OAuth scope.
24-
2. A GitHub Actions job presents a GitHub OIDC token and the package selected by its `<slug>@<version>` tag.
24+
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.
2525
3. The service verifies the signed package profile and its canonical repository before creating a pending connection request.
2626
4. The publisher checks the repository, workflow file, ref, and environment before confirming the repository connection.
2727
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.

apps/release-service/src/ui/App.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ describe("release-service web surfaces", () => {
184184
expect(screen.getByText("pnpm exec emdash-plugin release setup")).toBeTruthy();
185185
expect(
186186
screen.getByText(
187-
"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.",
187+
"Review and commit .github/workflows/emdash-release.yml. EmDash can follow packages released by Changesets, package tags, or manual GitHub Actions runs.",
188188
),
189189
).toBeTruthy();
190190
expect(screen.getAllByText("@publisher.example.com")).toHaveLength(1);

apps/release-service/src/ui/PublisherPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -641,7 +641,7 @@ export function PublisherPage() {
641641
<p className="text-kumo-subtle">
642642
{t(
643643
"publisher.workload.setupResult",
644-
"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.",
644+
"Review and commit .github/workflows/emdash-release.yml. EmDash can follow packages released by Changesets, package tags, or manual GitHub Actions runs.",
645645
)}
646646
</p>
647647
<p className="text-kumo-subtle">

docs/src/content/docs/plugins/creating-plugins/cli.mdx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ emdash-plugin publish Build, upload, and publish a releas
2323
emdash-plugin update-package [--yes] Preview or apply package-profile changes
2424
emdash-plugin profile setup Prepare the signed package profile for delegated releases
2525
emdash-plugin release setup Create the delegated-release GitHub Actions workflow
26+
emdash-plugin release plan Plan repository releases for GitHub Actions
2627
emdash-plugin release prepare <slug[@ver]> Prepare one repository package for GitHub Actions
2728
emdash-plugin login <handle-or-did> Sign in with your Atmosphere account
2829
emdash-plugin logout [--did <did>] Revoke the active session
@@ -178,9 +179,18 @@ It accepts the `profile setup` flags plus the following workflow options:
178179
| --- | --- | --- |
179180
| `--service-url <origin>` | `https://releases.emdashcms.com` | HTTPS origin used by the generated Action. |
180181
| `--action-ref <ref>` | `main` | EmDash repository ref containing the release Action. |
182+
| `--trigger <mode>` | `auto` | Release source: `changesets`, `tags`, or `manual`. `auto` offers Changesets when `.changeset/config.json` exists. |
181183
| `--force` | `false` | Replace an existing generated workflow. Without it, setup leaves the existing file unchanged. |
182184

183-
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.
185+
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.
186+
187+
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.
188+
189+
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.
190+
191+
## `release plan`
192+
193+
`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.
184194

185195
## `release prepare`
186196

docs/src/content/docs/plugins/creating-plugins/delegated-releases.mdx

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ pnpm exec emdash-plugin validate
7171

7272
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`.
7373

74-
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.
74+
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`.
75+
76+
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.
7577

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

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

8284
5. Start the release workflow.
8385

84-
Update the package version before creating the version tag. The following commands start a `1.2.3` release:
86+
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.
87+
88+
With the package-tag trigger, update the package version before creating the version tag. The following commands start a `1.2.3` release:
8589

8690
```sh
8791
git tag gallery@1.2.3
@@ -106,6 +110,56 @@ pnpm exec emdash-plugin validate
106110

107111
</Steps>
108112

113+
## Connect a Changesets workflow
114+
115+
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.
116+
117+
Changesets Action v2 uses the `published-packages` output. Add the following job output and caller to a workflow using Changesets CLI v3:
118+
119+
```yaml title=".github/workflows/release.yml"
120+
jobs:
121+
release:
122+
# Keep the existing runner, permissions, and steps.
123+
outputs:
124+
published: ${{ steps.changesets.outputs.published }}
125+
published-packages: ${{ steps.changesets.outputs['published-packages'] }}
126+
127+
publish-emdash-plugins:
128+
needs: release
129+
if: needs.release.outputs.published == 'true'
130+
uses: ./.github/workflows/emdash-release.yml
131+
with:
132+
published-packages: ${{ needs.release.outputs['published-packages'] }}
133+
permissions:
134+
contents: read
135+
id-token: write
136+
attestations: write
137+
```
138+
139+
Changesets Action v1 uses the camel-case `publishedPackages` step output. Use this expression for a workflow using Changesets CLI v2:
140+
141+
```yaml title=".github/workflows/release.yml"
142+
jobs:
143+
release:
144+
# Keep the existing runner, permissions, and steps.
145+
outputs:
146+
published: ${{ steps.changesets.outputs.published }}
147+
published-packages: ${{ steps.changesets.outputs.publishedPackages }}
148+
149+
publish-emdash-plugins:
150+
needs: release
151+
if: needs.release.outputs.published == 'true'
152+
uses: ./.github/workflows/emdash-release.yml
153+
with:
154+
published-packages: ${{ needs.release.outputs['published-packages'] }}
155+
permissions:
156+
contents: read
157+
id-token: write
158+
attestations: write
159+
```
160+
161+
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`.
162+
109163
## Add another package
110164

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

117-
Update the package version, then push its package tag:
171+
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:
118172

119173
```sh
120174
git tag comments@1.0.0
121175
git push origin comments@1.0.0
122176
```
123177

124-
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.
178+
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.
125179

126180
## What the release service verifies
127181

docs/technical-specs/delegated-release-service.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ The service normalizes omitted values to the protocol defaults. It validates the
134134

135135
`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.
136136

137+
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.
138+
137139
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.
138140

139141
### Signed release provenance

0 commit comments

Comments
 (0)